#!/usr/bin/env python3
import re

from disasm_script import ScriptDisassembler, get_constants
from glob import glob
import os

NAMESPACES = {
    "src/battle/area_kmr_part_1/stage/clouds.inc.c": "b_area_kmr_part_1_kmr_03",
    "src/common/foliage.inc.c": "kmr_03",
}

for filename in glob("src/battle/item/*.c"):
    parts = filename.split("/")[1:]
    parts[-1] = parts[-1].split(".")[0]
    NAMESPACES[filename] = "_".join(parts)

NAMESPACES["src/battle/item/UseItem.inc.c"] = "battle_item_food"

for filename in glob("src/world/*/*/*.c"):
    map_name = filename.split("/")[3]
    NAMESPACES[filename] = map_name


class UserException(Exception):
    pass


class Range:
    def __init__(self, start: int, end: int, symbol_name: str, namespace: str):
        self.start = start
        self.end = end
        self.symbol_name = symbol_name
        self.namespace = namespace


class Symbol:
    def __init__(self, ram_addr, rom_addr):
        self.ram_addr = ram_addr
        self.rom_addr = rom_addr


def parse_symbol_addrs():
    with open("ver/us/symbol_addrs.txt", "r") as f:
        lines = f.readlines()

    symbol_addrs = {}

    for line in lines:
        name = line[:line.find(" ")]

        attributes = line[line.find("//"):].split(" ")
        ram_addr = int(line[:line.find(";")].split("=")[1].strip(), base=0)
        rom_addr = next((int(attr.split(":")[1], base=0) for attr in attributes if attr.split(":")[0] == "rom"), None)

        symbol_addrs[name] = Symbol(ram_addr, rom_addr)

    return symbol_addrs


def find_old_script_ranges(lines, filename):
    """
    Finds all ranges that contain the old SCRIPT macro.
    """

    start_line_no = None
    symbol_name = None
    namespace = NAMESPACES.get(filename, filename.split("/")[-1].split(".")[0])

    for line_no, line_content in enumerate(lines):
        r = re.compile(r".*\.h")
        namespace_temp = list(filter(r.match, os.listdir(os.path.dirname(os.path.abspath(filename)))))

        if "#define NAMESPACE " in line_content:
            namespace = line_content.split(" ")[2].strip()
        #elif namespace == "events" or namespace == "header":
        #    namespace = NAMESPACES.get(filename, filename.split("/")[-2].split(".")[0])
        elif namespace_temp is not None:
            namespace = namespace_temp[0][:-2]

        if "SCRIPT({" in line_content:
            start_line_no = line_no
            symbol_name = eval_namespace(line_content.split(" ")[1], namespace)
        elif "});" in line_content and start_line_no is not None:
            yield Range(start_line_no, line_no, symbol_name, namespace)
            start_line_no = None


def eval_namespace(sym, namespace):
    if sym.startswith("N("):
        return namespace + "_" + sym[2:-1]
    else:
        return sym


def replace_old_script_macros(filename, symbol_addrs):
    with open(filename, "r") as f:
        lines = f.readlines()

    with open("ver/us/baserom.z64", "rb") as rom:
        num_scripts_replaced = 0

        while True:
            try:
                range = next(find_old_script_ranges(lines, filename))
            except StopIteration:
                break

            # Replace initial line
            macro_start_idx = lines[range.start].find("SCRIPT({\n")
            if macro_start_idx == -1:
                raise Exception("Could not find SCRIPT macro start")
            lines[range.start] = lines[range.start][:macro_start_idx] + "{\n"

            # Remove other lines
            lines = lines[:range.start + 1] + lines[range.end + 1:]

            # Find the symbol
            try:
                range_sym = symbol_addrs[range.symbol_name]
            except KeyError:
                raise UserException(f"Symbol {range.symbol_name} is not in symbol_addrs")

            if not range_sym.rom_addr:
                raise UserException(f"Symbol {range.symbol_name} lacks a rom address in symbol_addrs")

            # Make local symbol map, replacing namespaced symbols with N(sym)
            local_symbol_map = {}
            for sym in symbol_addrs:
                if sym.startswith(range.namespace):
                    key = "N(" + sym[len(range.namespace)+1:] + ")"
                else:
                    key = sym

                symbol = symbol_addrs[sym]

                if symbol.ram_addr in local_symbol_map:
                    cur_sym_name = local_symbol_map[symbol.ram_addr][0][1]
                    if cur_sym_name.startswith("N("):
                        continue

                local_symbol_map[symbol.ram_addr] = [[symbol.ram_addr, key]]

            # Disassemble the script
            rom.seek(range_sym.rom_addr)
            evt_code = ScriptDisassembler(rom,
                script_name=range.symbol_name,
                romstart=range_sym.rom_addr,
                prelude=False,
                symbol_map=local_symbol_map,
            ).disassemble()
            lines.insert(range.start + 1, f"{evt_code}}};\n")

            num_scripts_replaced += 1

    if num_scripts_replaced > 0:
        with open(filename, "w") as f:
            f.writelines(lines)

    return num_scripts_replaced


if __name__ == "__main__":
    import sys

    symbol_addrs = parse_symbol_addrs()
    get_constants()

    num_errors = 0

    if len(sys.argv) > 1:
        for filename in sys.argv[1:]:
            try:
                num_scripts_replaced = replace_old_script_macros(filename, symbol_addrs)
                if num_scripts_replaced > 0:
                    print(f"{num_scripts_replaced} old scripts replaced in {filename}")
            except UserException as e:
                print(f"{filename} ERROR: {e}")
                num_errors += 1

        if num_errors > 0:
            print(f"ERROR: {num_errors} files had errors.")
            exit(1)
    else:
        print("warning: no files specified")
        print("usage: ./tools/update_evts.c <files to modify>")
