import Gradient, { Spectral, ColorRamp } from '../../colors/Gradient';
import NumericalParameter, { NoParam } from '../../map/model/Parameter';
import Layer from 'ol/layer/Layer';
// import VectorLayer from 'ol/layer/Vector';
import ImageLayer from 'ol/layer/Image';
import BaseVectorLayer from 'ol/layer/BaseVector';
import { FeatureLike } from 'ol/Feature';
import Style, { StyleFunction } from 'ol/style/Style'
import Fill from 'ol/style/Fill';
import CircleStyle from 'ol/style/Circle';
import Stroke from 'ol/style/Stroke';
import GeometryType from 'ol/geom/GeometryType';
import { Observable } from 'typescript-observable/dist/observable';
import { IObservableEvent } from 'typescript-observable/dist/interfaces/observable-event';
import ColorUtils from '../../colors/ColorUtils';
import TemporalImageLayer from './TemporalImageLayer';

export interface HasColorScale {
  setLayerColorScale(colorScale: LayerColorScale): void;
  getLayerColorScale(): LayerColorScale;
  updateLayerColorScale(parameters: NumericalParameter[]): void;
  hasLayerColorScale(): boolean;
  getAvailableParameters(): NumericalParameter[];

  isAutoLayerColorScale(): boolean;
  setAutoLayerColorScale(auto: boolean): void;
}

export default class LayerColorScale extends Observable {
  public static KEY: string = 'colorScale';
  public static AUTO_COLOR_SCALE_KEY: string = 'autoColorScale';
  public static AUTO_COLOR_SCALE_DISABLE_VALUE: string = 'disable';

  private _selectedParameter: NumericalParameter = NoParam;
  private availableParameters: NumericalParameter[];
  private colorGradient: Gradient = new Gradient(Spectral, 0, 1);

  private registeredLayer: Layer[] = [];

  private readonly parameterChangeEvent: IObservableEvent = {
    parent: null,
    name: 'parameterChanged'
  };

  private readonly availableParametersChangeEvent: IObservableEvent = {
    parent: null,
    name: 'availableParametersChanged'
  };

  private customStyle!: ((feature: FeatureLike, colorScale: LayerColorScale) => Style[]);

  public constructor(availableParameters: NumericalParameter[]) {
    super();
    this.availableParameters = availableParameters;
    this.selectedParameter = availableParameters[0];
  }

  public registerImageLayer(layer: ImageLayer): void {
    this.registeredLayer.push(layer);
    // set color scale to layer
    layer.set(LayerColorScale.KEY, this);
    layer.dispatchEvent(LayerColorScale.KEY);
  }

  /**
   * Assoziates the given vector layer with this color scale.<br>
   * @param layer 
   */
  public registerVectorLayer(layer: BaseVectorLayer): void {
    let self: LayerColorScale = this;
    this.registeredLayer.push(layer);

    // set color scale to layer and set style function
    layer.set(LayerColorScale.KEY, this);
    layer.setStyle(this.getStyleFunction());
    layer.dispatchEvent(LayerColorScale.KEY);


    // register a post render hook, to capture the case that color gradient was updated during the last run
    layer.on('postcompose', () => {
      if (self.colorGradient.getMin() != self.selectedParameter.getMin() || self.colorGradient.getMax() != self.selectedParameter.getMax()) {
        // the color gradient adapted it self - update the parameter bounds
        self.selectedParameter.setMin(self.colorGradient.getMin());
        self.selectedParameter.setMax(self.colorGradient.getMax());

        // we need to redraw since the bounds of the gradient changed on the fly
        self.triggerLayerChanged();
      }
    });
  }

  public getSelectedParameter(): NumericalParameter {
    return this.selectedParameter;
  }

  public getAvailableParameters(): NumericalParameter[] {
    return this.availableParameters;
  }

  public getAvailableParameter(name: string): NumericalParameter | undefined {
    return this.availableParameters.find(parameter => parameter.getName() === name);
  }

  public getGradient(): Gradient {
    return this.colorGradient;
  }

  public get colorRamp(): ColorRamp {
    return this.colorGradient.getColorRamp();
  }

  public set colorRamp(colorRamp: ColorRamp) {
    this.colorGradient.setColorRamp(colorRamp);

    // trigger redraw of the layers
    this.triggerLayerChanged();
  }

  public get selectedParameter(): NumericalParameter {
    return this._selectedParameter;
  }

  public set selectedParameter(param: NumericalParameter) {
    this._selectedParameter = param;
    // the parameter changed - update the gradient with the new min max values
    this.colorGradient.updateMinMax(param.getMin(), param.getMax());

    // trigger a redraw of the layers
    this.triggerLayerChanged();

    // notify observers
    this.notify(this.parameterChangeEvent, param);
  }

  public updateParameters(parameters: NumericalParameter[]): void {
    this.availableParameters = parameters;

    // notify observers
    this.notify(this.availableParametersChangeEvent, this.availableParameters);

    // exchange selected parameter with same name
    let parameterFound = false;
    for (let p of parameters) {
      if (p.getName() === this.selectedParameter.getName()) {
        parameterFound = true;
        this.selectedParameter = p;
        break;
      }
    }

    if (!parameterFound) {
      // the previously selected parameter doesn't exist anymore - select first
      this.selectedParameter = parameters[0];
    }
  }

  private triggerLayerChanged(): void {
    for (let layer of this.registeredLayer) {
      // console.log('LayerColorScale::trigger layer changed: ' + layer.get('title'));

      if (layer instanceof TemporalImageLayer) {
        layer.updateSourceColorScale()
      } else {
        layer.changed();
      }

      layer.dispatchEvent(LayerColorScale.KEY);
    }
  }

  public setCustomStyleFunction(customStyle: (feature: FeatureLike, colorScale: LayerColorScale) => Style[]): void {
    this.customStyle = customStyle;

    if (this.registeredLayer.length > 0) {
      // update style functions for already registered layers
      for (let layer of this.registeredLayer) {
        if (layer instanceof BaseVectorLayer) {
          layer.setStyle(this.getStyleFunction());
          layer.dispatchEvent(LayerColorScale.KEY);
        }
      }
    }
  }

  /**
   * Returns the style function (custom or default) for this color scale
   */
  public getStyleFunction(): StyleFunction {
    let self = this;
    // return custom style if any
    if (this.customStyle) {
      return (feature: FeatureLike): Style[] => {
        return self.customStyle(feature, self);
      }
    } else {
      // return default style
      return (feature: FeatureLike): Style[] => {
        // get value of the currently selected parameter
        let value: number = feature.get(self.selectedParameter.getName());

        // get color for the value
        let color: string = isNaN(value) ? '#77777777' : self.colorGradient.getColor(value);

        switch (feature.getGeometry().getType()) {
          case GeometryType.POINT:
          case GeometryType.MULTI_POINT:
            let circle = new CircleStyle({
              radius: 4,
              fill: new Fill({
                color: color
              })
            });
            // fixme: workaround for missing function exception
            circle['getPixelRatio'] = function (arg) {
              return 1;
            };
            circle['getScaleArray'] = function () {
              return [1, 1];
            }
            return [
              new Style({
                image: circle
              })];
          case GeometryType.LINE_STRING:
          case GeometryType.MULTI_LINE_STRING:
            return [
              new Style({
                stroke: new Stroke({
                  color: color,
                  width: 4
                })
              })];
          case GeometryType.LINEAR_RING:
          case GeometryType.POLYGON:
          case GeometryType.MULTI_POLYGON:
            return [
              new Style({
                stroke: new Stroke({
                  color: color,
                  width: 4
                }),
                fill: new Fill({
                  color: ColorUtils.setAlpha(color, 0.4)

                })
              })];
          default:
            console.error('unsupported geometry', feature.getGeometry().getType());
            return [];
        }

      };
    }
  }
}