#!/usr/bin/env python3
"""Ejecuta SCEPTRE nuevo o antiguo y usa Tabular para generar un .simul."""
import argparse
import math
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile

HERE = Path(__file__).resolve().parent
EXTENSIONS = (".simul", ".dirac", ".ls1", ".ls2", ".log")


def pick(root, *names, required=True):
    for name in names:
        path = root / name
        if path.is_file():
            return path
    if required:
        raise ValueError(f"falta {names[0]} en {root}; compilar esa instalacion de SCEPTRE")
    return None


def installation(root):
    """Reconoce árboles compilados o prefijos instalados, sin mezclar versiones."""
    root = root.expanduser().resolve()
    phase1 = pick(root, "bin/sceptre1", "src/phase1/sceptre1", required=False)
    if phase1:
        kind = "inenco"
        main = pick(root, "lib/sceptre2.o", "src/phase2/sceptre2.o")
        library = pick(root, "lib/libsceptre.a", "src/phase2/libsceptre.a")
        auxiliary = pick(root, "lib/libsceptreaux.a", "src/auxpro/libsceptreaux.a")
    else:
        phase1 = pick(root, "bin/exec1", "x1/exec1")
        main = pick(root, "lib/exec2.o", "x2/exec2.o")
        library = pick(root, "lib/libx2.a", "x2/libx2.a")
        auxiliary = pick(root, "lib/libx3.a", "x3/libx3.a", required=False)
        kind = "antiguo" if auxiliary else "nuevo"
    return root, kind, phase1, main, library, auxiliary


def default_root():
    if os.environ.get("SCEPTRE_HOME"):
        return Path(os.environ["SCEPTRE_HOME"])
    local = HERE.parents[1] / "sceptre2novender/SC1-SD2-26131"
    if (local / "bin/exec1").is_file():
        return local
    for name in ("sceptre1", "exec1"):
        executable = shutil.which(name)
        if executable:
            return Path(executable).resolve().parents[1]
    return None


def executable(value):
    found = shutil.which(str(value))
    if not found:
        raise ValueError("no se encuentra el ejecutable: " + str(value))
    return Path(found).resolve()


def free_output(requested):
    for n in range(1000000):
        candidate = requested if n == 0 else requested.with_name(requested.stem + f"-{n}.simul")
        if not any(os.path.lexists(candidate.with_suffix(ext)) for ext in EXTENSIONS):
            return candidate
    raise ValueError("no se encuentra un nombre libre para la salida")


def publish(source, destination):
    """Publicar completo sin reemplazar archivos existentes, incluso con carreras."""
    with tempfile.NamedTemporaryFile(dir=destination.parent, prefix=".sceptre-tabular-", delete=False) as tmp:
        staging = Path(tmp.name)
        try:
            with source.open("rb") as stream:
                shutil.copyfileobj(stream, tmp)
        except BaseException:
            staging.unlink()
            raise
    try:
        os.link(staging, destination)
    finally:
        staging.unlink()


def run(command, work, log, timeout, output=None):
    log.write("\n$ " + shlex.join(str(x) for x in command) + "\n")
    log.flush()
    env = os.environ.copy()
    env["LC_ALL"] = "C"
    result = subprocess.run(list(map(str, command)), cwd=work, env=env,
                            stdin=subprocess.DEVNULL, stdout=output or log,
                            stderr=log, timeout=timeout)
    if result.returncode:
        raise RuntimeError(f"{Path(command[0]).name} termino con codigo {result.returncode}; revisar .log")


def simulate(args, source, work, log):
    if args.sceptre_dir is None:
        raise ValueError("indicar --sceptre-dir con la raiz de una instalacion compilada")
    root, kind, phase1, main, library, auxiliary = installation(args.sceptre_dir)
    model = args.modelos or pick(root, "lib/modellib.dat", "data/modellib.dat",
                                "share/sceptre-inenco/modellib.dat")
    userlib = args.biblioteca or pick(root, "lib/usrlib.a", "data/usrlib.a",
                                     "share/sceptre-inenco/usrlib.a")
    userlib = userlib.expanduser().resolve()
    shutil.copyfile(model.expanduser().resolve(), work / "modellib.dat")
    shutil.copyfile(source, work / ("circuito.d" if kind == "inenco" else "input.dat"))
    for data in args.dato:
        data = data.expanduser().resolve()
        reserved = {"input.dat", "modellib.dat", "pgmsave.for", "pgmsave.f",
                    "datasave.dat", "acsave.dat", "exec2", "sceptre.lis", "dirac.dat",
                    "resultado.dirac", "salida.simul", "corrida.log", "fase1.lis", "fase2.lis"}
        if (data.name in reserved or data.name.startswith(("fort.", "circuito.")) or
                (work / data.name).exists()):
            raise ValueError("nombre de dato reservado o repetido: " + data.name)
        shutil.copyfile(data, work / data.name)
    extras = [p.expanduser().resolve() for p in args.fuente]
    for companion in (source.with_suffix(".f90"), source.parent / "funciones.for"):
        if companion.is_file() and companion != source and companion not in extras:
            extras.append(companion)
    print(f"SCEPTRE {kind}: {root}", file=sys.stderr)
    log.write(f"Entrada: {source}\nInstalacion: {root} ({kind})\n")
    first_listing = work / ("circuito.ls1" if kind == "inenco" else "sceptre.lis")
    second_listing = work / ("circuito.ls2" if kind == "inenco" else "sceptre.lis")
    command = [phase1]
    if kind == "inenco":
        command += ["-m", "modellib.dat", "-o", ".", "circuito.d"]
    try:
        run(command, work, log, args.timeout)
    finally:
        if first_listing.is_file():
            first_listing.rename(work / "fase1.lis")
    program = work / ("circuito.for" if kind == "inenco" else "pgmsave.for")
    if not program.is_file() or not program.stat().st_size:
        raise RuntimeError("la fase 1 no genero Fortran; revisar .ls1")
    if kind != "nuevo":
        # Es la misma sustitución usada por los lanzadores históricos.
        converted = work / "pgmsave.f"
        converted.write_bytes(program.read_bytes().replace(b"$", b"_"))
        program = converted
    command = [args.fc, "-std=legacy", "-fno-automatic", "-fdollar-ok",
               "-Wno-line-truncation", "-I", source.parent]
    for include in (root / "x2", root / "src/phase2"):
        if include.is_dir():
            command += ["-I", include]
    command += ["-o", work / "exec2", program, *extras, main, userlib, library]
    if auxiliary:
        command.append(auxiliary)
    command += args.enlazar
    run(command, work, log, args.timeout)
    try:
        run([work / "exec2"], work, log, args.timeout)
    finally:
        if second_listing.is_file():
            second_listing.rename(work / "fase2.lis")
    binary = work / ("fort.7" if kind == "nuevo" else "dirac.dat")
    if not binary.is_file():
        raise RuntimeError("SCEPTRE no produjo series binarias; revisar .ls2 (solo condiciones iniciales no genera .simul)")
    listing = (work / "fase2.lis").read_text(errors="replace").upper()
    begin = "BEGIN POST PROCESSING" if kind == "nuevo" else "S C E P T R E   POST PROCESSING"
    if listing.count(begin) != listing.count("END OF POST PROCESSING"):
        raise RuntimeError("postprocesamiento incompleto; revisar .ls2")
    if "SMALLER MINIMUM STEP SIZE REQUIRED" in listing:
        raise RuntimeError("integracion interrumpida: requiere un paso menor al minimo; revisar .ls2")
    if "NO DC STEADY STATE SOLUTION" in listing:
        raise RuntimeError("fallo la solucion estacionaria; revisar .ls2")
    return binary


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("entrada", type=Path, help="circuito .d/.sceptre o binario POS/POS4")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-o", "--directorio", type=Path, default=Path.cwd(), help="directorio de salida (actual)")
    group.add_argument("--salida", type=Path, help="nombre .simul; si existe se agrega -1, -2, etc.")
    parser.add_argument("--sceptre-dir", type=Path, default=default_root(),
                        help="raiz compilada/instalada; reconoce nuevo, antiguo e INENCO")
    parser.add_argument("--tabular", default=str(HERE / "interpola") if (HERE / "interpola").is_file() else "interpola",
                        help="ejecutable interpola con --exportar-simul")
    parser.add_argument("--convertir", action="store_true", help="convertir un binario sin ejecutar SCEPTRE")
    parser.add_argument("-m", "--modelos", type=Path, help="modellib.dat")
    parser.add_argument("-u", "--biblioteca", type=Path, help="usrlib.a")
    parser.add_argument("--fuente", type=Path, action="append", default=[], help="Fortran adicional; repetible")
    parser.add_argument("--dato", type=Path, action="append", default=[], help="dato auxiliar; repetible")
    parser.add_argument("--enlazar", action="append", default=[], help="opcion de enlace; por ejemplo --enlazar=-lm")
    parser.add_argument("--fc", default="gfortran", help="compilador compatible con opciones de gfortran")
    parser.add_argument("--timeout", type=float, default=300, help="segundos por fase (300)")
    parser.add_argument("--conservar", action="store_true", help="conservar directorio temporal")
    args = parser.parse_args(argv)
    work = result = None
    status = 1
    try:
        source = args.entrada.expanduser().resolve()
        if not source.exists() and not source.suffix:
            source = source.with_suffix(".d")
        if not source.is_file():
            raise ValueError("no existe la entrada: " + str(source))
        if not math.isfinite(args.timeout) or args.timeout <= 0:
            raise ValueError("--timeout debe ser positivo y finito")
        tabular = executable(args.tabular)
        requested = (args.salida or args.directorio / (source.stem + ".simul")).expanduser().absolute()
        if requested.suffix != ".simul":
            raise ValueError("--salida debe terminar en .simul")
        requested.parent.mkdir(parents=True, exist_ok=True)
        result = free_output(requested)
        with source.open("rb") as stream:
            convert = args.convertir or stream.read(8) == b"DIRECTOR" or source.suffix.lower() == ".dirac"
        work = Path(tempfile.mkdtemp(prefix="sceptre-tabular-"))
        with (work / "corrida.log").open("w", encoding="utf-8") as log:
            binary = source if convert else simulate(args, source, work, log)
            # Un nombre fijo evita la sintaxis especial #K de Tabular y nombres
            # con espacios, comillas o caracteres de órdenes en el intérprete.
            shutil.copyfile(binary, work / "resultado.dirac")
            with (work / "salida.simul").open("wb") as output:
                run([tabular, "--exportar-simul", "resultado.dirac"], work, log, args.timeout, output)
        if not convert:
            publish(binary, result.with_suffix(".dirac"))
        publish(work / "salida.simul", result)
        print(result)
        status = 0
    except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as exc:
        print("sceptre-tabular: " + str(exc), file=sys.stderr)
    finally:
        if work is not None:
            try:
                for name, ext in (("corrida.log", ".log"), ("fase1.lis", ".ls1"), ("fase2.lis", ".ls2")):
                    if (work / name).is_file():
                        publish(work / name, result.with_suffix(ext))
                print("Diagnosticos: " + str(result.with_suffix(".log")), file=sys.stderr)
            except OSError as exc:
                args.conservar = True
                status = 1
                print("No se pudieron guardar diagnosticos: " + str(exc), file=sys.stderr)
            if args.conservar:
                print("Directorio de trabajo: " + str(work), file=sys.stderr)
            else:
                shutil.rmtree(work)
    return status


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