#!/usr/bin/env python3
import os
import sys
import subprocess
from pathlib import Path
import shutil
import platform

# ---------------------------------------------------------------------
# CONFIG
# ---------------------------------------------------------------------
SRC_DIR = Path("src")
TEST_DIR = Path("tests")
OBJ_DIR = Path("obj")
BIN_DIR = Path("bin")

TARGET = BIN_DIR / "sls"
TEST_TARGET = BIN_DIR / "sls_tests"

# Platform-specific settings
PICO_SDK_PATH = os.environ.get("PICO_SDK_PATH", Path.home() / "pico/pico-sdk")
PICO_BUILD_DIR = Path("build_pico")
PICO_TOOLCHAIN_PATH = Path("pico_arm_gcc_toolchain.cmake")

# Unix gcc/clang flags
COMMON_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Werror", "-Iinclude", "-g"]
TEST_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Wno-unused-function", "-Werror",
              "-Iinclude", "-g", "-O0"]

# macOS-specific flags (for cross-compilation if needed)
MACOS_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Werror", "-Iinclude", "-g",
               "-mmacosx-version-min=10.13"]

# Windows MSVC flags
MSVC_FLAGS = ["/std:c11", "/Zi", "/Iinclude"]
MSVC_TEST_FLAGS = MSVC_FLAGS + []

# RP2040 toolchain file template
PICO_TOOLCHAIN_TEMPLATE = """set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR cortex-m0plus)

# Specify the cross compiler
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)

# Compiler flags for Cortex-M0+
set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")
set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")
set(CMAKE_ASM_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")

# Don't run the linker on compiler check
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# Adjust the default behavior of the FIND_XXX() commands:
# search programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Search headers and libraries in the target environment
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
"""

# RP2040 settings
RP2040_CMAKE_TEMPLATE = """cmake_minimum_required(VERSION 3.13)

# Pico SDK initialization
set(PICO_SDK_PATH "{pico_sdk_path}")
include({pico_sdk_path}/external/pico_sdk_import.cmake)

project({project_name} C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

# Main executable
add_executable({project_name}
{source_files}
)

# Set output name with .elf extension
set_target_properties({project_name} PROPERTIES
    OUTPUT_NAME "{project_name}.elf"
    SUFFIX ""
)

# Add include directories
target_include_directories({project_name} PRIVATE
    ${{CMAKE_CURRENT_LIST_DIR}}/include
)

# Add compile definitions
target_compile_definitions({project_name} PRIVATE
    PICO_BUILD=1
    GIT_COMMIT_HASH="{git_hash}"
)

# Link libraries
target_link_libraries({project_name}
    pico_stdlib
    hardware_uart
    hardware_gpio
)

# Enable USB/UART output
pico_enable_stdio_usb({project_name} 1)
pico_enable_stdio_uart({project_name} 1)

# Create map/bin/hex/uf2 files
pico_add_extra_outputs({project_name})
"""

# ---------------------------------------------------------------------
# PLATFORM DETECTION
# ---------------------------------------------------------------------
def detect_platform():
    """Detect the current operating system"""
    system = platform.system()
    if system == "Darwin":
        return "macos"
    elif system == "Windows":
        return "windows"
    elif system == "Linux":
        return "linux"
    return "unknown"

PLATFORM = detect_platform()

# ---------------------------------------------------------------------
# COMPILER DETECTION
# ---------------------------------------------------------------------
def detect_compiler(target_platform=None):
    """Detect appropriate compiler for the target platform"""
    if target_platform == "rp2040":
        return ("arm-none-eabi-gcc", "gcc")
    
    target = target_platform or PLATFORM
    
    if target == "windows" or os.name == "nt":
        return ("cl", "msvc")
    elif target == "macos":
        # Prefer clang on macOS
        if shutil.which("clang"):
            return ("clang", "gcc")
        return ("gcc", "gcc")
    else:
        return ("gcc", "gcc")

CC, CC_KIND = detect_compiler()

# ---------------------------------------------------------------------
# PYTHON DETECTION
# ---------------------------------------------------------------------
def detect_python():
    if os.name == "nt":
        return "python"
    return "python3"

PYTHON = detect_python()


# ---------------------------------------------------------------------
# GIT COMMIT HASH
# ---------------------------------------------------------------------
def git_commit_hash():
    try:
        result_hash = subprocess.check_output(
            ["git", "describe", "--always", "--dirty", "--abbrev=7"],
            cwd=".",
            stderr=subprocess.DEVNULL,
            text=True
        ).strip()
        result_date = subprocess.check_output(
            ["git", "show", "-s", "--format=%ci"],
            cwd=".",
            stderr=subprocess.DEVNULL,
            text=True
        ).strip()
        return f"{result_hash} {result_date}"
    except Exception:
        return "unknown"


GIT_HASH = git_commit_hash()


# ---------------------------------------------------------------------
# UTILS
# ---------------------------------------------------------------------
def mkdir(p: Path):
    p.mkdir(parents=True, exist_ok=True)


def run(cmd):
    print(">>", " ".join(str(c) for c in cmd))
    subprocess.check_call(cmd)


def is_up_to_date(src, obj, dep):
    if "meta" in [src.stem, obj.stem, dep.stem]:
        return False
    if not obj.exists():
        return False
    if dep.exists() and dep.stat().st_mtime > obj.stat().st_mtime:
        return False
    return src.stat().st_mtime < obj.stat().st_mtime


# ---------------------------------------------------------------------
# BUILD RULES
# ---------------------------------------------------------------------
def compile_source(src: Path, is_test=False, target_platform=None):
    mkdir(OBJ_DIR)
    obj = OBJ_DIR / (src.stem + ".o")
    dep = OBJ_DIR / (src.stem + ".d")

    if is_up_to_date(src, obj, dep):
        return obj

    compiler, kind = detect_compiler(target_platform)
    
    if kind == "msvc":
        flags = MSVC_TEST_FLAGS if is_test else MSVC_FLAGS
        cmd = [compiler] + flags + ["/Fo" + str(obj), "/c", str(src),
                               f"/DGIT_COMMIT_HASH=\"{GIT_HASH}\""]
    else:
        if target_platform == "macos":
            flags = MACOS_FLAGS if not is_test else TEST_FLAGS + ["-mmacosx-version-min=10.13"]
        else:
            flags = TEST_FLAGS if is_test else COMMON_FLAGS
            
        cmd = [compiler] + flags + [
            f"-DGIT_COMMIT_HASH=\"{GIT_HASH}\"",
            "-MMD", "-MP",
            "-c", str(src), "-o", str(obj)
        ]

    run(cmd)
    return obj


def link_executable(objects, output: Path, target_platform=None):
    mkdir(BIN_DIR)
    compiler, kind = detect_compiler(target_platform)
    
    if kind == "msvc":
        cmd = [compiler] + list(map(str, objects)) + ["/Fe" + str(output)]
    else:
        cmd = [compiler] + list(map(str, objects)) + ["-lm", "-o", str(output)]
    run(cmd)


# ---------------------------------------------------------------------
# TEST CASE GENERATION
# ---------------------------------------------------------------------
def generate_tests():
    script = Path("../SLS_Tests/yaml_to_c_tests.py")
    yaml = Path("../SLS_Tests/cases.yaml")
    out = TEST_DIR / "lexer_tests.c"

    if not script.exists() or not yaml.exists():
        print("Test generation skipped (missing files).")
        return False

    if out.exists() and out.stat().st_mtime > yaml.stat().st_mtime:
        return True

    run([PYTHON, str(script), str(yaml), str(out)])
    return True


# ---------------------------------------------------------------------
# RP2040 BUILD
# ---------------------------------------------------------------------
def check_pico_sdk():
    """Check if Pico SDK is available"""
    sdk_path = Path(PICO_SDK_PATH)
    if not sdk_path.exists():
        print(f"ERROR: Pico SDK not found at {sdk_path}")
        print("Please set PICO_SDK_PATH environment variable or install SDK at ~/pico/pico-sdk")
        return False
    
    # Check for ARM toolchain
    if not shutil.which("arm-none-eabi-gcc"):
        print("ERROR: ARM toolchain not found!")
        print("Please install arm-none-eabi-gcc:")
        print("  Ubuntu/Debian: sudo apt install gcc-arm-none-eabi")
        print("  macOS: brew install arm-none-eabi-gcc")
        return False
    
    return True


def generate_pico_toolchain():
    """Generate ARM GCC toolchain file for CMake"""
    with open(PICO_TOOLCHAIN_PATH, "w") as f:
        f.write(PICO_TOOLCHAIN_TEMPLATE)
    print(f"Generated {PICO_TOOLCHAIN_PATH}")


def generate_pico_cmake(project_name="sls"):
    """Generate CMakeLists.txt for RP2040 build"""
    sources = list(SRC_DIR.glob("*.c"))
    # Exclude main.c, repl.c, file.c and test files for Pico build
    # pico_main.c will provide its own REPL implementation
    sources = [s for s in sources 
               if s.name not in ["main.c", "repl.c", "file.c"] 
               and "test" not in s.stem.lower()]
    
    # Check for pico_main.c
    pico_main = SRC_DIR / "pico_main.c"
    if not pico_main.exists():
        print(f"WARNING: {pico_main} not found! Please create it.")
        print("The Pico build requires a pico_main.c file.")
        return False
    
    sources.append(pico_main)
    
    source_files = "\n".join(f"    {s}" for s in sources)
    
    cmake_content = RP2040_CMAKE_TEMPLATE.format(
        project_name=project_name,
        source_files=source_files,
        git_hash=GIT_HASH,
        pico_sdk_path=PICO_SDK_PATH
    )
    
    cmake_file = Path("CMakeLists.txt")
    with open(cmake_file, "w") as f:
        f.write(cmake_content)
    
    print(f"Generated {cmake_file}")
    return True


def build_rp2040():
    """Build for RP2040 using CMake"""
    if not check_pico_sdk():
        return False
    
    print("\n=== Building for RP2040 ===\n")
    
    # Generate toolchain file
    generate_pico_toolchain()
    
    # Generate CMakeLists.txt
    if not generate_pico_cmake():
        return False
    
    # Create build directory
    mkdir(PICO_BUILD_DIR)
    
    # Run CMake with explicit toolchain
    cmake_cmd = [
        "cmake",
        "-B", str(PICO_BUILD_DIR),
        "-S", ".",
        f"-DCMAKE_TOOLCHAIN_FILE={PICO_TOOLCHAIN_PATH}"
    ]
    run(cmake_cmd)
    
    # Build
    build_cmd = ["cmake", "--build", str(PICO_BUILD_DIR), "-j4"]
    run(build_cmd)
    
    # Show output files
    uf2_file = PICO_BUILD_DIR / "sls.uf2"
    if uf2_file.exists():
        print(f"\nBuild successful! UF2 file: {uf2_file}")
        print("To flash: Copy this file to your Pico in BOOTSEL mode")
    
    return True


# ---------------------------------------------------------------------
# MACOS BUILD
# ---------------------------------------------------------------------
def build_macos():
    """Build for macOS (can be run on macOS or cross-compile on Linux)"""
    print("\n=== Building for macOS ===\n")
    
    if PLATFORM != "macos" and PLATFORM != "linux":
        print("ERROR: macOS builds require macOS or Linux with osxcross")
        return False
    
    sources = list(SRC_DIR.glob("*.c"))
    objects = [compile_source(s, is_test=False, target_platform="macos") for s in sources]
    
    macos_target = BIN_DIR / "sls_macos"
    link_executable(objects, macos_target, target_platform="macos")
    
    print(f"\nBuild successful! Binary: {macos_target}")
    return True


# ---------------------------------------------------------------------
# TARGETS
# ---------------------------------------------------------------------
def build_main():
    sources = list(SRC_DIR.glob("*.c"))
    objects = [compile_source(s, is_test=False) for s in sources]
    link_executable(objects, TARGET)


def build_tests():
    generate_tests()
    test_sources = list(TEST_DIR.glob("*.c"))
    main_sources = [s for s in SRC_DIR.glob("*.c") if s.name != "main.c"]

    test_objects = [compile_source(s, is_test=True) for s in test_sources]
    shared_objects = [compile_source(s, is_test=False) for s in main_sources]

    link_executable(test_objects + shared_objects, TEST_TARGET)


def clean():
    shutil.rmtree(OBJ_DIR, ignore_errors=True)
    shutil.rmtree(BIN_DIR, ignore_errors=True)
    shutil.rmtree(PICO_BUILD_DIR, ignore_errors=True)
    cmake_file = Path("CMakeLists.txt")
    if cmake_file.exists():
        cmake_file.unlink()
    toolchain_file = PICO_TOOLCHAIN_PATH
    if toolchain_file.exists():
        toolchain_file.unlink()
    print("Cleaned.")


def run_main():
    build_main()
    print("\n--- Running program ---\n")
    subprocess.call([str(TARGET)])


def run_tests():
    build_tests()
    print("\n--- Running tests ---\n")
    subprocess.call([str(TEST_TARGET)])


def debug_tests():
    build_tests()
    if os.name == "nt":
        print("Debugging on Windows requires Visual Studio debugger.")
    else:
        subprocess.call(["gdb", str(TEST_TARGET)])


def show_help():
    help_text = """
Build Script for Multi-Platform Compilation
============================================

Usage: python3 build.py [command]

Commands:
  all, main, build    Build for current platform
  run                 Build and run program
  test                Build and run tests
  debug               Build tests and run debugger
  clean               Remove build artifacts
  
  macos               Build for macOS
  pico, rp2040        Build for RP2040 (Raspberry Pi Pico)
  
  help                Show this help message

Environment Variables:
  PICO_SDK_PATH       Path to Pico SDK (default: ~/pico/pico-sdk)

Examples:
  python3 build.py build      # Build for current platform
  python3 build.py pico       # Build for RP2040
  python3 build.py macos      # Build for macOS
  python3 build.py clean      # Clean all build files
"""
    print(help_text)


# ---------------------------------------------------------------------
# ENTRY POINT
# ---------------------------------------------------------------------
def main():
    if len(sys.argv) < 2:
        show_help()
        return

    cmd = sys.argv[1]

    match cmd:
        case "all" | "main" | "build":
            build_main()
        case "run":
            run_main()
        case "test":
            run_tests()
        case "debug":
            debug_tests()
        case "clean":
            clean()
        case "macos":
            build_macos()
        case "pico" | "rp2040":
            build_rp2040()
        case "help" | "-h" | "--help":
            show_help()
        case _:
            print(f"Unknown target: {cmd}")
            print("Run 'python3 build.py help' for usage information")


if __name__ == "__main__":
    main()
