#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Клікабельні області на малюнку — НЕОБОВ'ЯЗКОВИЙ шар поверх готового уроку.

Вхід: PDF з полями (як його зібрав `fields_build.py`) + JSON з контурами,
намальованими людиною в `editor/regions.html`. Кожна область стає чекбоксом:
у стані `On` він заливає свій контур своїм кольором.

Конвеєр цього файлу не потребує: якщо JSON для уроку немає, збірка лишається
такою, як була. Тому це окремий скрипт, а не гілка в `fields_build.py`.

Три рішення, без яких виходить погано:

* **Заливка напівпрозора** (`ExtGState` з `/ca`, типово 0.45). Наш шар лежить
  ПОВЕРХ сторінки, і суцільна фарба сховала б чорні штрихи всередині області —
  складки одягу, риси обличчя, пасма волосся. З прозорістю штрихи проступають,
  і виходить розмальовка, а не пляма.
* **Парність заповнення `f*`** (even-odd): якщо в області кілька підшляхів,
  вкладений стає діркою — так малюється, скажімо, пояс усередині поділу.
* **Менші області додаються ОСТАННІМИ.** Прямокутник віджета ловить клік по
  всій своїй площі, тож дрібна деталь має лежати поверх великої, інакше в неї
  не влучиш узагалі (див. `verify/regions_click.py` — там це виміряно).

Механізм вигляду — той самий, що вже працює для `ws` і `paint` (§3.6):
`/MK <<>>`, `/BS <</W 0/S/S>>`, `/H /N`, порожній потік `Off`, і **ніякого
`widget.update()`** після призначення `/AP`.

Запуск:
    .venv/bin/python ua_build/regions_apply.py editor/regions_p16_q2.json
    .venv/bin/python ua_build/regions_apply.py editor/regions_p16_q2.json \\
        --mark r02,r04,r08 --render 150   # подивитися очима
"""
import argparse
import json
import os
import re
import sys

import fitz

SRC = "forms/Level2_UA_form_v06.pdf"
OUTDIR = "verify"                 # демо-PDF живе поруч із перевірками, не у forms/
STEM = "Level2_UA_regions"
ALPHA = 0.45


def next_version(d, stem):
    """`_v01`, `_v02`, … — ніколи не перезаписуємо (§2.7.0.1)."""
    rx = re.compile(r"^%s_v(\d+)\.pdf$" % re.escape(stem))
    mx = 0
    if os.path.isdir(d):
        for fn in os.listdir(d):
            m = rx.match(fn)
            if m:
                mx = max(mx, int(m.group(1)))
    return os.path.join(d, "%s_v%02d.pdf" % (stem, mx + 1))


def hex2rgb(s):
    s = s.lstrip("#")
    return tuple(int(s[i:i + 2], 16) / 255.0 for i in (0, 2, 4))


def bbox(region):
    xs = [p[0] for path in region["paths"] for p in path]
    ys = [p[1] for path in region["paths"] for p in path]
    return fitz.Rect(min(xs), min(ys), max(xs), max(ys))


def area(region):
    """Площа контуру (формула шнурівки) — за нею впорядковуємо шар."""
    tot = 0.0
    for path in region["paths"]:
        s = 0.0
        for i in range(len(path)):
            x0, y0 = path[i]
            x1, y1 = path[(i + 1) % len(path)]
            s += x0 * y1 - x1 * y0
        tot += abs(s) / 2.0
    return tot


def fill_stream(region, rect, alpha):
    """Потік вигляду стану `On`: контур області, залитий напівпрозорим кольором.

    Координати JSON — у пунктах PDF з origin у лівому верхньому куті (як у
    PyMuPDF). `/BBox [0 0 w h]` відображається на `/Rect`, тож
    `x = px - rect.x0`, `y = rect.y1 - py`.
    """
    r, g, b = hex2rgb(region["color"])
    out = ["q /GSa gs %.3f %.3f %.3f rg" % (r, g, b)]
    for path in region["paths"]:
        if len(path) < 3:
            continue
        out.append("%.2f %.2f m" % (path[0][0] - rect.x0, rect.y1 - path[0][1]))
        for x, y in path[1:]:
            out.append("%.2f %.2f l" % (x - rect.x0, rect.y1 - y))
        out.append("h")
    out.append("f* Q")                       # even-odd: вкладений підшлях = дірка
    return " ".join(out)


def add_region(page, name, region, alpha):
    doc = page.parent
    rect = bbox(region)
    w = fitz.Widget()
    w.field_name = name
    w.field_type = fitz.PDF_WIDGET_TYPE_CHECKBOX
    w.rect = rect
    w.fill_color = None                      # віджет лежить ПОВЕРХ малюнка
    w.border_color = None
    w.border_width = 0
    w.text_font = "ZaDb"
    w.field_value = False
    a = page.add_widget(w)

    on = doc.get_new_xref()
    doc.update_object(
        on, "<</Type/XObject/Subtype/Form/FormType 1/BBox[0 0 %.3f %.3f]"
            "/Resources<</ExtGState<</GSa<</Type/ExtGState/ca %.3f/CA %.3f"
            "/BM/Normal>>>>>>>>" % (rect.width, rect.height, alpha, alpha))
    doc.update_stream(on, fill_stream(region, rect, alpha).encode("latin-1"),
                      new=True)
    off = doc.get_new_xref()
    doc.update_object(off, "<</Type/XObject/Subtype/Form/FormType 1"
                           "/BBox[0 0 %.3f %.3f]/Resources<<>>>>"
                      % (rect.width, rect.height))
    doc.update_stream(off, b"", new=True)

    doc.xref_set_key(a.xref, "AP/N", "<</Off %d 0 R/Yes %d 0 R>>" % (off, on))
    doc.xref_set_key(a.xref, "AS", "/Off")
    doc.xref_set_key(a.xref, "MK", "<<>>")
    doc.xref_set_key(a.xref, "BS", "<</W 0/S/S>>")
    doc.xref_set_key(a.xref, "H", "/N")
    return a


def load(paths):
    docs = []
    for p in paths:
        with open(p, encoding="utf-8") as fh:
            j = json.load(fh)
        if j.get("format") != "bibletime-regions/1":
            raise SystemExit("%s: не той формат (%r)" % (p, j.get("format")))
        docs.append(j)
    return docs


def apply(src, jsons, out, alpha=None, mark=()):
    """`alpha=None` — брати прозорість із JSON (її задає редактор уроків)."""
    doc = fitz.open(src)
    total = 0
    for j in jsons:
        pno = j.get("page_index", j["page"] - 1)
        page = doc[pno]
        task = j.get("task", "q1")
        #  Порядок шару: великі знизу, дрібні зверху — інакше в дрібну деталь
        #  не влучиш (прямокутник великої накриває її цілком).
        #  Міряти треба ПРЯМОКУТНИК, а не контур: клік ловить саме прямокутник.
        #  На стор. 16 сортування за контуром давало 0 % влучань в «обличчя
        #  Ісуса» — воно більше за «волосся» площею фарби, але його прямокутник
        #  цілком лежить усередині прямокутника волосся.
        regs = sorted(j["regions"],
                      key=lambda r: (bbox(r).get_area(), area(r)), reverse=True)
        names = {}
        for i, reg in enumerate(regs, 1):
            name = "p%02d_%s_art%02d" % (pno + 1, task, i)
            add_region(page, name, reg,
                       alpha if alpha is not None else j.get("opacity", ALPHA))
            names[reg["id"]] = name
            total += 1
        if mark:
            want = {m.strip() for m in mark}
            for w in page.widgets():
                if w.field_name in {names[i] for i in names if i in want}:
                    doc.xref_set_key(w.xref, "AS", "/Yes")
                    doc.xref_set_key(w.xref, "V", "/Yes")
        print("стор. %d: %d областей, завдання %s" % (pno + 1, len(regs), task))

    doc.xref_set_key(doc.pdf_catalog(), "AcroForm/NeedAppearances", "false")
    doc.save(out, deflate=True, garbage=3)
    doc.close()
    print("%s: +%d полів, %.0f КБ (було %.0f КБ)"
          % (out, total, os.path.getsize(out) / 1024,
             os.path.getsize(src) / 1024))
    return out


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("json", nargs="+")
    ap.add_argument("--src", default=SRC)
    ap.add_argument("--out")
    ap.add_argument("--alpha", type=float, default=None,
                    help="перекрити прозорість із JSON (типово %.2f)" % ALPHA)
    ap.add_argument("--mark", default="", help="id областей через кому: r01,r04")
    ap.add_argument("--render", type=int, default=0, help="dpi рендера сторінок")
    a = ap.parse_args(argv)

    jsons = load(a.json)
    out = a.out or next_version(OUTDIR, STEM)
    os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
    apply(a.src, jsons, out, alpha=a.alpha,
          mark=[m for m in a.mark.split(",") if m])
    if a.render:
        doc = fitz.open(out)
        for j in jsons:
            pno = j.get("page_index", j["page"] - 1)
            fn = os.path.join(OUTDIR, "%s_p%02d.png"
                              % (os.path.splitext(os.path.basename(out))[0],
                                 pno + 1))
            doc[pno].get_pixmap(dpi=a.render).save(fn)
            print("рендер:", fn)
        doc.close()
    return 0


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