﻿#include "IdleGame.h"
#include <random>
// Includes everything I need in this cpp file
// Not including using namespace std to avoid conflicts with other libraries that may have functions that are named the same and such

std::string Encrypt(std::string stringInput);
std::string Decrypt(std::string stringInput);

int main()
{
    // Sets the cursor shown in the console to hidden, this allows for my selections to look much better and for the cursor not to make it look like the user needs to input something
    Utilities::SetCursor(false);

    std::string selection;
    do
    {
        std::cout << GAMEHEADER << "\n\n";
        
        // If selecting enter game it will go to that function :D
        selection = Selection({
            {"Enter Game", MainGame},
            {"Quit", Quit}});
        
    } while (selection != "Quit");

    // Will end the program with a code 0
    return 0;
}

void MainGame()
{
    // Stars the background calculations of time
    StartAsync(bSecondsCancelFlag, seconds, SecondsTask);
    
    // Starts the background calculations of the money
    StartAsync(bMoneymakerCancelFlag, moneymaker, MoneymakerTask);

    std::string selection;
    do
    {
        std::cout << GAMEHEADER << "\n\n";
    
        selection = Selection({
            {"Buy an Item", BuyItem},
            {"View Money", ViewMoney},
            {"High Score", HighScore},
            {"Quit", ExitGame}});
        
    } while (selection != "Quit");
}

// Counts the time that you were in game, this gives the user a stat for them to want to aim higher for :)
void SecondsTask(const std::atomic<bool>& bCancelFlag)
{
    while (!bCancelFlag.load())
    {
        ThreadSleep(bCancelFlag, 1);
        gameInfo.currentSeconds++;
    }
}

void MoneymakerTask(const std::atomic<bool>& bCancelFlag)
{
    // Creates this once as it will be reset in the code
    // This for loop will loop through the items you own. It will add an amount that the item generates to your wallet
    std::vector<std::string> generatedItemMoney;
    bool inside = false;
    
    while(!bCancelFlag.load())
    {
        // Every second it will update this task
        ThreadSleep(bCancelFlag, 1);

        // If the view money section bool is false make inside false this updates each second
        if (!bViewMoney)
        {
            inside = false;
        }

        // If you have nothing in your inventory or the seconds is not % 5 then it will not run this code block
        if (!gameInfo.itemsYouOwn.empty() && gameInfo.currentSeconds % 5 == 0)
        {
            // If view money then clear the lines to clear and update the lines. The inside bool would make it so if you are inside for more than one loop
            // it will clear more lines as they would be generated by then
            if (bViewMoney)
            {
                if (inside)
                {
                    Utilities::ClearLine(static_cast<int>(generatedItemMoney.size()) + 2);
                }
                else
                {
                    Utilities::ClearLine(2);
                    inside = true;
                }

                // After clearing the lines it adds this line back with your updated money amount
                std::cout << "You have $" << ConvertToCurrency(std::to_string(gameInfo.money)) << " in your wallet (To exit press the 'Enter' key.)\n\n";
            }
            
            // Resets generated money to nothing, I reset it here because I needed to keep the amount of items in the inventory, so I could know how many lines to remove
            generatedItemMoney = {};
            
            // For each item in the items you own it will go through them
            for (auto& itemSingular : gameInfo.itemsYouOwn)
            {
                // If the item is found then it does not generate the money for the item type and skips to the next item
                if (std::ranges::find(generatedItemMoney, itemSingular) != generatedItemMoney.end())
                {
                    continue;
                }

                // Adds the current item to make sure they don't get generated twice
                generatedItemMoney.push_back(itemSingular);
                
                // If the item is found in the inventory then calculate the price * by the amount you have so there is less text on the screen
                // The ranges is a new addition in C++20 and makes it less error-prone and more easy, I can also do std::find(items.start(), items.end(), itemSingular) this would go through the pointers and see if the item is in the inventory
                if (std::ranges::find(gameInfo.itemsYouOwn, itemSingular) != gameInfo.itemsYouOwn.end())
                {
                    // Gets the amount of items you own of the current single item
                    const int numberOfItems = static_cast<int>(std::ranges::count(gameInfo.itemsYouOwn, itemSingular));

                    // Calculates the money you will gain on that item by getting the single item from the map and getting the moneyEachPayout multiplied by the amount the user owns
                    const int moneyGained = items[itemSingular].moneyEachPayout * numberOfItems;
                    
                    // This is then added and set to the users money
                    gameInfo.money += moneyGained;

                    // This section will only be viewable if the bViewMoney is true
                    if (bViewMoney)
                    {
                        std::cout << "You have gained $" << moneyGained
                        << " from the " << itemSingular << " x" << numberOfItems << " you now have $"
                        << ConvertToCurrency(std::to_string(gameInfo.money)) << " in your wallet\n";
                    }
                }
            }
        }
    }
}

void BuyItem()
{
    std::string selectionYesNo;
    do
    {
        std::cout << GAMEHEADER << "\nYou have $" << ConvertToCurrency(std::to_string(gameInfo.money)) << " on you. Here are some options to do with it:\n";
        
        // Item selection menu for the user
        const std::string itemSelection = Selection({
            {"car - $50", "car"},
            {"house - $100", "house"},
            {"boat - $150", "boat"},
            {"plane - $200", "plane"},
            {"spaceship - $250", "spaceship"}});

        // If the user has less money than the item price they can't buy it, so it will break out of the buying while loop, if it is valid it will go on to the code after this statement
        if (gameInfo.money < items[itemSelection].price)
        {
            Utilities::ClearConsole();

            std::cout << GAMEHEADER "\n";
            std::cout << "You do not have enough money to buy that item\n";
            
            Utilities::Wait();
            Utilities::ClearConsole();
            break;
        }
        
        // Money is subtracted from the user's wallet by setting the money variable
        gameInfo.money -= items[itemSelection].price;
        
        Utilities::ClearConsole();
        
        // Adds the item to the user's inventory (using a vector because I have no idea how much the user will buy, and a vector can not be easily be resized at runtime)
        gameInfo.itemsYouOwn.push_back(itemSelection);

        std::cout << GAMEHEADER << "\nYou have bought " << itemSelection << " for $" << items[itemSelection].price << ". You now have $"
        << ConvertToCurrency(std::to_string(gameInfo.money)) << " left in your wallet\nDo you want to buy something else?\n";
        
        // This is a crude way to do this, but I have done it as you input the text that will stay at the top, and then it will clear the whole console adding back the top text and the selection back
        selectionYesNo = Selection({
            {"Yes", "Yes"},
            {"No", "No"}});
        
        Utilities::ClearConsole();

    } while (selectionYesNo != "No");
}

void ViewMoney()
{
    std::cout << GAMEHEADER "\n";
    
    // Pre showing the content before update, this is so the content does not wait a second for the update to happen
    if(gameInfo.itemsYouOwn.empty())
    {
        std::cout << "You own nothing, go and buy something to earn money\n";
    }
    
    std::cout << "You gain money every 5 seconds, the money varies with the items.\n";
    std::cout << "You have $" << ConvertToCurrency(std::to_string(gameInfo.money)) << " in your wallet (To exit press the 'Enter' key.)\n\n";

    // This makes it so the money update is updating and is viewable
    bViewMoney = true;

    // This stops this functionality when the user presses the return key
    bViewMoney = Utilities::Wait(false);
    Utilities::ClearConsole();
}

void HighScore()
{
    // Uses filesystem to see if a score exists is, so it will show the user
    std::string scoreFromFile;
    if (std::filesystem::exists(highScoreFile))
    {
        // Gets the score from the file, and then shows it to the user. If there is no score it will tell the user the high score does not exist
        std::ifstream readHighScore(highScoreFile);
        getline(readHighScore, scoreFromFile);
        readHighScore.close();
    }

    std::cout << GAMEHEADER "\n";

    if (std::filesystem::exists(highScoreFile))
    {
	    // Tests if the score is a valid int
	    const int scoreInt = ConvertToInt(Decrypt(scoreFromFile));
	    if (scoreInt == -1)
	    {
	        std::cerr << "Saved score is invalid please check or remove " << highScoreFile << '\n';
	        Utilities::Wait();
	        Utilities::ClearConsole();
	        return;
	    }

        std::cout << "Your high score is $" << ConvertToCurrency(std::to_string(scoreInt)) << "\n";
    }
    else
    {
        std::cout << "High score does not exist" << "\n";
    }

    Utilities::Wait();
    Utilities::ClearConsole();
}

void ExitGame()
{
    // Stops the async tasks by setting the cancel flag to true
    bSecondsCancelFlag.store(true);
    bMoneymakerCancelFlag.store(true);

    std::cout << GAMEHEADER "\n";
    
    // Checks if the user has bought any items
    if (gameInfo.itemsYouOwn.empty())
    {
        std::cout << "You have not bought any items\n";
    }
    else
    {
        // Loops through the items the user has bought and prints them
        std::cout << "You have bought the following item(s): ";

        std::vector<std::string> singleItemsVector;
        for (auto& singleItem : gameInfo.itemsYouOwn)
        {
            // If the item is already in the temp I have already counted it and added it to the output
            if (std::ranges::find(singleItemsVector, singleItem) != singleItemsVector.end())
            {
                continue;
            }

            // Adds the current item to make sure they don't get generated twice
            singleItemsVector.push_back(singleItem);
        }
        
        std::string ending;
        for (auto singleItem = singleItemsVector.begin(); singleItem != singleItemsVector.end(); ++singleItem)
        {
            
            const int count = static_cast<int>(std::ranges::count(gameInfo.itemsYouOwn, *singleItem));

            // The prev function makes it, so it checks if the item is the last one in the vector, if so it will add a new line instead of a comma to the end of the string
            ending += *singleItem + " x" + std::to_string(count) + (singleItem == std::prev(singleItemsVector.end()) ? "\n" : ", ");
        }

        // Prints out the string we created earlier
        std::cout << ending;
    }

    // Calls the save game function to make sure the high score is saved
    SaveGame();

    // Tells the user how long they have played the game for
    std::cout << "You played this game for " << gameInfo.currentSeconds / 60 << "m "<< gameInfo.currentSeconds % 60 << "s\n";
    
    // This will pause the program and display press the enter key to continue...
    Utilities::Wait();
    Utilities::ClearConsole();
}

void SaveGame()
{
    // Checks if the file exists
    if (std::filesystem::exists(highScoreFile))
    {
        // Opens the file and reads it, it gets the first line and sets it to the string variable I created
        std::ifstream readHighScore(highScoreFile);
        std::string scoreFromFile;
        getline(readHighScore, scoreFromFile);

        // Converts the string into a number, it will try to convert the score to an int, if it can't do that instead of crashing it will return an error.
        // If it is only alpha characters it will error and display that to the user
        const int scoreInt = ConvertToInt(Decrypt(scoreFromFile));
        if (scoreInt == -1)
        {
            std::cerr << "Saved score is invalid please check or remove " << highScoreFile << '\n';
            return;
        }
        
        readHighScore.close();

        // Checks if the saved score is higher than the current money, if so it will tell you that you did not get a new high score, and it will exit the function.
        // This exit is so the code that will run if the file does not exist or if the money gained is more than the saved score, it needs to be there because I needed
        // to check what the score was before saving it to the file. In this context I am recreating the file when I save the file, so I don't need a reference to
        // the old save file :D
        
        if (scoreInt > gameInfo.money)
        {
            std::cout << "Your high score for money was $" << ConvertToCurrency(std::to_string(scoreInt)) << " you got $" << ConvertToCurrency(std::to_string(gameInfo.money)) << " try again next time to get a new high score!\n";
            return;
        }
    }
    
    // This will only run if the file does not exist, or you get more score than what is saved
    std::cout << "Your high score for money was $" << ConvertToCurrency(std::to_string(gameInfo.money)) << " This is a new record! Yippee!\n";
    std::ofstream newHighScore(highScoreFile);
    newHighScore << Encrypt(std::to_string(gameInfo.money));
    newHighScore.close();
}

void Quit()
{
    std::cout << GAMEHEADER "\nThank you for playing! Hope you enjoyed the small game!\n";
    Utilities::Wait();
}

// Selection of converters. These are some custom converters I am using to help me manage strings to integers and strings to currency with commas

// Converts a string to an int
int ConvertToInt(const std::string& stringInput)
{
    std::string result;
    for (const char ch : stringInput)
    {
        // If the character is not a digit then return -1 as the user will not have negative numbers
        if (!isdigit(ch))
        {
            return -1;
        }
        
        result += std::string(1, ch);
    }

    // Tries to convert to int
    try
    {
        int temp = std::stoi(result);
    }
    catch (std::out_of_range&)
    {
        // If it is out of range then it will return the max value it can hold
        return INT32_MAX;
    }
    
    // Returns the string result as an int as it is a valid int with no alpha characters or symbols
    return std::stoi(result);
}

// Converts a string to a currency with commas such as 300200100 to 300,200,100
std::string ConvertToCurrency(const std::string& stringInput)
{
    std::string result;
    // Loops through the string
    for (int i = 0; i < static_cast<int>(stringInput.size()); i++)
    {
        // Checks if the current iteration is not the last or first item
        if (i != static_cast<int>(stringInput.length()) - 1 && i != 0)
        {
            // If the iteration modded by 3 == the length % 3 then it will add a comma. e.g. 1234 would check the 2 and that would be 1 (iteration) % 3 = 1 and 4 (length) % 3 = 1 which would add a comma
            if (i % 3 == stringInput.length() % 3)
            {
                result += ",";
            }
        }

        // Then it adds the number back to the result
        result += stringInput[i];
    }

    // Returns the result at the end
    return result;
}

std::string Encrypt(std::string stringInput)
{
    // Random seed for encryption
    srand(5);
    
    for (char& ch : stringInput)
    {
        ch = static_cast<char>(ch + rand() % 25);
    }

    return stringInput;
}

std::string Decrypt(std::string stringInput)
{
    // Random seed for encryption
    srand(5);
    
    for (char& ch : stringInput)
    {
        ch = static_cast<char>(ch - rand() % 25);
    }

    return stringInput;
}