/** Tuples. */
${javascript-tuples-code}

${javascript-class-jsdoc}
class ${prefix}_class {
    /** ${prefix}Enum declaration. It contains the single merged enum from the CIF model. */
    ${prefix}Enum = Object.freeze({
${javascript-enum-lits-code}
    })

    /** Should execution timing information be provided? */
    doInfoExec = true;

    /** Should executed event information be provided? */
    doInfoEvent = true;

    /** Should print output be provided? */
    doInfoPrintOutput = true;

    /** Should state output be provided? */
    doStateOutput = false;

    /** Should state output include algebraic variables? */
    doStateOutputAlgVars = false;

    /** Should transition output be provided? */
    doTransitionOutput = true;

    /** Whether the constants have been initialized already. */
    constantsInitialized = false;

    /** Whether the SVG copy and move declarations have been applied already. */
    svgCopiesAndMovesApplied = false;

    /** Whether this is the first time the code is (to be) executed. */
    firstExec;

    /** The names of all the events. */
    EVENT_NAMES = [
${javascript-event-name-code}
    ];
${javascript-const-decls}

    /** Variable 'time', tracks elapsed time for a session. */
    time;

    /**
     * The frequency in times per second, that the code should
     * be executed (if positive), or execute as fast as possible, that is
     * as many times per second as possible (if negative or zero).
     */
    frequency = ${js-frequency-default};

    /**
     * The speed at which the code is executed. Specify '1.0' for one time unit
     * taking one second, '2.0' for one time unit taking half a second, '0.5'
     * for one time unit taking 2 seconds, etc. Must be greater than zero.
     */
    speed = ${js-speed-default};

    /** The previous speed. See {@link ${prefix}_class#speed}. */
    prevSpeed;

    /**
     * Whether the next execution is the first execution of the session.
     * Used to initialize time-related variables for starting, pausing,
     * resuming or resetting each session.
     */
    first;

    /**
     * Whether the simulation is currently running, and should process
     * user input, or is paused.
     */
    playing;

    /** The start time of the current session. */
    startMilli;

    /** The previous time of the current session that the code was executed. */
    prevMilli;

    /**
     * The targeted end time of the current/next cycle, to ensure
     * that the duration of the cycle matches with the configured
     * frequency.
     */
    targetMilli;

    /** The model 'time' in milliseconds that has been stored and needs to be added to the model time. */
    storedTimeMilli;

${javascript-state-decls}
${javascript-state-input}

    /** SVG output elements. */
${javascript-svg-out-declarations}

    /**
     * SVG input queue with functions for handling clicked SVG
     * input elements, from first clicked at the head of the array
     * to last clicked at the tail of the array.
     */
    svgInQueue;

    /**
     * The SVG input id corresponding to the SVG input element that
     * was clicked. Is 'null' if no SVG input element was clicked so
     * far, or all clicks have already been processed.
     */
    svgInId;

    /**
     * The 0-based index of the event corresponding to the SVG input
     * element that was clicked. Is '-1' if no SVG input element was
     * clicked so far, or all clicks have already been processed.
     */
    svgInEvent;

    /** SVG input click event handlers. */
${javascript-svg-in-click-handlers-code}

    /** SVG input event setters. */
${javascript-svg-in-event-setters-code}

    /** Starts the simulation. */
    start() {
        if (!this.playing) {
            this.playing = true;
            this.exec();
        }
    }

    /** Stops the simulation. */
    stop() {
        if (this.playing) {
            this.playing = false;
        }
    }

    /** Resets the object to its initial state. */
    reset() {
        this.stop();
        this.firstExec = true;
        this.time = 0.0;
        this.first = true;
        this.timePaused = null;
        this.initState();
        this.initUI();
        this.updateUI();
    }

    /**
     * Execute the code once. Inputs are read, transitions are executed until
     * none are possible, outputs are written, etc.
     *
     * @param newTime The time in seconds, since the start of the first
     *      execution.
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    execOnce(newTime) {
        // Pre execution notification.
        this.preExec();

        // Update values of input variables.
        this.updateInputs();

        // Initialize the state.
        if (this.firstExec) {
            this.initState();
        }

        // Calculate time delta.
        var delta = newTime - this.time;

        // Update values of continuous variables.
        if (!this.firstExec) {
${javascript-cont-upd-code}
        }

        // Update time.
        this.time = newTime;

        // Apply print declarations.
        if (this.firstExec) {
            // For 'initial' transition.
            if (this.doInfoPrintOutput) this.printOutput(-3, true);
            if (this.doInfoPrintOutput) this.printOutput(-3, false);
            if (this.doStateOutput) ${prefix}.log('Initial state: ' + ${prefix}.getStateText(this.doStateOutputAlgVars));
            if (this.doStateOutput || this.doTransitionOutput) this.log('');

        } else {
            // For 'post' of time transition.
            if (this.doInfoPrintOutput) this.printOutput(-2, false);
        }

        // Execute environment events and SVG input events as long as they are possible, emptying the SVG input queue.
        while (true) {
            // Handle next element from SVG input queue, if not already processing one.
            if (this.svgInEvent == -1 && this.svgInQueue.length > 0) {
                var func = this.svgInQueue.shift(); // Remove head of the queue.
                func(); // Call function, to set the event to allow.
            }

            // Try to execute an SVG input mapping with updates for each environment event, and an edge for each SVG
            // input event.
            var anythingExecuted = false;
${javascript-edge-calls-code-svgin-updates}
${javascript-edge-calls-code-svgin-events}

            // Stop if no SVG input mapping and no edge was executed, and no more SVG input clicks are to be processed.
            if (!anythingExecuted && this.svgInQueue.length == 0) {
                break;
            }
        }

        // Make sure all outstanding SVG input clicks have been processed.
        console.assert(this.svgInEvent == -1 && this.svgInQueue.length == 0);

        // Execute uncontrollable edges as long as they are possible.
        while (true) {
            // Try to execute an edge for each event.
            var edgeExecuted = false;
${javascript-edge-calls-code-uncontrollables}

            // Stop if no edge was executed.
            if (!edgeExecuted) {
                break;
            }
        }

        // Execute controllable edges as long as they are possible.
        while (true) {
            // Try to execute an edge for each event.
            var edgeExecuted = false;
${javascript-edge-calls-code-controllables}

            // Stop if no edge was executed.
            if (!edgeExecuted) {
                break;
            }
        }

        // Apply print declarations for 'pre' of time transition.
        if (this.doInfoPrintOutput) this.printOutput(-2, true);

        // Post execution notification.
        this.postExec();

        // Done.
        this.firstExec = false;
    }

    /**
     * Calls {@link #execWhile}, which repeatedly {@link #execOnce executes the code}.
     *
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    exec() {
        this.execWhile(1);
    }

    /**
     * Repeatedly {@link #execOnce executes the code}.
     *
     * @param delay The delay before executing, in milliseconds.
     *
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    execWhile(delay) {
        setTimeout(
            function () {
                // Pre execution timing.
                var now = Date.now();
                var preMilli = now;

                // On first execution, initialize variables for timing.
                if (${prefix}.first) {
                    ${prefix}.first = false;
                    ${prefix}.prevSpeed = ${prefix}.speed;
                    ${prefix}.startMilli = now;
                    ${prefix}.prevMilli = now;
                    ${prefix}.targetMilli = now;
                    ${prefix}.storedTimeMilli = 0;
                }

                // Handle pausing/playing.
                if (!${prefix}.playing) {
                    ${prefix}.timePaused = now;
                    return;
                }

                if (${prefix}.timePaused) {
                    var deltaMilli = now - ${prefix}.timePaused;
                    ${prefix}.startMilli += deltaMilli;
                    ${prefix}.prevMilli += deltaMilli;
                    ${prefix}.targetMilli += deltaMilli;
                    ${prefix}.timePaused = null;
                }

                // Get frequency and cycle time.
                var frequency = ${prefix}.frequency;
                var cycleMilli = (frequency <= 0) ? -1 : 1e3 / frequency;

                // If the speed has changed, update the stored model time so far, based on the previous speed. Then
                // restart the measurement for the current speed.
                if (${prefix}.speed != ${prefix}.prevSpeed) {
                    // Add additional stored model time, from start of run to previous execution.
                    var deltaMilli = ${prefix}.prevMilli - ${prefix}.startMilli;
                    var deltaTimeMilli = deltaMilli * ${prefix}.prevSpeed;
                    ${prefix}.storedTimeMilli += deltaTimeMilli;

                    // Update to the new speed.
                    ${prefix}.prevSpeed = ${prefix}.speed;

                    // Update the speed measurement, to continue from the previous execution that has now been stored.
                    ${prefix}.startMilli = ${prefix}.prevMilli;
                }

                // Get current model 'time'.
                var deltaMilli = preMilli - ${prefix}.startMilli;
                var deltaTimeMilli = deltaMilli * ${prefix}.speed;
                var curTimeMilli = ${prefix}.storedTimeMilli + deltaTimeMilli;

                // Execute once.
                ${prefix}.execOnce(curTimeMilli / 1e3);

                // This execution becomes the next 'previous' execution.
                ${prefix}.prevMilli = preMilli;

                // Post execution timing.
                var postMilli = Date.now();
                var duration = postMilli - preMilli;
                if (${prefix}.doInfoExec) {
                    ${prefix}.infoExec(duration, cycleMilli);
                }

                // Ensure frequency.
                var remainderMilli = 0;
                if (frequency > 0) {
                    ${prefix}.targetMilli += cycleMilli;
                    remainderMilli = ${prefix}.targetMilli - postMilli;
                }

                // Execute again.
                ${prefix}.execWhile(remainderMilli > 0 ? remainderMilli : 0);
            },
        delay);
    }
${javascript-edge-methods-code}

    /**
     * Initializes the state.
     *
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    initState() {
        // Initialize constants, if not yet done so.
        if (!this.constantsInitialized) {
            this.constantsInitialized = true;
${javascript-const-init-code}
        }

        // Initialize SVG input.
        this.svgInQueue = [];
        this.svgInId = null;
        this.svgInEvent = -1;

${javascript-input-init}
        // CIF model state variables.
${javascript-state-init}
    }

    /**
     * Initializes the user interface, either when loading the page
     * or when resetting the simulation.
     */
    initUI() {
${javascript-frequency-slider-code}

${javascript-speed-slider-code}

        // Apply SVG copies and moves, if not done so before.
        if (!this.svgCopiesAndMovesApplied) {
            this.svgCopiesAndMovesApplied = true;

            // Apply SVG copy declarations.
${html-svg-copy-apply-code}

            // Apply SVG move declarations.
${html-svg-move-apply-code}
        }

        // Prepare SVG output.
${javascript-svg-out-assignments-code}

        // Prepare SVG input.
${javascript-svg-in-setup-code}
    }

    /**
     * Updates the user interface based on the latest state of
     * the model. Is called at the end of each cycle.
     *
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    updateUI() {
        // Apply CIF/SVG output mappings.
${javascript-svg-out-apply-code}
    }

    /**
     * Updates the values of the input variables. Other variables from the
     * state may not be accessed or modified.
     */
    updateInputs() {
        // Nothing is done here by default.
    }

    /** Logs a normal message. */
    log(message) {
        console.log(message);
${html-log-to-panel-code}
    }

    /** Logs an warning message. */
    warning(message) {
        console.log(message);
${html-warning-to-panel-code}
    }

    /** Logs an error message. */
    error(message) {
        console.log(message);
${html-error-to-panel-code}
    }

    /** Logs a runtime error of type ${prefix}Exception. */
    runtimeError(e, isCause = false) {
        console.assert(e instanceof ${prefix}Exception);
        if (isCause) {
            this.error("CAUSE: " + e.message);
        } else {
            this.error("ERROR: " + e.message);
        }
        if (e.cause) {
            this.runtimeError(e.cause, true);
        }
    }

    /**
     * Informs about the duration of a single execution.
     *
     * @param duration The duration of the execution, in milliseconds.
     * @param cycleTime The desired maximum duration of the execution, in
     *      milliseconds, or '-1' if not available.
     */
    infoExec(duration, cycleTime) {
        // Nothing is done here by default.
    }

    /**
     * Informs that an event will be or has been executed.
     *
     * @param idx The 0-based index of the event.
     * @param pre Whether the event will be executed ('true') or has
     *      been executed ('false').
     */
    infoEvent(idx, pre) {
        if (pre) {
            if (this.doTransitionOutput) ${prefix}.log(${prefix}Utils.fmt('Transition: event %s', ${prefix}.getEventName(idx)));
        } else {
            if (this.doStateOutput) ${prefix}.log('State: ' + ${prefix}.getStateText(this.doStateOutputAlgVars));
        }
    }

    /**
     * Informs that the code is about to be executed. For the
     * {@link #firstExec} the state has not yet been initialized, except for
     * {@link #time}.
     */
    preExec() {
        // Nothing is done here by default.
    }

    /** Informs that the code was just executed. */
    postExec() {
        this.updateUI();
    }

    /**
     * Returns the name of an event.
     *
     * @param idx The 0-based index of the event.
     * @return The name of the event.
     */
    getEventName(idx) {
        return this.EVENT_NAMES[idx];
    }

    /**
     * Returns a single-line textual representation of the model state.
     *
     * @param includeAlgVars Whether to include the algebraic variables.
     * @return The single-line textual representation of the model state.
     */
    getStateText(includeAlgVars) {
        var state = ${prefix}Utils.fmt('time=%s', ${prefix}.time);
${javascript-get-state-text-code}
        return state;
    }

${javascript-alg-var-code}
${javascript-deriv-code}
${javascript-funcs-code}

    /**
     * Print output for all relevant print declarations.
     *
     * @param idx The 0-based event index of the transition, or '-2' for
     *      time transitions, or '-3' for the 'initial' transition.
     * @param pre Whether to print output for the pre/source state of the
     *      transition ('true') or for the post/target state of the
     *      transition ('false').
     * @throws {${prefix}Exception} In case of a runtime error caused by code
     *      generated from the CIF model.
     */
    printOutput(idx, pre) {
${javascript-print-code}
    }

    /**
     * Informs that new print output is available.
     *
     * @param text The text being printed.
     * @param target The file or special target to which text is to be printed.
     *      If printed to a file, an absolute or relative local file system
     *      path is given. Paths may contain both '/' and '\\'
     *      as file separators. Use {@link ${prefix}Utils#normalizePrintTarget}
     *      to normalize the path to use '/' file separators. There are two
     *      special targets: ':stdout' to print to the standard output stream,
     *      and ':stderr' to print to the standard error stream.
     */
    infoPrintOutput(text, target) {
        if (target == ':stdout') {
            ${prefix}.log(text);
        } else if (target == ':stderr') {
            ${prefix}.error(text);
        } else {
            var path = ${prefix}Utils.normalizePrintTarget(target);
            ${prefix}.infoPrintOutput(path + ': ' + text, ':stdout');
        }
    }
}
