import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, Inject, forwardRef, AfterContentInit } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";

import screenfull from "screenfull";

import {
    PbBassin, PbCloison, Observer, IObservable, MermaidUtil
} from "jalhyd";

import mermaid from "mermaid";

import { HotkeysService, Hotkey } from "angular2-hotkeys";

import { I18nService } from "../../services/internationalisation.service";
import { PbSchema } from "../../formulaire/elements/pb-schema";
import { DialogNewPbCloisonComponent } from "../dialog-new-pb-cloison/dialog-new-pb-cloison.component";
import { GenericCalculatorComponent } from "../generic-calculator/calculator.component";
import { FormulairePrebarrage } from "../../formulaire/definition/form-prebarrage";
import { AppComponent } from "../../app.component";

import { fv } from "app/util";
import { ServiceFactory } from "app/services/service-factory";
import { DefinedBoolean } from "app/definedvalue/definedboolean";
import { PrebarrageService } from "app/services/prebarrage.service";

/**
 * The interactive schema for calculator type "PreBarrage" (component)
 */
@Component({
    selector: "pb-schema",
    templateUrl: "./pb-schema.component.html",
    styleUrls: [
        "./pb-schema.component.scss"
    ]
})
export class PbSchemaComponent implements AfterViewInit, AfterContentInit, OnInit, Observer {

    @Input()
    private pbSchema: PbSchema;

    @ViewChild("schema", { static: true })
    public schema: any;

    /** handle on SVG container */
    private nativeElement: any;

    /** flag de validité du composant */
    private _isValid: DefinedBoolean;

    /** événément de changement de validité */
    @Output()
    private validChange = new EventEmitter();

    /** événément de sélection d'un nœud du graphique Mermaid */
    @Output()
    private nodeSelected = new EventEmitter();

    /** Latest clicked item: a PbCloison, a PbBassin or undefined if river "Upstream" or "Downstream" was clicked */
    private _selectedItem: PbCloison | PbBassin;

    /** Records existing walls as they are built, to detect if multiple walls connect the same pair of basins */
    private existingWalls: { [key: string]: number };

    public constructor(
        @Inject(forwardRef(() => GenericCalculatorComponent)) private calculatorComponent: GenericCalculatorComponent,
        private i18nService: I18nService,
        private hotkeysService: HotkeysService,
        private newPbCloisonDialog: MatDialog,
        private predamService: PrebarrageService
    ) {
        this.hotkeysService.add(new Hotkey("del", AppComponent.onHotkey(this.removeOnHotkey, this)));
        this._isValid = new DefinedBoolean();
    }

    /** tracks the fullscreen state */
    public get isFullscreen() {
        if (screenfull.isEnabled) {
            return screenfull.isFullscreen;
        }
    }

    public async setFullscreen(element): Promise<void> {
        if (screenfull.isEnabled) {
            await screenfull.request(element);
            this.fullscreenChange(true);
        }
    }

    public async exitFullscreen(): Promise<void> {
        if (screenfull.isEnabled) {
            await screenfull.exit();
            this.fullscreenChange(false);
        }
    }

    /** called when fullscreen state changes */
    public fullscreenChange(isFullscreen: boolean) { }

    public ngAfterContentInit(): void {
        mermaid.initialize({
            flowchart: {
                curve: "basis",
                // useMaxWidth: true
            }
        });
        this.nativeElement = this.schema.nativeElement;
        this.render();
        // restore previously selected item
        this._selectedItem = this.pbSchema.form.selectedItem;
        if (this._selectedItem !== undefined) {
            // @WARNING clodo timeout to prevent ExpressionChangedAfterItHasBeenCheckedError
            // and select schema node after schema is refreshed by ngAfterViewInit()
            setTimeout(() => {
                this.selectNodeOnSchema(this._selectedItem);
            }, 20); // timeout has to be greater than the 10ms of ngAfterViewInit()
        }
        else {
            // select upstream basin since it's form is already displayed (and not undisplayable...)
            setTimeout(() => {
                this.selectUpstreamBasin();
            }, 20); // timeout has to be greater than the 10ms of ngAfterViewInit()
        }
    }

    private render() {
        this.nativeElement.innerHTML = ""; // or diagram goes blank when refreshing…
        // generate graph description
        const graphDefinition = this.graphDefinition();
        // draw
        try {
            mermaid.render("graphDiv", graphDefinition, (svgCode, bindFunctions) => {
                this.nativeElement.innerHTML = svgCode;
            });
        } catch (e) {
            console.error(e);
        }
        this.highlightErrorItems(null);
    }

    /**
     * Builds the interactive schema from the PreBarrage model
     */
    private refresh() {
        this.render();
        this.refreshEventListeners();
        this.updateValidity();
    }

    public ngAfterViewInit(): void {
        // subscribe to "refresh" event passed indirectly by FormulairePbCloison (change upstream/downstream basin)
        this.pbSchema.addObserver(this);
        // @WARNING clodo trick to prevent blank diagram when switching from a PreBarrage to another
        setTimeout(() => {
            this.refresh();
        }, 10);
    }

    /** Add click listener on every node and link in the graph */
    private refreshEventListeners() {
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            item.style.cursor = "pointer";
            item.addEventListener("click", () => {
                this.selectNode(item);
            });
        });
    }

    /**
     * Builds a Mermaid graph text definition, using Nodes
     * to represent basins as well as walls; sorts connexions
     * to prevent lines crossings
     */
    private graphDefinition() {
        this.existingWalls = {};
        this.pbSchema.wallsSuffixes = {};
        const def: string[] = [ "graph TB" ];

        const pbModel = this.predamService.model;

        // river upstream / downstream
        let upstreamLabel = this.i18nService.localizeText("INFO_LIB_AMONT");
        let downstreamLabel = this.i18nService.localizeText("INFO_LIB_AVAL");
        // add result data Z and Q, if any
        if (
            pbModel.result?.resultElements
            && pbModel.result.resultElements[0]?.ok
        ) {
            // when a parameter is variating, index of the variating parameter
            // values to build the data from
            const form = this.calculatorComponent.formulaire as FormulairePrebarrage;
            const idx = form.pbResults.variableIndex;
            const qValue = pbModel.prms.Q.isCalculated
                ? pbModel.result.resultElements[idx].vCalc
                : (
                    pbModel.prms.Q.hasMultipleValues
                        ? pbModel.prms.Q.getInferredValuesList(pbModel.variatingLength())[idx]
                        : pbModel.prms.Q.singleValue
                );
            // upstream
            upstreamLabel += "<br>";
            upstreamLabel += "Q = " + fv(qValue);
            upstreamLabel += "<br>";
            upstreamLabel += "Z = " + fv(
                pbModel.prms.Z1.isCalculated
                    ? pbModel.result.resultElements[idx].vCalc
                    : (
                        pbModel.prms.Z1.hasMultipleValues
                            ? pbModel.prms.Z1.getInferredValuesList(pbModel.variatingLength())[idx]
                            : pbModel.prms.Z1.singleValue
                    )
            );
            // downstream
            downstreamLabel += "<br>";
            downstreamLabel += "Q = " + fv(qValue);
            downstreamLabel += "<br>";
            downstreamLabel += "Z = " + fv(
                pbModel.prms.Z2.hasMultipleValues
                    ? pbModel.prms.Z2.getInferredValuesList(pbModel.variatingLength())[idx]
                    : pbModel.prms.Z2.singleValue
            );
        }
        // add to graph definition
        def.push(`${this.predamService.upstreamId}("${upstreamLabel}")`);
        def.push(`${this.predamService.downstreamId}("${downstreamLabel}")`);

        // styles
        def.push("classDef wall fill:#e8e8e8,stroke-width:0;");
        def.push("classDef basin fill:#e0f3fb,stroke:#003A80;"); // irstea-ocean 50 / 500
        def.push("classDef basin::first-line color:green,font-size:0.5em;");
        def.push("classDef node-highlighted fill:#4DBBE9;"); // irstea-ocean (material "accent"), 300
        def.push("classDef node-error fill:#ec7430;"); // irstea-rouille (material "accent"), 400
        def.push("classDef node-highlighted-error fill:#d92f03;"); // irstea-rouille (material "accent"), 900

        const sortedWalls: PbCloison[] = [];
        for (const c of pbModel.children) {
            if (c instanceof PbBassin) {
                def.push(`${c.uid}("${this.itemDescriptionWithResultData(c)}")`); // rounded edges
                def.push(`class ${c.uid} basin;`);
            } else if (c instanceof PbCloison) {
                // store, to draw later
                sortedWalls.push(c);
            }
        }

        // sort then draw walls
        sortedWalls.sort(this.triCloisonsGaucheDroite);
        for (const c of sortedWalls) {
            const upstreamBasinId = c.bassinAmont === undefined ? this.predamService.upstreamId : c.bassinAmont.uid;
            const downstreamBasinId = c.bassinAval === undefined ? this.predamService.downstreamId : c.bassinAval.uid;
            // record this wall
            const basinsPair = upstreamBasinId + "-" + downstreamBasinId;
            if (! (basinsPair in this.existingWalls)) {
                this.existingWalls[basinsPair] = 0;
            }
            // affect suffix if needed
            if (this.existingWalls[basinsPair] > 0) {
                this.pbSchema.wallsSuffixes[c.uid] = this.existingWalls[basinsPair];
            }
            this.existingWalls[basinsPair]++;
            // draw wall Node
            def.push(`${c.uid}["${this.itemDescriptionWithResultData(c)}"]`); // square edges
            def.push(`class ${c.uid} wall;`);
            // draw "arrow" with 2 lines
            def.push(`${upstreamBasinId}---${c.uid}-->${downstreamBasinId}`);
        }

        return def.join("\n");
    }

    /** gauche d'abord, droite ensuite */
    private triCloisonsGaucheDroite(a: PbCloison, b: PbCloison) {
        // ultra-gauchistes
        if (a.bassinAmont === undefined && a.bassinAval === undefined) {
            return -1;
        }
        if (b.bassinAmont === undefined && b.bassinAval === undefined) {
            return 1;
        }
        // si A est un super-gauchiste
        if (a.bassinAmont === undefined || a.bassinAval === undefined) {
            // B est-il aussi un super-gauchiste ?
            if (b.bassinAmont === undefined || b.bassinAval === undefined) {
                // comparer le bassin restant
                const bassinA = (a.bassinAmont === undefined ? a.bassinAval : a.bassinAmont);
                const bassinB = (b.bassinAmont === undefined ? b.bassinAval : b.bassinAmont);
                return (bassinA.findPositionInParent() <= bassinB.findPositionInParent()) ? -1 : 1;
            }
            // sinon A gagne
            return -1;
        }
        // si B est un super-gauchiste
        if (b.bassinAmont === undefined || b.bassinAval === undefined) {
            // B gagne (le cas de A super-gauchiste est éliminé avant)
            return 1;
        }
        // sinon, aucun des deux n'est super-gauchiste, comparaison des bassins amont et aval
        const sommeA = a.bassinAmont.findPositionInParent() + a.bassinAval.findPositionInParent();
        const sommeB = b.bassinAmont.findPositionInParent() + b.bassinAval.findPositionInParent();
        return (sommeA <= sommeB ? -1 : 1);
    }

    /**
     * @param item DOM element
     */
    private selectNode(item: any) {
        // console.debug(`PbSchemaComponent.selectNode(${item?.id})`);
        // highlight clicked element
        this.clearHighlightedItems();
        item.classList.add("node-highlighted");
        // find what was clicked
        this._selectedItem = this.predamService.findFromItemId(item.id);
        this.highlightErrorItems(item.id);
        // show proper form and hide results
        this.nodeSelected.emit({
            node: this._selectedItem === this.predamService.upstreamBassin ? undefined : this._selectedItem
        });
        // exit fullscreen
        this.exitFullscreen();
    }

    // for debug only
    public get graphDef(): string {
        return this.graphDefinition();
    }

    public get title(): string {
        return this.i18nService.localizeText("INFO_PB_SCHEMA");
    }

    /** Global Pb validity */
    public get isValid() {
        return this._isValid.value;
    }

    /** used for a cosmetics CSS trick only (mat-card-header right margin) */
    public get showInputData(): boolean {
        return this.calculatorComponent.showPBInputData;
    }

    public get prefixedItemDescription(): string {
        let desc = this.itemDescription(this._selectedItem);
        if (this._selectedItem instanceof PbCloison) {
            desc = this.i18nService.localizeText("INFO_PB_CLOISON") + " " + desc;
        }
        if (desc !== "") {
            desc += " : ";
        }
        return desc;
    }

    /**
     * Lorsque la passe est calculée, ajoute aux nœuds du schéma les valeurs de :
     *  - PV et YMOY pour les bassins
     *  - DH et Q pour les cloisons
     */
    private itemDescriptionWithResultData(item: PbCloison | PbBassin): string {
        let iDesc: string;
        if (item !== undefined) {
            iDesc = this.itemDescription(item);
            if (
                item?.result?.resultElements
                && item.result.resultElements[0]?.ok
            ) {
                // when a parameter is variating, index of the variating parameter
                // values to build the data from
                const form = this.calculatorComponent.formulaire as FormulairePrebarrage;
                const idx = form.pbResults.variableIndex;
                iDesc += "<br>";
                if (item instanceof PbCloison) {
                    iDesc += "Q = " + fv(item.result.resultElements[idx].vCalc); // Q is always the vCalc of PbCloison
                    iDesc += "<br>";
                    iDesc += "DH = " + fv(item.result.resultElements[idx].values.DH);
                } else if (item instanceof PbBassin) {
                    iDesc += "PV = " + fv(item.result.resultElements[idx].values.PV);
                    iDesc += "<br>";
                    iDesc += "YMOY = " + fv(item.result.resultElements[idx].values.YMOY);
                }
            }
        }
        return iDesc;
    }

    /** Returns a short description of the given item: wall or basin */
    private itemDescription(item: PbCloison | PbBassin): string {
        let desc = "";
        if (item !== undefined) {
            if (item.description !== undefined) {
                desc = this.i18nService.localizeMessage(item.description);
            }
            if (item instanceof PbCloison) {
                // there might be multiple walls between the same pair of basins
                if (item.uid in this.pbSchema.wallsSuffixes) {
                    desc += " (" + this.pbSchema.wallsSuffixes[item.uid] + ")";
                }
            }
        }
        return desc;
    }

    /**
     * Selects and highlights on the schema the given wall or basin
     */
    private selectNodeOnSchema(element: PbBassin | PbCloison) {
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            if (element !== undefined && MermaidUtil.isMermaidEqualIds(element.uid, item.id)) {
                this.selectNode(item);
            }
        });
    }

    /**
     * select upstream basin on schema
     */
    private selectUpstreamBasin() {
        let done = false; // optimisation : simulate break in forEach
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            if (!done) {
                if (MermaidUtil.isMermaidEqualIds("amont", item.id)) {
                    this.selectNode(item);
                    this._selectedItem = this.predamService.upstreamBassin;
                    done = true;
                }
            }
        });
    }

    // at this time @Input data is supposed to be already populated
    public ngOnInit() {
        this.predamService.model = this.pbSchema.pb;
    }

    public get enableAddItems(): boolean {
        return this.calculatorComponent.showPBInputData;
    }

    public get enableRemoveButton() {
        if (this._selectedItem === this.predamService.upstreamBassin) {
            return false;
        }
        // if deleting a PbCloison would replace it by a new one at
        // the same place (@see onRemoveClick), make it not deletable
        if (this._selectedItem instanceof PbCloison) {
            if ((
                this._selectedItem.bassinAmont !== undefined
                && this._selectedItem.bassinAmont.cloisonsAval.length === 1
                && this._selectedItem.bassinAval === undefined
            ) || (
                this._selectedItem.bassinAval !== undefined
                && this._selectedItem.bassinAval.cloisonsAmont.length === 1
                && this._selectedItem.bassinAmont === undefined
            )) {
                return false;
            }
        }
        return true;
    }

    /** Removes a basin or wall, and all related items */
    public onRemoveClick() {
        this.predamService.deleteSelected(this._selectedItem, ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
        this.clearResults();
        this.unselect();
        this.refreshWithSelection();
        this.calculatorComponent.showPBInputData = true;
    }

    public get uitextRemove() {
        return this.i18nService.localizeText("INFO_FIELDSET_REMOVE");
    }

    // listener for "del" hotkey
    protected removeOnHotkey() {
        if (this.enableRemoveButton) {
            this.onRemoveClick();
        }
    }

    public get enableCopyButton() {
        return (this._selectedItem !== undefined && this._selectedItem instanceof PbCloison);
    }

    /** Copies a wall */
    public onCopyClick() {
        const wallCopy: PbCloison = this.predamService.copyWall(this._selectedItem as PbCloison, ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
        this.clearResults();
        this.refreshWithSelection(wallCopy.uid);
        this.calculatorComponent.showPBInputData = true;
    }

    public get uitextCopy() {
        return this.i18nService.localizeText("INFO_FIELDSET_COPY");
    }

    /** Adds a new lone basin */
    public onAddBasinClick() {
        const newBasin = this.predamService.addBasin(ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
        this.clearResults();
        this.refreshWithSelection(newBasin.uid);
        this.calculatorComponent.showPBInputData = true;
    }

    public get uitextAddBasin() {
        return this.i18nService.localizeText("INFO_PB_ADD_BASIN");
    }

    public get enableAddWallButton(): boolean {
        return this.predamService.hasBasins;
    }

    /** Adds a new lone wall, opening a modal to choose connected basins */
    public onAddWallClick() {
        // open dialog
        const dialogRef = this.newPbCloisonDialog.open(
            DialogNewPbCloisonComponent,
            {
                data: {
                    basins: this.predamService.bassins
                },
                disableClose: true
            }
        );
        // apply modifications
        dialogRef.afterClosed().subscribe(result => {
            if (result.up !== undefined && result.down !== undefined) {
                const wall = this.predamService.addWall(result.up, result.down, ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
                this.clearResults();
                this.refreshWithSelection(wall.uid);
                this.calculatorComponent.showPBInputData = true;
            }
        });
    }

    public get uitextAddWall() {
        return this.i18nService.localizeText("INFO_PB_ADD_WALL");
    }

    public get enableUpButton() {
        return (
            this._selectedItem instanceof PbBassin
            && this.predamService.findBasinPosition(this._selectedItem.uid) !== 0
            && this.predamService.isStandaloneBasin(this._selectedItem)
        );
    }

    public onMoveBasinUpClick() {
        if (this._selectedItem instanceof PbBassin) {
            this.predamService.moveBasinUp(this._selectedItem.uid);
        }
        this.clearResults();
        this.refreshWithSelection(this._selectedItem.uid);
        this.calculatorComponent.showPBInputData = true;
    }

    public get uitextMoveBasinUp() {
        return this.i18nService.localizeText("INFO_PB_MOVE_BASIN_UP");
    }

    public get enableDownButton() {
        return (
            this._selectedItem instanceof PbBassin
            && !this.predamService.isLastBasin(this._selectedItem.uid)
            && this.predamService.isStandaloneBasin(this._selectedItem)
        );
    }

    public onMoveBasinDownClick() {
        if (this._selectedItem instanceof PbBassin) {
            this.predamService.moveBasinDown(this._selectedItem.uid);
        }
        this.clearResults();
        this.refreshWithSelection(this._selectedItem.uid);
        this.calculatorComponent.showPBInputData = true;
    }

    public get uitextMoveBasinDown() {
        return this.i18nService.localizeText("INFO_PB_MOVE_BASIN_DOWN");
    }

    public get uitextEnterFSTitle() {
        return this.i18nService.localizeText("INFO_CHART_BUTTON_TITLE_ENTER_FS");
    }

    public get uitextExitFSTitle() {
        return this.i18nService.localizeText("INFO_CHART_BUTTON_TITLE_EXIT_FS");
    }

    public get uitextExportImageTitle() {
        return this.i18nService.localizeText("INFO_CHART_BUTTON_TITLE_EXPORT_IMAGE");
    }

    // @see https://levelup.gitconnected.com/draw-an-svg-to-canvas-and-download-it-as-image-in-javascript-f7f7713cf81f
    public exportAsImage(element: HTMLDivElement) {
        const svgElement = element.querySelector("svg");
        const { width, height } = svgElement.getBBox();
        const clonedSvgElement = svgElement.cloneNode(true) as Element;
        const outerHTML = clonedSvgElement.outerHTML;
        // create BLOB URL for SVG image
        // add </br> for XML validity; Mermaid removes the </br> if they are added in schema description…
        const blob = new Blob([outerHTML.replace(/<br>/g, "<br></br>") ], { type: "image/svg+xml;charset=utf-8" });
        const blobURL = window.URL.createObjectURL(blob);
        // draw image to canvas
        const image = new Image();
        image.onload = () => {
            const canvas = document.createElement("canvas");
            canvas.width = width;
            canvas.height = height;
            const context = canvas.getContext("2d");
            context.drawImage(image, 0, 0, width, height);
            // export canvas the usual way
            AppComponent.exportAsImage(canvas);
        };
        image.src = blobURL;
    }

    /**
     * Computes the global Pab validity : validity of every cell of every row
     */
    private updateValidity() {
        // check that at least 1 basin is present and a route from river
        // upstream to river downstream exists (2nd check includes 1st)
        this._isValid.value = this.predamService.isValid();

        if (this._isValid.changed) {
            this.validChange.emit();
        }
    }

    /**
     * update all items validity rendering
     */
    public updateItemsValidity() {
        this.highlightErrorItems(this._selectedItem?.uid);
    }

    private clearHighlightedItems() {
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            item.classList.remove("node-highlighted");
        });
    }

    private highlightErrorItems(selectedUid: string) {
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            item.classList.remove("node-error");
            item.classList.remove("node-highlighted-error");
        });
        const invalidUids: string[] = this.pbSchema.form.checkParameters();
        selectedUid = this.predamService.toNubUid(selectedUid);
        if (invalidUids.length > 0) {
            this.nativeElement.querySelectorAll("g.node").forEach(item => {
                // in this case, item is a HTML node of the SVG schema which id is a nub uid
                const itemId = this.predamService.toNubUid(item.id);

                if (invalidUids.includes(itemId)) {  // if current item is among invalid ones
                    if (selectedUid === itemId) { // if current item is the selected item
                        item.classList.add("node-highlighted-error");
                    } else {
                        item.classList.add("node-error");
                    }
                }
            });
        }
    }

    private unselect() {
        // console.debug(`PbSchemaComponent.unselect()`);
        this._selectedItem = undefined;
        this.clearHighlightedItems();
        this.nodeSelected.emit({}); // nothing selected
    }

    /** clear all PB form results whenever the basins / walls layout is changed */
    private clearResults() {
        this.pbSchema.form.reset();
    }

    /**
     * Refreshes the schema; if uid is given, selects the node having this
     * nub uid, else keeps previous selection
     */
    private refreshWithSelection(uid?: string) {
        // console.debug(`PbSchemaComponent.refreshWithSelection(${uid})`);
        // remember previously selected node
        const selectedNodeUID = this._selectedItem?.uid;
        this.refresh();
        // select a specific node on the schema
        if (uid !== undefined) {
            this.selectNodeOnSchema(this.predamService.findChild(uid));
        } else if (selectedNodeUID !== undefined) {
            // re-select previously selected node
            this.selectNodeOnSchema(this.predamService.findChild(selectedNodeUID));
        }
    }

    // interface Observer

    public update(sender: IObservable, data: any) {
        // console.debug(`PbSchemaComponent.update:`, data);
        if (sender instanceof PbSchema) {
            if (data.action === "refresh") {
                this.refreshWithSelection(data.value);
            }
        }
    }

}