/*
 * Copyright (c) 2011-2013 The original author or authors
 *  ------------------------------------------------------
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *      The Eclipse Public License is available at
 *      http://www.eclipse.org/legal/epl-v10.html
 *
 *      The Apache License v2.0 is available at
 *      http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.core.http.impl;

import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.timeout.IdleStateHandler;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.impl.ContextImpl;
import io.vertx.core.spi.metrics.HttpClientMetrics;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
 */
class Http2Pool implements ConnectionManager.Pool<Http2ClientConnection> {

  // Pools must locks on the queue object to keep a single lock
  private final ConnectionManager.ConnQueue queue;
  private final Set<Http2ClientConnection> allConnections = new HashSet<>();
  private final Map<Channel, ? super Http2ClientConnection> connectionMap;
  final HttpClientImpl client;
  final HttpClientMetrics metrics;
  final int maxConcurrency;
  final boolean logEnabled;
  final int maxSockets;
  final int windowSize;

  public Http2Pool(ConnectionManager.ConnQueue queue, HttpClientImpl client, HttpClientMetrics metrics,
                   Map<Channel, ? super Http2ClientConnection> connectionMap,
                   int maxConcurrency, boolean logEnabled, int maxSize, int windowSize) {
    this.queue = queue;
    this.client = client;
    this.metrics = metrics;
    this.connectionMap = connectionMap;
    this.maxConcurrency = maxConcurrency;
    this.logEnabled = logEnabled;
    this.maxSockets = maxSize;
    this.windowSize = windowSize;
  }

  @Override
  public HttpVersion version() {
    return HttpVersion.HTTP_2;
  }

  @Override
  public boolean canCreateConnection(int connCount) {
    // We create at most one connection concurrently when all others when
    // all others are busy
    return connCount == allConnections.size() && connCount < maxSockets;
  }

  @Override
  public Http2ClientConnection pollConnection() {
    for (Http2ClientConnection conn : allConnections) {
      if (canReserveStream(conn)) {
        conn.streamCount++;
        return conn;
      }
    }
    return null;
  }

  void createConn(ContextImpl context, Channel ch, Waiter waiter, boolean upgrade) throws Http2Exception {
    ChannelPipeline p = ch.pipeline();
    synchronized (queue) {
      VertxHttp2ConnectionHandler<Http2ClientConnection> handler = new VertxHttp2ConnectionHandlerBuilder<Http2ClientConnection>()
          .connectionMap(connectionMap)
          .server(false)
          .useCompression(client.getOptions().isTryUseCompression())
          .initialSettings(client.getOptions().getInitialSettings())
          .connectionFactory(connHandler -> {
            Http2ClientConnection conn = new Http2ClientConnection(Http2Pool.this, queue.metric, context, ch, connHandler, metrics);
            Object metric = metrics.connected(conn.remoteAddress(), conn.remoteName());
            conn.metric(metric);
            return conn;
          })
          .logEnabled(logEnabled)
          .build();
      if (upgrade) {
        handler.onHttpClientUpgrade();
      }
      Http2ClientConnection conn = handler.connection;
      metrics.endpointConnected(queue.metric, conn.metric());
      p.addLast(handler);
      allConnections.add(conn);
      if (windowSize > 0) {
        conn.setWindowSize(windowSize);
      }
      conn.streamCount++;
      waiter.handleConnection(conn); // Should make same tests than in deliverRequest
      queue.deliverStream(conn, waiter);
      checkPending(conn);
    }
  }

  private boolean canReserveStream(Http2ClientConnection handler) {
    int maxConcurrentStreams = Math.min(handler.handler.connection().local().maxActiveStreams(), maxConcurrency);
    return handler.streamCount < maxConcurrentStreams;
  }

  void checkPending(Http2ClientConnection conn) {
    synchronized (queue) {
      Waiter waiter;
      while (canReserveStream(conn) && (waiter = queue.getNextWaiter()) != null) {
        conn.streamCount++;
        queue.deliverStream(conn, waiter);
      }
    }
  }

  void discard(Http2ClientConnection conn) {
    synchronized (queue) {
      if (allConnections.remove(conn)) {
        queue.connectionClosed();
      }
    }
    metrics.endpointDisconnected(queue.metric, conn.metric());
  }

  @Override
  public void recycle(Http2ClientConnection conn) {
    synchronized (queue) {
      conn.streamCount--;
      checkPending(conn);
    }
  }

  @Override
  public HttpClientStream createStream(Http2ClientConnection conn) throws Exception {
    return conn.createStream();
  }

  @Override
  public void closeAllConnections() {
    List<Http2ClientConnection> toClose;
    synchronized (queue) {
      toClose = new ArrayList<>(allConnections);
    }
    // Close outside sync block to avoid deadlock
    toClose.forEach(Http2ConnectionBase::close);
  }
}
