// Vita3K emulator project
// Copyright (C) 2026 Vita3K team
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#include "patch/patch.h"
#include "patch/instructions.h"
#include "patch/util.h"

#include <util/fs.h>
#include <util/log.h>
#include <util/string_utils.h>

Patches get_patches(fs::path &path, const std::string &titleid, const std::string &bin) {
    // Find a file in the path with the titleid
    Patches patches;

    for (auto &entry : fs::directory_iterator(path)) {
        auto filename = fs_utils::path_to_utf8(entry.path().filename());
        // Just in case users decide to use lowercase filenames
        std::transform(filename.begin(), filename.end(), filename.begin(), ::toupper);

        bool is_patchlist = filename.contains("PATCHLIST.TXT");

        if ((filename.contains(titleid) && filename.ends_with(".TXT")) || is_patchlist) {
            // Read the file
            std::ifstream file(entry.path().c_str());
            PatchHeader patch_header = PatchHeader{
                "",
                "eboot.bin"
            };

            auto line_number = 0;
            std::string line;
            // Parse the file
            while (std::getline(file, line)) {
                line_number++;

                // If this is a header, remember the binary the next patches are for
                if (!line.empty() && line[0] == '[') {
                    patch_header = read_header(line, is_patchlist);
                    continue;
                }

                // Ignore comments and patches for other binaries
                // And @ lines for now
                if (line.empty() || line[0] == '#' || line[0] == '@' || !bin.contains(patch_header.bin) || (is_patchlist && patch_header.titleid != titleid))
                    continue;

                try {
                    patches.push_back(parse_patch(line));
                } catch (std::exception &e) {
                    LOG_ERROR("Failed to parse patch line: {} [{}]", line_number, line);
                    LOG_ERROR("Failed with: {}", e.what());
                }
            }
        }
    }

    LOG_INFO("Found {} patches for titleid {}", patches.size(), titleid);

    return patches;
}

Patch parse_patch(const std::string &patch) {
    // FORMAT: <seg>:<offset> <values>
    // Example, equivalent to `t1_mov(0, 1)`:
    // 0:0xA994 0x01 0x20
    // Keep in mind that we are in little endian
    uint8_t seg = std::stoi(patch.substr(0, patch.find(':')));

    // Everything after the first colon, and before the first space, is the offset
    uint32_t offset = std::stoull(patch.substr(patch.find(':') + 1, patch.find(' ') - patch.find(':') - 1), nullptr, 16);

    // All following values (separated by spaces) are the values to be written
    std::string values = patch.substr(patch.find(' ') + 1);
    // set vblank to 1(60Hz) for now
    string_utils::replace(values, "4 - vblank", "3");
    string_utils::replace(values, "vblank - 1", "0");
    string_utils::replace(values, "vblank", "1");
    std::vector<uint8_t> values_vec;

    // Get all additional values separated by spaces
    size_t pos = 0;

    // Clean up potential instructions by removing spaces in between brackets
    // Eg. t1_mov(0, 1) becomes t1_mov(0,1)
    strip_arg_spaces(values);

    // If there is only one value, set pos to the end of the string
    if ((pos = values.find(' ')) == std::string::npos)
        pos = values.length() - 1;

    do {
        pos = values.find(' ');
        std::string val = values.substr(0, pos);

        // Strip 0x from the value if it exists
        if (val.length() > 2 && val[0] == '0' && val[1] == 'x')
            val.erase(0, 2);

        unsigned long long bytes;
        uint8_t byte_count = 0;
        std::string inst = strip_args(val);
        Instruction instruction = to_instruction(inst);

        if (instruction != Instruction::INVALID) {
            auto args = get_args(val);
            std::vector<uint32_t> arg_conv;

            arg_conv.reserve(args.size());
            std::transform(args.begin(), args.end(), std::back_inserter(arg_conv), [](std::string &s) { return std::stoull(s, nullptr, 16); });

            bytes = translate(inst, arg_conv);

            LOG_INFO("Translated {} to 0x{:X}", val, bytes);
        } else {
            bytes = std::stoull(values.substr(0, pos), nullptr, 16);
            // We need to count this, as patches may have bytes of zeros that we don't want to just ignore by passing 0 to toBytes
            byte_count = val.length() % 2 == 0 ? val.length() / 2 : val.length() / 2 + 1;
        }

        auto byte_vec = to_bytes(bytes, byte_count);

        values_vec.reserve(values_vec.size() + byte_vec.size());
        values_vec.insert(values_vec.end(), byte_vec.begin(), byte_vec.end());

        values.erase(0, pos + 1);
    } while (pos != std::string::npos);

    return Patch{ seg, offset, values_vec };
}
