from dataclasses import dataclass
from pathlib import Path
from typing import cast, Dict, List, Literal, Mapping, Optional, Set, Type, TypeVar

from util import compiler
from util.compiler import Compiler


@dataclass
class SplatOpts:
    # Debug / logging
    verbose: bool
    dump_symbols: bool
    modes: List[str]

    # Project configuration

    # Determines the base path of the project. Everything is relative to this path
    base_path: Path
    # Determines the path to the target binary
    target_path: Path
    # Determines the platform of the target binary
    platform: str
    # Determines the compiler used to compile the target binary
    compiler: Compiler
    # Determines the endianness of the target binary
    endianness: Literal["big", "little"]
    # Determines the default section order of the target binary
    # this can be overridden per-segment
    section_order: List[str]
    # Determines the code that is inserted by default in generated .c files
    generated_c_preamble: str
    # Determines the code that is inserted by default in generated .s files
    generated_s_preamble: str
    # Determines whether to use .o as the suffix for all binary files?... TODO document
    use_o_as_suffix: bool
    # the value of the $gp register to correctly calculate offset to %gp_rel relocs
    gp: Optional[int]

    # Paths
    asset_path: Path
    # Determines the path to the symbol addresses file(s)
    # A symbol_addrs file is to be updated/curated manually and contains addresses of symbols
    # as well as optional metadata such as rom address, type, and more
    #
    # It's possible to use more than one file by supplying a list instead of a string
    symbol_addrs_paths: List[Path]
    reloc_addrs_paths: List[Path]
    # Determines the path to the project build directory
    build_path: Path
    # Determines the path to the source code directory
    src_path: Path
    # Determines the path to the asm code directory
    asm_path: Path
    # Determines the path to the asm data directory
    data_path: Path
    # Determines the path to the asm nonmatchings directory
    nonmatchings_path: Path
    # Determines the path to the cache file (used when supplied --use-cache via the CLI)
    cache_path: Path

    # Determines whether to create an automatically-generated undefined functions file
    # this file stores all functions that are referenced in the code but are not defined as seen by splat
    create_undefined_funcs_auto: bool
    # Determines the path to the undefined_funcs_auto file
    undefined_funcs_auto_path: Path

    # Determines whether to create an automatically-generated undefined symbols file
    # this file stores all symbols that are referenced in the code but are not defined as seen by splat
    create_undefined_syms_auto: bool
    # Determines the path to the undefined_symbols_auto file
    undefined_syms_auto_path: Path

    # Determines the path in which to search for custom splat extensions
    extensions_path: Optional[Path]

    # Determines the path to library files that are to be linked into the target binary
    lib_path: Path

    # TODO document
    elf_section_list_path: Optional[Path]

    # Linker script
    # Determines the default subalign value to be specified in the generated linker script
    subalign: int
    # The following option determines whether to automatically configure the linker script to link against
    # specified sections for all "base" (asm/c) files when the yaml doesn't have manual configurations
    # for these sections.
    auto_all_sections: List[str]
    # Determines the desired path to the linker script that splat will generate
    ld_script_path: Path
    # Determines the desired path to the linker symbol header,
    # which exposes externed definitions for all segment ram/rom start/end locations
    ld_symbol_header_path: Optional[Path]
    # Determines whether to add a discard section to the linker script
    ld_discard_section: bool
    # Determines the list of section labels that are to be added to the linker script
    ld_section_labels: List[str]
    # Determines whether to add wildcards for section linking in the linker script (.rodata* for example)
    ld_wildcard_sections: bool
    # Determines whether to use use "follows" settings to determine locations of overlays in the linker script.
    # If disabled, this effectively ignores "follows" directives in the yaml.
    ld_use_follows: bool

    ################################################################################
    # C file options
    ################################################################################
    # Determines whether to create new c files if they don't exist
    create_c_files: bool
    # Determines whether to "auto-decompile" empty functions
    auto_decompile_empty_functions: bool
    # Determines whether to detect matched/unmatched functions in existing c files
    # so we can avoid creating .s files for already-decompiled functions
    do_c_func_detection: bool
    # Determines the newline char(s) to be used in c files
    c_newline: str

    ################################################################################
    # (Dis)assembly-related options
    ################################################################################
    # The following options determine the format that symbols should be named by default
    symbol_name_format: str
    # Same as above but for symbols with no rom address
    symbol_name_format_no_rom: str
    # Determines whether to detect and hint to the user about likely file splits when disassembling
    find_file_boundaries: bool
    # Determines whether to detect and hint to the user about possible rodata sections corresponding to a text section
    pair_rodata_to_text: bool
    # Determines whether to attempt to automatically migrate rodata into functions
    # (only works in certain circumstances)
    migrate_rodata_to_functions: bool
    # Determines the header to be used in every asm file that's included from c files
    asm_inc_header: str
    # Determines the macro used to declare functions in asm files
    asm_function_macro: str
    # Determines the macro used to declare jumptable labels in asm files
    asm_jtbl_label_macro: str
    # Determines the macro used to declare data symbols in asm files
    asm_data_macro: str
    # Determines the macro used at the end of a function, such as endlabel or .end
    asm_end_label: str
    # Determines including the macro.inc file on non-migrated rodata variables
    include_macro_inc: bool
    # Determines the number of characters to left align before the TODO finish documenting
    mnemonic_ljust: int
    # Determines whether to pad the rom address
    rom_address_padding: bool
    # Determines which ABI names to use for general purpose registers
    mips_abi_gpr: str
    # Determines which ABI names to use for floating point registers
    # Valid values: 'numeric', 'o32', 'n32', 'n64'
    # o32 is highly recommended, as it provides logically named registers for floating point instructions
    # For more info, see https://gist.github.com/EllipticEllipsis/27eef11205c7a59d8ea85632bc49224d
    mips_abi_float_regs: str
    # Determines whether to ad ".set gp=64 to asm/hasm files"
    add_set_gp_64: bool
    # Generate .asmproc.d dependency files for each C file which still reference functions in assembly files
    create_asm_dependencies: bool
    # Global option for rodata string encoding. This can be overriden per segment
    string_encoding: Optional[str]
    # Global option for allowing data symbols using addends on symbol references. It can be overriden per symbol
    allow_data_addends: bool

    ################################################################################
    # N64-specific options
    ################################################################################
    # Determines the encoding of the header
    header_encoding: str
    # Determines the type gfx ucode (used by gfx segments)
    # Valid options are ['f3d', 'f3db', 'f3dex', 'f3dexb', 'f3dex2']
    gfx_ucode: str
    # Use named libultra symbols by default. Those will need to be added to a linker script manually by the user
    libultra_symbols: bool
    # Use named hardware register symbols by default. Those will need to be added to a linker script manually by the user
    hardware_regs: bool

    ################################################################################
    # Gamecube-specific options
    ################################################################################
    # Path where the iso's filesystem will be extracted to
    filesystem_path: Optional[Path]

    ################################################################################
    # Compiler-specific options
    ################################################################################
    # Determines whether to use a legacy INCLUDE_ASM macro format in c files
    # only applies to GCC/SN64
    use_legacy_include_asm: bool

    # Returns whether the given mode is currently enabled
    def is_mode_active(self, mode: str) -> bool:
        return mode in self.modes or "all" in self.modes


opts: SplatOpts


T = TypeVar("T")


class OptParser:
    _read_opts: Set[str]

    def __init__(self, yaml: Mapping[str, object]) -> None:
        self._yaml = yaml
        self._read_opts = set()

    def parse_opt(self, opt: str, t: Type[T], default: Optional[T] = None) -> T:
        if opt not in self._yaml:
            if default is not None:
                return default
            raise ValueError(f"Missing required option {opt}")
        self._read_opts.add(opt)
        value = self._yaml[opt]
        if isinstance(value, t):
            return value
        if t is float and isinstance(value, int):
            return cast(T, float(value))
        raise ValueError(f"Expected {opt} to have type {t}, got {type(value)}")

    def parse_optional_opt(self, opt: str, t: Type[T]) -> Optional[T]:
        if opt not in self._yaml:
            return None
        return self.parse_opt(opt, t)

    def parse_opt_within(
        self, opt: str, t: Type[T], within: List[T], default: Optional[T] = None
    ) -> T:
        value = self.parse_opt(opt, t, default)
        if value not in within:
            raise ValueError(f"Invalid value for {opt}: {value}")
        return value

    def parse_path(
        self, base_path: Path, opt: str, default: Optional[str] = None
    ) -> Path:
        return base_path / Path(self.parse_opt(opt, str, default))

    def parse_optional_path(self, base_path: Path, opt: str) -> Optional[Path]:
        if opt not in self._yaml:
            return None
        return self.parse_path(base_path, opt)

    def parse_path_list(self, base_path: Path, opt: str, default: str) -> List[Path]:
        paths = self.parse_opt(opt, object, default)

        if isinstance(paths, str):
            return [base_path / paths]
        elif isinstance(paths, list):
            return [base_path / path for path in paths]
        else:
            raise ValueError(f"Expected str or list for '{opt}', got {type(paths)}")

    def check_no_unread_opts(self) -> None:
        opts = [opt for opt in self._yaml if opt not in self._read_opts]
        if opts:
            raise ValueError(f"Unrecognized YAML option(s): {', '.join(opts)}")


def _parse_yaml(
    yaml: Dict,
    config_paths: List[str],
    modes: List[str],
    verbose: bool = False,
) -> SplatOpts:
    p = OptParser(yaml)

    basename = p.parse_opt("basename", str)
    platform = p.parse_opt_within("platform", str, ["n64", "psx", "gc", "ps2"])
    comp = compiler.for_name(p.parse_opt("compiler", str, "IDO"))

    base_path = Path(config_paths[0]).parent / p.parse_opt("base_path", str)
    asm_path: Path = p.parse_path(base_path, "asm_path", "asm")

    def parse_endianness() -> Literal["big", "little"]:
        endianness = p.parse_opt_within(
            "endianness",
            str,
            ["big", "little"],
            "little" if platform in ["psx", "ps2"] else "big",
        )

        if endianness == "big":
            return "big"
        elif endianness == "little":
            return "little"
        else:
            raise ValueError(f"Invalid endianness: {endianness}")

    ret = SplatOpts(
        verbose=verbose,
        dump_symbols=p.parse_opt("dump_symbols", bool, False),
        modes=modes,
        base_path=base_path,
        target_path=p.parse_path(base_path, "target_path"),
        platform=platform,
        compiler=comp,
        endianness=parse_endianness(),
        section_order=p.parse_opt(
            "section_order", list, [".text", ".data", ".rodata", ".bss"]
        ),
        generated_c_preamble=p.parse_opt(
            "generated_c_preamble", str, '#include "common.h"'
        ),
        generated_s_preamble=p.parse_opt("generated_s_preamble", str, ""),
        use_o_as_suffix=p.parse_opt("o_as_suffix", bool, False),
        gp=p.parse_opt("gp_value", int, 0),
        asset_path=p.parse_path(base_path, "asset_path", "assets"),
        symbol_addrs_paths=p.parse_path_list(
            base_path, "symbol_addrs_path", "symbol_addrs.txt"
        ),
        reloc_addrs_paths=p.parse_path_list(
            base_path, "reloc_addrs_path", "reloc_addrs.txt"
        ),
        build_path=p.parse_path(base_path, "build_path", "build"),
        src_path=p.parse_path(base_path, "src_path", "src"),
        asm_path=asm_path,
        data_path=p.parse_path(asm_path, "data_path", "data"),
        nonmatchings_path=p.parse_path(asm_path, "nonmatchings_path", "nonmatchings"),
        cache_path=p.parse_path(base_path, "cache_path", ".splache"),
        create_undefined_funcs_auto=p.parse_opt(
            "create_undefined_funcs_auto", bool, True
        ),
        undefined_funcs_auto_path=p.parse_path(
            base_path, "undefined_funcs_auto_path", "undefined_funcs_auto.txt"
        ),
        create_undefined_syms_auto=p.parse_opt(
            "create_undefined_syms_auto", bool, True
        ),
        undefined_syms_auto_path=p.parse_path(
            base_path, "undefined_syms_auto_path", "undefined_syms_auto.txt"
        ),
        extensions_path=p.parse_optional_path(base_path, "extensions_path"),
        lib_path=p.parse_path(base_path, "lib_path", "lib"),
        elf_section_list_path=p.parse_optional_path(base_path, "elf_section_list_path"),
        subalign=p.parse_opt("subalign", int, 16),
        auto_all_sections=p.parse_opt(
            "auto_all_sections", list, [".data", ".rodata", ".bss"]
        ),
        ld_script_path=p.parse_path(base_path, "ld_script_path", f"{basename}.ld"),
        ld_symbol_header_path=p.parse_optional_path(base_path, "ld_symbol_header_path"),
        ld_discard_section=p.parse_opt("ld_discard_section", bool, True),
        ld_section_labels=p.parse_opt(
            "ld_section_labels",
            list,
            [".text", ".data", ".rodata", ".bss"],
        ),
        ld_wildcard_sections=p.parse_opt("ld_wildcard_sections", bool, False),
        ld_use_follows=p.parse_opt("ld_use_follows", bool, True),
        create_c_files=p.parse_opt("create_c_files", bool, True),
        auto_decompile_empty_functions=p.parse_opt(
            "auto_decompile_empty_functions", bool, True
        ),
        do_c_func_detection=p.parse_opt("do_c_func_detection", bool, True),
        c_newline=p.parse_opt("c_newline", str, comp.c_newline),
        symbol_name_format=p.parse_opt("symbol_name_format", str, "$VRAM"),
        symbol_name_format_no_rom=p.parse_opt(
            "symbol_name_format_no_rom", str, "$VRAM_$SEG"
        ),
        find_file_boundaries=p.parse_opt("find_file_boundaries", bool, True),
        pair_rodata_to_text=p.parse_opt("pair_rodata_to_text", bool, True),
        migrate_rodata_to_functions=p.parse_opt(
            "migrate_rodata_to_functions", bool, True
        ),
        asm_inc_header=p.parse_opt("asm_inc_header", str, comp.asm_inc_header),
        asm_function_macro=p.parse_opt(
            "asm_function_macro", str, comp.asm_function_macro
        ),
        asm_jtbl_label_macro=p.parse_opt(
            "asm_jtbl_label_macro", str, comp.asm_jtbl_label_macro
        ),
        asm_data_macro=p.parse_opt("asm_data_macro", str, comp.asm_data_macro),
        asm_end_label=p.parse_opt("asm_end_label", str, comp.asm_end_label),
        include_macro_inc=p.parse_opt(
            "include_macro_inc", bool, comp.include_macro_inc
        ),
        mnemonic_ljust=p.parse_opt("mnemonic_ljust", int, 11),
        rom_address_padding=p.parse_opt("rom_address_padding", bool, False),
        mips_abi_gpr=p.parse_opt_within(
            "mips_abi_gpr",
            str,
            ["numeric", "o32", "n32", "n64"],
            "o32",
        ),
        mips_abi_float_regs=p.parse_opt_within(
            "mips_abi_float_regs",
            str,
            ["numeric", "o32", "n32", "n64"],
            "numeric",
        ),
        add_set_gp_64=p.parse_opt("add_set_gp_64", bool, True),
        create_asm_dependencies=p.parse_opt("create_asm_dependencies", bool, False),
        string_encoding=p.parse_optional_opt("string_encoding", str),
        allow_data_addends=p.parse_opt("allow_data_addends", bool, True),
        header_encoding=p.parse_opt("header_encoding", str, "ASCII"),
        gfx_ucode=p.parse_opt_within(
            "gfx_ucode",
            str,
            ["f3d", "f3db", "f3dex", "f3dexb", "f3dex2"],
            "f3dex2",
        ),
        libultra_symbols=p.parse_opt("libultra_symbols", bool, False),
        hardware_regs=p.parse_opt("hardware_regs", bool, False),
        use_legacy_include_asm=p.parse_opt("use_legacy_include_asm", bool, True),
        filesystem_path=p.parse_optional_path(base_path, "filesystem_path"),
    )
    p.check_no_unread_opts()
    return ret


def initialize(
    config: Dict,
    config_paths: List[str],
    modes: Optional[List[str]] = None,
    verbose=False,
):
    global opts

    if not modes:
        modes = ["all"]

    opts = _parse_yaml(config["options"], config_paths, modes, verbose)
