import { CompositionHistoryItem, HistoryItem } from "./HistoryItem";
import { ModelManipulator } from "../Model/ModelManipulator";
import { EventDispatcher } from "../Utils";
import { IDiagramController } from "../IDiagramController";

export class History {
    private static MAX_HISTORY_ITEM_COUNT: number = 100;

    historyItems: HistoryItem[] = [];
    startDataSyncItem: HistoryItem;
    currentIndex: number = -1;
    transaction: CompositionHistoryItem;

    private incrementalId: number = -1;
    private transactionLevel: number = -1;
    private unmodifiedIndex: number = -1;

    private currTransactionId: number = 0;

    onChanged: EventDispatcher<IHistoryChangesListener> = new EventDispatcher();

    constructor(public modelManipulator: ModelManipulator, public diagram: IDiagramController) {
    }

    public isModified(): boolean {
        if(this.unmodifiedIndex === this.currentIndex)
            return false;
        const startIndex = Math.min(this.unmodifiedIndex, this.currentIndex);
        const endIndex = Math.max(this.unmodifiedIndex, this.currentIndex);
        for(let i = startIndex + 1; i <= endIndex; i++)
            if(this.historyItems[i].changeModified())
                return true;

        return false;
    }
    public undo() {
        if(!this.canUndo())
            return;

        this.historyItems[this.currentIndex].undo(this.modelManipulator);
        this.currentIndex--;

        this.raiseChanged();
    }
    public redo() {
        if(!this.canRedo())
            return;

        if(this.startDataSyncItem) {
            this.startDataSyncItem.undo(this.modelManipulator);
            this.startDataSyncItem = undefined;
        }

        this.currentIndex++;
        this.historyItems[this.currentIndex].redo(this.modelManipulator);
        this.raiseChanged();
    }
    public canUndo(): boolean {
        return this.currentIndex >= 0;
    }
    public canRedo(): boolean {
        return this.currentIndex < this.historyItems.length - 1;
    }
    public beginTransaction(): number {
        this.transactionLevel++;
        if(this.transactionLevel === 0)
            this.transaction = new CompositionHistoryItem();

        const id: number = this.currTransactionId++;
        return id;
    }
    public endTransaction(isDataSyncTransaction?: boolean) {
        if(--this.transactionLevel >= 0)
            return;

        const transactionLength = this.transaction.historyItems.length;
        if(transactionLength > 0) {
            const historyItem = transactionLength > 1 ? this.transaction : this.transaction.historyItems.pop();
            if(isDataSyncTransaction)
                this.addDataSyncItem(historyItem);
            else
                this.addInternal(historyItem);
        }
        if(transactionLength > 0 && !isDataSyncTransaction)
            this.raiseChanged();
        delete this.transaction;
    }
    public addAndRedo(historyItem: HistoryItem) {
        this.add(historyItem);
        historyItem.redo(this.modelManipulator);
        this.raiseChanged();
    }
    public add(historyItem: HistoryItem) {
        if(this.transactionLevel >= 0)
            this.transaction.add(historyItem);
        else
            this.addInternal(historyItem);
    }
    private addInternal(historyItem: HistoryItem) {
        if(this.currentIndex < this.historyItems.length - 1) {
            this.historyItems.splice(this.currentIndex + 1);
            this.unmodifiedIndex = Math.min(this.unmodifiedIndex, this.currentIndex);
        }
        this.historyItems.push(historyItem);
        this.currentIndex++;

        this.deleteOldItems();
    }
    private addDataSyncItem(historyItem: HistoryItem) {
        const toHistoryItem = this.historyItems[this.currentIndex];
        if(toHistoryItem) {
            let compositionHistoryItem: CompositionHistoryItem;
            if(toHistoryItem instanceof CompositionHistoryItem)
                compositionHistoryItem = toHistoryItem;
            else {
                this.historyItems.splice(this.currentIndex, 1);
                compositionHistoryItem = new CompositionHistoryItem();
                this.historyItems.push(compositionHistoryItem);
                compositionHistoryItem.historyItems.push(toHistoryItem);
            }
            compositionHistoryItem.dataSyncItems.push(historyItem);
        }
        else if(this.historyItems.length)
            this.startDataSyncItem = historyItem;

    }

    private deleteOldItems() {
        const exceedItemsCount: number = this.historyItems.length - History.MAX_HISTORY_ITEM_COUNT;
        if(exceedItemsCount > 0 && this.currentIndex > exceedItemsCount) {
            this.historyItems.splice(0, exceedItemsCount);
            this.currentIndex -= exceedItemsCount;
        }
    }
    public getNextId() {
        this.incrementalId++;
        return this.incrementalId;
    }
    public clear() {
        this.currentIndex = -1;
        this.unmodifiedIndex = -1;
        this.incrementalId = -1;
        this.historyItems = [];
        delete this.transaction;
        this.transactionLevel = -1;
    }
    public resetModified() {
        this.unmodifiedIndex = this.currentIndex;
    }
    public getCurrentItemId(): number {
        if(this.currentIndex === -1)
            return -1;
        const currentItem = this.historyItems[this.currentIndex];
        if(currentItem.uniqueId === -1)
            currentItem.uniqueId = this.getNextId();
        return currentItem.uniqueId;
    }
    public undoTransaction() {
        this.diagram.beginUpdateCanvas();
        const items = this.transaction.historyItems;
        while(items.length)
            items.pop().undo(this.modelManipulator);
        this.diagram.endUpdateCanvas();
    }

    public undoTransactionTo(item: HistoryItem) {
        this.diagram.beginUpdateCanvas();
        const items = this.transaction.historyItems;
        while(items.length) {
            const ti = items.pop();
            ti.undo(this.modelManipulator);
            if(ti === item)
                break;
        }
        this.diagram.endUpdateCanvas();
    }
    raiseChanged() {
        if(this.transactionLevel === -1)
            this.onChanged.raise("notifyHistoryChanged");
    }
}

export interface IHistoryChangesListener {
    notifyHistoryChanged();
}
