import createLinearGradient from 'color-mapper/lib/index';
import LinearGradient from 'color-mapper/lib/index';
import { Observable } from 'typescript-observable/dist/observable'
import { IObservableEvent } from 'typescript-observable/dist/interfaces/observable-event';
import ColorUtils from './ColorUtils';

export class ColorRamp {
    public constructor(public readonly name: string, public readonly colors: string[]) {
    }
}

// ['red', 'orange', 'yellow', 'lime', 'aqua', 'blue'] ;
// const SpectralColors: string[] = ['#d7191c', '#fdae61', '#ffffbf', '#abdda4', '#2b83ba'];
// const TravelTest: string[] = ['#feebe2', '#fbb4b9', '#f768a1', '#c51b8a', '#7a0177'];

const Spectral: ColorRamp = new ColorRamp('Spectral', ['#d7191c', '#fdae61', '#ffffbf', '#abdda4', '#2b83ba']);
const YellowOrRed: ColorRamp = new ColorRamp('YellowOrRed', ['#ffffb2', '#fecc5c', '#fd8d3c', '#f03b20', '#bd0026']);
const Magma: ColorRamp = new ColorRamp('Magma', ['#000004', '#4a1079', '#a1307e', '#f1605d', '#feaa74', '#fcfdbf']);
const RedOrBlue: ColorRamp = new ColorRamp('RedOrBlue', ['#ca0020', '#f4a582', '#f7f7f7', '#92c5de', '#0571b0']);
const ColdToHot: ColorRamp = new ColorRamp('ColdToHot', ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']);
const Precipitation: ColorRamp = new ColorRamp('Pr', ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']);

// pre-defined color ramps
const PredefinedColorRamps: ColorRamp[] = [
    Spectral,
    ColdToHot,
    YellowOrRed,
    Magma,
    RedOrBlue,
    Precipitation
];

export { PredefinedColorRamps, Spectral, YellowOrRed, Magma, RedOrBlue, ColdToHot, Precipitation }

class SingleColor {
    public constructor(private color: string) {
    }
    public getColor(value: number): string {
        return this.color;
    }
}

export interface HasGradient {
    getGradient(): Gradient | undefined;
    setGradient(gradient: Gradient): void;
}

/**
 * Simple wrapper around the gradient provided by color-mapper
 */
export default class Gradient extends Observable {

    private static DEFAULT_COLOR = "#0000ff";

    private colorRamp: ColorRamp;
    private gradient: LinearGradient;
    private min: number;
    private max: number;

    private minColorRgba: number[];
    private maxColorRgba: number[];

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


    public constructor(colorRamp: ColorRamp, min: number, max: number) {
        super();
        this.colorRamp = colorRamp;
        this.updateMinMaxColors();
        this.min = min;
        this.max = max;
        this.updateMinMax(min, max);
    }

    private updateMinMaxColors(): void {
        this.minColorRgba = ColorUtils.hex2Rgb(this.colorRamp.colors[0]);
        this.minColorRgba.push(255);

        this.maxColorRgba = ColorUtils.hex2Rgb(this.colorRamp.colors[this.colorRamp.colors.length - 1]);
        this.maxColorRgba.push(255);
    }

    public setColorRamp(colorRamp: ColorRamp): void {
        let changed = colorRamp !== this.colorRamp;
        this.colorRamp = colorRamp;

        this.updateMinMaxColors();

        // update the underlying linear gradient
        this.updateMinMax(this.min, this.max);


        if (changed) {
            this.changed();
        }
    }

    public updateMinMax(min?: number, max?: number): void {
        let minChanged = false;
        let maxChanged = false;

        if (min !== undefined && this.min != min) {
            this.min = min;
            minChanged = true;
        }

        if (max !== undefined && this.max != max) {
            this.max = max;
            maxChanged = true;
        }

        if (this.min !== this.max) {
            this.gradient = createLinearGradient(min, max);
            // equal steps between colors
            let step = 1.0 / (this.colorRamp.colors.length - 1);
            let value = 0.0;

            // set color stops
            for (let color of this.colorRamp.colors) {
                this.gradient.addColorStop(value, color);
                value += step;
            }
        } else {
            // this is no gradient because the range is zero (min == max)
            this.gradient = new SingleColor(this.colorRamp.colors[0]);
        }

        if (minChanged || maxChanged) {
            this.changed();
        }
    }

    private changed(): void {
        this.notify(this.changeEvent, this);
    }

    public getColor(value: number): string {
        if (value !== undefined) {
            // auto tune min max values
            if (value < this.min) {
                this.updateMinMax(value, this.max);
            }

            if (value > this.max) {
                this.updateMinMax(this.min, value);
            }

            return this.gradient.getColor(value);
        } else if (this.min == this.max) {
            // we have no real gradient anyway - return single color
            return this.gradient.getColor(value);
        } else {
            return Gradient.DEFAULT_COLOR;
        }
    }

    public getColorRGBA(value: number): number[] {
        if (value !== undefined) {
            // auto tune min max values
            if (value < this.min) {
                this.updateMinMax(value, this.max);
            }

            if (value > this.max) {
                this.updateMinMax(this.min, value);
            }

            if (value === this.max) {
                return this.maxColorRgba;
            } else if (value === this.min) {
                return this.minColorRgba;
            }

            try {
                let rgba = this.gradient.getColor(value);
                rgba[3] = 255;
                return rgba;
            } catch (e) {
                console.error('error in gradient', this.gradient, value);
                // console.log(e);
            }
        }

        return [0, 0, 0, 255];
    }

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

    public getMin(): number {
        return this.min;
    }

    public getMax(): number {
        return this.max;
    }

}