from __future__ import annotations
from pathlib import Path
from typing import Optional, Callable, Tuple

from curvpyutils.file_utils import open_write_iff_change
from curvtools.cli.curvcfg.lib.util.config_parsing import SchemaOracle
from curvtools.cli.curvcfg.lib.util.config_parsing.util.types import Artifact
from curvtools.cli.curvcfg.lib.util.config_parsing.util.helpers import render_template_to_str

__all__ = [
    "emit_artifacts",
]

def emit_artifacts(
    schema_oracle: SchemaOracle,
    svpkg_out_path: Path,
    svh_out_path: Path,
    env_out_path: Path,
    mk_out_path: Path,
    svpkg_template: Optional[Path] = None,
    svh_template: Optional[Path] = None,
    mk_template: Optional[Path] = None,
    env_template: Optional[Path] = None,
) -> Tuple[bool, bool, bool, bool]:
    """
    Emits the artifacts based on `schema_oracle`, e.g., for cfgvars:
        - curvcfgpkg.sv
        - curvcfg.svh
        - curv.env
        - curv.mk
    
    No dep file because the build recipes to rebuild these are already baked into the Makefile recipes.

    Args:
        schema_oracle: SchemaOracle object.
        svpkg_out_path: output path for the SystemVerilog package.
        svh_out_path: output path for the SystemVerilog include file.
        env_out_path: output path for the environment file.
        mk_out_path: output path for the make fragment.
        svpkg_template: optional template override for the SystemVerilog package.
        svh_template: optional template override for the SystemVerilog include file.
        mk_template: optional template override for the make fragment.
        env_template: optional template override for the environment file.

    Returns:
        A tuple of 4 bools indicating whether each output file was modified:
            (svpkg_changed, svh_changed, env_changed, mk_changed)
        The order matches the order of the output path arguments.
    """
    unresolved = list(schema_oracle.iter_unresolved())
    if unresolved:
        missing = ", ".join(name for name, _ in unresolved)
        print(f"Error: unresolved schema variables: {missing}")
        return (False, False, False, False)

    for p in (svpkg_out_path, svh_out_path, env_out_path, mk_out_path):
        p.parent.mkdir(parents=True, exist_ok=True)

    svpkg_changed = _emit_one(
        artifact=Artifact.SVPKG,
        schema_oracle=schema_oracle,
        out_path=svpkg_out_path,
        template_path=svpkg_template,
        render_fn=_emit_sv_pkg,
    )
    svh_changed = _emit_one(
        artifact=Artifact.SVH,
        schema_oracle=schema_oracle,
        out_path=svh_out_path,
        template_path=svh_template,
        render_fn=_emit_svh_defines,
    )
    env_changed = _emit_one(
        artifact=Artifact.ENV,
        schema_oracle=schema_oracle,
        out_path=env_out_path,
        template_path=env_template,
        render_fn=_emit_env_file,
    )
    mk_changed = _emit_one(
        artifact=Artifact.MK,
        schema_oracle=schema_oracle,
        out_path=mk_out_path,
        template_path=mk_template,
        render_fn=_emit_makefile,
    )

    return (svpkg_changed, svh_changed, env_changed, mk_changed)


def _emit_one(
    artifact: Artifact,
    schema_oracle: SchemaOracle,
    out_path: Path,
    template_path: Optional[Path],
    render_fn: Callable[[SchemaOracle, Path], bool],
) -> bool:
    if template_path is not None:
        rendered = render_template_to_str(template_path, schema_oracle)
        cm = open_write_iff_change(out_path, "w")
        with cm as f:
            f.write(rendered)
        return cm.changed or False

    return render_fn(schema_oracle, out_path)


def _emit_makefile(schema_oracle: SchemaOracle, out_path: Path) -> bool:
    guard_name = f"__{out_path.name.replace('.', '_').upper()}__"
    vars_for_mk = schema_oracle.get_vars_for_artifact(Artifact.MK)

    cm = open_write_iff_change(out_path, "w")
    with cm as f:
        f.write(f"ifndef {guard_name}\n")
        f.write(f"{guard_name} := 1\n\n")
        f.write("# Autogenerated by curvcfg. Do not edit.\n")
        for name in sorted(vars_for_mk.keys()):
            var = vars_for_mk[name]
            f.write(f"{name} := {var.mk_display()}\n")
        f.write("\n")
        f.write(f"endif # {guard_name}\n")
    return cm.changed or False


def _emit_env_file(schema_oracle: SchemaOracle, out_path: Path) -> bool:
    vars_for_env = schema_oracle.get_vars_for_artifact(Artifact.ENV)
    cm = open_write_iff_change(out_path, "w")
    with cm as f:
        f.write("# -----------------------------------------------------------------------------\n")
        f.write("# Autogenerated by curvcfg. Do not edit.\n")
        f.write("# -----------------------------------------------------------------------------\n\n")
        for name in sorted(vars_for_env.keys()):
            var = vars_for_env[name]
            f.write(f"{name}={var.mk_display()}\n")
    return cm.changed or False


def _emit_svh_defines(schema_oracle: SchemaOracle, out_path: Path) -> bool:
    vars_for_svh = schema_oracle.get_vars_for_artifact(Artifact.SVH)
    filename = out_path.name.upper().replace(".", "_")
    guard = f"__{filename}__"

    cm = open_write_iff_change(out_path, "w")
    with cm as f:
        f.write("// -----------------------------------------------------------------------------\n")
        f.write("// Autogenerated by curvcfg. Do not edit.\n")
        f.write("// -----------------------------------------------------------------------------\n\n")
        f.write(f"`ifndef {guard}\n`define {guard}\n\n")

        if not vars_for_svh:
            f.write("// (No defines selected by schema locations)\n")
        else:
            longest = max(len(k) for k in vars_for_svh.keys())
            for name in sorted(vars_for_svh.keys()):
                var = vars_for_svh[name]
                lit = var.sv_literal(for_macro=True)
                padding = name.ljust(longest + 4)
                f.write(f"`define {padding} {lit}\n")

        f.write(f"\n`endif // {guard}\n")
    return cm.changed or False


def _emit_sv_pkg(schema_oracle: SchemaOracle, out_path: Path) -> bool:
    vars_for_pkg = schema_oracle.get_vars_for_artifact(Artifact.SVPKG)
    pkg_name = out_path.stem.lower()

    cm = open_write_iff_change(out_path, "w")
    with cm as f:
        f.write("// -----------------------------------------------------------------------------\n")
        f.write("// Autogenerated by curvcfg. Do not edit.\n")
        f.write("// -----------------------------------------------------------------------------\n\n")
        f.write(f"package {pkg_name};\n\n")
        f.write("  // verilator lint_off UNUSEDPARAM\n")

        for name in sorted(vars_for_pkg.keys()):
            var = vars_for_pkg[name]
            line = var.sv_display()
            if not line.strip().endswith(";"):
                line = line.rstrip() + ";"
            f.write(f"  {line}\n")

        f.write("  // verilator lint_on UNUSEDPARAM\n\n")
        f.write(f"endpackage : {pkg_name}\n")
    return cm.changed or False
