#!/usr/bin/env python3

###
# Generates build files for the project.
# This file also includes the project configuration,
# such as compiler flags and the object matching status.
#
# Usage:
#   python3 configure.py
#   ninja
#
# Append --help to see available options.
###

import argparse
import sys
from pathlib import Path
from typing import Any, Dict, List

from tools.project import (
    Object,
    ProgressCategory,
    ProjectConfig,
    calculate_progress,
    generate_build,
    is_windows,
)

# Game versions
DEFAULT_VERSION = 1
VERSIONS = [
    "SALE4Q", # 0
    "SALP4Q" # 1
]

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    choices=["configure", "progress"],
    default="configure",
    help="script mode (default: configure)",
    nargs="?",
)
parser.add_argument(
    "-v",
    "--version",
    choices=VERSIONS,
    type=str.upper,
    default=VERSIONS[DEFAULT_VERSION],
    help="version to build",
)
parser.add_argument(
    "--build-dir",
    metavar="DIR",
    type=Path,
    default=Path("build"),
    help="base build directory (default: build)",
)
parser.add_argument(
    "--binutils",
    metavar="BINARY",
    type=Path,
    help="path to binutils (optional)",
)
parser.add_argument(
    "--compilers",
    metavar="DIR",
    type=Path,
    help="path to compilers (optional)",
)
parser.add_argument(
    "--map",
    action="store_true",
    help="generate map file(s)",
)
parser.add_argument(
    "--debug",
    action="store_true",
    help="build with debug info (non-matching)",
)
if not is_windows():
    parser.add_argument(
        "--wrapper",
        metavar="BINARY",
        type=Path,
        help="path to wibo or wine (optional)",
    )
parser.add_argument(
    "--dtk",
    metavar="BINARY | DIR",
    type=Path,
    help="path to decomp-toolkit binary or source (optional)",
)
parser.add_argument(
    "--objdiff",
    metavar="BINARY | DIR",
    type=Path,
    help="path to objdiff-cli binary or source (optional)",
)
parser.add_argument(
    "--sjiswrap",
    metavar="EXE",
    type=Path,
    help="path to sjiswrap.exe (optional)",
)
parser.add_argument(
    "--verbose",
    action="store_true",
    help="print verbose output",
)
parser.add_argument(
    "--non-matching",
    dest="non_matching",
    action="store_true",
    help="builds equivalent (but non-matching) or modded objects",
)
parser.add_argument(
    "--warn",
    dest="warn",
    type=str,
    choices=["all", "off", "error"],
    help="how to handle warnings",
)
parser.add_argument(
    "--no-progress",
    dest="progress",
    action="store_false",
    help="disable progress calculation",
)
args = parser.parse_args()

config = ProjectConfig()
config.version = str(args.version)
version_num = VERSIONS.index(config.version)

# Apply arguments
config.build_dir = args.build_dir
config.dtk_path = args.dtk
config.objdiff_path = args.objdiff
config.binutils_path = args.binutils
config.compilers_path = args.compilers
config.generate_map = args.map
config.non_matching = args.non_matching
config.sjiswrap_path = args.sjiswrap
config.progress = args.progress
if not is_windows():
    config.wrapper = args.wrapper
# Don't build asm unless we're --non-matching
if not config.non_matching:
    config.asm_dir = None

# Tool versions
config.binutils_tag = "2.42-1"
config.compilers_tag = "20250520"
config.dtk_tag = "v1.6.0"
config.objdiff_tag = "v3.0.0-beta.8"
config.sjiswrap_tag = "v1.2.1"
config.wibo_tag = "0.6.16"


# Project
config.config_path = Path("config") / config.version / "config.yml"
config.check_sha_path = Path("config") / config.version / "build.sha1"
config.asflags = [
    "-mgekko",
    "--strip-local-absolute",
    "-I include",
    f"-I build/{config.version}/include",
    f"--defsym BUILD_VERSION={version_num}",
]
config.ldflags = [
    "-fp hardware",
    "-nodefaults",
]
if args.debug:
    config.ldflags.append("-gdwarf-2")  # Or -gdwarf-2 for Wii linkers
if args.map:
    config.ldflags.append("-mapunused")
    config.ldflags.append("-listclosure") # For Wii linkers

# Use for any additional files that should cause a re-configure when modified
config.reconfig_deps = []

# Optional numeric ID for decomp.me preset
# Can be overridden in libraries or objects
config.scratch_preset_id = None

# Base flags, common to most GC/Wii games.
# Generally leave untouched, with overrides added below.
cflags_base = [
    "-nodefaults",
    "-nosyspath",
    "-gccinc",

    "-maxerrors 1",
    "-nowraplines",

    "-proc gekko",
    "-align powerpc",
    "-enum int",
    "-fp hardware",

    '-pragma "cats off"',
    '-pragma "warning off (10184)"',
    '-pragma "warn_notinlined off"',
    '-pragma "warn_filenamecaps on"',
    '-pragma "warn_filenamecaps_system on"',

    "-RTTI on",
    "-Cpp_exceptions off",
    "-str noreuse",

    "-i src",
    f"-i build/{config.version}/include",
    f"-DBUILD_VERSION={version_num}",
    f"-DVERSION_{config.version}"
]

# Debug flags
if args.debug:
    cflags_base.extend(["-sym on", "-DDEBUG=1"])
else:
    cflags_base.append("-DNDEBUG=1")

# Warning flags
if args.warn == "all":
    cflags_base.append("-W all")
elif args.warn == "off":
    cflags_base.append("-W off")
elif args.warn == "error":
    cflags_base.append("-W error")

config.linker_version = "Wii/1.0"


Matching = True                   # Object matches and should be linked
NonMatching = False               # Object does not match and should not be linked
Equivalent = config.non_matching  # Object should be linked when configured with --non-matching


# Object is only matching for specific versions
def MatchingFor(*versions):
    return config.version in versions


config.warn_missing_config = True
config.warn_missing_source = True
config.libs = [
    {
        "lib": "Runtime",
        "mw_version": config.linker_version,
        "cflags": [
            *cflags_base,
            "-O4,p",
            "-lang=c99",
            "-inline auto",
            "-func_align 4",
            "-rostr"
        ],
        "host": False,
        "progress_category": "sdk",
        "objects": [
            Object(Matching, "PowerPC_EABI_Support/Runtime/__init_cpp_exceptions.cpp"),
            Object(Matching, "PowerPC_EABI_Support/Runtime/Gecko_ExceptionPPC.cpp"),
            Object(Matching, "PowerPC_EABI_Support/Runtime/global_destructor_chain.c"),
            Object(Matching, "PowerPC_EABI_Support/Runtime/__mem.c"),
            Object(Matching, "PowerPC_EABI_Support/Runtime/__va_arg.c"),
            Object(MatchingFor("SALP4Q"), "PowerPC_EABI_Support/Runtime/runtime.c"),
            Object(MatchingFor("SALP4Q"), "PowerPC_EABI_Support/Runtime/ptmf.c")
        ]
    },
    {
        "lib": "Revolution",
        "mw_version": config.linker_version,
        "cflags": [
            *cflags_base,
            "-O4,p",
            "-lang=c99",
            "-d NDEBUG"
        ],
        "host": False,
        "progress_category": "sdk",
        "objects": [
            # BASE
            Object(MatchingFor("SALP4Q"), "Revolution/BASE/PPCArch.c"),
            # NAND
            Object(MatchingFor("SALP4Q"), "Revolution/NAND/NANDLogging.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/NAND/NANDOpenClose.c"),
            # OS
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSArena.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSIPC.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSLink.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSMemory.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSSync.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/OSTime.c"),
            Object(Matching, "Revolution/OS/__start.c"),
            Object(MatchingFor("SALP4Q"), "Revolution/OS/__ppc_eabi_init.c")
        ]
    },
    {
        "lib": "MSL C",
        "mw_version": config.linker_version,
        "cflags": [
            *cflags_base,
            "-O4,p",
            "-lang=c99",
            "-use_lmw_stmw on",
            "-func_align 4"
        ],
        "host": False,
        "progress_category": "sdk",
        "objects": [
            Object(MatchingFor("SALP4Q"), "PowerPC_EABI_Support/MSL/MSL_C/MSL_Common/mem.c"),
            Object(MatchingFor("SALP4Q"), "PowerPC_EABI_Support/MSL/MSL_C/MSL_Common/secure_error.c"),
        ]
    },
    {
        "lib": "libs/zlib",
        "mw_version": config.linker_version,
        "cflags": cflags_base,
        "host": False,
        "progress_category": "lib",
        "objects": [
            Object(MatchingFor(), "Libraries/zlib/inffast.c"),
            Object(MatchingFor(), "Libraries/zlib/inftrees.c"),
            Object(MatchingFor(), "Libraries/zlib/zutil.c"),
        ]
    },
    {
        "lib": "Alice",
        "mw_version": config.linker_version,
        "cflags": [
            *cflags_base,
            "-O4,p",
            "-func_align 4",
            "-inline all",
            "-fp_contract on",
            "-rostr"
        ],
        "host": False,
        "progress_category": "game",  # str | List[str]
        "objects": [
            # Engine
            Object(MatchingFor(), "K/KCore/Main/CKYellowPages.cpp", extra_cflags=[
                "-opt nopeephole",
            ]),
            Object(MatchingFor(), "K/KServices/KServiceCounter/CKTimeCounter.cpp", extra_cflags=[
                "-opt nopeephole",
            ]),
            # 2- Hooks
            Object(MatchingFor(), "Alice/K/KAliceHero/CKHkAliceHero.cpp"),
            # 4 - Groups
            Object(MatchingFor(), "Alice/K/KAliceHero/CKGrpAliceHero.cpp")
        ]
    }
]


# Optional callback to adjust link order. This can be used to add, remove, or reorder objects.
# This is called once per module, with the module ID and the current link order.
#
# For example, this adds "dummy.c" to the end of the DOL link order if configured with --non-matching.
# "dummy.c" *must* be configured as a Matching (or Equivalent) object in order to be linked.
def link_order_callback(module_id: int, objects: List[str]) -> List[str]:
    # Don't modify the link order for matching builds
    if not config.non_matching:
        return objects
    if module_id == 0:  # DOL
        return objects + ["dummy.c"]
    return objects
# Uncomment to enable the link order callback.
# config.link_order_callback = link_order_callback


# Optional extra categories for progress tracking
# Adjust as desired for your project
config.progress_categories = [
    ProgressCategory("game", "Game Code"),
    ProgressCategory("sdk", "SDK Code"),
    ProgressCategory("lib", "Libraries")
]
config.progress_each_module = args.verbose
# Optional extra arguments to `objdiff-cli report generate`
config.progress_report_args = [
    # Marks relocations as mismatching if the target value is different
    # Default is "functionRelocDiffs=none", which is most lenient
    # "--config functionRelocDiffs=data_value",
]

if args.mode == "configure":
    # Write build.ninja and objdiff.json
    generate_build(config)
elif args.mode == "progress":
    # Print progress information
    calculate_progress(config)
else:
    sys.exit("Unknown mode: " + args.mode)
