import {addTupleType, deepCopy, Tuple} from "@synerty/vortexjs";
import {diagramTuplePrefix} from "../PluginNames";
import SerialiseUtil from "@synerty/vortexjs/src/vortex/SerialiseUtil";
import * as moment from "moment";
import {BranchLiveEditTuple} from "./BranchLiveEditTuple";
import {PrivateDiagramLookupService} from "../services/PrivateDiagramLookupService";


let serUril = new SerialiseUtil();

interface UndoDataI {
    disps: any[];
    anchors: any[];
    updatedByUser: string;
    lastStage: number;
    needsSave: boolean;
    replacementIds: {};
}

export interface BranchLastEditPositionI {
    x: number;
    y: number;
    zoom: number;
    coordSetKey: string
}

/** Diagram Branch Tuple
 *
 * This tuple is used internally to transfer branches from the client tuple provider,
 * and transfer branches to the client wrapped in a TupleAction.
 *
 */
@addTupleType
export class BranchTuple extends Tuple {
    public static readonly tupleName = diagramTuplePrefix + "BranchTuple";

    protected _rawJonableFields = ['packedJson__'];

    // The list of deltas for this branch
    packedJson__: any[] = [];

    private static readonly __ID_NUM = 0;
    private static readonly __COORD_SET_ID_NUM = 1;
    private static readonly __KEY_NUM = 2;
    private static readonly __VISIBLE_NUM = 3;
    private static readonly __UPDATED_DATE = 4;
    private static readonly __CREATED_DATE = 5;
    private static readonly __DISPS_NUM = 6;
    private static readonly __ANCHOR_DISP_KEYS_NUM = 7;
    private static readonly __UPDATED_BY_USER_NUM = 8;
    private static readonly __NEEDS_SAVE_NUM = 9; // Not stored in DB
    private static readonly __LAST_EDIT_POSITION = 10;
    private static readonly __LAST_INDEX_NUM = 10;


    // This structure stores IDs that need to be updated, from disps that have been
    // cloned from disps their replacing (their ID is re-assigned)
    // This is only a temporary structure only needed during an editing session.
    private _replacementIds = {};
    private _dispsById = {};
    private _lastStage = 1;
    private _contextUpdateCallback: ((modelUpdateRequired: boolean) => void) | null;

    // Undo / Redo
    private undoQueue: string[] = [];
    private redoQueue: string[] = [];
    private readonly MAX_UNDO = 20;


    constructor() {
        super(BranchTuple.tupleName);
        for (let i = this.packedJson__.length; i < BranchTuple.__LAST_INDEX_NUM + 1; i++)
            this.packedJson__.push(null);
    }

    static createBranch(coordSetId: number, branchKey: string): any {
        let branch = new BranchTuple();
        branch.packedJson__[BranchTuple.__ID_NUM] = this._makeUniqueId();
        branch.packedJson__[BranchTuple.__COORD_SET_ID_NUM] = coordSetId;
        branch.packedJson__[BranchTuple.__KEY_NUM] = branchKey;
        branch.packedJson__[BranchTuple.__VISIBLE_NUM] = true;
        branch.setUpdatedDate(null);
        branch.setCreatedDate(new Date());
        branch.packedJson__[BranchTuple.__DISPS_NUM] = [];
        branch.packedJson__[BranchTuple.__ANCHOR_DISP_KEYS_NUM] = [];
        branch.packedJson__[BranchTuple.__UPDATED_BY_USER_NUM] = null;
        branch.packedJson__[BranchTuple.__NEEDS_SAVE_NUM] = false;
        branch.resetUndo();
        return branch;
    }

    static unpackJson(packedJsonStr: string): BranchTuple {
        // Create the new object
        let newSelf = new BranchTuple();
        newSelf.packedJson__ = JSON.parse(packedJsonStr);
        newSelf.assignIdsToDisps();
        newSelf.sortDisps();
        newSelf.resetUndo();
        return newSelf;
    }

    private assignIdsToDisps(): void {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let array = this._array(BranchTuple.__DISPS_NUM);
        for (let i = 0; i < array.length; ++i) {
            let disp = array[i];
            BranchTuple._setNewDispId(disp, i);
            this._dispsById[DispBase.id(disp)] = disp;
            if (this._lastStage < DispBase.branchStage(disp))
                this._lastStage = DispBase.branchStage(disp);
        }
    }

    private static _makeUniqueId(index = -1): any {
        return <any>`NEW_${(new Date()).getTime()}_${index}`;
    }

    private static _setNewDispId(disp, index): void {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let newId = this._makeUniqueId(index);

        if (DispBase.id(disp) == null)
            DispBase.setId(disp, newId);

        if (DispBase.hashId(disp) == null)
            DispBase.setHashId(disp, newId);
    }

    private _array(num: number): any[] {
        if (this.packedJson__[num] == null)
            this.packedJson__[num] = [];
        return this.packedJson__[num];
    }

    get needsSave(): boolean {
        return this.packedJson__[BranchTuple.__NEEDS_SAVE_NUM];
    }

    get isUndoPossible(): boolean {
        return this.undoQueue.length > 1;
    }

    get isRedoPossible(): boolean {
        return this.redoQueue.length != 0
    }

    private resetUndo(): void {
        this.undoQueue = [this.serialiseDisps()];
    }

    private resetRedo(): void {
        this.redoQueue = [];
    }

    branchHasBeenSaved(): void {
        this.packedJson__[BranchTuple.__NEEDS_SAVE_NUM] = false;
    }

    private setNeedsSave(): void {
        this.packedJson__[BranchTuple.__NEEDS_SAVE_NUM] = true;
    }

    get id(): number {
        return this.packedJson__[BranchTuple.__ID_NUM];
    }

    get coordSetId(): number {
        return this.packedJson__[BranchTuple.__COORD_SET_ID_NUM];
    }

    get key(): string {
        return this.packedJson__[BranchTuple.__KEY_NUM];
    }

    get updatedByUser(): string {
        return this.packedJson__[BranchTuple.__UPDATED_BY_USER_NUM];
    }

    set updatedByUser(val: string) {
        this.packedJson__[BranchTuple.__UPDATED_BY_USER_NUM] = val;
    }

    set lastEditPosition(position: BranchLastEditPositionI) {
        this.packedJson__[BranchTuple.__LAST_EDIT_POSITION] = JSON.stringify(position);
    }

    get lastEditPosition(): BranchLastEditPositionI | null {
        const jsonStr = this.packedJson__[BranchTuple.__LAST_EDIT_POSITION];
        if (jsonStr == null || jsonStr.length == 0)
            return null;
        return JSON.parse(jsonStr);
    }

    addStage(): number {
        return this._lastStage++;
    }

    /** Add or Update Disps
     *
     * This method adds an array of disps to the branch.
     *
     * @param disps: An array of disps to add.
     * @param preventModelUpdate: Prevents this update from calling model compile.
     */
    addOrUpdateDisps(disps: any, preventModelUpdate = false): any[] {
        let modelUpdateRequired = false;
        let returnedDisps = [];

        for (let disp of disps) {
            let result = this.addOrUpdateSingleDisp(disp);
            modelUpdateRequired = modelUpdateRequired || result.modelUpdateRequired;
            returnedDisps.push(result.disp);
        }

        for (let disp of returnedDisps)
            this.updateReplacedIds(disp);

        this.sortDisps();
        this.touchUpdateDate(modelUpdateRequired && !preventModelUpdate);
        return returnedDisps;
    }

    /** Add or Update Disp
     *
     * This method adds the disp to the branch if it's new.
     *
     * If the disp is existing, but in a different stage, it clones and returns the disp
     * in the new stage.
     *
     * @param disp
     * @param preventModelUpdate: Prevents this update from calling model compile.
     */
    addOrUpdateDisp(disp: any, preventModelUpdate = false): any {
        let result = this.addOrUpdateSingleDisp(disp);
        this.updateReplacedIds(result);
        this.sortDisps();
        this.touchUpdateDate(result.modelUpdateRequired && !preventModelUpdate);
        return result.disp;
    }

    isDispInBranch(disp: any): boolean {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        return this._dispsById[DispBase.id(disp)] != null
            || this._replacementIds[DispBase.id(disp)] != null;
    }

    private updateReplacedIds(disp) {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let newGroupId = this._replacementIds[DispBase.groupId(disp)];
        if (newGroupId != null)
            DispBase.setGroupId(disp, newGroupId);
    }

    private addOrUpdateSingleDisp(disp: any): { disp: any, modelUpdateRequired: boolean } {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let DispGroupPointer = require("peek_plugin_diagram/canvas-shapes/DispGroupPointer")["DispGroupPointer"];
        let array = this._array(BranchTuple.__DISPS_NUM);

        // If we've already replaced this Disp, then just return the replacement disp
        // This can happen when the select delegates call addOrUpdate disp multiple
        // times. (They have a selected and related array)
        if (this._replacementIds[DispBase.id(disp)] != null) {
            let newId = this._replacementIds[DispBase.id(disp)];
            return {disp: this._dispsById[newId], modelUpdateRequired: false};
        }

        // Find out if the disp is in this branch
        let dispInBranch = this._dispsById[DispBase.id(disp)];

        // If the Disp has an ID and it's not in this branch, then:
        // 1) we make a copy of the disp
        // 2) we set its "replacesHashId" to the "hashId" it's replacing.
        if (DispBase.id(disp) != null && dispInBranch == null) {
            let branchDisp = DispBase.cloneDisp(disp);

            DispBase.setReplacesHashId(branchDisp, DispBase.hashId(disp));
            DispGroupPointer.setTargetGroupId(branchDisp, null);
            DispBase.setHashId(branchDisp, null);

            // Deal with the new updated ID
            DispBase.setId(branchDisp, null);
            BranchTuple._setNewDispId(branchDisp, array.length);
            this._replacementIds[DispBase.id(disp)] = DispBase.id(branchDisp);

            disp = branchDisp;
        }

        BranchTuple._setNewDispId(disp, array.length);

        if (dispInBranch == null) {
            DispBase.setBranchStage(disp, this._lastStage);
            this._dispsById[DispBase.id(disp)] = disp;
            array.push(disp);
            return {disp: disp, modelUpdateRequired: true};
        }

        if (DispBase.branchStage(dispInBranch) == DispBase.branchStage(disp)) {
            return {disp: disp, modelUpdateRequired: false};
        }

        // Else, it's a new stage, clone the disp.

        this._dispsById[DispBase.id(disp)] = disp;
        array.push(disp);
        return {disp: disp, modelUpdateRequired: true};

    }

    private sortDisps(): void {
        const sortDisps = require("peek_plugin_diagram/canvas/PeekCanvasModelUtil.web")
            ["sortDisps"];

        let array = this._array(BranchTuple.__DISPS_NUM);
        this.packedJson__[BranchTuple.__DISPS_NUM] = sortDisps(array);
    }

    /** Add New Disps
     *
     * This method adds newly created disps to tbe branch in bulk.
     *
     * @param disps
     */
    addNewDisps(disps: any[]): void {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let array = this._array(BranchTuple.__DISPS_NUM);

        for (let disp of disps) {
            BranchTuple._setNewDispId(disp, array.length);
            DispBase.setBranchStage(disp, this._lastStage);
            this._dispsById[DispBase.id(disp)] = disp;
            array.push(disp);
        }
        this.sortDisps();
        this.touchUpdateDate(true);

    }

    removeDisps(disps: any[]): void {
        let DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        let DispNull = require("peek_plugin_diagram/canvas-shapes/DispNull")["DispNull"];

        let dispIdsToRemove = {};
        for (let disp of disps) {
            dispIdsToRemove[DispBase.id(disp)] = disp;
        }

        let array = this._array(BranchTuple.__DISPS_NUM);
        let branchDispsToBeConvertedToNullDisps = [];

        // Filter out disps in this branch that we're deleting
        // If the disp we're deleting replacing anoter disp (not in this branch)
        //      make a note of that.
        this.packedJson__[BranchTuple.__DISPS_NUM] = array
            .filter(disp => {
                let id = DispBase.id(disp);
                if (dispIdsToRemove[id] != null) {
                    if (DispBase.replacesHashId(disp) != null)
                        branchDispsToBeConvertedToNullDisps.push(disp);
                    delete dispIdsToRemove[id];
                    return false;
                }
                return true;

            });

        // Update the dispId dict to remove the disps we just filtered out
        this.assignIdsToDisps();

        // For all the Disps that are not part of this branch, we need to add a DispNull
        // to make it's deletion

        let nullDispsToCreate = [];

        function createNullDisp(dispToDelete, replacesHashId) {
            if (dispToDelete.bounds == null)
                throw new Error("Can not delete a disp with no bounds");

            let nullDisp = {
                // Type
                '_tt': DispBase.TYPE_DN,

                // Level
                'le': dispToDelete.le,
                'lel': dispToDelete.lel,

                // Layer
                'la': dispToDelete.la,
                'lal': dispToDelete.lal,
            };

            DispNull.setGeomFromBounds(nullDisp, dispToDelete.bounds);
            DispBase.setReplacesHashId(nullDisp, replacesHashId);
            nullDispsToCreate.push(nullDisp);
        }

        // For all the Disps that are not part of this branch, we need to add a DispNull
        // to make it's deletion
        // Create NULL disps for disps being deleted that are not part of this branch
        for (let dispId of Object.keys(dispIdsToRemove)) {
            let disp = dispIdsToRemove[dispId];
            createNullDisp(disp, DispBase.hashId(disp));
        }

        // For all disps that are part of this branch, but replace other disps,
        // Create NULL disps for disps in this branch that replace other disps
        for (let disp of branchDispsToBeConvertedToNullDisps) {
            createNullDisp(disp, DispBase.replacesHashId(disp));
        }

        if (nullDispsToCreate.length != 0)
            this.addNewDisps(nullDispsToCreate);
        else
            this.touchUpdateDate(false);
    }


    get disps(): any[] {
        return this._array(BranchTuple.__DISPS_NUM).slice();
    }

    addAnchorDispKey(key: string): void {
        let anchors = this._array(BranchTuple.__ANCHOR_DISP_KEYS_NUM);
        if (anchors.filter(k => k == key).length != 0)
            return;

        this.touchUpdateDate(false);
        anchors.push(key);
    }

    get anchorDispKeys(): string[] {
        return this._array(BranchTuple.__ANCHOR_DISP_KEYS_NUM).slice();
    }


    set visible(value: boolean) {
        this.packedJson__[BranchTuple.__VISIBLE_NUM] = value;
    }

    get visible(): boolean {
        return this.packedJson__[BranchTuple.__VISIBLE_NUM];
    }

    get updatedDate(): Date | null {
        let packedDate = this.packedJson__[BranchTuple.__UPDATED_DATE];
        if (packedDate == null)
            return packedDate;

        return serUril.fromStr(packedDate, SerialiseUtil.T_DATETIME);
    }

    private setUpdatedDate(value: Date | null): void {
        if (value == null)
            this.packedJson__[BranchTuple.__UPDATED_DATE] = null;
        else
            this.packedJson__[BranchTuple.__UPDATED_DATE] = serUril.toStr(value);
    }

    get createdDate(): Date {
        return serUril.fromStr(this.packedJson__[BranchTuple.__CREATED_DATE],
            SerialiseUtil.T_DATETIME);
    }

    private setCreatedDate(value: Date): void {
        this.packedJson__[BranchTuple.__CREATED_DATE] = serUril.toStr(value);
    }

    touchUpdateDate(modelUpdateRequired: boolean = false): void {
        this.setNeedsSave();
        this.setUpdatedDate(new Date());
        if (this._contextUpdateCallback != null)
            this._contextUpdateCallback(modelUpdateRequired);
    }

    touchUndo(): void {
        const current = this.serialiseDisps();

        // If there is no change, queue nothing
        const len = this.undoQueue.length;
        if (len != 0 && this.undoQueue[len - 1] == current)
            return;

        this.undoQueue.push(current);
        this.resetRedo();

        while (this.undoQueue.length > this.MAX_UNDO)
            this.undoQueue.shift();
    }

    doUndo(lookupService: PrivateDiagramLookupService): void {
        if (this.undoQueue.length <= 1)
            return;

        const len = this.undoQueue.length;
        this.redoQueue.push(this.undoQueue.pop());
        this.restoreSerialisedDisps(this.undoQueue[len - 2]);

        this.linkDisps(lookupService);
        this.touchUpdateDate(true);
    }

    doRedo(lookupService: PrivateDiagramLookupService): void {
        if (this.redoQueue.length == 0)
            return;

        const redo = this.redoQueue.pop();
        this.undoQueue.push(redo);
        this.restoreSerialisedDisps(redo);

        this.linkDisps(lookupService);
        this.touchUpdateDate(true);
    }

    toJsonField(value: any,
                jsonDict: {} | null = null,
                name: string | null = null): any {
        const DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];
        if (name != "packedJson__")
            return Tuple.prototype.toJsonField(value, jsonDict, name);

        const convertedValue = deepCopy(value, DispBase.DEEP_COPY_FIELDS_TO_IGNORE);

        let disps = convertedValue[BranchTuple.__DISPS_NUM];

        this.cleanClonedDisps(disps);

        /* Now assign the value and it's value type if applicable */
        if (name != null && jsonDict != null)
            jsonDict[name] = convertedValue;

        return convertedValue;
    }

    private serialiseDisps(): string {
        const DispBase = require("peek_plugin_diagram/canvas-shapes/DispBase")["DispBase"];

        const disps = deepCopy(this._array(BranchTuple.__DISPS_NUM),
            DispBase.DEEP_COPY_FIELDS_TO_IGNORE);
        this.cleanClonedDisps(disps);

        let data: UndoDataI = {
            disps: disps,
            anchors: this.anchorDispKeys,
            updatedByUser: this.updatedByUser,
            lastStage: this._lastStage,
            needsSave: this.needsSave,
            replacementIds: this._replacementIds
        };

        return JSON.stringify(data);
    }

    private restoreSerialisedDisps(undoJsonStr: string): void {
        const data: UndoDataI = JSON.parse(undoJsonStr);

        this.packedJson__[BranchTuple.__DISPS_NUM] = data.disps;
        this.packedJson__[BranchTuple.__ANCHOR_DISP_KEYS_NUM] = data.anchors;
        this.packedJson__[BranchTuple.__UPDATED_BY_USER_NUM] = data.updatedByUser;
        this.packedJson__[BranchTuple.__NEEDS_SAVE_NUM] = data.needsSave;

        this._replacementIds = data.replacementIds;
        this._lastStage = data.lastStage;
        this.assignIdsToDisps();
    }

    private cleanClonedDisps(disps: any[]): void {
        for (let disp of disps) {
            for (let key of Object.keys(disp)) {
                let dispval = disp[key];

                // Nulls are not included
                if (dispval == null)
                    delete disp[key];

                // Delete all the linked lookups, we just want the IDs
                else if (dispval['__rst'] != null) // VortexJS Serialise Class Type
                    delete disp[key];

            }
        }
    }

    linkDisps(lookupService: PrivateDiagramLookupService) {
        for (let disp of this.disps) {
            lookupService._linkDispLookups(disp);
        }
    }

    setContextUpdateCallback(contextUpdateCallback: ((modelUpdateRequired: boolean) => void) | null) {
        this._contextUpdateCallback = contextUpdateCallback;

    }

    applyLiveUpdate(liveEditTuple: BranchLiveEditTuple): boolean {
        let liveUpdateTuple = liveEditTuple.branchTuple;

        if (this.updatedDate == null
            || moment(this.updatedDate).isBefore(liveUpdateTuple.updatedDate)
            || liveEditTuple.updateFromSave) {
            this.packedJson__ = liveUpdateTuple.packedJson__;
            this.assignIdsToDisps();
            this.resetUndo();
            this.resetRedo();
            return true;
        }
        return false;
    }
}
