Source: interface/CLI/ViewNavigator.js

const ToolsCLI = require('./ToolsCLI');
const NavigatorOption = require('./NavigatorOption');
const ListTiles = require('@CLI/templates/ListTiles');
const Prompt = require('@services/Prompt');
const { QuestionModel } = require('./PoolForm');

/**
 * Default navigation questions for the ViewNavigator class.
 * @type {Object}
 */
const navDefaultQuestions = {
    startQuestion: 'navigation',
    questions: [
        {
            id: 'navigation',
            text: 'Choose an option above to navigate, type the index of the choosed option: ',
            required: true
        }
    ]
};

/**
 * Class representing a view navigator with navigation options.
 * @extends ToolsCLI
 */
class ViewNavigator extends ToolsCLI {
    static navDefaultQuestions = navDefaultQuestions;

    /**
     * Creates an instance of ViewNavigator.
     * @constructor
     * @param {Object} [setup] - The configuration for the view navigator instance.
     * @param {string} [setup.type='nav'] - The type of navigation ('nav' or 'doc-list').
     * @param {Array} [setup.options=[]] - Array of navigation options.
     * @param {Function} [setup.navSuccessCallback] - Callback function on successful navigation.
     * @param {Function} [setup.navErrorCallback] - Callback function on navigation error.
     * @param {Object} [setup.question] - Question configuration for prompting user input.
     * @param {ViewCLI} parentView - The parent view associated with this navigator.
     */
    constructor(setup = {
        type: 'nav',
        options: [],
        navSuccessCallback,
        navErrorCallback,
        question
    }, parentView) {
        super();
        const { type, options, question, navSuccessCallback, navErrorCallback } = setup || {};
        const self = this;

        /**
         * The type of navigation ('nav' or 'doc-list').
         * @type {string}
         */
        this.type = type || 'nav';

        /**
         * Array of navigation options.
         * @type {Array}
         */
        this.options = [];

        /**
         * Question configuration for prompting user input.
         * @type {Object}
         */
        this.question = question ? new QuestionModel(question) : {};

        /**
         * Callback function on successful navigation.
         * @type {Function}
         */
        this.navSuccessCallback = navSuccessCallback && navSuccessCallback.bind(this);

        /**
         * Callback function on navigation error.
         * @type {Function}
         */
        this.navErrorCallback = navErrorCallback && navErrorCallback.bind(this);

        /**
         * The parent view associated with this navigator.
         * @type {ViewCLI}
         */
        this._parentView = () => parentView;

        if (Array.isArray(options)) {
            this.options = options.map((opt, index) => new NavigatorOption({ ...opt, index }, self));
        }
    }

    /**
     * Retrieves the parent view associated with this navigator.
     * @returns {ViewCLI} The parent view instance.
     */
    get parentView() {
        return this._parentView();
    }

    /**
     * Navigates to a specific navigation option by its index.
     * @param {string} index - The index of the navigation option to navigate to.
     * @param {Object} [params] - Additional parameters to be passed to the view.
     * @throws {Error.Log} When an error occurs during navigation.
     */
    async navTo(index, params) {
        const opt = this.getOption(index);

        if (!opt) {
            this.print(`Navigation option for index "${index}" wasn't found!`, '[ERROR]');
        }

        if (typeof opt.trigger === 'function') {
            return opt.trigger.call(opt, this);
        }

        if (this.parentView && opt && opt.targetView){
            return await this.parentView.goToView(opt.targetView, {
                ...opt.viewParams,
                ...params,
                defaultData: opt.defaultData,
                docData: opt.doc
            });
        }
    }

    /**
     * Retrieves a navigation option by its index.
     * @param {string} index - The index of the navigation option to retrieve.
     * @returns {NavigatorOption} The navigation option instance.
     */
    getOption(index) {
        return this.options.find(opt => opt.index === index);
    }

    /**
     * Adds a new navigation option to the navigator.
     * @param {Object} data - Data for the new navigation option.
     * @returns {ViewNavigator} The current ViewNavigator instance.
     */
    addOption(data) {
        const newOption = new NavigatorOption(data, this);

        newOption.type = this.type;
        this.options.push(newOption);
        return this;
    }

    /**
     * Sets or updates an existing navigation option by its index.
     * @param {string} index - The index of the navigation option to set or update.
     * @param {Object} data - Data for the navigation option.
     * @param {boolean} [override=false] - Indicates whether to override the existing option or merge the data.
     */
    setOption(index, data, override = false) {
        const i = String(index);

        if (override) {
            this.options[i] = new NavigatorOption(data, this);
        } else {
            this.options[i] = { ...this.options[i], ...data };
        }
    }

    /**
     * Renders the navigation options and displays them to the user.
     * @param {Object} [params] - Additional parameters for rendering.
     * @returns {string} The rendered string output of the navigation options.
     * @throws {Error.Log} When an error occurs during rendering.
     */
    render(params) {
        let { exclude } = params || {};
        let options = [];

        if (this.type === 'doc-list') {
            this.options.map((opt) => options.push(opt.doc));
        } else {
            options = this.options;
            exclude = ['type', 'targetView', ...(exclude || [])];
        }

        try {
            if (Array.isArray(options) && options.length) {
                const template = new ListTiles({ items: options });
                const stringOutput = template.renderToString();
                
                this.printTemplate(stringOutput);
                return stringOutput;
            } else {
                return '';
            }
        } catch (err) {
            throw new Error.Log(err);
        }
    }

    /**
     * Prompts user input based on the provided question text and navigates to the chosen option.
     * @param {Object} [params] - Additional parameters for rendering and navigation.
     * @returns {Promise} A promise resolving to the result of navigation.
     * @throws {Error.Log} When an error occurs during navigation.
     */
    async fire(params) {
        try {
            this.render(params);
            const questionText = this.question && this.question.text || '';

            const fired = await this.prompt.question(questionText);
            if (fired instanceof Error.Log) {
                throw fired;
            }

            return await this.navTo(fired);
        } catch (err) {
            throw new Error.Log(err);
        }
    }
}

module.exports = ViewNavigator;