import { Nub } from "../nub";
import { ParamCalculability } from "../param/param-definition";
import { Message } from "../util/message";
import { Result } from "../util/result";
import { ParallelStructureParams } from "./parallel_structure_params";
import { Structure } from "./structure";
import { IParamDefinitionIterator, ParamsEquation, ParamsEquationArrayIterator } from "../param/params-equation";
import { ParamValues } from "../param/param-values";

/**
 * Interface pour mémoriser le n° d'ouvrage et le paramètre à calculer
 */
interface IStructureVarCalc {
    index: number;
    prm: string;
}

/**
 * Calcul de une ou plusieurs structures hydrauliques en parallèles
 * reliées par les cotes amont et aval et dont le débit est égal à la
 * somme des débits de chaque structure.
 */
export class ParallelStructure extends Nub {

    /** Tableau des structures hydrauliques en parallèle */
    public structures: Structure[] = [];

    /**
     * paramètres castés au bon type
     */
    get prms(): ParallelStructureParams {
        return this._prms as ParallelStructureParams;
    }

    /**
     * Mise à jour de Z1 pour toutes les structures en parallèle
     */
    set Z1(Z1: number) {
        this.prms.Z1.v = Z1;
        this.updateStructuresH1H2();
    }

    /**
     * Mise à jour de Z2 pour toutes les structures en parallèle
     */
    set Z2(Z2: number) {
        this.prms.Z2.v = Z2;
        this.updateStructuresH1H2();
    }

    public get parameterIterator(): IParamDefinitionIterator {
        const prms: ParamsEquation[] = [];
        prms.push(this._prms);
        for (const st of this.structures)
            prms.push(st.parameters);
        return new ParamsEquationArrayIterator(prms);
    }

    /**
     * Ajout d'une structure en parallèle
     * @param structure La structure à rajouter
     * @param after position après laquelle insérer la structure, à la fin sinon
     */
    public addStructure(structure: Structure, after?: number) {
        if (after !== undefined)
            this.structures.splice(after + 1, 0, structure);
        else
            this.structures.push(structure);
    }

    /**
     * remplace une structure hydraulique
     * @param index indice de la structure dans le tableau
     * @param structure nouvelle structure
     */
    public replaceStructure(index: number, structure: Structure) {
        if (index > -1 && index < this.structures.length) {
            this.structures[index] = structure;
        } else {
            throw new Error(`ParallelStructure.replaceStructure invalid index ${index}`);
        }
    }

    /**
     * @return true si la structure donnée est dans la liste
     */
    public hasStructure(structure: Nub): boolean {
        for (const s of this.structures)
            if (s.uid == structure.uid)
                return true;
        return false;
    }

    /**
     * déplace une structure d'une position vers le début de la liste
     */
    public moveStructureUp(structure: Structure) {
        let i = 0;
        for (const s of this.structures) {
            if (s.uid == structure.uid && i > 0) {
                const t = this.structures[i - 1];
                this.structures[i - 1] = this.structures[i];
                this.structures[i] = t;
                return;
            }
            i++;
        }
    }

    /**
     * déplace une structure d'une position vers la fin de la liste
     */
    public moveStructureDown(structure: Structure) {
        let i = 0;
        for (const s of this.structures) {
            if (s.uid == structure.uid && i < this.structures.length - 1) {
                const t = this.structures[i];
                this.structures[i] = this.structures[i + 1];
                this.structures[i + 1] = t;
                return;
            }
            i++;
        }
    }

    /**
     * Supprime une structure hydraulique
     * @param index numéro de la structure dans le tableau
     */
    public deleteStructure(index: number) {
        if (index > -1) {
            this.structures.splice(index, 1);
        } else {
            throw new Error("ParallelStructure.deleteStructure invalid index=" + index);
        }
    }

    /**
     * Calcul du débit des structures en parallèle (sans détail pour chaque structure)
     * @param sVarCalc Variable à calculer (Q uniquement)
     */
    public Equation(sVarCalc: string): Result {
        Structure.CheckEquation(sVarCalc);
        this.updateStructuresH1H2();
        return this.CalcQ();
    }

    /**
     * Calcul de la somme des débits de chaque structure
     * @param iExcept Index de la structure à ne pas additionner (optionel)
     */
    public CalcQ(iExcept?: number): Result {
        if (iExcept !== undefined) {
            if (iExcept < 0 || iExcept >= this.structures.length) {
                throw new Error(
                    "ParallelStructure.CalcQ iExcept not in [0;" + (this.structures.length - 1) + "]",
                );
            }
        }
        const calcRes: Result = new Result(0);
        let qTot: number = 0;
        for (let i = 0; i < this.structures.length; i++) {
            if (i !== iExcept) {
                const res: Result = this.structures[i].Calc("Q");
                calcRes.resultElement.AddResultElementToExtra(res.resultElement, `ouvrage[${i}].Q`);
                qTot += res.vCalc;
            }
        }
        // Assigne le débit total dans le résultat
        calcRes.resultElement.vCalc = qTot;
        return calcRes;
    }

    /**
     * Calcul du débit total, de la cote amont ou aval ou d'un paramètre d'une structure
     * @param sVarCalc Nom du paramètre à calculer :
     *                 "Q", "Z1", "Z2" ou "n.X" avec "n" l'index de l'ouvrage et "X" son paramètre
     * @param rInit Valeur initiale
     * @param rPrec Précision attendue
     */
    public Calc(sVarCalc: string, rInit?: number, rPrec: number = 0.001): Result {
        let res: Result;
        switch (sVarCalc) {
            case "Z1":
            case "Z2":
            case "Q":
                res = super.Calc(sVarCalc, rInit, rPrec);
                if (res.ok) {
                    this.getParameter(sVarCalc).setValue(res.vCalc);
                }
                break;
            default:
                // Pour les caractéristiques des ouvrages
                const sVC = this.getStructureVarCalc(sVarCalc);
                res = this.CalcStructPrm(sVC, rInit, rPrec);
                if (res.ok) {
                    this.structures[sVC.index].getParameter(sVC.prm).setValue(res.vCalc);
                }
        }
        if (res.ok) {
            // Recalcul du débit total pour récupérer les résultats des ouvrages dans les résultats complémentaires
            const resQtot: Result = this.CalcQ();
            for (const extraResKey in resQtot.extraResults) {
                if (resQtot.extraResults.hasOwnProperty(extraResKey)) {
                    res.resultElement.addExtraResult(extraResKey, resQtot.extraResults[extraResKey]);
                }
            }
        }
        this._result = res;
        return res;
    }

    /**
     * paramétrage de la calculabilité des paramètres
     */
    protected setParametersCalculability() {
        this.prms.Q.calculability = ParamCalculability.EQUATION;
        this.prms.Z1.calculability = ParamCalculability.DICHO;
        this.prms.Z2.calculability = ParamCalculability.DICHO;
    }

    /**
     * Mise à jour de Z1, Z2, h1 et h2 pour tous les ouvrages
     */
    private updateStructuresH1H2() {
        for (const structure of this.structures) {
            structure.prms.Z1.v = this.prms.Z1.v;
            structure.prms.Z2.v = this.prms.Z2.v;
            structure.prms.update_h1h2();
        }
    }

    /**
     * Renvoie le n° de structure et le paramètre à calculer
     * @param sVarCalc Nom du paramètre à calculer : "n.X" avec "n" l'index de l'ouvrage et "X" son paramètre
     */
    private getStructureVarCalc(sVarCalc: string): IStructureVarCalc {
        let sIndex: string;
        let sPrm: string;
        if (sVarCalc.indexOf(".") == -1)
            throw new Error(`getStructureVarCalc() : erreur d'analyse de ${sVarCalc}, (pas de la forme n.X)`);
        [sIndex, sPrm] = sVarCalc.split(".");
        const i = parseInt(sIndex, 10);
        if (i === NaN)
            throw new Error(`getStructureVarCalc() : erreur d'analyse de ${sVarCalc} (${sIndex} n'est pas un nombre)`);
        return { index: i, prm: sPrm };
    }

    /**
     * Renvoie le n° de structure et le paramètre à calculer
     * @param sVarCalc Nom du paramètre à calculer : "ouvrage[n].X" avec "n" l'index de l'ouvrage et "X" son paramètre
     */
    private getStructureVarCalc2(sVarCalc: string): IStructureVarCalc {
        const re = /([A-Z,a-z]+)\[(\d+)\]\.(.+)/;
        const match = re.exec(sVarCalc);
        if (match === null || match[1] != "ouvrage")
            throw new Error(`getStructureVarCalc2() : erreur d'analyse de ${sVarCalc}, (pas de la forme ouvrage[n].X)`);

        return { index: +match[2], prm: match[3] };
    }

    /**
     * Calcul du paramètre d'un des ouvrages en parallèle
     * @param sVC Index de l'ouvrage et paramètre à calculer
     * @param rInit Valeur initiale
     * @param rPrec Précision attendue
     */
    private CalcStructPrm(sVC: IStructureVarCalc, rInit?: number, rPrec: number = 0.001): Result {
        // Le débit restant sur la structure en calcul est :
        this.structures[sVC.index].prms.Q.setValue(this.prms.Q.v - this.CalcQ(sVC.index).vCalc);

        // Calcul du paramètre de la structure en calcul
        return this.structures[sVC.index].Calc(sVC.prm, rInit, rPrec);
    }

    // interface IReferencedNub

    public getReferencedParamValues(desc: string): ParamValues {
        try {
            // analyse ouvrage[n].X
            const i: IStructureVarCalc = this.getStructureVarCalc2(desc);
            return this.structures[i.index].getParameter(i.prm).paramValues;
        }
        catch (e) {
            // pas de la forme ouvrage[n].X ou erreur sur n ou X
            const param = this.getParameter(desc);
            if (param === undefined)
                return undefined;
            return param.paramValues;
        }
    }

    public getReferencedExtraResult(desc: string): any {
        return this._result.getExtraResult(desc);
    }

    /**
 * liste des valeurs (paramètre, résultat, résultat complémentaire) liables à un paramètre
 * @param src objet qui sert de clé de recherche des paramètres liables, de type INamedObject | string
 * @returns tableau d'objets de la forme { "name":string, "value":NamedIterableValues, "nub":Nub}, nub=Nub d'origine de la "value"
 */
    public getLinkableValues(src: any): any[] {
        let res = super.getLinkableValues(src);

        let i = 0;
        for (const s of this.structures) {
            const l = s.getLinkableValues(src, `${i}.`);
            res = res.concat(l);
            i++;
        }
        return res;
    }
}