import HashMap from "../../util/HashMap";
import ArrayUtils from '../../util/ArrayUtils';

export default class SprDatabase {

    private inputParametersMap: Map<string, string[]>;
    private outputRangeMap: Map<string, number[]>;

    public constructor(private dataMap: HashMap<object>, private inputParameters: string[], private outputParameters: string[], 
        uniqueInputParameterMap: Map<string, Set<string>>, private labelMap : Map<string, Map<string, number>>) {
        this.inputParametersMap = this.setsToArrays(uniqueInputParameterMap);
        this.outputRangeMap = this.determineOutputRanges(dataMap, outputParameters);
    }

    private setsToArrays(setMap: Map<string, Set<string>>): Map<string, string[]> {
        let arrayMap: Map<string, string[]> = new Map();

        for (let [key, set] of setMap) {
            let valueArray: string[] = [...set];
            arrayMap.set(key, valueArray);
        }

        return arrayMap;
    }

    private determineOutputRanges(dataMap: HashMap<object>, outputParameters: string[]): Map<string, number[]> {
        let rangeMap: Map<string, number[]> = new Map();

        // loop over all outputs and determine min/max ranges
        for (let output of dataMap.values()) {
            for (let outputParameter of outputParameters) {
                const outputValue: number | string = output[outputParameter];

                let value: number = NaN;

                if(this.labelMap.has(outputParameter)) {
                    value = this.getLabelMapping(outputParameter, outputValue as string);
                } else {
                    value = outputValue as number;
                }

                if (isNaN(value)) {
                    // ignore nan values
                    continue;
                }

                if (rangeMap.has(outputParameter)) {
                    // update range
                    let range: number[] = rangeMap.get(outputParameter);
                    range[0] = Math.min(range[0], value);
                    range[1] = Math.max(range[1], value);
                } else {
                    // init range
                    rangeMap.set(outputParameter, [value, value]);
                }
            }
        }

        //check if every output parameter has a rangemap (in case of all NaN values add range [0,0])
        outputParameters.forEach(element => {
            if(!rangeMap.has(element)){
                rangeMap.set(element, [0, 0]);
            }
        });

        return rangeMap;
    }

    public getOutputValueRange(outputParameter: string): number[] {
        return this.outputRangeMap.get(outputParameter);
    }

    /**
     * Returns the list of available input parameters
     */
    public getInputParameters(): string[] {
        return ArrayUtils.clone(this.inputParameters);
    }

    public updateInputParameterValue(inputParameter: string, oldValue: string, newValue: string): void {
        if (!oldValue) {
            return;
        }

        let values: string[] = this.inputParametersMap.get(inputParameter);
        if (values) {
            var index = values.indexOf(oldValue);

            if (~index) {
                values[index] = newValue;
            }

            this.inputParametersMap.set(inputParameter, values);
        }

        // update datamap keys
        for (let key of [...this.dataMap.keys()]) {
            if (key[inputParameter] == oldValue) {
                // update the key
                let newKey = Object.assign({}, key);
                newKey[inputParameter] = newValue;
                this.dataMap.updateKey(key, newKey);
            }
        }
    }

    /**
     * Returns the list of available output parameters
     */
    public getOutputParameters(): string[] {
        return ArrayUtils.clone(this.outputParameters);
    }

    public addNewOutputParameter(outputParameterName: string, getValue: (outputValues: object) => number): void {
        for(let outputValue of this.dataMap.values()) {
            let newValue = getValue(outputValue);
            if(newValue == null){
                return;
            }
            outputValue[outputParameterName] = newValue;
        }

        // add new output parameter to output parameter list
        this.outputParameters.push(outputParameterName);
        this.outputRangeMap = this.determineOutputRanges(this.dataMap, this.outputParameters);
    }

    /**
     * Returns a list of unique values for the given parameter
     * 
     * @param parameter - requested input parameter
     */
    public getUniqueInputParameters(parameter: string): string[] {
        return this.inputParametersMap.get(parameter);
    }

    /**
     * Traverses the input-output relations in the order of the given parameters array, considering the fixedParameters mapping
     * @param parameters - permutation of input parameters
     * @param fixedParameters - mapping of fixed parameter values
     * @param callbackfn - callback called for each input-output relation
     */
    public foreach(parameters: string[], fixedParameters: Map<string, string>, callbackfn: (input: object, output: object) => void): void {
        // create key instance and traverse the parameters
        let key: object = {};
        // this.fixedParams.clear;

        if (fixedParameters && fixedParameters.size > 0) {
            for (let [parameter, value] of fixedParameters.entries()) {
                key[parameter] = value;
                // this.fixedParams.set(parameter, value);
            }
        }

        // ignore the fixed parameters if in parameters
        let parameterList: string[] = [];
        for (let p of parameters) {
            if (!fixedParameters.has(p)) {
                parameterList.push(p);
            }
        }

        // TODO: ensure valid parameters list (must be a permutation of 'this.inputparameters')
        this.traverseParameters(key, parameterList, callbackfn);
    }

    private traverseParameters(key: object, parameters: string[], callbackfn: (input: object, output: object) => void): void {
        // remove next parameter from stack
        let p = parameters[0];
        // iterate over all unique values of this parameter
        for (let parameterValue of this.inputParametersMap.get(p)) {
            key[p] = parameterValue;

            // did we reach the end of the parameter stack?
            if (parameters.length == 1) {
                // last parameter - trigger callback
                let output = this.dataMap.get(key);
                if (output) {
                    callbackfn(Object.assign({}, key), output)
                } else {
                    // FIXME: here we could allow sparse data and simply skip non existing entries
                    throw new Error('invalid data mapping, no output for input found: ' + JSON.stringify(key));
                }
            } else {
                // still parameters left on the stack - continue traversing
                this.traverseParameters(key, parameters.slice(1), callbackfn);
            }
        }
    }

    /**
     * Provides a label mapping with the following structure:
     * ['outputKey' => ['label' => number]]
     * @returns 
     */
    public getLabelMap(): Map<string, Map<string, number>> {
        return this.labelMap;
    }

    public getLabelMapping(outputKey: string, outputValue: string): number {
        if(this.labelMap && this.labelMap.has(outputKey) && this.labelMap.get(outputKey).has(outputValue)) {
            return this.labelMap.get(outputKey).get(outputValue);
        } else {
            console.warn('unknown label mapping requested for ', [outputKey, outputValue]);
            return Number.NaN;
        }
    }
}
