import type {Scene} from 'cesium';
import type {Collection, Map as OLMap, Overlay} from 'ol';
import type {CollectionEvent} from 'ol/Collection.js';
import {unByKey as olObservableUnByKey} from 'ol/Observable.js';
import type {EventsKey} from 'ol/events.js';
import SynchronizedOverlay from './SynchronizedOverlay.js';
import {getUid} from './util.js';

export default class OverlaySynchronizer {
  private overlayCollection_: Collection<Overlay>;
  private overlayContainerStopEvent_: HTMLDivElement;
  private overlayContainer_: HTMLDivElement;
  private overlayMap_: Map<number, SynchronizedOverlay> = new Map();
  private overlayEvents = [
    'click',
    'dblclick',
    'mousedown',
    'touchstart',
    'pointerdown',
    'mousewheel',
    'wheel',
  ];
  private listenerKeys_: EventsKey[] = [];

  /**
   * @param map
   * @param scene
   * @api
   */
  constructor(
    protected map: OLMap,
    protected scene: Scene,
  ) {
    this.map = map;
    this.overlayCollection_ = this.map.getOverlays();
    this.scene = scene;
    this.overlayContainerStopEvent_ = document.createElement('div');
    this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent';
    this.overlayEvents.forEach((name) => {
      this.overlayContainerStopEvent_.addEventListener(name, (evt) =>
        evt.stopPropagation(),
      );
    });
    this.scene.canvas.parentElement.appendChild(
      this.overlayContainerStopEvent_,
    );

    this.overlayContainer_ = document.createElement('div');
    this.overlayContainer_.className = 'ol-overlaycontainer';
    this.scene.canvas.parentElement.appendChild(this.overlayContainer_);
  }

  /**
   * Get the element that serves as a container for overlays that don't allow
   * event propagation. Elements added to this container won't let mousedown and
   * touchstart events through to the map, so clicks and gestures on an overlay
   * don't trigger any {@link ol.MapBrowserEvent}.
   * @return The map's overlay container that stops events.
   */
  getOverlayContainerStopEvent(): Element {
    return this.overlayContainerStopEvent_;
  }

  /**
   * Get the element that serves as a container for overlays.
   * @return The map's overlay container.
   */
  getOverlayContainer(): Element {
    return this.overlayContainer_;
  }

  /**
   * Destroy all and perform complete synchronization of the overlays.
   * @api
   */
  synchronize() {
    this.destroyAll();
    this.overlayCollection_.forEach((overlay) => {
      this.addOverlay(overlay);
    });
    this.listenerKeys_.push(
      this.overlayCollection_.on('add', (evt: CollectionEvent<Overlay>) =>
        this.addOverlay(evt.element),
      ),
    );
    this.listenerKeys_.push(
      this.overlayCollection_.on('remove', (evt: CollectionEvent<Overlay>) =>
        this.removeOverlay(evt.element),
      ),
    );
  }

  /**
   * @param overlay
   * @api
   */
  addOverlay(overlay: Overlay) {
    if (!overlay) {
      return;
    }
    const cesiumOverlay = new SynchronizedOverlay({
      scene: this.scene,
      synchronizer: this,
      parent: overlay,
    });

    this.overlayMap_.set(getUid(overlay), cesiumOverlay);
  }

  /**
   * Removes an overlay from the scene
   * @param overlay
   * @api
   */
  removeOverlay(overlay: Overlay) {
    const overlayId = getUid(overlay);
    const csOverlay = this.overlayMap_.get(overlayId);
    if (csOverlay) {
      csOverlay.destroy();
      this.overlayMap_.delete(overlayId);
    }
  }

  /**
   * Destroys all the created Cesium objects.
   */
  protected destroyAll() {
    this.overlayMap_.forEach((overlay: SynchronizedOverlay) => {
      overlay.destroy();
    });
    this.overlayMap_.clear();
    olObservableUnByKey(this.listenerKeys_);
    this.listenerKeys_.length = 0;
  }
}
