/********************************************************************************
 * File: mod_funcs.cpp
 * Author: ppkantorski
 * Description:
 *   This source file provides the implementations of functions declared in
 *   mod_funcs.hpp. These functions handle the conversion of `.pchtxt` mod files
 *   into `.ips` binary patches used by the Ultrahand Overlay project. This includes
 *   parsing, validating, and encoding patch data into the IPS format.
 *
 *   For the latest updates and contributions, visit the project's GitHub repository.
 *   (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
 *
 *   Note: Please be aware that this notice cannot be altered or removed. It is a part
 *   of the project's documentation and must remain intact.
 * 
 *  Licensed under both GPLv2 and CC-BY-4.0
 *  Copyright (c) 2024 ppkantorski
 ********************************************************************************/

#include <mod_funcs.hpp>

namespace ult {

    //const std::string CHEAT_HEADER = "// auto generated by pchtxt2cheat\n\n";
    const std::string CHEAT_TYPE = "04000000";
    const std::string CHEAT_EXT = ".txt";
    const std::string CHEAT_ENCODING = "ascii";
    
    
    
    /**
     * @brief Checks if a cheat already exists in the cheat file.
     * @param cheatFilePath The path to the cheat file.
     * @param newCheat The new cheat to check.
     * @return True if the cheat exists, otherwise false.
     */
    bool cheatExists(const std::string& cheatFilePath, const std::string& newCheat) {
    #if !USING_FSTREAM_DIRECTIVE
        FILE* cheatFile = fopen(cheatFilePath.c_str(), "r");  // Open the cheat file in read mode
        if (!cheatFile) {
            return false;  // Return false if the file cannot be opened
        }
    
        //size_t len;
        char buffer[1024];  // Buffer to store each line
        while (fgets(buffer, sizeof(buffer), cheatFile)) {
            // Remove newline character, if present
            const size_t len = strlen(buffer);
            if (len > 0 && buffer[len - 1] == '\n') {
                buffer[len - 1] = '\0';
            }
            if (newCheat == buffer) {
                fclose(cheatFile);  // Close the file before returning
                return true;  // Cheat exists
            }
        }
    
        fclose(cheatFile);  // Close the file after processing
        return false;  // Cheat does not exist
    #else
        std::ifstream cheatFile(cheatFilePath);
        if (!cheatFile) {
            return false;  // Return false if the file cannot be opened
        }
    
        std::string line;
        while (std::getline(cheatFile, line)) {
            if (line == newCheat) {
                return true;  // Cheat exists
            }
        }
        return false;  // Cheat does not exist
    #endif
    }
    
    /**
     * @brief Appends a new cheat to the cheat file.
     * @param cheatFilePath The path to the cheat file.
     * @param newCheat The new cheat to append.
     */
    void appendCheatToFile(const std::string& cheatFilePath, const std::string& newCheat) {
    #if !USING_FSTREAM_DIRECTIVE
        FILE* cheatFile = fopen(cheatFilePath.c_str(), "a");  // Open the cheat file in append mode
        if (!cheatFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Failed to open cheat file for appending: " + cheatFilePath);
            #endif
            return;  // Handle the error accordingly
        }
    
        fprintf(cheatFile, "%s\n", newCheat.c_str());  // Write the new cheat followed by a newline
        fclose(cheatFile);  // Close the file
    #else
        std::ofstream cheatFile(cheatFilePath, std::ios::app);
        if (!cheatFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Failed to open cheat file for appending: " + cheatFilePath);
            #endif
            return;  // Handle the error accordingly
        }
    
        cheatFile << newCheat << std::endl;  // Write the new cheat
    #endif
    }
    
    
    /**
     * @brief Extracts the cheat name from the given file path.
     * @param filePath The full file path.
     * @return The extracted cheat name.
     */
    std::string extractCheatName(const std::string &filePath) {
        const size_t lastSlash = filePath.find_last_of("/\\");
        if (lastSlash == std::string::npos) {
            return "";
        }
        const size_t secondLastSlash = filePath.find_last_of("/\\", lastSlash - 1);
        if (secondLastSlash == std::string::npos) {
            return "";
        }
        const std::string lastDir = filePath.substr(secondLastSlash + 1, lastSlash - secondLastSlash - 1);
        std::string fileName = filePath.substr(lastSlash + 1);
        const size_t dotPos = fileName.find_last_of('.');
        if (dotPos != std::string::npos) {
            fileName = fileName.substr(0, dotPos);
        }
    
        // If lastDir contains " - ", extract the part after " - "
        const size_t dashPos = lastDir.find(" - ");
        std::string cheatName = lastDir;
        if (dashPos != std::string::npos) {
            cheatName = lastDir.substr(dashPos + 3);
        }
    
        return cheatName + " " + fileName;
    }
    
    // Helper function to determine if a string is a valid title ID
    bool isValidTitleID(const std::string &str) {
        if (str.length() != 16) return false;
        for (char c : str) {
            if (!std::isxdigit(c)) return false; // Check if each character is a hexadecimal digit
        }
        return true;
    }
    
    // Function to find the title ID in the text, avoiding the @nsobid- line
    std::string findTitleID(const std::string &text) {
        const size_t nsobidPos = text.find("@nsobid-");
        const size_t startPos = (nsobidPos != std::string::npos) ? nsobidPos + 40 + 8 : 0; // Skip past @nsobid- and its value
        
        std::string potentialID;
        for (size_t i = startPos; i <= text.length() - 16; ++i) {
            potentialID = text.substr(i, 16);
            if (isValidTitleID(potentialID)) {
                return potentialID;
            }
        }
        return "";
    }
    
    
    /**
     * @brief Converts a .pchtxt file to a cheat file.
     * @param pchtxtPath The file path to the .pchtxt file.
     * @param cheatName The name of the cheat.
     * @param outCheatPath The file path for the output cheat file.
     * @return True if the conversion was successful, false otherwise.
     */
    bool pchtxt2cheat(const std::string &pchtxtPath, std::string cheatName, std::string outCheatPath) {
        #if USING_LOGGING_DIRECTIVE
        if (!disableLogging)
            logMessage("Starting pchtxt2cheat with pchtxtPath: " + pchtxtPath);
        #endif
    
    #if !USING_FSTREAM_DIRECTIVE
        FILE* pchtxtFile = fopen(pchtxtPath.c_str(), "r");
        if (!pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false;
        }
    
        // Read the entire file into a string
        std::string pchtxt;
        char buffer[4096];
        while (fgets(buffer, sizeof(buffer), pchtxtFile)) {
            pchtxt += buffer;
        }
        fclose(pchtxtFile);
    #else
        std::ifstream pchtxtFile(pchtxtPath);
        if (!pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false;
        }
    
        std::string pchtxt((std::istreambuf_iterator<char>(pchtxtFile)), std::istreambuf_iterator<char>());
    #endif
    
        const size_t nsobidPos = pchtxt.find("@nsobid-");
        if (nsobidPos == std::string::npos) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Could not find bid in pchtxt file, the file is likely invalid.");
            #endif
            return false;
        }
    
        const std::string bid = pchtxt.substr(nsobidPos + 8, 40);
        const std::string bidShort = bid.substr(0, 16);
    
        const std::string tid = findTitleID(pchtxt);
        if (tid.empty()) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Could not find TID in pchtxt file, the file is likely invalid.");
            #endif
            return false;
        }
    
        std::string cheatFilePath;
    
        if (outCheatPath.empty()) {
            const std::string folderPath = "sdmc:/atmosphere/contents/" + tid + "/cheats/";
            createDirectory(folderPath);
            cheatFilePath = folderPath + bidShort + CHEAT_EXT;
        } else {
            cheatFilePath = outCheatPath;
        }
    
    #if !USING_FSTREAM_DIRECTIVE
        FILE* existingCheatFile = fopen(cheatFilePath.c_str(), "r");
        bool cheatNameExists = false;
        if (existingCheatFile) {
            char line[256];
            while (fgets(line, sizeof(line), existingCheatFile)) {
                if (std::string(line) == "[" + cheatName + "]\n") {
                    cheatNameExists = true;
                    break;
                }
            }
            fclose(existingCheatFile);
        }
    #else
        std::ifstream existingCheatFile(cheatFilePath);
        bool cheatNameExists = false;
        if (existingCheatFile) {
            std::string line;
            while (std::getline(existingCheatFile, line)) {
                if (line == "[" + cheatName + "]") {
                    cheatNameExists = true;
                    break;
                }
            }
        }
        existingCheatFile.close();
    #endif
    
        // Open output cheat file
    #if !USING_FSTREAM_DIRECTIVE
        FILE* outCheatFile = fopen(cheatFilePath.c_str(), "a");
        if (!outCheatFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to create cheat file " + cheatFilePath);
            #endif
            return false;
        }
    #else
        std::ofstream outCheatFile(cheatFilePath, std::ios::app);
        if (!outCheatFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to create cheat file " + cheatFilePath);
            #endif
            return false;
        }
    #endif
    
        if (!cheatNameExists) {
    #if !USING_FSTREAM_DIRECTIVE
            fprintf(outCheatFile, "[%s]\n", cheatName.c_str());
    #else
            outCheatFile << "[" << cheatName << "]\n";
    #endif
        }
    
        int offset = 0;
        bool enabled = true;
        int validCheatsProcessed = 0; // ADDED: Track number of valid cheats processed
        StringStream iss(pchtxt);  // Use your custom StringStream
        std::string line;
        
        std::string addrStr, valStr;
        size_t spacePos;
        int codeOffset;
        std::string cheatLine;
        char offsetBuffer[9];
    
        // Use your custom getline method instead of std::getline
        while (iss.getline(line, '\n')) {  // Custom getline with newline as the delimiter
    
            // strip inline C++-style comments
            const auto slashPos = line.find("//");
            if (slashPos != std::string::npos)
                line = line.substr(0, slashPos);
            
            // strip inline hash comments (but leave full-line # for headers)
            const auto hashPos = line.find('#');
            if (hashPos != std::string::npos && hashPos > 0)
                line = line.substr(0, hashPos);
            
            trim(line);
            if (line.empty() || line[0] == '#') continue;
    
            if (line.find("@flag offset_shift ") == 0) {
                const std::string offsetStr = line.substr(19);
                offset = (offsetStr.find("0x") == 0 ? std::strtol(offsetStr.c_str(), nullptr, 16) : std::strtol(offsetStr.c_str(), nullptr, 10)) - 0x100;
                continue;
            }
    
            if (line.find("@enabled") == 0) {
                enabled = true;
                continue;
            }
    
            if (line.find("@disabled") == 0) {
                enabled = false;
                continue;
            }
    
            if (line.find("@stop") == 0) {
                break;
            }
    
            if (!enabled) {
                continue;
            }
    
            spacePos = line.find(' ');
            if (spacePos == std::string::npos) {
                continue;
            }
    
            addrStr = line.substr(0, spacePos);
            valStr = line.substr(spacePos + 1);
    
            if (addrStr.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos || valStr.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) {
                continue;
            }
    
            codeOffset = std::strtol(valStr.c_str(), nullptr, 16) + offset;
            snprintf(offsetBuffer, sizeof(offsetBuffer), "%08X", codeOffset);
            cheatLine = CHEAT_TYPE + " " + addrStr + " " + hexToReversedHex(offsetBuffer);
            
    #if !USING_FSTREAM_DIRECTIVE
            // Check if cheat already exists
            FILE* checkFile = fopen(cheatFilePath.c_str(), "r");
            bool exists = false;
            if (checkFile) {
                char checkLine[256];
                while (fgets(checkLine, sizeof(checkLine), checkFile)) {
                    if (std::string(checkLine) == cheatLine + "\n") {
                        exists = true;
                        break;
                    }
                }
                fclose(checkFile);
            }
    
            if (!exists) {
                fprintf(outCheatFile, "%s\n", cheatLine.c_str());
                validCheatsProcessed++; // ADDED: Increment counter for new cheats
            }
    #else
            if (!cheatExists(cheatFilePath, cheatLine)) {
                outCheatFile << cheatLine << "\n";
                validCheatsProcessed++; // ADDED: Increment counter for new cheats
            }
    #endif
        }
    
    #if !USING_FSTREAM_DIRECTIVE
        fclose(outCheatFile);
    #else
        outCheatFile.close();
    #endif
    
        // ADDED: Check if any valid cheats were processed
        if (validCheatsProcessed == 0) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Warning: No valid patch data found to convert to cheats in " + pchtxtPath);
            #endif
            return false;
        }
    
        return true;
    }
    
    
    // Corrected helper function to convert values to big-endian format
    uint32_t toBigEndian(uint32_t value) {
        return ((value & 0x000000FF) << 24) |
               ((value & 0x0000FF00) << 8)  |
               ((value & 0x00FF0000) >> 8)  |
               ((value & 0xFF000000) >> 24);
    }
    
    uint16_t toBigEndian(uint16_t value) {
        return ((value & 0x00FF) << 8) |
               ((value & 0xFF00) >> 8);
    }
    
    // Helper function to convert a vector of bytes to a hex string for logging
    
    std::string hexToString(const std::vector<uint8_t>& bytes) {
        StringStream oss;  // Use your custom StringStream
        oss.hex();  // Enable hex mode for the stream
    
        for (uint8_t byte : bytes) {
            if (byte < 0x10) {
                oss << "0";  // Append leading zero for single-digit hex values
            }
            oss << static_cast<int>(byte);  // Convert byte to int and then append it
        }
    
        return oss.str();  // Return the final hex string
    }


    
    
        
    /**
     * @brief Converts a .pchtxt file to an IPS file using fstream.
     *
     * This function reads the contents of a .pchtxt file, extracts the address-value pairs,
     * and generates an IPS file with the provided output folder.
     *
     * @param pchtxtPath The file path to the .pchtxt file.
     * @param outputFolder The folder path for the output IPS file.
     * @return True if the conversion was successful, false otherwise.
     */
    bool pchtxt2ips(const std::string& pchtxtPath, const std::string& outputFolder) {
    #if !USING_FSTREAM_DIRECTIVE
        // Use FILE* for reading
        FILE* pchtxtFile = fopen(pchtxtPath.c_str(), "r");
        if (!pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false;
        }
    
        std::vector<std::pair<uint32_t, std::vector<uint8_t>>> patches;
        char lineBuffer[512];  // Fixed: Use char buffer for fgets
        std::string line;
        uint32_t lineNum = 0;
        std::string nsobid;
        int offset = 0; // Default offset
        bool enabled = true;
    
        uint32_t address;
        uint8_t byte;
        std::vector<uint8_t> valueBytes;
        std::string offsetStr;
    
        while (fgets(lineBuffer, sizeof(lineBuffer), pchtxtFile) != nullptr) {
            line = lineBuffer;  // Convert to string
            ++lineNum;
            if (line.empty() || line.front() == '@' || !enabled) {
                if (line.find("@nsobid-") == 0) {
                    nsobid = line.substr(8);
                }
                if (line.find("@flag offset_shift ") == 0) {
                    offsetStr = line.substr(19);
                    offset = (offsetStr.find("0x") == 0 ? std::strtol(offsetStr.c_str(), nullptr, 16) : std::strtol(offsetStr.c_str(), nullptr, 10));
                }
                if (line.find("@enabled") == 0) {
                    enabled = true;
                    continue;
                }
                if (line.find("@disabled") == 0) {
                    enabled = false;
                    continue;
                }
                if (line.find("@stop") == 0) {
                    break;
                }
                continue;  // Skip empty lines and lines starting with '@'
            }
            
            StringStream iss(line);
            std::string addressStr, valueStr;
    
            if (!(iss >> addressStr >> valueStr)) {
                continue;
            }
            
            char* endPtr;
            address = std::strtoul(addressStr.c_str(), &endPtr, 16) + offset; // Adjust address by offset
            if (*endPtr != '\0') {
                continue;
            }
            
            for (size_t i = 0; i < valueStr.length(); i += 2) {
                byte = ult::stoi(valueStr.substr(i, 2), nullptr, 16);
                valueBytes.push_back(byte);
            }
            
            if (valueBytes.empty()) {
                continue;
            }
            
            patches.push_back(std::make_pair(address, valueBytes));
            valueBytes.clear();
        }
        
        fclose(pchtxtFile);
    
    #else
        // Use fstream for reading
        std::ifstream pchtxtFile(pchtxtPath);
        if (!pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false;
        }
    
        std::vector<std::pair<uint32_t, std::vector<uint8_t>>> patches;
        std::string line;
        uint32_t lineNum = 0;
        std::string nsobid;
        int offset = 0; // Default offset
    
        uint32_t address;
        uint8_t byte;
        std::vector<uint8_t> valueBytes;
        std::string offsetStr;
    
        while (std::getline(pchtxtFile, line)) {
            ++lineNum;
            if (line.empty() || line.front() == '@') {
                if (line.find("@nsobid-") == 0) {
                    nsobid = line.substr(8);
                }
                if (line.find("@flag offset_shift ") == 0) {
                    offsetStr = line.substr(19);
                    offset = (offsetStr.find("0x") == 0 ? std::strtol(offsetStr.c_str(), nullptr, 16) : std::strtol(offsetStr.c_str(), nullptr, 10));
                }
                if (line.find("@stop") == 0) {
                    break;
                }
                continue;  // Skip empty lines and lines starting with '@'
            }
    
            StringStream iss(line);
            std::string addressStr, valueStr;
    
            if (!(iss >> addressStr >> valueStr)) {
                continue;
            }
    
            char* endPtr;
            address = std::strtoul(addressStr.c_str(), &endPtr, 16) + offset; // Adjust address by offset
            if (*endPtr != '\0') {
                continue;
            }
    
            for (size_t i = 0; i < valueStr.length(); i += 2) {
                byte = ult::stoi(valueStr.substr(i, 2), nullptr, 16);
                valueBytes.push_back(byte);
            }
    
            if (valueBytes.empty()) {
                continue;
            }
    
            patches.push_back(std::make_pair(address, valueBytes));
            valueBytes.clear();
        }
    
        pchtxtFile.close();
    #endif
    
        // CHECK: Return false if no patches were found
        if (patches.empty()) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Warning: No valid patches found in " + pchtxtPath);
            #endif
            return false;
        }
    
        if (nsobid.empty()) {
            nsobid = pchtxtPath.substr(pchtxtPath.find_last_of("/\\") + 1);
            nsobid = nsobid.substr(0, nsobid.find_last_of("."));
        }
    
        // Trim any newline characters from nsobid
        trim(nsobid);
        trimNewline(nsobid);
    
        const std::string ipsFileName = nsobid + ".ips";
        const std::string ipsFilePath = outputFolder + ipsFileName;
    
    #if !USING_FSTREAM_DIRECTIVE
        // Use FILE* for writing
        FILE* ipsFile = fopen(ipsFilePath.c_str(), "wb");
        if (!ipsFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to create IPS file " + ipsFilePath);
            #endif
            return false;
        }
    
        fwrite(IPS32_HEAD_MAGIC, sizeof(char), std::strlen(IPS32_HEAD_MAGIC), ipsFile);
    
        uint16_t valueLength;
        uint32_t bigEndianAddress;
    
        for (const auto& patch : patches) {
            bigEndianAddress = toBigEndian(patch.first);  // Convert address to big-endian
            fwrite(&bigEndianAddress, sizeof(bigEndianAddress), 1, ipsFile);  // Write address
    
            valueLength = toBigEndian(static_cast<uint16_t>(patch.second.size()));  // Convert length to big-endian
            fwrite(&valueLength, sizeof(valueLength), 1, ipsFile);  // Write length of value
    
            fwrite(patch.second.data(), sizeof(uint8_t), patch.second.size(), ipsFile);  // Write value
        }
    
        fwrite(IPS32_FOOT_MAGIC, sizeof(char), std::strlen(IPS32_FOOT_MAGIC), ipsFile);
        fclose(ipsFile);
    
    
        FILE* _pchtxtFile = fopen(pchtxtPath.c_str(), "r");
        if (!_pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false; // Changed: Return false if we can't read the file for title ID
        }
    
        // Read the entire file into a string
        std::string pchtxt;
        char buffer[4096];
        while (fgets(buffer, sizeof(buffer), _pchtxtFile)) {
            pchtxt += buffer;
        }
        fclose(_pchtxtFile);
    
    
        const std::string tid = findTitleID(pchtxt);
        if (!tid.empty()) {
            const std::string tidFilePath = outputFolder + tid;
            FILE* tidFile = fopen(tidFilePath.c_str(), "w");
            if (tidFile) {
                fclose(tidFile);  // Creates an empty file
            }
        } else {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Warning: Could not find Title ID in " + pchtxtPath);
            #endif
        }
    #else
        // Use fstream for writing
        std::ofstream ipsFile(ipsFilePath, std::ios::binary);
        if (!ipsFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to create IPS file " + ipsFilePath);
            #endif
            return false;
        }
    
        ipsFile.write(IPS32_HEAD_MAGIC, std::strlen(IPS32_HEAD_MAGIC));
    
        uint16_t valueLength;
        uint32_t bigEndianAddress;
        for (const auto& patch : patches) {
            bigEndianAddress = toBigEndian(patch.first);  // Convert address to big-endian
            ipsFile.write(reinterpret_cast<const char*>(&bigEndianAddress), sizeof(bigEndianAddress));  // Write address
    
            valueLength = toBigEndian(static_cast<uint16_t>(patch.second.size()));  // Convert length to big-endian
            ipsFile.write(reinterpret_cast<const char*>(&valueLength), sizeof(valueLength));  // Write length of value
    
            ipsFile.write(reinterpret_cast<const char*>(patch.second.data()), patch.second.size());  // Write value
        }
    
        ipsFile.write(IPS32_FOOT_MAGIC, std::strlen(IPS32_FOOT_MAGIC));
        ipsFile.close();
    
    
        std::ifstream _pchtxtFile(pchtxtPath);
        if (!_pchtxtFile) {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Error: Unable to open file " + pchtxtPath);
            #endif
            return false; // Changed: Return false if we can't read the file for title ID
        }
    
        std::string pchtxt((std::istreambuf_iterator<char>(_pchtxtFile)), std::istreambuf_iterator<char>());
    
        const std::string tid = findTitleID(pchtxt);
        if (!tid.empty()) {
            const std::string tidFilePath = outputFolder + tid;
            std::ofstream tidFile(tidFilePath);
            tidFile.close();  // Creates an empty file
        } else {
            #if USING_LOGGING_DIRECTIVE
            if (!disableLogging)
                logMessage("Warning: Could not find Title ID in " + pchtxtPath);
            #endif
        }
    #endif
    
        return true;
    }

}
