const {Config} = require("./Config");
const {Trace} = require("./Trace");
const {EventFilter} = require("./EventFilter");
const {checkType} = require("./Utility");

let doBreak;
let runFn = undefined;
let bpGenericName = 0;
const breakpointList = [];

class Breakpoint extends EventFilter {
    /**
     * Used to stop running code when specific criteria are met
     *
     * @param   {object} criteria Critera for when the Breakpoint should stop code from running. See {@link EventFilter} for details
     * @param   {string} name     Optional name for this breakpoint. If no name is specified, a default name will be assigned.
     * @returns {Breakpoint}      The Breakpoint that was created
     */
    constructor(criteria, name) {
        // setup breakpoint
        let {count} = criteria;
        delete criteria.count;
        let {once} = criteria;
        delete criteria.once;
        let {every} = criteria;
        delete criteria.every;
        let {disabled} = criteria;
        delete criteria.disabled;

        // if breaking on every event, use dummy criteria
        if (every) {
            criteria = {
                sourceName: "__allEvents__",
                all: true,
            };
        }

        super("allow", criteria);

        // TODO: check that count is a number

        this.count = count;
        this.currentCount = 0;
        this.once = !!once;
        this.every = !!every;
        this.disabled = (typeof disabled === "boolean") ? disabled : false;
        this.name = name || `bp${++bpGenericName}`;

        // save breakpoint
        breakpointList.push(this);
    }

    /**
     * Clears this breakpoint by removing it from the global breakpoint list
     */
    clear() {
        this.disabled = true;

        let idx = breakpointList.indexOf(this);

        // not found, ignore
        if (idx === -1) {
            return;
        }

        // delete this item from the list
        breakpointList.splice(idx, 1);
    }

    /**
     * Disables this breakpoint
     */
    disable() {
        this.disabled = true;
    }

    /**
     * Enables this Breakpoint
     */
    enable() {
        this.disabled = false;
    }

    /**
     * Determines if this Breakpoint matches event 'e' based on the 'criteria' specified when the Breakpoint was created. Used to determine if the program should stop.
     *
     * @param   {EventBase} e The event to be evaluated
     * @returns {boolean}   Returns `true` if the event matches this Breakpoint's criteria, `false` otherwise
     */
    matchEvent(e) {
        if (this.disabled) {
            return false;
        }

        if (this.every) {
            return true;
        }

        let match = super.matchEvent(e);

        if (match && this.count) {
            this.currentCount++;
            if (this.currentCount === this.count) {
                this.currentCount = 0;
                return true;
            }

            return false;
        }

        return match;
    }

    /**
     * Converts the Breakpoint to a human-readable String
     */
    toString() {
        // bp1: "sourceType:xyz,eventType:abc" (0/100) [disabled,once,all]
        let {name} = this;
        let count = this.count ? ` (${this.currentCount}/${this.count})` : "";

        // build criteria string
        let critList = [];

        if (this._criteria.sourceType) {
            critList.push(`sourceType:${this._criteria.sourceType}`);
        }

        if (this._criteria.sourceName) {
            critList.push(`sourceName:${this._criteria.sourceName}`);
        }

        if (this._criteria.eventType) {
            critList.push(`eventType:${this._criteria.eventType}`);
        }

        let criteria = critList.join(",");

        if (this._criteria.all) {
            criteria = `all::${criteria}`;
        } else if (this._criteria.any) {
            criteria = `any::${criteria}`;
        } else if (this._criteria.none) {
            criteria = `none::${criteria}`;
        } else {
            criteria = `<unknown>::${criteria}`;
        }

        if (this.every) {
            criteria = "*";
        }

        // build flag string
        let flagList = [];
        if (this.disabled) {
            flagList.push("disabled");
        }

        if (this.once) {
            flagList.push("once");
        }

        // if (this.every) {
        //     flagList.push("every");
        // }

        let flags = flagList.length ? ` [${flagList.join(",")}]` : "";

        return `${name}: "${criteria}"${count}${flags}`;
    }

    /**
     * Initialize breakpoints, typically called by init()
     */
    static init() {
        doBreak = Config.get("debug-break-on-entry");
    }

    /**
     * Evaluates whether the specified event triggers a breakpoint
     *
     * @param   {EventBase}   e  An event that is derived from EventBase
     * @param   {Function} cb A callback function for when resuming from the break
     * @returns {Promise}      a Promise that resolves when resuming from the breakpoint
     */
    static async checkBreak(e, cb) {
        Trace.addEvent(e);

        let bp;
        // check all breakpoints in list
        for (let i = 0; i < breakpointList.length; i++) {
            bp = breakpointList[i];
            if (bp.matchEvent(e)) {
                doBreak = true;
                if (bp.once) {
                    bp.disable();
                }

                break;
            }
        }
        // console.debug(`Breakpoint evaluation (${doBreak}): ${e}`);

        // if we hit a breakpoint
        if (doBreak) {
            if (bp) {
                console.info("Stopping execution for breakpoint:", bp.toString());
            } else {
                console.info("Breakpoint encountered.");
            }

            console.debug("Breakpoint triggered by event:", e.toString());
            if (Config.get("debug-sync-environment")) {
                checkType("checkBreak", "runFn", runFn, "undefined");
            }

            const {Synchronize} = require("./Synchronize");
            Synchronize.pauseWatchdog();

            return new Promise((resolve) => {
                runFn = () => {
                    resolve(cb());
                };
            });
        }

        return cb();
    }

    /**
     * Sets a new breakpoint
     */
    static setBreakpoint() {
        doBreak = true;
    }

    /**
     * Resumes running after a breakpoint has been triggered
     */
    static run() {
        checkType("Breakpoint.run", "runFn", runFn, "function");
        setImmediate(runFn);

        const {Synchronize} = require("./Synchronize");
        Synchronize.startWatchdog();

        runFn = undefined;
        doBreak = false;
    }

    /**
     * Find breakpoints with the corresponding 'name'
     *
     * @param   {string} name The name of the breakpoint(s) to find
     * @returns {Array<Breakpoint>}      An Array of Breakpoints that were found, or an empty array if none were found.
     */
    static find(name) {
        if (typeof name === "number") {
            let bp = breakpointList[name];
            if (bp === undefined) {
                return [];
            }

            return [bp];
        }

        return breakpointList.filter((bp) => bp.name === name);
    }

    /**
     * Delete a breakpoint
     *
     * @param   {string | number} name The name or number of the breakpoint. If multiple breakpoints with the same name are found, they are all cleared.
     * @returns {boolean}      Returns `true` if the breakpoint was found and enabled, false otherwise.
     */
    static clear(name) {
        return findAndAct(name, "clear");
    }

    /**
     * Disable a breakpoint so that it still exists, but doesn't trigger
     *
     * @param   {string | number} name The name or number of the breakpoint. If multiple breakpoints with the same name are found, they are all disabled.
     * @returns {boolean}      Returns `true` if the breakpoint was found and enabled, false otherwise.
     */
    static disable(name) {
        return findAndAct(name, "disable");
    }

    /**
     * Enable a previously disabled breakpoint
     *
     * @param   {string | number} name The name or number of the breakpoint. If multiple breakpoints with the same name are found, they are all enabled.
     * @returns {boolean}      Returns `true` if the breakpoint was found and enabled, false otherwise.
     */
    static enable(name) {
        return findAndAct(name, "enable");
    }

    /**
     * Clear all breakpoints
     */
    static clearAll() {
        breakpointList.length = 0;
        bpGenericName = 0;
    }

    /** true if a break has been triggered */
    static get inBreak() {
        return !!runFn;
    }

    /** Array of breakpoint strings, as returned by `toString` */
    static get list() {
        let list = [];
        breakpointList.forEach((bp) => list.push(bp.toString()));
        return list;
    }
}

function findAndAct(name, action, ... args) {
    let bpList = Breakpoint.find(name);

    if (bpList.length === 0) {
        return false;
    }

    bpList.forEach((bp) => bp[action](... args));

    return true;
}

module.exports = {
    Breakpoint,
};