Source: dsa-cli/dsa-trainer.js

const SettingsManager = require('./settings-manager');
const ProblemsManager = require('./problems-manager');
const StorableReport = require('./StorableReport');


const { getPromptDict } = require('./prompt');

const constants = require('./constants');
const axios = require('axios');
const FormData = require('form-data');

const { renderPromptDescription, get_random, getCurrentDateTimeIso } = require('./functions');
const { Toggle, AutoComplete, Input } = require('enquirer');
const { ProblemMetadata } = require('./structures');
const fs = require('fs');
const { show_image_if_isurl } = require('./functions');
const Settings = require('../settings');


const DEBUG = false;

/**
 * @class DSATrainer - Responsible of presenting problems, and interacting with problems managers, settings, and others charts and user data visualization
 */
class DSATrainer {

    /**
     * Creates a new DSATrainer object
     * @param {List} [skip_problems]: List[str] A list of problems to skip (problems slug names)
     */
    constructor({ skip_problems = ["hello-world", "simple-sum"] } = {}) {
        /**
         * @property {SettingsManager} settings_manager - Configurations management such as which code editor to use.
         * @property {ProblemsManager} problems_manager - management of DSA Problems
         * 
         * @property {ProblemsManager} loaded_problem_manager - management of DSA Problems once it finishes loading.
         * @property {Object} user_settings - User settings configured on `settings.json`
         * @property {string[]} skip_problems - A list of problems to skip (problems slug names)
         * 
         * @property {StorableReport} problemReport - A report of the problems solved by the user
         * @property {string[]} order_categories - The order of the categories to be solved
         * 
         * @property {ProblemMetaData[]} first_non_completed_category_non_completed_problems - A list of problems that are not completed yet
         * 
         * @property {ProblemMetaData[]} first_non_only_hard_left_category_non_hard_problems - A list of problems that are not completed yet, and are not hard
         * @property {ProblemMetaData[]} completed_problems_sorted_by_times_completed - A list of problems that are not completed yet, sorted by the number of times they have been completed
         * @property {String} uploadCodeFileUrl - The url to upload the code file
         */
        this.settings_manager = new SettingsManager();
        this.problems_manager = new ProblemsManager({ skip_problems: skip_problems });

        this.loaded_problem_manager = this.problems_manager.autoPopulateUsingTestDictionary();
        this.user_settings = this.settings_manager.settings;
        this.skip_problems = skip_problems;

        this.problemReport = new StorableReport({ filename: 'problem_report' });
        this.order_categories = Object.values(constants.PROBLEM_CATEGORIES).map(category => category.slug);


        this.first_non_completed_category_non_completed_problems = this.getFirstNonCompletedCategoryNonCompletedProblems();
        this.first_non_only_hard_left_category_non_hard_problems = this.getFirstNonOnlyHardLeftCategoryNonHardProblems();
        this.completed_problems_sorted_by_times_completed = this.getCompletedProblemsSortedByTimesCompleted();

        this.uploadCodeFileUrl = `${constants.CONSTANTS.API_URL}/utils/upload_file`;
        this.uploadCodeMetadataUrl = `${constants.CONSTANTS.API_URL}/performance/code_file`;
    }

    /**
     * Populates the recommendation queues
     * @returns {void}
     * Call this when problmeManager had been populated
     */
    populateRecommendationQueues() {
        this.first_non_completed_category_non_completed_problems = this.getFirstNonCompletedCategoryNonCompletedProblems();
        this.first_non_only_hard_left_category_non_hard_problems = this.getFirstNonOnlyHardLeftCategoryNonHardProblems();
        this.completed_problems_sorted_by_times_completed = this.getCompletedProblemsSortedByTimesCompleted();
    }


    /**
     * Gets the list of recommended problems to solve
     * @param {int} non_completed The number of non completed problems to get
     * @param {int} non_hard The number of non hard problems to get
     * @param {int} completed_practice The number of completed problems to get
     * @param {bool} refresh_recommendation_queues Whether to refresh the recommendation queues or not
     * 
     * @returns {List[ProblemMetaData]} A list of recommended problems
     */
    async getRecommendedProblems({ non_completed = 2, non_hard = 1, completed_practice = 2, refresh_recommendation_queues = true } = {}) {
        const recommended_problems = [];

        // Load the problems_manager problems
        // await this.problems_manager.loadProblems();
        await this.loaded_problem_manager;

        if (refresh_recommendation_queues) {
            this.populateRecommendationQueues();
        }

        // Gets the first two problems from first_non_completed_category_non_completed_problems
        recommended_problems.push(...this.first_non_completed_category_non_completed_problems.slice(0, non_completed));


        // Add 1 problem from first_non_only_hard_left_category_non_hard_problems
        recommended_problems.push(...this.first_non_only_hard_left_category_non_hard_problems.slice(0, non_hard));

        // Add 2 problem from completed_problems_sorted_by_times_completed
        recommended_problems.push(...this.completed_problems_sorted_by_times_completed.slice(0, completed_practice));

        return recommended_problems;

    }


    /**
     * Gets a list of problems that are not completed yet 
     * !note that the the this wont work if problem_manager is not loaded
     * @returns {ProblemMetaData[]} A list of problems that are not completed yet
     */
    getFirstNonCompletedCategoryNonCompletedProblems() {


        for (let i = 0; i < this.order_categories.length; i++) {

            const category = this.order_categories[i];
            const problems = this.problems_manager.getProblemsByCategory(category);

            const non_completed_problems = problems.filter(problem => !this.problemReport.isProblemCompleted(problem.slug));

            if (non_completed_problems.length > 0) {
                return non_completed_problems;
            }
            // Otherwise skip to the next category
        }
        return [];
    }


    /**
     * 
     * @returns {ProblemMetaData[]} A list of problems that are not completed yet, and are not hard
     */
    getFirstNonOnlyHardLeftCategoryNonHardProblems() {
        for (let category of this.order_categories) {

            const problems = this.problems_manager.getProblemsByCategory(category);



            // Get the non hard problems
            const non_hard_problems = problems.filter(problem => problem.difficulty != constants.difficulty.hard);

            // Also check that the non hard problems are not completed
            const non_completed_non_hard_problems = non_hard_problems.filter(problem => !this.problemReport.isProblemCompleted(problem.slug));

            // If there are non completed non hard problems, return them
            if (non_completed_non_hard_problems.length > 0) {
                return non_completed_non_hard_problems;
            }
            // Otherwise skip to the next category
        }
        return [];
    }

    /**
     * 
     * @returns {ProblemMetaData[]} A list of problems that are not completed yet, sorted by the number of times they have been completed
     */
    getCompletedProblemsSortedByTimesCompleted() {
        const completed_problems = this.problems_manager.getProblems().filter(problem => this.problemReport.isProblemCompleted(problem.slug));
        const sorted_problems = completed_problems.sort((a, b) => this.problemReport.getAnswerFor(a.slug) - this.problemReport.getAnswerFor(b.slug));
        return sorted_problems;
    }

    /**
     * Populates and opens a random problem, tests it, and returns the status of the problem.
     * @returns {ProblemStatus} The status of the problem
     */
    async openRandomProblem() {
        const problem = this.problems_manager.getRandomProblem();
        const problem_response = await this.solveProblem(problem);

        problem_response.is_problem_solved = problem_response.problem_status == constants.ProblemStatus.solved;
        return problem_response;
    }

    async openRandomClozeDSAProblem() {
        // const _ = await this.problemReport.getReport();
        await this.loaded_problem_manager;
        const selectedClozeProblem = this.problems_manager.getRandomProblemSlugWithCloze();
        // console.log("selectedClozeProblem", selectedClozeProblem);
        // console.log("problem manager problems?", this.problems_manager.problems)
        const problem = this.problems_manager.getProblem(selectedClozeProblem.problem_slug);
        // console.log("problem | problem received", problem);

        // Populate with that problem slug
        // this.problems_manager.copyFileToTemp(selectedClozeProblem.file_path, { base: constants.PATHS.base_cloze });
        problem.is_cloze = true;
        const problem_response = await this.solveProblem(problem, { base: constants.PATHS.base_cloze, populate_with_cloze_filepath: selectedClozeProblem.file_path });

        problem_response.is_problem_solved = problem_response.problem_status == constants.ProblemStatus.solved;
        return problem_response;
    }


    async postProblemSolution(problem, { attempts_timestamp = [], comments = [], comm = "" } = { }) {
        const absoluteFilePath = this.problems_manager.absolute_problem_file_path;

        const ACCOUNT_ID = Settings.account_id ?? 1;

        const formData = new FormData();
        formData.append('file', fs.createReadStream(absoluteFilePath), {
            filename: `_${ACCOUNT_ID}_${problem.slug}_solution.js`,
            contentType: 'text/javascript', // Set the content type to text/javascript
        });

        axios({
            method: 'post',
            url: this.uploadCodeFileUrl,
            data: formData,
            headers: {
                ...formData.getHeaders(), // Include necessary headers for form data
                'accept': 'application/json',
            },
        })
            .then(response => {
                // Handle the response here
                if (response.status === 200) {
                    const file_url = response.data.location;
                    console.log('File uploaded successfully to ' + file_url);


                    // Now post the metadata into 

                    /** Metadata
                     * {
                        "comments": [
                            "string"
                        ],
                        "code_url": "string",
                        "language": "string",
                        "date": "2023-10-03T17:57:26.591Z",
                        "attempt_counts": 0,
                        "attempt_timestamps": [
                            "2023-10-03T17:57:26.591Z"
                        ],
                        "is_correct": true,
                        "problem_slug": "string",
                        "account_id": 0
                        }
                     */

                    const metadata = {
                        "comments": comments,
                        "code_url": file_url,
                        "language": "javascript",
                        "date": getCurrentDateTimeIso(),
                        "attempt_counts": attempts_timestamp.length,
                        "attempt_timestamps": attempts_timestamp,
                        "is_correct": true,
                        "problem_slug": problem.slug,
                        "account_id": ACCOUNT_ID
                    };


                    // console.log("Posting metadata", metadata,"to url", uploadCodeMetadataUrl)
                    axios({
                        method: 'post',
                        url: this.uploadCodeMetadataUrl,
                        data: metadata,
                        headers: {
                            'accept': 'application/json',
                        },
                    }).then(
                        console.log("Metadata posted successfully", metadata)
                    ).catch(
                        error => {
                            console.log("Error from metadata", error);
                        }

                    );


                } else {
                    console.error('File upload failed');
                }
            })
            .catch(error => {
                console.error('Error:', error);
            });


    }


    /**
     * Updates the problem status, such as interfacing with the problem report and problem attempted (in the future this would create a report of things done.)
     * @param {ProblemMetadata} problem the problem to solve
     * @param {Response<this.openAndTest>} results 
     * @param {Object} statusMetadata reference to object contianing information such as failed attempts, etc that is being updated internally
     */
    updateProblemStatus(problem, results, statusMetadata = {}) {

        // Update internally the amounts of failed attempts
        statusMetadata.failed_attempts = results.details.failed_attempts || 0;
        console.log("Failed attempts", statusMetadata.failed_attempts);
        this.setCurrentProblemAttempts(results.details.failed_attempts);

        // Update the problem report
        if (DEBUG) console.log("problem_details", results.problem_details);
        statusMetadata.problem_details = results.problem_details;

        // Score to increase given this problem
        const scoreGivenDifficulty = {
            [constants.difficulty.easy]: 1,
            [constants.difficulty.medium]: 2,
            [constants.difficulty.hard]: 4
        };

        // Lowercase
        const difficulty_l = problem.difficulty.toLowerCase();
        if (DEBUG) console.log("problem.difficulty being increased", problem.difficulty, difficulty_l, scoreGivenDifficulty[problem.difficulty], scoreGivenDifficulty)

        statusMetadata.score_to_increase = scoreGivenDifficulty[difficulty_l] || 0;

    }

    /**
     * Wraps into continue solving until the problem is solved method
     * @param {ProblemMetaData} problem Information of the problem to solve
     * @param {boolean} tryUntilSolved If true, the problem will be reprompted until it is solved. If false, the problem will be solved once.
     * @returns {ProblemStatus} The status of the problem
     */
    async solveProblem(problem, { tryUntilSolved: try_until_solved = true, store_progress = true,
        populate_problem = true,
        populate_with_cloze_filepath = "", base = "" } = {}) {
        if (populate_problem) {

            if (populate_with_cloze_filepath != "") {

                this.problems_manager.populateTemplate({ file_path: populate_with_cloze_filepath }, { base: base });
            } else {

                this.problems_manager.populateTemplate(problem);

            }
        }

        let did_pass_all_tests = false
        const statusMetadata = {
            failed_attempts: this.getCurrentProblemAttempts(),
            is_cloze: problem.is_cloze ?? false,
        };

        // Try to solve the problem once.
        let results = await this.openAndTest(problem, { failed_attempts: statusMetadata.failed_attempts });
        let status = results.status;
        this.updateProblemStatus(problem, results, statusMetadata);



        while (!did_pass_all_tests && try_until_solved) {

            if (status == constants.ProblemStatus.aborted) {
                statusMetadata.status = constants.ProblemStatus.aborted;
                return statusMetadata;
            }

            else if (status == constants.ProblemStatus.solved) {
                this.problemReport.increaseAnswerFor(problem.slug);
                if (DEBUG) { console.log("Times the problem was solved.", this.problemReport.getAnswerFor(problem.slug)); }
                this.cleanCurrentProblem();
                did_pass_all_tests = true;
                statusMetadata.status = constants.ProblemStatus.solved;
                return statusMetadata;
            }

            else if (status == constants.ProblemStatus.unsolved) {
                continue; // Try again
            }

            const results = await this.openAndTest(problem, { failed_attempts: statusMetadata.failed_attempts });
            status = results.status;
            this.updateProblemStatus(problem, results, statusMetadata);

        }
    }

    /**
     * 
    * @param {ProblemMetaData} problem 
    * @param {boolean} open_problem_temporal If true, the problem temporal file will be opened
    * @param {boolean} open_solution If true, the solution file will be opened
    * @param {boolean} open_basecode If true, the basecode file will be opened
    * @param {boolean} open_markdown If true, the markdown file will be opened
    * @param {boolean} open_test_cases If true, the test cases file will be opened
    * @returns {Promise} A promise that resolves when the problem is opened
     */
    async openProblemMetadataInTerminal(problem, { open_problem_temporal = true, open_solution = false, open_basecode = false, open_markdown = false, open_test_cases = false } = {}) {


        let problem_details = this.problems_manager.getProblem(problem.slug);
        /**
            slug: 'character-replacement',
            file_path: 'character-replacement.js',  test_slug: 'character-replacement',
            name: 'Character Replacement',
            description: 'Longest Repeating Character Replacement',  
            difficulty: 'medium',
            tags: [ 'neetcode', 'medium', 'sliding-window' ],        
            absolute_solution_path: 'C:\\github\\testing\\mastery-cli\\utils\\dsa-cli\\solutions\\character-replacement.js'        
            }
        */

        let promblem_prompt = await getPromptDict(problem.slug);



        if (DEBUG) console.log("Problem prompt selected: ", promblem_prompt, "for problem", problem, "cloze?", problem.is_cloze);
        renderPromptDescription(promblem_prompt, problem_details, { is_cloze: problem.is_cloze ?? false });

        const editor_instruction = this.user_settings.common_editors[this.user_settings.editor];
        if (open_problem_temporal) {
            const _ = await this.problems_manager.openTemporalProblemFile({ editor_instruction: editor_instruction });
        }

        if (open_solution) {
            const _ = await this.problems_manager.openSolutionFile(problem.slug, { editor_instruction: editor_instruction });
        }
        if (open_basecode) {
            const _ = await this.problems_manager.openBaseCodeFile(problem.slug, { editor_instruction: editor_instruction });
        }
        if (open_markdown) {
            const _ = await this.problems_manager.openPromptMarkdownFile(problem.slug, { editor_instruction: editor_instruction });
        }

        if (open_test_cases) {
            const _ = await this.problems_manager.openTestCaseFile(problem.slug, { editor_instruction: editor_instruction })
        }

    }


    /**
     * Opens and tests prints a menu where user can choose to test, or other operations, returns once the user is finished with the problem or aborts
     * @param {ProblemMetadata} problem The problem to open and test
     * @returns {constants.ProblemStatus} The status of the problem (aborted | solved | unsolved)
     */
    async openAndTest(problem, { failed_attempts = 0, attempts_timestamp = [], comments = [], hintsGiven = [] } = {}) {
        if (DEBUG) console.log(
            "Opening problem: ", problem.slug,
        );
        // Print the problem markdown.

        // console.log("Keys from prompt_dict", Object.keys(prompt_dict));
        let problem_details = this.problems_manager.getProblem(problem.slug);
        await this.openProblemMetadataInTerminal(problem);
        
        let hints = problem
        let question_state_flag = true;
        let did_pass_all_tests_before = false;
        let cloze_problem_list = this.problems_manager.getProblemClozes(problem.slug);

        const choices = {

            "Modify": async () => {
                question_state_flag = true;
                await this.openProblemMetadataInTerminal(problem, { open_problem_temporal: true }); //By default opens the temrporal probelm file
            },
            
            'Run Tests': async () => {
                try {
                    // Sometimes errors can occur.
                    const did_pass_all_tests = await this.problems_manager.runProblem(problem);
                    if (did_pass_all_tests) {
                        did_pass_all_tests_before = true;

                    } else {
                        failed_attempts += 1;
                        attempts_timestamp.push(getCurrentDateTimeIso());
                    }
                    return { status: constants.ProblemStatus.unsolved, problem_details: problem_details, details: { failed_attempts: failed_attempts } };
                }
                catch (e) {
                    console.log("Error running tests: ", e);
                    return false;
                }
            },

            "Hint": async () => {
                // TO Complete
                let hintsMssage = "No hints available";
                if(hintsGiven.length < problem.hints.length){
                    hintsMssage = problem.hints[hintsGiven.length];
                    hintsGiven.push(hintsMssage);
                }
                question_state_flag = true;
                
                console.log(hintsGiven)
                show_image_if_isurl(hintsMssage);

            },
            "Copy Link": async () => {
                question_state_flag = true;
                // console.log(problem_details)
                console.log("Copy Link: ", problem_details?.link ?? "");

            }
            ,
            "Show solution": async () => {
                question_state_flag = true;
                this.openProblemMetadataInTerminal(problem, { open_problem_temporal: false, open_solution: true });

                // return constants.ProblemStatus.unsolved;
            },
            "Re Base": async () => {
                question_state_flag = true;
                // Repopulates the 
                // this.problems_manager.repopulateCode(problem.slug);
                this.problems_manager.populateTemplate(problem);
                // return constants.ProblemStatus.unsolved;
            },
            'Post Solution': async () => {
                question_state_flag = true;
                this.postProblemSolution(problem, { attempts_timestamp: attempts_timestamp, comments: comments });

            },
            'Comment': async () => {
                question_state_flag = true;
                // Ask for comment
                const prompt_comment = new Input(
                    {
                        name: 'comment',
                    }
                )

                const comment = await prompt_comment.run();
                comments.push(comment);
                console.log("All Comments: ");
                console.log(comments);
            },
            'Quit': async () => {
                question_state_flag = false;
                return { status: constants.ProblemStatus.aborted, problem_details: problem_details, details: { failed_attempts: failed_attempts } };
            }

        }

        const choices_dev_mode = {
            "Edit BaseJS": async () => {
                // Open the problem's base

                question_state_flag = true;
                this.openProblemMetadataInTerminal(problem, { open_problem_temporal: false, open_basecode: true });
            },
            "Edit Markdown prompt": async () => {
                // Open the problem's base

                question_state_flag = true;
                this.openProblemMetadataInTerminal(problem, { open_problem_temporal: false, open_markdown: true });
            },
            "Open test cases": async () => {
                question_state_flag = true;
                this.openProblemMetadataInTerminal(problem, { open_problem_temporal: false, open_test_cases: true });
            }
        }



        if (constants.DEV_MODE) Object.assign(choices, choices_dev_mode); // Add dev mode choices

        if (cloze_problem_list.length > 0) {

            Object.assign(choices, {
                cloze: async () => {
                    // Choose a random cloze problem to be solved
                    question_state_flag = true;
                    console.log("Populating the base prompt with a cloze problem");
                    const cloze_problems = cloze_problem_list;
                    // console.log("DEBUG | Cloze problems: ", cloze_problems);
                    if (cloze_problems.length == 0) {
                        console.log("No cloze problems found for this problem");
                        return
                    }

                    const selected_cloze_problem = get_random(cloze_problems);
                    // console.log("DEBUG | Selected cloze problem: ", selected_cloze_problem);
                    this.problems_manager.copyFileToTemp(selected_cloze_problem.file_path, { base: constants.PATHS.base_cloze });
                    console.log(" ==> CLOZE PROBLEM HAS BEEN COPIED TO  CURRENT PROBLEM <==");
                    // Open using modify to update the version
                    await this.openProblemMetadataInTerminal(problem, { open_problem_temporal: true });


                },
            });
        }

        let res = constants.ProblemStatus.unsolved;
        while (question_state_flag) {

            const selectable_choices_prompt = {};
            // Remove Submit, if test never passed before
            if (did_pass_all_tests_before) {

                Object.assign(selectable_choices_prompt, {
                    'Submit': async () => {
                        if (!did_pass_all_tests_before) {
                            console.log("You must pass all tests before submitting!");
                            // return false;
                            this.postProblemSolution(problem, { attempts_timestamp: attempts_timestamp, comments: comments });

                        } else {
                            console.log("Submission running", constants.ProblemStatus.solved);
                            question_state_flag = false;
                            // TODO Submit the current code that was there at least. to an post documnet.

                            return { status: constants.ProblemStatus.solved, details: { failed_attempts: failed_attempts }, problem_details: problem_details };

                        }
                    }
                })
            }

            Object.assign(selectable_choices_prompt, choices)
            // New prompt has to 
            let selectable_choices = Object.keys(selectable_choices_prompt);

            const prommpt_problem_menu = new AutoComplete({
                name: 'problem_menu',
                message: `Select action:`,
                choices: selectable_choices,
            });
            const choice_selected = await prommpt_problem_menu.run();
            res = await selectable_choices_prompt[choice_selected](); //Run the selected choice.
        }
        return res;
    }

    setCurrentProblem(problem_slug) {
        this.problemReport.setAnswerFor("current_problem", problem_slug);
    }

    getCurrentProblem() {
        return this.problemReport.getAnswerFor("current_problem");
    }

    setCurrentProblemAttempts(attempts) {
        this.problemReport.setAnswerFor("current_problem_attempts", attempts);
    }

    getCurrentProblemAttempts() {
        return this.problemReport.getAnswerFor("current_problem_attempts");

    }

    cleanCurrentProblem() {
        this.problemReport.setAnswerFor("current_problem", 0);
        this.problemReport.setAnswerFor("current_problem_attempts", 0);
    }


    /**
     * Renders a menu of recommended problems, and allows the user to select a problem to solve
     */
    async showRecommendedProblems() {

        const recommended_problems = await this.getRecommendedProblems();
        const problem_slugs = recommended_problems.map(problem => problem.slug);

        return await this.showMenuOfProblems({ allow_continue_last: true, show_progress: true, show_tags: true, show_specific_problems: problem_slugs });

    }

    /**
     * Renders a menu of problems, and allows the user to select a problem to solve
     * @param {boolean} allow_continue_last If true, the user will be allowed to continue the last problem solved. If false, the user will be forced to select a new problem.
     * @param {boolean} showProgress If true, the user will be shown the progress of the problems solved as ** attached to the problem. If false, the user will not be shown the progress.
     * @param {list[str]} show_specific_problems List of slugs of problems to show. If empty, all problems will be shown.
     * @returns 
     */
    async showMenuOfProblems({ allow_continue_last = true, show_progress = true, show_tags = true, show_specific_problems = [] } = {}) {
        const _ = await this.problemReport.getReport();


        /**
         * 
         * @param {list[str]} problemsSlugs List of slugs
         * @param {boolean} show_progress If true, the user will be shown the progress of the problems solved as ** attached to the problem. If false, the user will not be shown the progress.
         * @param {boolean} show_tags If true, the user will be shown the tags of the problems solved as ** attached to the problem. If false, the user will not be shown the tags.
         * OPTIONAL
         * @param {int} max_stars Maximum number of stars to show
         * @returns 
         */
        const createFormattedProblemMap = (problemsSlugs, { show_progress = true, max_stars = 5, show_tags = true }) => {
            const formattedProblems = {};
            for (const problemSlug of problemsSlugs) {
                let new_name = problemSlug
                if (show_progress) {
                    // Get the number of times the problem has been answered or the max number of stars, whichever is smallest
                    const times_answered = Math.min(this.problemReport.getAnswerFor(problemSlug), max_stars);
                    // console.log("Times answered: ", times_answered, "type", typeof times_answered)
                    const stars = times_answered > 0 ? "*".repeat(times_answered) : " [!] ";

                    new_name += stars;
                }

                if (show_tags) {
                    const tags = this.problems_manager.getTagsForProblem(problemSlug);
                    if (tags.length > 0) {
                        new_name += " (" + tags.join(", ") + ")";
                    }
                }
                formattedProblems[new_name] = problemSlug;
            }

            return formattedProblems; //Map of problem slug to formatted problem
        }

        // console.log("Loading problems...", this.loaded_problem_manager);
        await this.loaded_problem_manager;

        // Show specific problems, or show all problems
        const problems_to_show_slugs = show_specific_problems.length > 0 ? show_specific_problems : this.problems_manager.problemSlugs;
        const formattedProblems = createFormattedProblemMap(problems_to_show_slugs, { show_progress: show_progress, show_tags: show_tags });
        const current_problem_prompt = "Continue last problem";


        // So by default the first on on the list will be selected
        const choices = [];
        if (this.getCurrentProblem() != 0) {
            choices.push(current_problem_prompt)
        }
        choices.push(...Object.keys(formattedProblems));

        const prompt = new AutoComplete({
            name: 'problem',
            message: 'Select a problem',
            choices: choices,
            initial: current_problem_prompt in formattedProblems ? current_problem_prompt : 0
        });


        const problem_selected = await prompt.run();


        const getProblem = (choice_selected) => {
            if (choice_selected == current_problem_prompt) {
                return this.problems_manager.getProblem(this.getCurrentProblem());
            }
            const problem_slug = formattedProblems[problem_selected];
            const problem = this.problems_manager.getProblem(problem_slug);
            this.setCurrentProblem(problem_slug);
            return problem;

        }

        // return await this.openAndTest(problem);

        const problem = getProblem(problem_selected);
        const is_new_problem = problem_selected != current_problem_prompt;
        const problem_response = await this.solveProblem(problem, { populate_problem: is_new_problem });

        problem_response.is_problem_solved = problem_response.status == constants.ProblemStatus.solved;
        return problem_response;
    }


}



module.exports = DSATrainer;