/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import Collection from 'ol/Collection';
import * as condition from 'ol/events/condition';
import type { Extent } from 'ol/extent';
import Feature from 'ol/Feature';
import Geometry from 'ol/geom/Geometry';
import DragBox, { DragBoxEvent } from 'ol/interaction/DragBox';
import Select, { Options as SelectOptions, SelectEvent } from 'ol/interaction/Select';
import VectorLayer from 'ol/layer/Vector';
import Style, { StyleFunction } from 'ol/style/Style';
import { Observable, Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { InteractionMode } from '../interaction-mode';

export class SelectionInteractionMode extends InteractionMode {
  /**
   * The underlying Openlayer Select Interaction
   */
  readonly select: Select;
  /**
   * The undeerlying Openlayer DragBox Interaction
   */
  readonly boxSelect = new DragBox({
    condition: condition.shiftKeyOnly,
    // eslint-disable-next-line no-empty-function
    onBoxEnd: () => {},
  });

  private _selection = new Subject<Feature[]>();
  /**
   * Observable emitting currently selected features everytime they change.
   */
  readonly selected: Observable<Feature[]> = this._selection.asObservable().pipe(debounceTime(100));
  private _featuresSelectedPreviously = new Set<any>();
  private _selectedFeatures: Collection<Feature>;

  /**
   * Key-function to use for round-robin selection.
   * @param feature a Feature
   * @returns key for feature
   */
  featureKeyFunction: (feature: Feature) => any = (feature) => feature;
  /**
   * Priority-comparison method to sort candidates for round-robin selection.
   * @param a a feature
   * @param b a feature
   * @returns numeric comparison result (<0; 0; >0)
   */
  roundRobinCompareFn: (a: Feature, b: Feature) => number = (a, b) => {
    return Math.sign(this.getAreaOfExtent(a.getGeometry()) - this.getAreaOfExtent(b.getGeometry()));
  };
  /**
   * Condition whether to use round-robin for the selection of a single feature.
   * @param event Selection event from the map
   * @returns True if round-robbin should be applied, false otherwise.
   */
  roundRobinCondition: (event: SelectEvent) => boolean = (event) => condition.noModifierKeys(event.mapBrowserEvent);

  constructor(private selectionStyle?: StyleFunction | Style | Style[], selectionOptions: SelectOptions = {}) {
    super();
    this.select = new Select({
      style: this.selectionStyle,
      condition: condition.singleClick,
      addCondition: condition.platformModifierKeyOnly,
      removeCondition: condition.shiftKeyOnly,
      hitTolerance: 5,
      multi: true,
      ...selectionOptions,
    });
    this._selectedFeatures = this.select.getFeatures();
  }

  /**
   * List of currently selected features.
   * @returns Selected features.
   */
  get selectedFeatures() {
    return this._selectedFeatures.getArray();
  }

  /**
   * Set selected features.
   * @param features Features to be selected.
   */
  set selectedFeatures(features: Feature[]) {
    const previouslySelectedFeatures = this._selectedFeatures.getArray();
    const deselectedFeatures = previouslySelectedFeatures.filter((feature) => features.indexOf(feature) < 0);
    const newlySelectedFeatures = features.filter((feature) => previouslySelectedFeatures.indexOf(feature) < 0);
    if (deselectedFeatures.length || newlySelectedFeatures.length) {
      deselectedFeatures.forEach((f) => this._selectedFeatures.remove(f));
      newlySelectedFeatures.forEach((f) => this._selectedFeatures.push(f));
      this._selectedFeatures.changed();
      features.forEach((feature) => this._featuresSelectedPreviously.add(this.featureKeyFunction(feature)));
    }
  }

  /**
   * Get the Openlayer Collection of selected features of the select interaction, including support for change handling.
   * @returns Openlayer Collection of selected features.
   */
  get selectedFeaturesCollection() {
    return this._selectedFeatures;
  }

  activate() {
    this.activateInteraction(this.select);
    this.registerListener(
      this.select.on('select', (event: SelectEvent) => {
        if (!!event.selected.length) {
          // Special round-robin treatment
          if (this.roundRobinCondition(event)) {
            const selectedFeature = this.roundRobinSelection(event.selected);
            event.selected = [selectedFeature];
            event.deselected.push(
              ...this._selectedFeatures
                .getArray()
                .filter((feature) => feature !== selectedFeature)
                .filter((feature) => event.deselected.indexOf(feature) < 0)
            );
            this.clearInternalSelection();
            this._selectedFeatures.extend(event.selected);
          }
        } else {
          // Loose round-robin selection memory upon deselection
          this._featuresSelectedPreviously.clear();
        }
        this._selectedFeatures.changed();
      })
    );
    this.activateInteraction(this.boxSelect);
    this.registerListener(this.boxSelect.on('boxend', (event: DragBoxEvent) => this.selectionBoxEnd(event)));
    this.registerListener(
      this._selectedFeatures.on('change', () => {
        this._selection.next(this.selectedFeatures);
      })
    );

    super.activate();
  }

  /**
   * Set function to derive hash key from feature.
   * @param keyFunction Function to derive identifier from a feature.
   */
  setFeatureKeyFunction(keyFunction: (feature) => number | string) {
    this.featureKeyFunction = keyFunction;
    this._featuresSelectedPreviously.clear();
  }

  public getAreaOfExtent(geometry: Geometry) {
    if (!geometry || !geometry.getExtent()) {
      return 0;
    } else {
      const [x1, y1, x2, y2] = geometry.getExtent();
      return Math.abs(x1 - x2) * Math.abs(y1 - y2);
    }
  }

  protected selectionBoxEnd(event: DragBoxEvent) {
    const extent = this.boxSelect.getGeometry().getExtent();
    const previouslySelected = this._selectedFeatures.getArray();
    if (this.isDeselection(event.mapBrowserEvent)) {
      this.selectWithBox(extent)
        .filter((feature) => previouslySelected.indexOf(feature) >= 0)
        .forEach((feature) => this._selectedFeatures.remove(feature));
    } else {
      this._selectedFeatures.extend(
        this.selectWithBox(extent).filter((feature) => previouslySelected.indexOf(feature) < 0)
      );
      this._selectedFeatures.changed();
    }
  }

  protected isDeselection(event) {
    return event.originalEvent.ctrlKey || event.originalEvent.metaKey;
  }

  protected selectWithBox(boxExtend: Extent) {
    const features: Feature[] = [];
    this.map.getLayers().forEach((mapLayer) => {
      if (mapLayer instanceof VectorLayer) {
        (mapLayer as VectorLayer).getSource().forEachFeatureIntersectingExtent(boxExtend, (feature) => {
          features.push(feature);
          return false;
        });
      }
    });
    return features;
  }

  private clearInternalSelection() {
    try {
      this._selectedFeatures.clear();
    } catch (e) {
      // there sometimes occurs an error in openlayers upon deselection
      console.warn('Error {} on deselection.', e);
    }
  }

  private roundRobinSelection(features: Feature[]): Feature {
    features.sort(this.roundRobinCompareFn);

    let selectedFeature;
    // select next feature, that has not been selected previously
    if (features.length > 1) {
      selectedFeature = features
        .filter((feature) => !this._featuresSelectedPreviously.has(this.featureKeyFunction(feature)))
        .shift();
    }
    // ...or start from the beginning
    if (!selectedFeature && features.length > 0) {
      this._featuresSelectedPreviously.clear();
      selectedFeature = features[0];
    }
    // ...and add it to list of previously selected features
    if (!!selectedFeature) {
      this._featuresSelectedPreviously.add(this.featureKeyFunction(selectedFeature));
    }
    return selectedFeature;
  }
}
