/*
 * 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.mock;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import io.confluent.kafkarest.Time;
import io.confluent.kafkarest.entities.ConsumerRecord;
import kafka.common.KafkaException;
import kafka.consumer.FetchedDataChunk;
import kafka.consumer.PartitionTopicInfo;
import kafka.message.ByteBufferMessageSet;
import kafka.message.Message;
import scala.collection.JavaConversions;

/**
 * Mock blocking queue that can be used to back a KafkaQueue for mock consumers. This class requires
 * that all data to be produced is available at construction. It also only works for binary
 * consumers since it uses Kafka consumer internal classes that work with raw data before
 * deserialization.
 */
public class MockConsumerQueue implements BlockingQueue<FetchedDataChunk> {

  private Time time;
  private PriorityQueue<ScheduledItems> scheduled = new PriorityQueue<ScheduledItems>();
  private Queue<ConsumerRecord<byte[], byte[]>> ready
      = new LinkedList<ConsumerRecord<byte[], byte[]>>();

  public MockConsumerQueue(Time time, Map<Integer,
      List<ConsumerRecord<byte[], byte[]>>> schedule) {
    this.time = time;
    for (Map.Entry<Integer, List<ConsumerRecord<byte[], byte[]>>> item : schedule.entrySet()) {
      scheduled.add(new ScheduledItems(item.getKey(), item.getValue()));
    }
  }

  @Override
  public boolean add(FetchedDataChunk fetchedDataChunk) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean offer(FetchedDataChunk fetchedDataChunk) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void put(FetchedDataChunk fetchedDataChunk) throws InterruptedException {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean offer(FetchedDataChunk fetchedDataChunk, long timeout, TimeUnit unit)
      throws InterruptedException {
    throw new UnsupportedOperationException();
  }

  @Override
  public FetchedDataChunk take() throws InterruptedException {
    FetchedDataChunk result = null;
    while (result == null) {
      poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
    }
    return result;
  }

  @Override
  public FetchedDataChunk poll(long timeout, TimeUnit unit) throws InterruptedException {
    long now = time.milliseconds();

    while (!scheduled.isEmpty() && scheduled.peek().time <= now) {
      ScheduledItems readyItems = scheduled.poll();
      ready.addAll(readyItems.records);
    }

    if (ready.isEmpty()) {
      long msToSleep;
      if (scheduled.isEmpty()) {
        msToSleep = timeout;
      } else {
        msToSleep = Math.min(scheduled.peek().time - now, timeout);
      }
      time.sleep(msToSleep);
      now = time.milliseconds();
    }

    while (!scheduled.isEmpty() && scheduled.peek().time <= now) {
      ScheduledItems readyItems = scheduled.poll();
      ready.addAll(readyItems.records);
    }

    if (!ready.isEmpty()) {
      ConsumerRecord<byte[], byte[]> c = ready.remove();
      // Special case that schedules can use to indicate we should throw an exception
      if (c == null) {
        throw new KafkaException("null value in mock consumer used to trigger KafkaException");
      }

      ByteBufferMessageSet msgSet = new ByteBufferMessageSet(
          JavaConversions.asScalaBuffer(Arrays.asList(new Message(c.getValue(), c.getKey(), -1L, (byte) 0)))
      );
      AtomicLong consumedOffset = new AtomicLong(0);
      AtomicLong fetchOffset = new AtomicLong(0);
      PartitionTopicInfo
          pti =
          new PartitionTopicInfo(c.getTopic(),
                                 c.getPartition(),
                                 null,
                                 consumedOffset,
                                 fetchOffset,
                                 null,
                                 "clientId");
      return new FetchedDataChunk(msgSet, pti, fetchOffset.get());
    }

    return null;
  }

  @Override
  public int remainingCapacity() {
    return 0;
  }

  @Override
  public boolean remove(Object o) {
    return false;
  }

  @Override
  public boolean contains(Object o) {
    return false;
  }

  @Override
  public int drainTo(Collection<? super FetchedDataChunk> c) {
    return 0;
  }

  @Override
  public int drainTo(Collection<? super FetchedDataChunk> c, int maxElements) {
    return 0;
  }

  @Override
  public FetchedDataChunk remove() {
    return null;
  }

  @Override
  public FetchedDataChunk poll() {
    return null;
  }

  @Override
  public FetchedDataChunk element() {
    return null;
  }

  @Override
  public FetchedDataChunk peek() {
    return null;
  }

  @Override
  public int size() {
    return 0;
  }

  @Override
  public boolean isEmpty() {
    return false;
  }

  @Override
  public Iterator<FetchedDataChunk> iterator() {
    return null;
  }

  @Override
  public Object[] toArray() {
    return new Object[0];
  }

  @Override
  public <T> T[] toArray(T[] a) {
    return null;
  }

  @Override
  public boolean containsAll(Collection<?> c) {
    return false;
  }

  @Override
  public boolean addAll(Collection<? extends FetchedDataChunk> c) {
    return false;
  }

  @Override
  public boolean removeAll(Collection<?> c) {
    return false;
  }

  @Override
  public boolean retainAll(Collection<?> c) {
    return false;
  }

  @Override
  public void clear() {

  }

  private class ScheduledItems implements Comparable<ScheduledItems> {

    long time;
    List<ConsumerRecord<byte[], byte[]>> records;

    private ScheduledItems(long time, List<ConsumerRecord<byte[], byte[]>> records) {
      this.time = time;
      this.records = records;
    }

    @Override
    public int compareTo(ScheduledItems o) {
      if (time < o.time) {
        return -1;
      } else if (time == o.time) {
        return 0;
      } else {
        return 1;
      }
    }
  }
}
