import { assert } from "../../assert.js";
import List from "../../collection/list/List.js";
import Signal from "../../events/signal/Signal.js";
import { min2 } from "../../math/min2.js";
import { Mark } from "./Mark.js";

/**
 * @template C, M
 * @constructor
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ActionProcessor {
    /**
     * @template C, M
     * @param {C} context
     * @constructor
     */
    constructor(context) {
        /**
         *
         * @type {List<Action>}
         */
        this.history = new List();

        /**
         *
         * @type {C}
         */
        this.context = context;

        /**
         *
         * @type {Array<Mark<M>>}
         */
        this.marks = [];

        /**
         * @type {number}
         */
        this.cursor = 0;

        /**
         *
         * @type {number}
         */
        this.historyMemoryUsage = 0;

        /**
         * History beyond this limit will be dropped when a new mark is added
         * @type {number}
         */
        this.historyMarkLimit = 10000;

        /**
         * Maximum amount of memory allowed for the history
         * @type {number}
         */
        this.historyMemoryLimit = 268435456;

        /**
         *
         * @type {Signal<M> | Signal}
         */
        this.onMarkAdded = new Signal();

        /**
         *
         * @type {Signal<M> | Signal}
         */
        this.onMarkRemoved = new Signal();

        /**
         *
         * @type {Promise<never>}
         * @private
         */
        this.__last_promise = Promise.resolve();
    }

    /**
     *
     * @returns {number}
     */
    getMarkCount() {
        return this.marks.length;
    }

    /**
     *
     * @param {number} index
     * @returns {M}
     * @throws {Error} when index is out of range
     */
    getMarkDescription(index) {
        const mark = this.marks[index];

        if (mark === undefined) {
            throw new Error(`Index ${index} is out of range (mark count = ${this.marks.length})`);
        }

        return mark.description;
    }

    /**
     * Remove all of the history
     */
    clear() {
        // drop all of the history
        this.dropHistory(Infinity);
    }

    /**
     * Drop number of records from the history
     *
     * NOTE valid use-cases for this method from the outside are: 1) memory management and 1) removing access to a set of previous actions
     * NOTE this method is used internally by {@link #mark} method
     *
     * @example if there are 2 marks A at record 9 and B at record 11, and requested limit is 10 - 9 records will be kept as records 10 and 11 belong to the same batch and will be removed together.
     * @param {number} drop_count maximum number of records to drop
     */
    dropHistory(drop_count) {
        const recordsToDrop = Math.min(drop_count, this.history.length);
        if (recordsToDrop <= 0) {
            return;
        }

        //update marks
        let i = 0;
        const marks = this.marks;

        let markCount = marks.length;
        for (; i < markCount && marks[i].index <= recordsToDrop; i++) {
            //find the cur-off point for marks to keep
        }

        //drop the marks
        const dropped_marks = marks.splice(0, i);

        //update mark count
        markCount = marks.length;

        //update remaining marks
        for (i = 0; i < markCount; i++) {
            const mark = marks[i];
            mark.index -= recordsToDrop;
        }

        //update cursor
        this.cursor -= recordsToDrop;

        // dispatch signal
        dropped_marks.forEach(this.onMarkRemoved.send1, this.onMarkRemoved);

        // update history

    }

    /**
     * Insert a mark into the timeline, all actions between marks are grouped logically for UNDO and REDO mechanism
     * @param {D} description
     */
    async mark(description) {
        await this.__barrier();

        const marks = this.marks;
        const numMarks = marks.length;

        //check current cursor, we might need to drop some marks
        if (numMarks > 0) {
            const lastMarkIndex = marks[numMarks - 1].index;
            if (lastMarkIndex >= this.cursor) {
                //drop marks also
                let markIndexToKeep;
                for (markIndexToKeep = numMarks - 1; markIndexToKeep >= 0; markIndexToKeep--) {
                    if (marks[markIndexToKeep].index <= this.cursor) {
                        break;
                    }
                }

                const removed_marks = marks.splice(markIndexToKeep, numMarks - markIndexToKeep);
                // dispatch signal
                for( let mark of removed_marks){
                    this.onMarkRemoved.send1(mark);
                }
            }
        }

        const mark = new Mark(this.cursor, description);
        mark.memoryUsage = this.historyMemoryUsage;

        marks.push(mark);
        this.onMarkAdded.send1(description);

        this.cleanupHistory();
    }

    cleanupHistory() {
        const marks = this.marks;

        let lastMark = min2(marks.length, this.historyMemoryLimit) - 1;

        if (this.historyMemoryUsage >= this.historyMemoryLimit) {


            for (; lastMark > 0 && marks[lastMark].memoryUsage > this.historyMemoryUsage; lastMark--) {
            }


        }

        if (lastMark + 1 < marks.length) {
            const marksToDrop = lastMark - this.historyMarkLimit;

            //get first mark to keep
            const newFirstMark = marks[marksToDrop];

            //drop history before first mark's index
            this.dropHistory(newFirstMark.index);

            assert.equal(newFirstMark.index, 0, "New First Mark's index must be 0");
        }


    }

    /**
     *
     * @param {M} descriptor
     * @returns {number[]}
     * @private
     */
    __find_mark_indices_by_description(descriptor) {
        const result = [];

        const marks = this.marks;
        const mark_count = marks.length;
        for (let i = 0; i < mark_count; i++) {
            const mark = marks[i];

            if (mark.description === descriptor) {
                result.push(i);
            }
        }

        return result;
    }

    /**
     * Navigate to a specific mark back or forward
     * @param {M} mark_descriptor specific mark description
     * @returns {boolean} true on success, false otherwise (e.g. mark not found)
     * @throws {Error} when mark is non-unique
     */
    async navigateTo(mark_descriptor) {
        await this.__barrier();

        const p = this.__unsafe_navigateTo(mark_descriptor);

        this.__last_promise = p;

        return await p;
    }

    /**
     * Navigate to a specific mark back or forward
     * @param {M} mark_descriptor specific mark description
     * @returns {boolean} true on success, false otherwise (e.g. mark not found)
     * @throws {Error} when mark is non-unique
     * @private
     */
    async __unsafe_navigateTo(mark_descriptor) {

        // find all matching marks
        const matching_marks = this.__find_mark_indices_by_description(mark_descriptor);

        if (matching_marks.length === 0) {
            // not found
            return false;
        }

        if (matching_marks.length > 1) {
            throw new Error(`Attempting to navigate to a non-unique mark descriptor '${mark_descriptor}'. Mark descriptor must be unique, instead ${matching_marks.length} matches were found`);
        }

        const match_index = matching_marks[0];

        let target;
        if (match_index + 1 >= this.marks.length) {
            // mark is already at the end, go to the end of the history
            target = this.history.length;
        } else {
            target = this.marks[match_index + 1].index;
        }

        let temp_cursor = this.cursor;

        if (target > this.cursor) {

            while (target > this.cursor) {
                // navigate forward
                await this.__unsafe_redo();

                if (temp_cursor === this.cursor) {
                    // no cursor change
                    return false;
                }

                temp_cursor = this.cursor;
            }

        } else if (target < this.cursor) {

            while (target < this.cursor) {
                // navigate backwards
                await this.__unsafe_undo();

                if (temp_cursor === this.cursor) {
                    // no cursor change
                    return false;
                }

                temp_cursor = this.cursor;
            }

        }

        return true;
    }

    async undo() {
        await this.__barrier();

        const p = this.__unsafe_undo();

        this.__last_promise = p;

        await p;
    }

    /**
     *
     * @returns {Promise<void>}
     * @private
     */
    async __unsafe_undo() {
        let markAddress = 0;
        /**
         *
         * @type {Mark}
         */
        let mark = null;

        //find mark
        const marks = this.marks;

        for (let i = marks.length - 1; i >= 0; i--) {
            mark = marks[i];
            markAddress = mark.index;
            if (markAddress < this.cursor) {
                break;
            }
        }

        if (this.cursor === markAddress) {
            //this only happens when cursor sits on the last mark
            markAddress = 0;
        }

        //keep rewinding until we hit a mark
        while (this.cursor > markAddress && this.cursor > 0) {
            this.cursor--;
            const action = this.history.get(this.cursor);

            await action.revert(this.context);
        }

        if (mark !== null) {
            this.historyMemoryUsage = mark.memoryUsage;
        }
    }

    /**
     * redo previous sequence of actions on the timeline, up to the next mark
     */
    async redo() {
        await this.__barrier();

        const p = this.__unsafe_redo();

        this.__last_promise = p;

        await p;
    }

    /**
     *
     * @returns {Promise<void>}
     * @private
     */
    async __unsafe_redo() {
        let mark = this.history.length;

        //find mark
        const marks = this.marks;

        for (let i = marks.length - 1; i >= 0; i--) {
            const m = marks[i];
            const mI = m.index;
            if (mI > this.cursor) {
                mark = mI;
            } else {
                break;
            }
        }

        //keep apply action until we hit the mark
        while (this.cursor < mark) {
            const action = this.history.get(this.cursor);

            await action.apply(this.context);

            this.historyMemoryUsage += action.computeByteSize();

            this.cursor++;
        }
    }

    /**
     *
     * @param {Action[]} actions
     */
    async doMany(actions) {
        await this.__barrier();

        const p = new Promise(async (resolve, reject) => {

            const n = actions.length;

            for (let i = 0; i < n; i++) {

                const action = actions[i];

                await this.__unsafe_do(action);

            }

            resolve();
        });

        this.__last_promise = p;

        await p;
    }

    /**
     * Execute a given action and add it to the timeline
     * @param {Action} action
     */
    async do(action) {
        await this.__barrier();

        const p = this.__unsafe_do(action);

        this.__last_promise = p;

        await p;
    }

    /**
     *
     * @param {Action} action
     * @returns {Promise<void>}
     * @private
     */
    async __unsafe_do(action) {
        await action.apply(this.context);

        this.historyMemoryUsage += action.computeByteSize();

        const history = this.history;

        //doing action invalidates "future" branch of history, we need to clear all actions beyond cursor
        if (this.cursor < history.length) {
            history.crop(0, this.cursor);
            // TODO clear future marks
        }

        history.add(action);

        this.cursor++;
    }

    /**
     * Await for synchronization barrier
     * @returns {Promise<void>}
     * @private
     */
    async __barrier() {
        try {
            await this.__last_promise;
        } catch (e) {
            // ignore
        }
    }
}
