"""
Generator wsadowy PSD: buteleczki "Klaudia Buczma" (4 gosci per PSD).

Otwiera szablon `Klaudia Buczma.psd`, dla kazdej z 4 warstw Smart Object
(Warstwa 2, Warstwa 2 kopia, Warstwa 2 kopia 2, Warstwa 2 kopia 3) wchodzi
do srodka i podmienia warstwe tekstowa `gosc` (sciezka: Warstwa 4 -> gosc).
Zapisuje jako `Klaudia Buczma NN.psd` w `_gotowe/`.

Lista gosci pobierana z .docx (komorki tabeli).
Grupy < 4 osob -> brakujace SO sa ukrywane (visible=False).

Uzycie:
    python buteleczki_klaudia_buczma_batch.py                  # wszystkie grupy
    python buteleczki_klaudia_buczma_batch.py --only 1         # tylko pierwsza grupa (test)
    python buteleczki_klaudia_buczma_batch.py --from 5 --to 7  # zakres grup
"""

import argparse
import os
import subprocess
import sys
import time

import photoshop.api as ps
from docx import Document


PROJEKT_DIR = r"d:\pomysloweprezenty.pl\projekty\buteleczki - indywidualny wzór"
SZABLON_PATH = os.path.join(PROJEKT_DIR, "Klaudia Buczma.psd")
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe_edytowalne")
DOCX_PATH = r"d:\temp\pomysloweprezenty.pl\buteleczki.docx"

SO_LAYER_NAMES = [
    "Warstwa 2 kopia 2",   # lewy gora
    "Warstwa 2 kopia 3",   # prawy gora
    "Warstwa 2 kopia",     # lewy dol
    "Warstwa 2",           # prawy dol
]

# Sciezka do warstwy `gosc` wewnatrz SO
SO_GROUP_NAME = "Warstwa 4"
GOSC_LAYER_NAME = "gosc"

PHOTOSHOP_EXE_CANDIDATES = [
    r"C:\Program Files\Adobe\Adobe Photoshop 2024\Photoshop.exe",
    r"C:\Program Files\Adobe\Adobe Photoshop 2023\Photoshop.exe",
    r"C:\Program Files\Adobe\Adobe Photoshop 2025\Photoshop.exe",
    r"C:\Program Files\Adobe\Adobe Photoshop 2026\Photoshop.exe",
]


def ensure_photoshop():
    try:
        return ps.Application()
    except Exception:
        for exe in PHOTOSHOP_EXE_CANDIDATES:
            if os.path.exists(exe):
                subprocess.Popen([exe])
                break
        for _ in range(30):
            time.sleep(2)
            try:
                return ps.Application()
            except Exception:
                continue
        raise RuntimeError("Nie udalo sie uruchomic Photoshopa")


def open_smart_object(app):
    """Otwiera zawartosc aktywnej warstwy Smart Object do edycji."""
    desc = ps.ActionDescriptor()
    ref = ps.ActionReference()
    ref.putEnumerated(
        app.stringIDToTypeID("layer"),
        app.stringIDToTypeID("ordinal"),
        app.stringIDToTypeID("targetEnum"),
    )
    desc.putReference(app.stringIDToTypeID("null"), ref)
    app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc)


def delete_active_layer(app):
    """Usuwa aktywna warstwe (przez action manager - dziala na surowych COM)."""
    desc = ps.ActionDescriptor()
    ref = ps.ActionReference()
    ref.putEnumerated(
        app.stringIDToTypeID("layer"),
        app.stringIDToTypeID("ordinal"),
        app.stringIDToTypeID("targetEnum"),
    )
    desc.putReference(app.stringIDToTypeID("null"), ref)
    app.executeAction(app.stringIDToTypeID("delete"), desc)


def smart_object_make_copy(app):
    """Layer > Smart Objects > New Smart Object Via Copy.
    Odlacza aktywny SO od pozostalych zlinkowanych instancji."""
    app.executeAction(
        app.stringIDToTypeID("placedLayerMakeCopy"), ps.ActionDescriptor()
    )


def set_text(layer, new_text):
    """Zmienia tekst zachowujac srodek bounding boxa."""
    b = [float(x) for x in layer.bounds]
    cx_before = (b[0] + b[2]) / 2
    cy_before = (b[1] + b[3]) / 2
    layer.textItem.contents = new_text
    b2 = [float(x) for x in layer.bounds]
    cx_after = (b2[0] + b2[2]) / 2
    cy_after = (b2[1] + b2[3]) / 2
    dx = cx_before - cx_after
    dy = cy_before - cy_after
    if dx or dy:
        layer.translate(dx, dy)


def split_two_lines(name):
    """Lamie nazwe na 2 linie - przy spacji najblizszej srodkowi.
    Dla 1-slowowych zwraca bez zmian."""
    parts = name.split(" ")
    if len(parts) < 2:
        return name
    mid = len(name) / 2
    best_i = None
    best_dist = None
    pos = 0
    for i, part in enumerate(parts[:-1]):
        pos += len(part)
        dist = abs(pos - mid)
        if best_dist is None or dist < best_dist:
            best_dist = dist
            best_i = i
        pos += 1  # spacja
    left = " ".join(parts[: best_i + 1])
    right = " ".join(parts[best_i + 1 :])
    return f"{left}\r{right}"


def read_names(docx_path):
    doc = Document(docx_path)
    names = []
    for t in doc.tables:
        for row in t.rows:
            for cell in row.cells:
                txt = cell.text.strip()
                if txt:
                    names.append(txt)
    return names


def chunk4(items):
    for i in range(0, len(items), 4):
        yield items[i:i + 4]


def find_layer_by_name(container, name):
    """Rekurencyjnie szuka warstwy po nazwie w doc/layerSet."""
    for ls in container.layerSets:
        if ls.name == name:
            return ls
        found = find_layer_by_name(ls, name)
        if found is not None:
            return found
    for al in container.artLayers:
        if al.name == name:
            return al
    return None


def edit_so_gosc(app, doc, so_layer_name, gosc_text):
    """Wchodzi do SO, podmienia gosc, zapisuje i wraca."""
    target = find_layer_by_name(doc, so_layer_name)
    if target is None:
        raise RuntimeError(f"Nie znaleziono warstwy {so_layer_name}")
    doc.activeLayer = target
    # 4 SO w szablonie sa zlinkowane (wspolny kontener). "New Smart Object Via
    # Copy" tworzy niezalezna kopie obok zrodla. Kasujemy oryginal i
    # przemianowujemy kopie - efekt: ta sama warstwa w tym samym miejscu,
    # ale juz odpieta od pozostalych instancji.
    smart_object_make_copy(app)
    independent = doc.activeLayer
    independent_name_tmp = independent.name
    # Skasuj oryginal (target) - aktywuj go i usun przez action manager
    doc.activeLayer = target
    delete_active_layer(app)
    # Wroc do niezaleznej kopii i nadaj jej nazwe oryginalu
    doc.activeLayer = independent
    independent.name = so_layer_name
    open_smart_object(app)
    so_doc = app.activeDocument
    gosc_layer = find_layer_by_name(so_doc, GOSC_LAYER_NAME)
    if gosc_layer is None:
        raise RuntimeError(f"Nie znaleziono warstwy '{GOSC_LAYER_NAME}' w SO {so_layer_name}")
    old = gosc_layer.textItem.contents
    set_text(gosc_layer, gosc_text)
    so_doc.save()
    so_doc.close()
    print(f"    {so_layer_name}: '{old.strip()}' -> '{gosc_text}'")


def generate_group(app, names_group, index):
    """Generuje jeden PSD dla grupy do 4 nazwisk."""
    os.makedirs(GOTOWE_DIR, exist_ok=True)
    out_name = f"Klaudia Buczma {index:02d}.psd"
    out_path = os.path.join(GOTOWE_DIR, out_name)

    doc = app.open(SZABLON_PATH)
    print(f"[{index:02d}] otwarto szablon ({len(names_group)} osob)")

    for slot_idx, so_name in enumerate(SO_LAYER_NAMES):
        if slot_idx < len(names_group):
            edit_so_gosc(app, doc, so_name, split_two_lines(names_group[slot_idx]))
        else:
            doc.artLayers[so_name].visible = False
            print(f"    {so_name}: UKRYTE (brak osoby)")

    psd_opts = ps.PhotoshopSaveOptions()
    doc.saveAs(out_path, psd_opts, True)
    print(f"[{index:02d}] zapisano: {out_path}")
    doc.close(ps.SaveOptions.DoNotSaveChanges)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--only", type=int, help="Tylko jedna grupa (1-based)")
    parser.add_argument("--from", dest="from_", type=int, default=1, help="Od grupy N")
    parser.add_argument("--to", type=int, help="Do grupy N (wlacznie)")
    parser.add_argument(
        "--names",
        help="Lista nazwisk oddzielona ';' (1-4 sztuk). Generuje jeden PSD.",
    )
    parser.add_argument(
        "--index",
        type=int,
        help="Numer wynikowego PSD przy uzyciu --names (np. 24 -> 'Klaudia Buczma 24.psd')",
    )
    args = parser.parse_args()

    app = ensure_photoshop()

    while app.documents.length > 0:
        app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges)

    if args.names:
        if args.index is None:
            raise SystemExit("--names wymaga --index N")
        custom = [n.strip() for n in args.names.split(";") if n.strip()]
        if not (1 <= len(custom) <= 4):
            raise SystemExit("--names: 1-4 nazwisk oddzielonych ';'")
        generate_group(app, custom, args.index)
        print("Gotowe!")
        return

    names = read_names(DOCX_PATH)
    groups = list(chunk4(names))
    print(f"Wczytano {len(names)} osob -> {len(groups)} grup po 4")

    if args.only:
        indices = [args.only]
    else:
        end = args.to or len(groups)
        indices = list(range(args.from_, end + 1))

    for idx in indices:
        if not (1 <= idx <= len(groups)):
            print(f"Pominieto grupe {idx} (poza zakresem)")
            continue
        generate_group(app, groups[idx - 1], idx)

    print("Gotowe!")


if __name__ == "__main__":
    main()
