# -*- coding: utf-8 -*-
"""gui_api batch automation: a dock panel that runs a pipeline over a folder of projects.

1. Pick an input folder and press "Scan": every .zprj in it (optionally sub folders) is listed.
2. Tick the steps to run: simulate, 3D / 2D thumbnails, OBJ, GLB, POM, BOM, save a project copy.
3. "Run selected" processes the highlighted project, "Run all" the whole list. Each project is opened,
   the steps run in order, and the table shows the result. A failing step is logged and the batch goes on.
4. Results go to <output>/<project name>/, plus run_log.csv for the whole batch.
   The panel settings are kept in <output>/automation_settings.json for the next run.

The steps run inside the button callback on CLO's main thread, so the window is busy until the batch ends.
"""
import csv
import datetime
import json
import os
import traceback

import ApiTypes
import export_api
import gui_api
import import_api
import pattern_api
import utility_api

if not gui_api.IsGuiAvailable():
    raise SystemExit("gui_api needs the CLO GUI (not available in headless mode)")

DEFAULT_INPUT = os.path.join(os.path.expanduser("~"), "Documents", "CLO").replace(os.sep, "/")  # folder with .zprj files
DEFAULT_OUTPUT = os.path.join(os.path.expanduser("~"), "Documents", "CLO", "Automation").replace(os.sep, "/")
SETTINGS_NAME = "automation_settings.json"

# (key, label, default) in run order
STEPS = [
    ("simulate", "Simulate", False),
    ("thumb3d", "3D thumbnail (PNG)", True),
    ("thumb2d", "2D thumbnail (PNG)", True),
    ("obj", "Export OBJ", False),
    ("glb", "Export GLB", False),
    ("pom", "Export POM", False),
    ("bom", "Export BOM", False),
    ("zprj", "Save project copy (ZPRJ)", False),
]

gui_api.CloseAllWindows()  # a re-run must not leave the previous panel open

ui = {"steps": {}}
state = {"files": [], "status": {}}


# ------------------------------------------------------------------ helpers
_FORMULA_PREFIXES = ("=", "+", "-", "@", "\t", "\r")


def csv_cell(value):
    """Neutralise spreadsheet formula injection (CWE-1236): file names come from disk."""
    text = str(value)
    try:
        float(text)
        return text
    except ValueError:
        pass
    return "'" + text if text.startswith(_FORMULA_PREFIXES) else text


def now():
    return datetime.datetime.now().strftime("%H:%M:%S")


def log(text):
    line = "[%s] %s" % (now(), text)
    state.setdefault("log", []).append(line)
    gui_api.SetWidgetValue(ui["log"], "\n".join(state["log"][-200:]))


def status(text):
    gui_api.SetWidgetText(ui["status"], text)


def value(key):
    return gui_api.GetWidgetValue(ui[key]).strip().strip('"')


def checked(key):
    return gui_api.GetWidgetValue(ui["steps"][key]).lower() in ("true", "1")


def output_root():
    return value("output") or DEFAULT_OUTPUT


def project_name(path):
    return os.path.splitext(os.path.basename(path))[0]


def refresh_table():
    rows = []
    for path in state["files"]:
        st = state["status"].get(path, {})
        rows.append([project_name(path), st.get("result", "pending"), st.get("patterns", ""),
                     st.get("time", ""), st.get("detail", "")])
    gui_api.SetTableRows(ui["table"], rows)


# ------------------------------------------------------------------ settings
def settings_path():
    return os.path.join(output_root(), SETTINGS_NAME)


def save_settings():
    data = {"input": value("input"), "output": output_root(), "recursive": checked_box("recursive"),
            "sim_steps": value("sim_steps"), "steps": {k: checked(k) for k, _, _ in STEPS}}
    try:
        os.makedirs(output_root(), exist_ok=True)
        with open(settings_path(), "w", encoding="utf-8") as f:
            json.dump(data, f, indent=1)
    except OSError as e:
        log("settings not saved: %s" % e)


def load_settings():
    for candidate in (os.path.join(DEFAULT_OUTPUT, SETTINGS_NAME),):
        try:
            with open(candidate, encoding="utf-8") as f:
                return json.load(f)
        except (OSError, ValueError):
            pass
    return {}


def checked_box(key):
    return gui_api.GetWidgetValue(ui[key]).lower() in ("true", "1")


# ------------------------------------------------------------------ pipeline
def run_steps(path):
    """Open one project and run the ticked steps. Returns (result, detail)."""
    name = project_name(path)
    out_dir = os.path.join(output_root(), name)
    os.makedirs(out_dir, exist_ok=True)
    base = os.path.join(out_dir, name).replace(os.sep, "/")

    if not import_api.ImportZprj(path, ApiTypes.ImportZPRJOption()):
        return "FAIL", "could not open"
    state["status"][path]["patterns"] = str(pattern_api.GetPatternCount())

    done, failed = [], []

    def step(label, fn, hint=""):
        try:
            result = fn()
            ok = bool(result) if not isinstance(result, (list, tuple)) else len(result) > 0
        except Exception as e:  # keep the batch going, report the step
            ok = False
            log("  %s: %s" % (label, e))
        (done if ok else failed).append(label)
        log("  %s %s" % (label, "ok" if ok else ("FAILED (%s)" % hint if hint else "FAILED")))

    if checked("simulate"):
        steps = max(1, int(float(value("sim_steps") or 0)))
        step("simulate", lambda: utility_api.Simulate(steps))
    if checked("thumb3d"):
        step("thumb3d", lambda: export_api.ExportThumbnail3D(base + "_3d.png"))
    if checked("thumb2d"):
        step("thumb2d", lambda: export_api.ExportThumbnail2D(base + "_2d.png"), "needs the 2D window to be visible")
    if checked("obj"):
        step("obj", lambda: export_api.ExportOBJ(base + ".obj", ApiTypes.ImportExportOption()))
    if checked("glb"):
        step("glb", lambda: export_api.ExportGLB(base + ".glb", ApiTypes.ImportExportOption()))
    if checked("pom"):
        step("pom", lambda: export_api.ExportPOM(base + "_pom.csv"), "nothing to export when the project has no POM measures")
    if checked("bom"):
        step("bom", lambda: export_api.ExportBOM(base + "_bom.xml"))
    if checked("zprj"):
        step("zprj", lambda: export_api.ExportZPrj(base + "_out.zprj"))

    if failed:
        return ("PARTIAL" if done else "FAIL"), "failed: " + ", ".join(failed)
    return "OK", ", ".join(done) if done else "no step ticked"


def run(paths):
    if not paths:
        status("nothing to run: scan a folder first")
        return
    if not any(checked(k) for k, _, _ in STEPS):
        status("tick at least one step")
        return
    save_settings()
    started = datetime.datetime.now()
    rows = []
    for i, path in enumerate(paths):
        name = project_name(path)
        status("(%d/%d) %s ..." % (i + 1, len(paths), name))
        log("%s" % name)
        state["status"][path] = {"result": "running"}
        refresh_table()
        t0 = datetime.datetime.now()
        try:
            result, detail = run_steps(path)
        except Exception:
            result, detail = "FAIL", traceback.format_exc().strip().splitlines()[-1]
            log("  " + detail)
        seconds = (datetime.datetime.now() - t0).total_seconds()
        state["status"][path].update(result=result, detail=detail, time="%.1fs" % seconds)
        rows.append([name, result, state["status"][path].get("patterns", ""), "%.1f" % seconds, detail, path])
        gui_api.SetWidgetValue(ui["progress"], str(int((i + 1) * 100 / len(paths))))
        refresh_table()

    log_path = os.path.join(output_root(), "run_log.csv")
    try:
        with open(log_path, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.writer(f)
            writer.writerow(["Project", "Result", "Patterns", "Seconds", "Detail", "Path"])
            for row in rows:
                writer.writerow([csv_cell(c) for c in row])
    except OSError as e:
        log("run_log.csv not written: %s" % e)
    ok = sum(1 for r in rows if r[1] == "OK")
    status("done: %d / %d OK in %.0fs -> %s" % (ok, len(rows), (datetime.datetime.now() - started).total_seconds(), output_root()))


# ------------------------------------------------------------------ actions
def scan():
    folder = value("input")
    if not os.path.isdir(folder):
        status("folder not found: %s" % folder)
        return
    found = []
    if checked_box("recursive"):
        for root, _, files in os.walk(folder):
            found += [os.path.join(root, f) for f in files if f.lower().endswith(".zprj")]
    else:
        found = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(".zprj")]
    state["files"] = sorted(p.replace(os.sep, "/") for p in found)
    state["status"] = {}
    refresh_table()
    gui_api.SetWidgetValue(ui["progress"], "0")
    status("%d project(s) found" % len(state["files"]))


def run_selected():
    row = gui_api.GetWidgetSelectedIndex(ui["table"])
    if row < 0 or row >= len(state["files"]):
        status("select a project in the table first")
        return
    run([state["files"][row]])


def run_all():
    if state["files"] and not gui_api.AskConfirm("Batch automation", "Run the ticked steps on %d project(s)?\nThe current scene will be replaced." % len(state["files"]), "Run", "Cancel"):
        return
    run(list(state["files"]))


def open_output():
    os.makedirs(output_root(), exist_ok=True)
    os.startfile(output_root())


def set_all_steps(on):
    for key, _, _ in STEPS:
        gui_api.SetWidgetValue(ui["steps"][key], "true" if on else "false")


# ------------------------------------------------------------------ layout
saved = load_settings()
panel = gui_api.CreateDockPanel("Batch Automation", "left", "batch_automation_left")

source = gui_api.AddGroup(panel, "Input")
ui["input"] = gui_api.AddTextInput(source, saved.get("input", DEFAULT_INPUT), "folder with .zprj files")
row = gui_api.AddRow(source)
ui["recursive"] = gui_api.AddCheckBox(row, "Include sub folders", bool(saved.get("recursive", False)))
gui_api.AddButton(row, "Scan", scan)

steps = gui_api.AddGroup(panel, "Steps")
saved_steps = saved.get("steps", {})
for key, label, default in STEPS:
    if key == "simulate":
        sim_row = gui_api.AddRow(steps)
        ui["steps"][key] = gui_api.AddCheckBox(sim_row, label, bool(saved_steps.get(key, default)))
        gui_api.AddLabel(sim_row, "frames")
        ui["sim_steps"] = gui_api.AddNumberInput(sim_row, float(saved.get("sim_steps", 300) or 300), 1, 100000, 0)
    else:
        ui["steps"][key] = gui_api.AddCheckBox(steps, label, bool(saved_steps.get(key, default)))
toggle = gui_api.AddRow(steps)
gui_api.AddButton(toggle, "All", lambda: set_all_steps(True))
gui_api.AddButton(toggle, "None", lambda: set_all_steps(False))

target = gui_api.AddGroup(panel, "Output")
ui["output"] = gui_api.AddTextInput(target, saved.get("output", DEFAULT_OUTPUT), "output folder")
gui_api.AddButton(target, "Open output folder", open_output)

queue = gui_api.AddGroup(panel, "Projects")
ui["table"] = gui_api.AddTable(queue, ["Project", "Result", "Patterns", "Time", "Detail"], [])
actions = gui_api.AddRow(queue)
gui_api.AddButton(actions, "Run selected", run_selected)
gui_api.AddButton(actions, "Run all", run_all)
ui["progress"] = gui_api.AddProgressBar(queue, 0)
ui["status"] = gui_api.AddLabel(queue, "scan a folder to start")

history = gui_api.AddGroup(panel, "Log")
ui["log"] = gui_api.AddTextArea(history, "", True)

gui_api.ShowWindow(panel)
# The script returns here. The panel stays docked and all buttons keep working.
