import IHasFilter, { isFilterable } from "./HasFilter";
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import { Observable } from 'typescript-observable/dist/observable';
import { IObservableEvent } from 'typescript-observable/dist/interfaces/observable-event';

// TODO: the filter needs to be observable!!!
export default class Filter extends Observable {

    private readonly minChangeEvent: IObservableEvent = {
        parent: null,
        name: 'changed:min'
    };

    private readonly maxChangeEvent: IObservableEvent = {
        parent: null,
        name: 'changed:max'
    };

    private readonly thresholdChangeEvent: IObservableEvent = {
        parent: null,
        name: 'changed:threshold'
    };

    private readonly attributeChangeEvent: IObservableEvent = {
        parent: null,
        name: 'changed:attribute'
    };

    private readonly useTimeseriesChangeEvent: IObservableEvent = {
        parent: null,
        name: 'changed:useTimeseries'
    };    

    private static readonly ORIGINAL_FEATURES_KEY = 'originalFeatures';
    private static readonly FILTER_KEY = "filter";

    public static readonly MEDIAN_TOP = "Median (top)";
    public static readonly MEDIAN_BOTTOM = "Median (bottom)";
    public static readonly QUARTILE_R1 = "Quartile (R1)";
    public static readonly QUARTILE_R2 = "Quartile (R2)";
    public static readonly QUARTILE_R3 = "Quartile (R3)";
    public static readonly QUARTILE_R4 = "Quartile (R4)";

    private _attribute: string;
    private _useTimeseries = true;
    private _threshold: [number, number] = [Number.NaN, Number.NaN];
    private _min: number = 0;
    private _max: number = 0;
    private _median: number = NaN;
    private _quartileQ1: number = NaN;
    private _quartileQ3: number = NaN;
    private _protected: boolean = false;

    public get isProtected(): boolean {
        return this._protected;
    }

    public set isProtected(value: boolean) {
        this._protected = value;
    }

    public get attribute(): string {
        return this._attribute;
    }

    public set attribute(value: string) {
        let changed = value != this._attribute;

        this._attribute = value;

        if (changed) {
            this.notify(this.attributeChangeEvent, value);
        }
    }

    public get useTimeseries(): boolean {
        return this._useTimeseries;
    }

    public set useTimeseries(value: boolean) {
        let changed = value != this._useTimeseries;

        this._useTimeseries = value;

        if (changed) {
            this.notify(this.useTimeseriesChangeEvent, value);
        }
    }

    public set min(value: number) {
        let changed = value != this._min;

        this._min = value;

        if (changed) {
            this.notify(this.minChangeEvent, value);
        }
    }
    public get min(): number {
        return this._min;
    }
    public set max(value: number) {
        let changed = value != this._max;

        this._max = value;

        if (changed) {
            this.notify(this.maxChangeEvent, value);
        }
    }
    public get max(): number {
        return this._max;
    }

    public set median(value:number){
        this._median = value;
    }
    public get median():number{
        return this._median;
    }

    public set quartileQ1(value:number){
        this._quartileQ1 = value;
    }
    public get quartileQ1():number{
        return this._quartileQ1;
    }

    public set quartileQ3(value:number){
        this._quartileQ3 = value;
    }
    public get quartileQ3():number{
        return this._quartileQ3;
    }


    public setThreshold(lower: number, upper: number): void {
        let changed = lower != this.threshold[0] || upper != this.threshold[1];
        this._threshold[0] = lower;
        this._threshold[1] = upper;

        if (changed) {
            this.notify(this.thresholdChangeEvent, this._threshold);
        }
    }

    public get threshold(): [number, number] {
        return this._threshold;
    }

    public reset(): void {
        this.setThreshold(this._min, this._max);
    }

    public isValid(): boolean {
        return this.attribute !== undefined && this.attribute.length > 0 && this.threshold !== undefined && this.threshold.length > 0;
    }

    /**
     * Applies this filter to the given object
     * @param obj 
     */
    public apply(obj: object): void {
        // console.log('applying filter', this, obj);
        if (isFilterable(obj)) {
            let filterable: IHasFilter = obj as IHasFilter;
            filterable.setFilter(this);
        } else if (Filter.isVectorLayerAndVectorSource(obj)) {
            // we can filter any vector layer with a vector source
            let vectorLayer: VectorLayer = obj as VectorLayer;
            let vectorSource: VectorSource = vectorLayer.getSource() as VectorSource;

            // store the filter in the layer
            vectorLayer.set(Filter.FILTER_KEY, this);

            // get original feature collection
            let origFeatures = vectorLayer.get(Filter.ORIGINAL_FEATURES_KEY);
            if (origFeatures === undefined) {
                // get original features from original source
                origFeatures = vectorSource.getFeatures();
                // store original features in layer
                vectorLayer.set(Filter.ORIGINAL_FEATURES_KEY, origFeatures);
            }

            let filteredFeatures: Feature[] = [];
            for (let feature of origFeatures) {
                var featureValue = feature.get(this.attribute);
                if (this.filter(featureValue)) {
                    filteredFeatures.push(feature);
                }
            }

            // clear the source and add features satisfying the filter
            vectorSource.clear();
            vectorSource.addFeatures(filteredFeatures);

        } else {
            console.warn('ignoring attempt to apply a filter to an unsupported object', this, obj);
        }
    }

    /**
     * Extracts a filter from the given object
     * @param obj 
     */
    public static extract(obj: object): Filter | undefined {
        if (isFilterable(obj)) {
            return (obj as IHasFilter).getFilter();
        } else if (Filter.isVectorLayerAndVectorSource(obj)) {
            return (obj as VectorLayer).get(Filter.FILTER_KEY);
        } else {
            console.warn('attempt to extract filter from unsupported object', obj);
        }

        return undefined;
    }

    public static canFilter(obj: object): boolean {
        return isFilterable(obj) || Filter.isVectorLayerAndVectorSource(obj);
    }

    private static isVectorLayerAndVectorSource(obj: object): boolean {
        return obj instanceof VectorLayer && (obj as VectorLayer).getSource() instanceof VectorSource;
    }

    /**
     * @param value -> check value
     * @returns automatic filter 
     */
    public filter(value: number): boolean {
        return value >= this._threshold[0] && value <= this._threshold[1];
    }

    /**
     * @returns cql filter 
     */
    public getQuery(): string {
        if (this.attribute != "" && this._threshold[0] != NaN && this._threshold[1] != NaN) {
            if (this._threshold[0] == this._threshold[1]) {
                return this.attribute + " = " + this._threshold[0];
            } else {
                return this.attribute + " BETWEEN " + this._threshold[0] + " AND " + this._threshold[1];
            }

        }

        return "";
    }

    public getPossibleThresholds(): string[]{
        let options: string[] = [];

        if(this.median!=NaN){
            options.push(Filter.MEDIAN_BOTTOM);
            options.push(Filter.MEDIAN_TOP);
        }
        if(this.quartileQ1!=NaN && this.quartileQ3!=NaN){
            options.push(Filter.QUARTILE_R1);
            options.push(Filter.QUARTILE_R2);
            options.push(Filter.QUARTILE_R3);
            options.push(Filter.QUARTILE_R4);
        }

        return options;
    }

}