package io.kubernetes.client.informer.impl;

import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.informer.ListerWatcher;
import io.kubernetes.client.informer.ResourceEventHandler;
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.informer.cache.Cache;
import io.kubernetes.client.informer.cache.Controller;
import io.kubernetes.client.informer.cache.DeltaFIFO;
import io.kubernetes.client.informer.cache.Indexer;
import io.kubernetes.client.informer.cache.ProcessorListener;
import io.kubernetes.client.informer.cache.SharedProcessor;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultSharedIndexInformer<
        ApiType extends KubernetesObject, ApiListType extends KubernetesListObject>
    implements SharedIndexInformer<ApiType> {

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

  private static final long MINIMUM_RESYNC_PERIOD_MILLIS = 1000L;

  // resyncCheckPeriod is how often we want the reflector's resync timer to fire so it can call
  // shouldResync to check if any of our listeners need a resync.
  private long resyncCheckPeriodMillis;
  // defaultEventHandlerResyncPeriod is the default resync period for any handlers added via
  // AddEventHandler (i.e. they don't specify one and just want to use the shared informer's default
  // value).
  private long defaultEventHandlerResyncPeriod;

  private Indexer<ApiType> indexer;

  private SharedProcessor<ApiType> processor;

  private Controller<ApiType, ApiListType> controller;

  private Thread controllerThread;

  private volatile boolean started = false;
  private volatile boolean stopped = false;

  public DefaultSharedIndexInformer(
      Class<ApiType> apiTypeClass,
      ListerWatcher<ApiType, ApiListType> listerWatcher,
      long resyncPeriod) {
    this(apiTypeClass, listerWatcher, resyncPeriod, new Cache<>());
  }

  public DefaultSharedIndexInformer(
      Class<ApiType> apiTypeClass,
      ListerWatcher<ApiType, ApiListType> listerWatcher,
      long resyncPeriod,
      Cache<ApiType> cache) {
    this(
        apiTypeClass,
        listerWatcher,
        resyncPeriod,
        // down-casting should be safe here because one delta FIFO instance only serves one resource
        // type
        new DeltaFIFO(
            (Function<KubernetesObject, String>) cache.getKeyFunc(),
            (Cache<KubernetesObject>) cache),
        cache);
  }

  public DefaultSharedIndexInformer(
      Class<ApiType> apiTypeClass,
      ListerWatcher<ApiType, ApiListType> listerWatcher,
      long resyncPeriod,
      DeltaFIFO deltaFIFO,
      Indexer<ApiType> indexer) {
    this.resyncCheckPeriodMillis = resyncPeriod;
    this.defaultEventHandlerResyncPeriod = resyncPeriod;

    this.processor = new SharedProcessor<>();
    this.indexer = indexer;
    this.controller =
        new Controller<ApiType, ApiListType>(
            apiTypeClass,
            deltaFIFO,
            listerWatcher,
            this::handleDeltas,
            processor::shouldResync,
            resyncCheckPeriodMillis);

    controllerThread =
        new Thread(controller::run, "informer-controller-" + apiTypeClass.getSimpleName());
  }

  /** add event callback */
  @Override
  public void addEventHandler(ResourceEventHandler<ApiType> handler) {
    addEventHandlerWithResyncPeriod(handler, defaultEventHandlerResyncPeriod);
  }

  /** add event callback with a resync period */
  @Override
  public void addEventHandlerWithResyncPeriod(
      ResourceEventHandler<ApiType> handler, long resyncPeriodMillis) {
    if (stopped) {
      log.info(
          "DefaultSharedIndexInformer#Handler was not added to shared informer because it has stopped already");
      return;
    }

    if (resyncPeriodMillis > 0) {
      if (resyncPeriodMillis < MINIMUM_RESYNC_PERIOD_MILLIS) {
        log.warn(
            "DefaultSharedIndexInformer#resyncPeriod {} is too small. Changing it to the minimum allowed rule of {}",
            resyncPeriodMillis,
            MINIMUM_RESYNC_PERIOD_MILLIS);
        resyncPeriodMillis = MINIMUM_RESYNC_PERIOD_MILLIS;
      }

      if (resyncPeriodMillis < this.resyncCheckPeriodMillis) {
        if (started) {
          log.warn(
              "DefaultSharedIndexInformer#resyncPeriod {} is smaller than resyncCheckPeriod {} and the informer has already started. Changing it to {}",
              resyncPeriodMillis,
              resyncCheckPeriodMillis);
          resyncPeriodMillis = resyncCheckPeriodMillis;
        } else {
          // if the event handler's resyncPeriod is smaller than the current resyncCheckPeriod,
          // update resyncCheckPeriod to match resyncPeriod and adjust the resync periods of all
          // the listeners accordingly
          this.resyncCheckPeriodMillis = resyncPeriodMillis;
        }
      }
    }

    ProcessorListener<ApiType> listener =
        new ProcessorListener(
            handler, determineResyncPeriod(resyncCheckPeriodMillis, this.resyncCheckPeriodMillis));
    if (!started) {
      this.processor.addListener(listener);
      return;
    }

    this.processor.addAndStartListener(listener);
    List<ApiType> objectList = this.indexer.list();
    for (Object item : objectList) {
      listener.add(new ProcessorListener.AddNotification(item));
    }
  }

  @Override
  public String lastSyncResourceVersion() {
    if (!started) {
      return "";
    }

    return this.controller.lastSyncResourceVersion();
  }

  @Override
  public void run() {
    if (started) {
      return;
    }

    started = true;

    this.processor.run();

    controllerThread.start();
  }

  @Override
  public void stop() {
    if (!started) {
      return;
    }

    stopped = true;

    controller.stop();
    controllerThread.interrupt();

    processor.stop();
  }

  @Override
  public boolean hasSynced() {
    // Waiting for controller initialize.
    // Because informer.run() and hasSynced() are called by different threads.
    return controller != null && this.controller.hasSynced();
  }

  /**
   * handleDeltas handles deltas and call processor distribute.
   *
   * @param deltas deltas
   */
  private void handleDeltas(Deque<MutablePair<DeltaFIFO.DeltaType, KubernetesObject>> deltas) {
    if (CollectionUtils.isEmpty(deltas)) {
      return;
    }

    // from oldest to newest
    for (MutablePair<DeltaFIFO.DeltaType, KubernetesObject> delta : deltas) {
      DeltaFIFO.DeltaType deltaType = delta.getLeft();
      switch (deltaType) {
        case Sync:
        case Added:
        case Updated:
          boolean isSync = deltaType == DeltaFIFO.DeltaType.Sync;
          Object oldObj = this.indexer.get((ApiType) delta.getRight());
          if (oldObj != null) {
            this.indexer.update((ApiType) delta.getRight());
            this.processor.distribute(
                new ProcessorListener.UpdateNotification(oldObj, delta.getRight()), isSync);
          } else {
            this.indexer.add((ApiType) delta.getRight());
            this.processor.distribute(
                new ProcessorListener.AddNotification(delta.getRight()), isSync);
          }
          break;
        case Deleted:
          this.indexer.delete((ApiType) delta.getRight());
          this.processor.distribute(
              new ProcessorListener.DeleteNotification(delta.getRight()), false);
          break;
      }
    }
  }

  @Override
  public void addIndexers(Map<String, Function<ApiType, List<String>>> indexers) {
    if (started) {
      throw new IllegalStateException("cannot add indexers to a running informer");
    }
    indexer.addIndexers(indexers);
  }

  @Override
  public Indexer getIndexer() {
    return this.indexer;
  }

  private long determineResyncPeriod(long desired, long check) {
    if (desired == 0) {
      return desired;
    }
    if (check == 0) {
      return 0;
    }
    if (desired < check) {
      return check;
    }
    return desired;
  }
}