//
// DUMP utilities
// http://stackedboxes.org/2020/01/05/dump-of-unsorted-morsels-for-programmers/
// Code is licensed under the MIT license
//
// Copyright 2019-2020 Leandro Motta Barros
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


namespace dump {

    //
    // Step-by-step execution
    //

    /** Thrown when a StepByStepFunction is aborted. */
    class StepByStepFunctionAborted extends Error {
        constructor() {
            super("Aborted StepByStepFunction (If you see this, this is a bug in DUMP utils!)");
            Object.setPrototypeOf(this, new.target.prototype);
            this.name = StepByStepFunctionAborted.name;
        }
    }

    /**
     * Provides support for step-by-step function execution, which is handy for
     * demos.
     *
     * Worth noting: step-by-step execution is not automatic. For example, you
     * need to call wait() to indicate where a step ends, and step() to tell
     * when to execute the next step. You are also supposed to call end() when
     * the function ends.
     */
    export class StepByStepFunction {
        /**
         * Constructs a StepByStepFunction. The function is not started.
         * @param id An ID to identify this StepByStepFunction.
         * @param func The function to be "controlled" by this
         *      StepByStepFunction.
         */
        constructor(id: string, func: () => Promise<void>) {
            let f = StepByStepFunction.funcMap_.get(id);
            if (f != undefined && f.isRunning_ && !f.isFinished_) {
                throw new Error(
                    `Creating StepByStepFunction with same ID as an existing running function: ${id}.`);
            }
            StepByStepFunction.funcMap_.set(id, this);
            this.id_ = id;
            this.func_ = new Promise( async () => {
                await this.wait();

                try {
                    await func();
                } catch (e) {
                    if (e instanceof StepByStepFunctionAborted) {
                        // Function was aborted, so what? This was expected. Swallow this exception.
                    } else {
                        throw e;
                    }
                }

                this.isFinished_ = true;

                this.isRunning_ = false;
                this.nextStepPlease_ = false;
                this.abortPlease_ = false;

                if (this.postStepCallback_ != undefined) {
                    this.postStepCallback_();
                    this.postStepCallback_ = undefined;
                }
            });
        }

        /** Starts the function. Throws if already running. */
        public async start() {
            if (this.isRunning_) {
                throw new Error(`Starting an already running StepByStepFunction (ID: ${this.id_}).`);
            }

            this.isRunning_ = true;
            this.nextStepPlease_ = true;
            await this.func_;
        }

        /** Did this function finished running to completion? */
        public isFinished() {
            return this.isFinished_;
        }

        /** Is this function running? */
        public isRunning() {
            return this.isRunning_;
        }

        /**
         * Waits until the next step is requested or the function is aborted. If
         * the function is aborted, this throws a StepByStepFunctionAborted.
         */
        public async wait() {
            if (this.postStepCallback_ != undefined) {
                this.postStepCallback_();
                this.postStepCallback_ = undefined;
            }

            while (!this.nextStepPlease_) {
                await sleep(0.15);
                if (this.abortPlease_) {
                    this.isRunning_ = false;
                    this.nextStepPlease_ = false;
                    this.abortPlease_ = false;

                    if (this.postAbortCallback_ != undefined) {
                        this.postAbortCallback_();
                        this.postAbortCallback_ = undefined;
                    }

                    throw new StepByStepFunctionAborted();
                }
            }
            this.nextStepPlease_ = false;
        }

        /**
         * Requests the execution of this function's next step.
         * @param postStepCallback If provided, this is set as callback that
         *      will be called as soon as the step is actually executed.
         */
        public step(postStepCallback?: () => void) {
            if (this.isFinished_) {
                throw new Error(`Trying to step on a finished StepByStepFunction (ID: ${this.id_})`);
            }
            this.nextStepPlease_ = true;
            this.postStepCallback_ = postStepCallback;
        }

        /** Aborts the execution of this function. */
        public abort(postAbortCallback?: () => void) {
            this.abortPlease_ = true;
            this.postAbortCallback_ = postAbortCallback;
        }

        /** Gets the instance associated with a given ID. */
        public static instanceByID(id: string) {
            if (!this.funcMap_.has(id)) {
                throw new Error(`Trying to get instance for nonexistent ID ${id}.`);
            }
            return this.funcMap_.get(id) as StepByStepFunction;
        }

        /** Generic local data store, to be used freely by the demos. */
        public data = new Map<string, any>();

        /** The function to run step-by-step. */
        private func_: Promise<void>;

        /** The function ID. */
        private id_: string;

        /** Are we running? */
        private isRunning_ = false;

        /** Are we supposed to run the next step? */
        private nextStepPlease_ = false;

        /** Are we supposed to abort the function execution? */
        private abortPlease_ = false;

        /** Did the function finished running? */
        private isFinished_ = false;

        /** A callback to be called as soon as a step is executed. */
        private postStepCallback_: (() => void) | undefined = undefined;

        /** A callback to be called as soon as the function execution is aborted. */
        private postAbortCallback_: (() => void) | undefined = undefined;

        /** The static map from IDs to StepByStepFunctions. */
        private static funcMap_ = new Map<string, StepByStepFunction>();
    }


    //
    // Assorted utilities
    //

    /** Generic global data store, to be used freely by the demos. */
    export let data = new Map<string, any>();

    /** Sleeps for a given number of seconds. */
    export function sleep(timeInSecs: number) {
        return new Promise(resolve => setTimeout(resolve, timeInSecs*1000));
    }
}
