import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory

from ci.bin_2_elf import bin_to_elf
from ci.elf import dump_symbol_sizes
from ci.map_dump import map_dump


def cpp_filt(cpp_filt_path: Path, input_text: str) -> str:
    """
    Demangle C++ symbols using c++filt.

    Args:
        cpp_filt_path (Path): Path to c++filt executable.
        input_text (str): Text to demangle.

    Returns:
        str: Demangled text.
    """
    if not cpp_filt_path.exists():
        raise FileNotFoundError(f"cppfilt not found at '{cpp_filt_path}'")
    command = [str(cpp_filt_path), "-t", "-n"]
    process = subprocess.Popen(
        command,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    stdout, stderr = process.communicate(input=input_text)
    if process.returncode != 0:
        raise RuntimeError(f"Error running c++filt: {stderr}")
    return stdout


def demangle_gnu_linkonce_symbols(cpp_filt_path: Path, map_text: str) -> str:
    """
    Demangle .gnu.linkonce.t symbols in the map file.

    Args:
        cpp_filt_path (Path): Path to c++filt executable.
        map_text (str): Content of the map file.

    Returns:
        str: Map file content with demangled symbols.
    """
    # Extract all .gnu.linkonce.t symbols
    pattern = r"\.gnu\.linkonce\.t\.(.+?)\s"
    matches = re.findall(pattern, map_text)

    if not matches:
        return map_text

    # Create a block of text with the extracted symbols
    symbols_block = "\n".join(matches)

    # Demangle the symbols
    demangled_block = cpp_filt(cpp_filt_path, symbols_block)

    # Create a dictionary of mangled to demangled symbols
    demangled_dict = dict(zip(matches, demangled_block.strip().split("\n")))

    # Replace the mangled symbols with demangled ones in the original text
    for mangled, demangled in demangled_dict.items():
        map_text = map_text.replace(
            f".gnu.linkonce.t.{mangled}", f".gnu.linkonce.t.{demangled}"
        )

    return map_text


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Convert a binary file to ELF using map file."
    )
    parser.add_argument("--first", action="store_true", help="Inspect the first board")
    parser.add_argument("--cwd", type=Path, help="Custom working directory")

    return parser.parse_args()


def load_build_info(build_info_path: Path) -> dict:
    """
    Load build information from a JSON file.

    Args:
        build_info_path (Path): Path to the build_info.json file.

    Returns:
        dict: Parsed JSON data.
    """
    if not build_info_path.exists():
        raise FileNotFoundError(f"Build info JSON not found at '{build_info_path}'")
    return json.loads(build_info_path.read_text())


def main() -> int:
    args = parse_args()
    if args.cwd:
        root_build_dir = args.cwd / ".build"
    else:
        root_build_dir = Path(".build")

    board_dirs = [d for d in root_build_dir.iterdir() if d.is_dir()]
    if not board_dirs:
        print(f"No board directories found in {root_build_dir.absolute()}")
        return 1

    print("Available boards:")
    for i, board_dir in enumerate(board_dirs):
        print(f"[{i}]: {board_dir.name}")

    which = (
        0
        if args.first
        else int(input("Enter the number of the board you want to inspect: "))
    )
    board_dir = board_dirs[which]
    build_info_json = board_dir / "build_info.json"

    build_info = load_build_info(build_info_json)
    board = board_dir.name
    board_info = build_info.get(board) or build_info[next(iter(build_info))]

    # Validate paths from build_info.json
    elf_path = Path(board_info.get("prog_path", ""))
    if not elf_path.exists():
        print(
            f"Error: ELF path '{elf_path}' does not exist. Check the 'prog_path' in build_info.json."
        )
        return 1

    bin_file = elf_path.with_suffix(".bin")
    if not bin_file.exists():
        # use .hex or .uf2 if .bin doesn't exist
        bin_file = elf_path.with_suffix(".hex")
        if not bin_file.exists():
            bin_file = elf_path.with_suffix(".uf2")
            if not bin_file.exists():
                print(f"Error: Binary file not found for '{elf_path}'")
                return 1
    cpp_filt_path = Path(board_info["aliases"]["c++filt"])
    ld_path = Path(board_info["aliases"]["ld"])
    as_path = Path(board_info["aliases"]["as"])
    nm_path = Path(board_info["aliases"]["nm"])
    objcopy_path = Path(board_info["aliases"]["objcopy"])
    nm_path = Path(board_info["aliases"]["nm"])
    map_file = board_dir / "firmware.map"
    if not map_file.exists():
        # Search for the map file
        map_file = bin_file.with_suffix(".map")
        if not map_file.exists():
            possible_map_files = list(board_dir.glob("**/firmware.map"))
            if possible_map_files:
                map_file = possible_map_files[0]
            else:
                print("Error: firmware.map file not found")
                return 1

    try:
        with TemporaryDirectory() as temp_dir:
            temp_dir_path = Path(temp_dir)
            output_elf = bin_to_elf(
                bin_file,
                map_file,
                as_path,
                ld_path,
                objcopy_path,
                temp_dir_path / "output.elf",
            )
            out = dump_symbol_sizes(nm_path, cpp_filt_path, output_elf)
            print(out)
    except Exception as e:
        print(
            f"Error while converting binary to ELF, binary analysis will not work on this build: {e}"
        )

    map_dump(map_file)

    # Demangle .gnu.linkonce.t symbols and print map file
    print("\n##################################################")
    print("# Map file dump:")
    print("##################################################\n")
    map_text = map_file.read_text()
    demangled_map_text = demangle_gnu_linkonce_symbols(cpp_filt_path, map_text)
    print(demangled_map_text)

    return 0


if __name__ == "__main__":
    sys.exit(main())
