# -*- coding: utf-8 -*-
"""gui_api BOM workflow: a dock panel that loads a project and summarises its BOM.

1. Type or paste a .zprj path and press "Load project" (or "Use current" for the open scene).
2. Every pattern is listed. Selecting one shows its details (fabric, area, size, grain, ...).
3. The fabric summary table groups the patterns by fabric with piece count, total area and an
   estimated fabric length (area / fabric width, plus a waste allowance you can change).
4. "Export CSV" writes the summary, "Export BOM package" runs export_api.ExportBOM.
"""
import csv
import os
import tempfile

import ApiTypes
import export_api
import fabric_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_PROJECT = ""  # type or paste the path of a .zprj file in the panel
OUTPUT_DIR = os.path.join(os.path.expanduser("~"), "Documents", "CLO", "BOM")

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

# Left side: the right side is shared with Object Browser / Property Editor, the left has the full height.
panel = gui_api.CreateDockPanel("BOM Workflow", "left", "bom_workflow_left")
ui = {}
data = {"patterns": [], "fabrics": {}}


# ------------------------------------------------------------------ helpers
def fmt(value, digits=1):
    return ("%%.%df" % digits) % value


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


def csv_cell(value):
    """Neutralise spreadsheet formula injection (CWE-1236).

    Pattern and fabric names come from the project file, so a name such as "=HYPERLINK(...)" would run as a
    formula when the CSV is opened in Excel. Text that starts with a formula character gets a leading quote.
    Plain numbers (including negative ones such as a grain angle) are written unchanged.
    """
    text = str(value)
    try:
        float(text)
        return text
    except ValueError:
        pass
    return "'" + text if text.startswith(_FORMULA_PREFIXES) else text


def write_csv_row(writer, row):
    writer.writerow([csv_cell(cell) for cell in row])


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


def read_fabric(index):
    if index < 0 or index >= fabric_api.GetFabricCount():
        return {"name": "(none)", "width_mm": 0.0, "info": {}}
    return {
        "name": fabric_api.GetFabricName(index) or "Fabric %d" % index,
        "width_mm": fabric_api.GetFabricWidth(index),
        "info": dict(fabric_api.GetFabricInformation(index)),
    }


def collect():
    """Read every pattern and fabric of the current scene into `data`."""
    count = pattern_api.GetPatternCount()
    fabrics = {}
    patterns = []
    for i in range(count):
        fabric_index = pattern_api.GetPatternPieceFabricIndex(i)
        if fabric_index not in fabrics:
            fabrics[fabric_index] = read_fabric(fabric_index)
        bbox = dict(pattern_api.GetBoundingBoxOfPattern(i))
        patterns.append({
            "index": i,
            "name": pattern_api.GetPatternPieceName(i) or "Pattern %d" % i,
            "fabric": fabric_index,
            "area_mm2": pattern_api.GetPatternPieceArea(i),
            "width_mm": float(bbox.get("Width", 0) or 0),
            "height_mm": float(bbox.get("Height", 0) or 0),
            "grain": pattern_api.GetPatternPieceGrainDirection(i),
            "classification": pattern_api.GetPatternPieceClassification(i),
        })
        gui_api.SetWidgetValue(ui["progress"], str(int((i + 1) * 100 / max(count, 1))))
    data["patterns"] = patterns
    data["fabrics"] = fabrics


def fabric_summary_rows():
    waste = float(gui_api.GetWidgetValue(ui["waste"]) or 0) / 100.0
    rows = []
    for fabric_index, fabric in sorted(data["fabrics"].items()):
        pieces = [p for p in data["patterns"] if p["fabric"] == fabric_index]
        area_m2 = sum(p["area_mm2"] for p in pieces) / 1e6
        width_m = fabric["width_mm"] / 1000.0
        length_m = (area_m2 / width_m) * (1.0 + waste) if width_m > 0 else 0.0
        rows.append([fabric["name"], str(len(pieces)), fmt(area_m2, 3), fmt(width_m * 100, 1), fmt(length_m, 2)])
    return rows


def refresh_views():
    gui_api.SetWidgetItems(ui["patterns"], ["%s   |   %s   |   %s cm2" % (p["name"], data["fabrics"][p["fabric"]]["name"], fmt(p["area_mm2"] / 100.0, 0))
                                            for p in data["patterns"]])
    gui_api.SetTableRows(ui["detail"], [])
    gui_api.SetTableRows(ui["summary"], fabric_summary_rows())
    total_area = sum(p["area_mm2"] for p in data["patterns"]) / 1e6
    project = utility_api.GetProjectName() or "(untitled)"
    status("%s: %d patterns, %d fabrics, %s m2" % (project, len(data["patterns"]), len(data["fabrics"]), fmt(total_area, 3)))
    thumb = export_api.ExportThumbnail3DW(os.path.join(tempfile.gettempdir(), "gui_api_bom_thumb.png"))
    if thumb:
        if ui.get("thumb"):
            gui_api.SetWidgetValue(ui["thumb"], thumb)
        else:
            ui["thumb"] = gui_api.AddImage(ui["preview"], thumb, 200, 150)


# ------------------------------------------------------------------ actions
def load_project():
    path = gui_api.GetWidgetValue(ui["path"]).strip().strip('"')
    if not os.path.isfile(path):
        status("file not found: %s" % path)
        return
    status("loading %s ..." % os.path.basename(path))
    gui_api.SetWidgetValue(ui["progress"], "0")
    if not import_api.ImportZprj(path, ApiTypes.ImportZPRJOption()):
        status("failed to load %s" % os.path.basename(path))
        return
    use_current()


def use_current():
    collect()
    refresh_views()


def on_pattern_selected(row):
    if row < 0 or row >= len(data["patterns"]):
        return
    p = data["patterns"][row]
    fabric = data["fabrics"].get(p["fabric"], read_fabric(p["fabric"]))
    info = fabric["info"]
    gui_api.SetTableRows(ui["detail"], [
        ["Pattern", p["name"]],
        ["Fabric", fabric["name"]],
        ["Area", "%s cm2" % fmt(p["area_mm2"] / 100.0)],
        ["Size (W x H)", "%s x %s cm" % (fmt(p["width_mm"] / 10.0), fmt(p["height_mm"] / 10.0))],
        ["Grain", "%s deg" % fmt(p["grain"])],
        ["Classification", p["classification"]],
        ["Fabric width", "%s cm" % fmt(fabric["width_mm"] / 10.0)],
        ["Content", info.get("Content", "")],
        ["Weight", info.get("Weight", "")],
        ["Thickness", info.get("Thickness", "")],
        ["Color", info.get("ColorCode", info.get("Color", ""))],
    ])
    # follow the same fabric in the summary table
    for i, fabric_index in enumerate(sorted(data["fabrics"])):
        if fabric_index == p["fabric"]:
            gui_api.SetWidgetSelectedIndex(ui["summary"], i)


def export_csv():
    if not data["patterns"]:
        status("load a project first")
        return
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    name = (utility_api.GetProjectName() or "untitled").replace(" ", "_")
    path = os.path.join(OUTPUT_DIR, name + "_bom.csv")
    with open(path, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        write_csv_row(writer, ["Pattern", "Fabric", "Area (cm2)", "Width (cm)", "Height (cm)", "Grain (deg)"])
        for p in data["patterns"]:
            write_csv_row(writer, [p["name"], data["fabrics"][p["fabric"]]["name"], fmt(p["area_mm2"] / 100.0),
                                   fmt(p["width_mm"] / 10.0), fmt(p["height_mm"] / 10.0), fmt(p["grain"])])
        writer.writerow([])
        write_csv_row(writer, ["Fabric", "Pieces", "Total area (m2)", "Fabric width (cm)", "Est. length (m)"])
        for row in fabric_summary_rows():
            write_csv_row(writer, row)
    status("CSV written: %s" % path)


def export_bom_package():
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    name = (utility_api.GetProjectName() or "untitled").replace(" ", "_")
    target = os.path.join(OUTPUT_DIR, name + ".xml")
    ok = export_api.ExportBOM(target)
    status(("BOM package written to %s" % os.path.join(OUTPUT_DIR, name)) if ok else "ExportBOM failed")


# ------------------------------------------------------------------ layout
project = gui_api.AddGroup(panel, "Project")
ui["path"] = gui_api.AddTextInput(project, DEFAULT_PROJECT, "path to a .zprj file")
row = gui_api.AddRow(project)
gui_api.AddButton(row, "Load project", load_project)
gui_api.AddButton(row, "Use current", use_current)
ui["progress"] = gui_api.AddProgressBar(project, 0)
ui["status"] = gui_api.AddLabel(project, "no project loaded")
ui["preview"] = gui_api.AddRow(project)

patterns_group = gui_api.AddGroup(panel, "Patterns")
ui["patterns"] = gui_api.AddList(patterns_group, [], -1, on_pattern_selected)
ui["detail"] = gui_api.AddTable(patterns_group, ["Property", "Value"], [])

summary_group = gui_api.AddGroup(panel, "Fabric summary")
waste_row = gui_api.AddRow(summary_group)
gui_api.AddLabel(waste_row, "Waste allowance (%)")
ui["waste"] = gui_api.AddNumberInput(waste_row, 15, 0, 100, 0,
                                     lambda v: gui_api.SetTableRows(ui["summary"], fabric_summary_rows()))
ui["summary"] = gui_api.AddTable(summary_group, ["Fabric", "Pieces", "Area (m2)", "Width (cm)", "Est. length (m)"], [])

actions = gui_api.AddRow(panel)
gui_api.AddButton(actions, "Export CSV", export_csv)
gui_api.AddButton(actions, "Export BOM package", export_bom_package)

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