/*
 * Copyright 2018 Confluent Inc.
 *
 * Licensed under the Confluent Community License (the "License"); you may not use
 * this file except in compliance with the License.  You may obtain a copy of the
 * License at
 *
 * http://www.confluent.io/confluent-community-license
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 */

package io.confluent.kafkarest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import kafka.javaapi.consumer.SimpleConsumer;

/**
 * The SizeLimitedSimpleConsumerPool keeps a pool of SimpleConsumers
 * and can increase the pool within a specified limit
 */
public class SimpleConsumerPool {

  private static final Logger log = LoggerFactory.getLogger(SimpleConsumerPool.class);

  // maxPoolSize = 0 means unlimited
  private final int maxPoolSize;
  // poolInstanceAvailabilityTimeoutMs = 0 means there is no timeout
  private final int poolInstanceAvailabilityTimeoutMs;
  private final Time time;

  private final SimpleConsumerFactory simpleConsumerFactory;
  private final Map<String, SimpleConsumer> simpleConsumers;
  private final Queue<String> availableConsumers;

  public SimpleConsumerPool(
      int maxPoolSize,
      int poolInstanceAvailabilityTimeoutMs,
      Time time,
      SimpleConsumerFactory simpleConsumerFactory
  ) {
    this.maxPoolSize = maxPoolSize;
    this.poolInstanceAvailabilityTimeoutMs = poolInstanceAvailabilityTimeoutMs;
    this.time = time;
    this.simpleConsumerFactory = simpleConsumerFactory;

    simpleConsumers = new HashMap<String, SimpleConsumer>();
    availableConsumers = new LinkedList<String>();
  }

  public synchronized SimpleFetcher get(final String host, final int port) {

    final long expiration = time.milliseconds() + poolInstanceAvailabilityTimeoutMs;

    while (true) {
      // If there is a SimpleConsumer available
      if (availableConsumers.size() > 0) {
        final String consumerId = availableConsumers.remove();
        return new SimpleFetcher(simpleConsumers.get(consumerId), this);
      }

      // If not consumer is available, but we can instantiate a new one
      if (simpleConsumers.size() < maxPoolSize || maxPoolSize == 0) {
        final SimpleConsumer simpleConsumer = simpleConsumerFactory.createConsumer(host, port);
        simpleConsumers.put(simpleConsumer.clientId(), simpleConsumer);
        return new SimpleFetcher(simpleConsumer, this);
      }

      // If no consumer is available and we reached the limit
      try {
        // The behavior of wait when poolInstanceAvailabilityTimeoutMs=0 is consistent as it
        // won't timeout
        wait(poolInstanceAvailabilityTimeoutMs);
      } catch (InterruptedException e) {
        log.warn("A thread requesting a SimpleConsumer has been interrupted while waiting", e);
      }

      // In some cases ("spurious wakeup", see wait() doc), the thread will resume before the
      // timeout
      // We have to guard against that and throw only if the timeout has expired for real
      if (time.milliseconds() > expiration && poolInstanceAvailabilityTimeoutMs != 0) {
        throw Errors.simpleConsumerPoolTimeoutException();
      }
    }
  }

  public synchronized void release(SimpleFetcher simpleFetcher) {
    log.debug("Releasing into the pool SimpleConsumer with id " + simpleFetcher.clientId());
    availableConsumers.add(simpleFetcher.clientId());
    notify();
  }

  public void shutdown() {
    for (SimpleConsumer simpleConsumer : simpleConsumers.values()) {
      simpleConsumer.close();
    }
  }

  public int size() {
    return simpleConsumers.size();
  }
}
