#include "cobor_virtual_machine.hpp"

using namespace CoborVM;

void CoborVM::CoborVirtualMachine::_bind_methods()
{
    godot::ClassDB::bind_method(godot::D_METHOD("parse_source_code", "source_code"), &CoborVirtualMachine::parse_source_code);
    godot::ClassDB::bind_method(godot::D_METHOD("set_registers", "size", "preloaded_values"), &CoborVirtualMachine::set_registers);
    godot::ClassDB::bind_method(godot::D_METHOD("get_registers"), &CoborVirtualMachine::get_registers);
    godot::ClassDB::bind_method(godot::D_METHOD("get_program_counter"), &CoborVirtualMachine::get_program_counter);
    godot::ClassDB::bind_method(godot::D_METHOD("get_program_size"), &CoborVirtualMachine::get_program_size);
    godot::ClassDB::bind_method(godot::D_METHOD("run_step"), &CoborVirtualMachine::run_step);
    godot::ClassDB::bind_method(godot::D_METHOD("run_all"), &CoborVirtualMachine::run_all);
}

godot::PackedStringArray CoborVM::CoborVirtualMachine::parse_source_code(godot::String source_code)
{
    godot::PackedStringArray parseFaults;
    program.clear();

    source_code = source_code.to_upper();
    auto lines = source_code.split("\n");
    int lineNumber = 0;
    for (auto &line : lines)
    {
        lineNumber++;
        ExecutableStep step;
        step.instruction = EMPTY;
        step.lineNumber = lineNumber;

        line = line.strip_edges();
        if (!line.is_empty())
        {
            auto words = line.split(" ", false);
            Instruction instruction;
            if (!string_to_instruction(words[0], instruction))
            {
                parseFaults.append(godot::String("Error: Invalid instruction at line ") +
                                   godot::String::num_int64(lineNumber) +
                                   godot::String(": {") + line + godot::String("}"));
                continue;
            }
            if (!instruction_to_executable_step(instruction, words, step))
            {
                parseFaults.append(godot::String("Error: Invalid arguments at line ") +
                                   godot::String::num_int64(lineNumber) +
                                   godot::String(": {") + line + godot::String("}"));
                continue;
            }
        }

        program.emplace_back(step);
    }

    return parseFaults;
}

godot::String CoborVM::CoborVirtualMachine::set_registers(int size, godot::PackedInt32Array preloaded_values)
{
    if (size <= 0)
        return godot::String("Size must be one or higher");
    register_memory = std::vector<int32_t>(size);
    preloaded_values.resize(size);
    for (int value : preloaded_values)
    {
        register_memory[i] = value;
    }
    return godot::String();
}

godot::PackedInt32Array CoborVM::CoborVirtualMachine::get_registers()
{
    godot::PackedInt32Array ret;
    for (int32_t value : register_memory)
    {
        ret.append(value);
    }
    return ret;
}

int CoborVM::CoborVirtualMachine::get_program_counter()
{
    return register_memory[0];
}

int CoborVM::CoborVirtualMachine::get_program_size()
{
    return program.size();
}

godot::String CoborVM::CoborVirtualMachine::run_step()
{
    if (program.empty())
        return godot::String("No program is compiled to run.");
    if (register_memory.empty())
        return godot::String("Registers are not set.");

    int program_counter = register_memory[0];
    if (program_counter < 0 || program_counter >= program.size())
        return godot::String("Program counter pointing outside program scope: ") +
               godot::String::num_int64(program_counter);

    ExecutableStep step = program[program_counter];
    auto err = run_executable_step(step);
    if (!err.is_empty())
    {
        return godot::String("Execution failed at line: ") +
               godot::String::num_int64(step.lineNumber) +
               godot::String(" with error: ") +
               err;
    }

    register_memory[0]++;
    return godot::String();
}

godot::String CoborVM::CoborVirtualMachine::run_all()
{
    while (true)
    {
        int program_counter = register_memory[0];
        if (program.size() <= program_counter)
            break;
        auto result = run_step();
        if (!result.is_empty())
            return result;
    }
    return godot::String();
}

godot::String CoborVM::CoborVirtualMachine::run_executable_step(ExecutableStep step)
{
    godot::String error_message;

    auto check_register = [this, &error_message](int64_t reg) -> bool
    {
        if (reg >= register_memory.size())
        {
            error_message = godot::String("Pointing outside register memory at /") + godot::String::num_int64(reg);
            return false;
        }
        return true;
    };

    switch (step.instruction)
    {
    case EMPTY:
        break;

    case SET:
    {
        if (!check_register(step.arg2))
            return error_message;
        register_memory[step.arg2] = step.arg1;
        break;
    }

    case CLR:
    {
        if (!check_register(step.arg1))
            return error_message;
        register_memory[step.arg1] = 0;
        break;
    }

    case CPY:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2))
            return error_message;
        register_memory[step.arg2] = register_memory[step.arg1];
        break;
    }

    case SWP:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2))
            return error_message;
        std::swap(register_memory[step.arg1], register_memory[step.arg2]);
        break;
    }

    case ADD:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2) || !check_register(step.arg3))
            return error_message;
        register_memory[step.arg3] = register_memory[step.arg1] + register_memory[step.arg2];
        break;
    }

    case SUB:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2) || !check_register(step.arg3))
            return error_message;
        register_memory[step.arg3] = register_memory[step.arg1] - register_memory[step.arg2];
        break;
    }

    case MUL:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2) || !check_register(step.arg3))
            return error_message;
        register_memory[step.arg3] = register_memory[step.arg1] * register_memory[step.arg2];
        break;
    }

    case DIV:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2) || !check_register(step.arg3))
            return error_message;
        if (register_memory[step.arg2] == 0)
        {
            return godot::String("Dividing by zero! Check register: ") + godot::String::num_int64(step.arg2);
        }
        register_memory[step.arg3] = register_memory[step.arg1] / register_memory[step.arg2];
        break;
    }

    case SKIP:
    {
        register_memory[0]++;
        break;
    }

    case JMP:
    {
        register_memory[0] += step.arg1;
        break;
    }

    case JMPTO:
    {
        register_memory[0] = step.arg1;
        break;
    }

    case EQL:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2))
            return error_message;
        if (register_memory[step.arg1] == register_memory[step.arg2])
            register_memory[0]++;
        break;
    }

    case BIGR:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2))
            return error_message;
        if (register_memory[step.arg1] < register_memory[step.arg2])
            register_memory[0]++;
        break;
    }

    case SMLR:
    {
        if (!check_register(step.arg1) || !check_register(step.arg2))
            return error_message;
        if (register_memory[step.arg1] > register_memory[step.arg2])
            register_memory[0]++;
        break;
    }

    case ZERO:
    {
        if (!check_register(step.arg1))
            return error_message;
        if (register_memory[step.arg1] == 0)
            register_memory[0]++;
        break;
    }

    default:
        return godot::String("Unknown instruction! This is not on you.");
    }
    return godot::String();
}

bool CoborVM::CoborVirtualMachine::string_to_instruction(godot::String command_string, Instruction &outInstruction)
{
    std::string cmd = command_string.utf8().get_data();

    auto it = stringToInstructionMap.find(cmd);
    if (it != stringToInstructionMap.end())
    {
        outInstruction = it->second;
        return true;
    }

    return false;
}

bool CoborVM::CoborVirtualMachine::instruction_to_executable_step(Instruction instruction, godot::PackedStringArray line, ExecutableStep &outExecutableStep)
{
    outExecutableStep.instruction = instruction;

    auto it = instructionPatterns.find(instruction);
    if (it == instructionPatterns.end())
    {
        return false; // Unknown instruction
    }

    const std::vector<bool> &pattern = it->second;
    if (line.size() != pattern.size() + 1)
    {                 // +1 for the instruction itself
        return false; // Incorrect number of arguments
    }

    int arg1 = 0, arg2 = 0, arg3 = 0;
    for (size_t i = 0; i < pattern.size(); ++i)
    {
        bool isPointer = pattern[i];
        godot::String arg = line[i + 1]; // +1 to skip the instruction

        if (isPointer)
        {
            if (!validate_and_parse_pointer(arg, (i == 0) ? arg1 : (i == 1) ? arg2
                                                                            : arg3))
            {
                return false;
            }
        }
        else
        {
            if (!validate_and_parse_literal(arg, (i == 0) ? arg1 : (i == 1) ? arg2
                                                                            : arg3))
            {
                return false;
            }
        }
    }

    outExecutableStep.arg1 = arg1;
    outExecutableStep.arg2 = arg2;
    outExecutableStep.arg3 = arg3;
    return true;
}

bool CoborVM::CoborVirtualMachine::validate_and_parse_pointer(const godot::String &arg, int &outValue)
{
    if (!arg.begins_with("/"))
    {
        return false;
    }
    godot::String trimmedArg = arg.trim_prefix("/");
    if (!trimmedArg.is_valid_int())
    {
        return false;
    }
    outValue = trimmedArg.to_int();
    return true;
}

bool CoborVM::CoborVirtualMachine::validate_and_parse_literal(const godot::String &arg, int &outValue)
{
    if (!arg.is_valid_int())
    {
        return false;
    }
    outValue = arg.to_int();
    return true;
}