import type {ImageryLayer, ImageryLayerCollection, Scene} from 'cesium';
import type BaseLayer from 'ol/layer/Base.js';
import BaseVectorLayer from 'ol/layer/BaseVector.js';
import LayerGroup from 'ol/layer/Group.js';
import type Map from 'ol/Map.js';
import type Projection from 'ol/proj/Projection.js';
import olcsAbstractSynchronizer from './AbstractSynchronizer.js';
import {
  type LayerWithParents,
  tileLayerToImageryLayer,
  updateCesiumLayerProperties,
} from './core.js';
import {getUid} from './util.js';

export default class RasterSynchronizer extends olcsAbstractSynchronizer<ImageryLayer> {
  private cesiumLayers_: ImageryLayerCollection;
  private ourLayers_: ImageryLayerCollection;
  /**
   * This object takes care of one-directional synchronization of
   * Openlayers raster layers to the given Cesium globe.
   * @param map
   * @param scene
   */
  constructor(map: Map, scene: Scene) {
    super(map, scene);

    this.cesiumLayers_ = scene.imageryLayers;
    this.ourLayers_ = new Cesium.ImageryLayerCollection();
  }

  addCesiumObject(object: ImageryLayer): void {
    this.cesiumLayers_.add(object);
    this.ourLayers_.add(object);
  }

  destroyCesiumObject(object: ImageryLayer): void {
    object.destroy();
  }

  removeSingleCesiumObject(object: ImageryLayer, destroy: boolean): void {
    this.cesiumLayers_.remove(object, destroy);
    this.ourLayers_.remove(object, false);
  }

  removeAllCesiumObjects(destroy: boolean): void {
    for (let i = 0; i < this.ourLayers_.length; ++i) {
      this.cesiumLayers_.remove(this.ourLayers_.get(i), destroy);
    }
    this.ourLayers_.removeAll(false);
  }

  /**
   * Creates an array of Cesium.ImageryLayer.
   * May be overriden by child classes to implement custom behavior.
   * The default implementation handles tiled imageries in EPSG:4326 or
   * EPSG:3859.
   * @param olLayer
   * @param viewProj
   */
  protected convertLayerToCesiumImageries(
    olLayer: BaseLayer,
    viewProj: Projection,
  ): ImageryLayer[] {
    const result = tileLayerToImageryLayer(this.map, olLayer, viewProj);
    return result ? [result] : null;
  }

  createSingleLayerCounterparts(
    olLayerWithParents: LayerWithParents,
  ): ImageryLayer[] {
    const olLayer = olLayerWithParents.layer;
    const uid = getUid(olLayer).toString();
    const viewProj = this.view.getProjection();
    console.assert(!!viewProj);
    const cesiumObjects = this.convertLayerToCesiumImageries(olLayer, viewProj);
    if (cesiumObjects) {
      const listenKeyArray = [];
      [olLayerWithParents.layer]
        .concat(olLayerWithParents.parents)
        .forEach((olLayerItem) => {
          listenKeyArray.push(
            olLayerItem.on(['change:opacity', 'change:visible'], () => {
              // the compiler does not seem to be able to infer this
              console.assert(!!cesiumObjects);
              for (let i = 0; i < cesiumObjects.length; ++i) {
                updateCesiumLayerProperties(
                  olLayerWithParents,
                  cesiumObjects[i],
                );
              }
            }),
          );
        });

      if (olLayer instanceof BaseVectorLayer) {
        let previousStyleFunction = olLayer.getStyleFunction();
        // there is no convenient way to detect a style function change in OL
        listenKeyArray.push(
          olLayer.on('change', () => {
            const currentStyleFunction = olLayer.getStyleFunction();
            if (previousStyleFunction === currentStyleFunction) {
              return;
            }
            previousStyleFunction = currentStyleFunction;
            for (let i = 0; i < cesiumObjects.length; ++i) {
              const csObj = cesiumObjects[i];
              // clear cache and set new style
              // @ts-ignore TS2341
              if (csObj._imageryCache) {
                // @ts-ignore TS2341
                csObj._imageryCache = {};
              }

              const ip = csObj.imageryProvider;
              if (ip) {
                // @ts-ignore TS2341
                ip.tileCache?.clear();
                // @ts-ignore TS2341
                ip.styleFunction_ = currentStyleFunction;
              }
            }
            this.scene.requestRender();
          }),
        );
      }

      for (let i = 0; i < cesiumObjects.length; ++i) {
        updateCesiumLayerProperties(olLayerWithParents, cesiumObjects[i]);
      }

      // there is no way to modify Cesium layer extent,
      // we have to recreate when OpenLayers layer extent changes:
      listenKeyArray.push(
        olLayer.on('change:extent', (e) => {
          for (let i = 0; i < cesiumObjects.length; ++i) {
            this.cesiumLayers_.remove(cesiumObjects[i], true); // destroy
            this.ourLayers_.remove(cesiumObjects[i], false);
          }
          delete this.layerMap[getUid(olLayer)]; // invalidate the map entry
          this.synchronize();
        }),
      );

      listenKeyArray.push(
        olLayer.on('change', (e) => {
          // when the source changes, re-add the layer to force update
          for (let i = 0; i < cesiumObjects.length; ++i) {
            const position = this.cesiumLayers_.indexOf(cesiumObjects[i]);
            if (position >= 0) {
              this.cesiumLayers_.remove(cesiumObjects[i], false);
              this.cesiumLayers_.add(cesiumObjects[i], position);
            }
          }
        }),
      );

      this.olLayerListenKeys[uid].push(...listenKeyArray);
    }

    return Array.isArray(cesiumObjects) ? cesiumObjects : null;
  }

  /**
   * Order counterparts using the same algorithm as the Openlayers renderer:
   * z-index then original sequence order.
   */
  protected override orderLayers() {
    const layers = [];
    const zIndices: Record<number, number> = {};
    const queue: Array<BaseLayer | LayerGroup> = [this.mapLayerGroup];

    while (queue.length > 0) {
      const olLayer = queue.splice(0, 1)[0];
      layers.push(olLayer);
      zIndices[getUid(olLayer)] = olLayer.getZIndex() || 0;

      if (olLayer instanceof LayerGroup) {
        const sublayers = olLayer.getLayers();
        if (sublayers) {
          // Prepend queue with sublayers in order
          queue.unshift(...sublayers.getArray());
        }
      }
    }

    // We assume sort is stable (which has been in the spec since a long time already).
    // See https://caniuse.com/mdn-javascript_builtins_array_sort_stable
    layers.sort(
      (layer1, layer2) => zIndices[getUid(layer1)] - zIndices[getUid(layer2)],
    );

    layers.forEach((olLayer) => {
      const olLayerId = getUid(olLayer).toString();
      const cesiumObjects = this.layerMap[olLayerId];
      if (cesiumObjects) {
        cesiumObjects.forEach((cesiumObject) => {
          this.raiseToTop(cesiumObject);
        });
      }
    });
  }

  raiseToTop(counterpart: ImageryLayer) {
    this.cesiumLayers_.raiseToTop(counterpart);
  }
}
