/**
 * @fileoverview A class of the code path.
 * @author Toru Nagashima
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const CodePathState = require("./code-path-state");
const IdGenerator = require("./id-generator");

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

/**
 * A code path.
 */
class CodePath {

    /**
     * Creates a new instance.
     * @param {Object} options Options for the function (see below).
     * @param {string} options.id An identifier.
     * @param {string} options.origin The type of code path origin.
     * @param {CodePath|null} options.upper The code path of the upper function scope.
     * @param {Function} options.onLooped A callback function to notify looping.
     */
    constructor({ id, origin, upper, onLooped }) {

        /**
         * The identifier of this code path.
         * Rules use it to store additional information of each rule.
         * @type {string}
         */
        this.id = id;

        /**
         * The reason that this code path was started. May be "program",
         * "function", "class-field-initializer", or "class-static-block".
         * @type {string}
         */
        this.origin = origin;

        /**
         * The code path of the upper function scope.
         * @type {CodePath|null}
         */
        this.upper = upper;

        /**
         * The code paths of nested function scopes.
         * @type {CodePath[]}
         */
        this.childCodePaths = [];

        // Initializes internal state.
        Object.defineProperty(
            this,
            "internal",
            { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }
        );

        // Adds this into `childCodePaths` of `upper`.
        if (upper) {
            upper.childCodePaths.push(this);
        }
    }

    /**
     * Gets the state of a given code path.
     * @param {CodePath} codePath A code path to get.
     * @returns {CodePathState} The state of the code path.
     */
    static getState(codePath) {
        return codePath.internal;
    }

    /**
     * The initial code path segment. This is the segment that is at the head
     * of the code path.
     * This is a passthrough to the underlying `CodePathState`.
     * @type {CodePathSegment}
     */
    get initialSegment() {
        return this.internal.initialSegment;
    }

    /**
     * Final code path segments. These are the terminal (tail) segments in the
     * code path, which is the combination of `returnedSegments` and `thrownSegments`.
     * All segments in this array are reachable.
     * This is a passthrough to the underlying `CodePathState`.
     * @type {CodePathSegment[]}
     */
    get finalSegments() {
        return this.internal.finalSegments;
    }

    /**
     * Final code path segments that represent normal completion of the code path.
     * For functions, this means both explicit `return` statements and implicit returns,
     * such as the last reachable segment in a function that does not have an
     * explicit `return` as this implicitly returns `undefined`. For scripts,
     * modules, class field initializers, and class static blocks, this means
     * all lines of code have been executed.
     * These segments are also present in `finalSegments`.
     * This is a passthrough to the underlying `CodePathState`.
     * @type {CodePathSegment[]}
     */
    get returnedSegments() {
        return this.internal.returnedForkContext;
    }

    /**
     * Final code path segments that represent `throw` statements.
     * This is a passthrough to the underlying `CodePathState`.
     * These segments are also present in `finalSegments`.
     * @type {CodePathSegment[]}
     */
    get thrownSegments() {
        return this.internal.thrownForkContext;
    }

    /**
     * Traverses all segments in this code path.
     *
     *     codePath.traverseSegments((segment, controller) => {
     *         // do something.
     *     });
     *
     * This method enumerates segments in order from the head.
     *
     * The `controller` argument has two methods:
     *
     * - `skip()` - skips the following segments in this branch
     * - `break()` - skips all following segments in the traversal
     *
     * A note on the parameters: the `options` argument is optional. This means
     * the first argument might be an options object or the callback function.
     * @param {Object} [optionsOrCallback] Optional first and last segments to traverse.
     * @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse.
     * @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse.
     * @param {Function} callback A callback function.
     * @returns {void}
     */
    traverseSegments(optionsOrCallback, callback) {

        // normalize the arguments into a callback and options
        let resolvedOptions;
        let resolvedCallback;

        if (typeof optionsOrCallback === "function") {
            resolvedCallback = optionsOrCallback;
            resolvedOptions = {};
        } else {
            resolvedOptions = optionsOrCallback || {};
            resolvedCallback = callback;
        }

        // determine where to start traversing from based on the options
        const startSegment = resolvedOptions.first || this.internal.initialSegment;
        const lastSegment = resolvedOptions.last;

        // set up initial location information
        let record;
        let index;
        let end;
        let segment = null;

        // segments that have already been visited during traversal
        const visited = new Set();

        // tracks the traversal steps
        const stack = [[startSegment, 0]];

        // segments that have been skipped during traversal
        const skipped = new Set();

        // indicates if we exited early from the traversal
        let broken = false;

        /**
         * Maintains traversal state.
         */
        const controller = {

            /**
             * Skip the following segments in this branch.
             * @returns {void}
             */
            skip() {
                skipped.add(segment);
            },

            /**
             * Stop traversal completely - do not traverse to any
             * other segments.
             * @returns {void}
             */
            break() {
                broken = true;
            }
        };

        /**
         * Checks if a given previous segment has been visited.
         * @param {CodePathSegment} prevSegment A previous segment to check.
         * @returns {boolean} `true` if the segment has been visited.
         */
        function isVisited(prevSegment) {
            return (
                visited.has(prevSegment) ||
                segment.isLoopedPrevSegment(prevSegment)
            );
        }

        /**
         * Checks if a given previous segment has been skipped.
         * @param {CodePathSegment} prevSegment A previous segment to check.
         * @returns {boolean} `true` if the segment has been skipped.
         */
        function isSkipped(prevSegment) {
            return (
                skipped.has(prevSegment) ||
                segment.isLoopedPrevSegment(prevSegment)
            );
        }

        // the traversal
        while (stack.length > 0) {

            /*
             * This isn't a pure stack. We use the top record all the time
             * but don't always pop it off. The record is popped only if
             * one of the following is true:
             *
             * 1) We have already visited the segment.
             * 2) We have not visited *all* of the previous segments.
             * 3) We have traversed past the available next segments.
             *
             * Otherwise, we just read the value and sometimes modify the
             * record as we traverse.
             */
            record = stack.at(-1);
            segment = record[0];
            index = record[1];

            if (index === 0) {

                // Skip if this segment has been visited already.
                if (visited.has(segment)) {
                    stack.pop();
                    continue;
                }

                // Skip if all previous segments have not been visited.
                if (segment !== startSegment &&
                    segment.prevSegments.length > 0 &&
                    !segment.prevSegments.every(isVisited)
                ) {
                    stack.pop();
                    continue;
                }

                visited.add(segment);


                // Skips the segment if all previous segments have been skipped.
                const shouldSkip = (
                    skipped.size > 0 &&
                    segment.prevSegments.length > 0 &&
                    segment.prevSegments.every(isSkipped)
                );

                /*
                 * If the most recent segment hasn't been skipped, then we call
                 * the callback, passing in the segment and the controller.
                 */
                if (!shouldSkip) {
                    resolvedCallback.call(this, segment, controller);

                    // exit if we're at the last segment
                    if (segment === lastSegment) {
                        controller.skip();
                    }

                    /*
                     * If the previous statement was executed, or if the callback
                     * called a method on the controller, we might need to exit the
                     * loop, so check for that and break accordingly.
                     */
                    if (broken) {
                        break;
                    }
                } else {

                    // If the most recent segment has been skipped, then mark it as skipped.
                    skipped.add(segment);
                }
            }

            // Update the stack.
            end = segment.nextSegments.length - 1;
            if (index < end) {

                /*
                 * If we haven't yet visited all of the next segments, update
                 * the current top record on the stack to the next index to visit
                 * and then push a record for the current segment on top.
                 *
                 * Setting the current top record's index lets us know how many
                 * times we've been here and ensures that the segment won't be
                 * reprocessed (because we only process segments with an index
                 * of 0).
                 */
                record[1] += 1;
                stack.push([segment.nextSegments[index], 0]);
            } else if (index === end) {

                /*
                 * If we are at the last next segment, then reset the top record
                 * in the stack to next segment and set its index to 0 so it will
                 * be processed next.
                 */
                record[0] = segment.nextSegments[index];
                record[1] = 0;
            } else {

                /*
                 * If index > end, that means we have no more segments that need
                 * processing. So, we pop that record off of the stack in order to
                 * continue traversing at the next level up.
                 */
                stack.pop();
            }
        }
    }
}

module.exports = CodePath;
