package io.kubernetes.client.extended.workqueue;

import com.google.common.primitives.Longs;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.Temporal;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/** The default delaying queue implementation. */
public class DefaultDelayingQueue<T> extends DefaultWorkQueue<T> implements DelayingQueue<T> {

  public static Duration heartBeatInterval = Duration.ofSeconds(10);

  private DelayQueue<WaitForEntry<T>> delayQueue;
  private ConcurrentMap<T, WaitForEntry<T>> waitingEntryByData;
  protected BlockingQueue<WaitForEntry<T>> waitingForAddQueue;

  public DefaultDelayingQueue(ExecutorService waitingWorker) {
    this.delayQueue = new DelayQueue<>();
    this.waitingEntryByData = new ConcurrentHashMap<>();
    this.waitingForAddQueue = new LinkedBlockingQueue<>(1000);
    waitingWorker.submit(this::waitingLoop);
  }

  public DefaultDelayingQueue() {
    this(Executors.newSingleThreadExecutor());
  }

  public void addAfter(T item, Duration duration) {
    // don't add if we're already shutting down
    if (super.isShuttingDown()) {
      return;
    }

    // immediately add things w/o delay
    if (duration.isZero()) {
      super.add(item);
      return;
    }
    WaitForEntry<T> entry = new WaitForEntry<>(item, duration.addTo(Instant.now()));
    this.waitingForAddQueue.offer(entry);
  }

  private void waitingLoop() {
    try {
      while (true) {
        // underlying work-queue is shutting down, quit the loop.
        if (super.isShuttingDown()) {
          return;
        }
        // peek the first item from the delay queue
        WaitForEntry<T> entry = delayQueue.peek();
        // default next ready-at time to "never"
        Duration nextReadyAt = heartBeatInterval;
        if (entry != null) {
          // the delay-queue isn't empty, so we deal with the item in the following logic:
          // 1. check if the item is ready to fire
          //   a. if ready, remove it from the delay-queue and push it into underlying work-queue
          //   b. if not, refresh the next ready-at time.
          Instant now = Instant.now();
          if (!Duration.between(entry.readyAtMillis, now).isNegative()) {
            delayQueue.remove(entry);
            super.add(entry.data);
            this.waitingEntryByData.remove(entry.data);
            continue;
          } else {
            nextReadyAt = Duration.between(now, entry.readyAtMillis);
          }
        }

        WaitForEntry<T> waitForEntry =
            waitingForAddQueue.poll(nextReadyAt.toMillis(), TimeUnit.MILLISECONDS);
        if (waitForEntry != null) {
          if (Duration.between(waitForEntry.readyAtMillis, Instant.now()).isNegative()) {
            // the item is not yet ready, insert it to the delay-queue
            insert(this.delayQueue, this.waitingEntryByData, waitForEntry);
          } else {
            // the item is ready as soon as received, fire it to the work-queue directly
            super.add(waitForEntry.data);
          }
        }
      }
    } catch (InterruptedException e) {
      // empty block
    }
  }

  private void insert(
      DelayQueue<WaitForEntry<T>> q, Map<T, WaitForEntry<T>> knownEntries, WaitForEntry entry) {
    WaitForEntry existing = knownEntries.get((T) entry.data);
    if (existing != null) {
      if (Duration.between(existing.readyAtMillis, entry.readyAtMillis).isNegative()) {
        q.remove(existing);
        existing.readyAtMillis = entry.readyAtMillis;
        q.add(existing);
      }

      return;
    }

    q.offer(entry);
    knownEntries.put((T) entry.data, entry);
  }

  // WaitForEntry holds the data to add and the time it should be added.
  private class WaitForEntry<T> implements Delayed {

    private WaitForEntry(T data, Temporal readyAtMillis) {
      this.data = data;
      this.readyAtMillis = readyAtMillis;
    }

    private T data;
    private Temporal readyAtMillis;

    @Override
    public long getDelay(TimeUnit unit) {
      Duration duration = Duration.between(Instant.now(), readyAtMillis);
      return unit.convert(duration.toMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
      return Longs.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
    }
  }
}