#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Перенести області розмальовки з одного буклета на той самий малюнок в іншому.

Малюнки в програмі повторюються між рівнями (Захарій та Єлисавета на стор. 1
Level0_A12 і Level1_A12 — той самий вектор, лише в іншому місці й масштабі).
Області, які Редактор обвів раз, можна перерахувати, а не обводити знову.

Збіг шукається по САМИХ контурах: у кожної фігури береться «відбиток» —
точки, нормовані до її прямокутника, — і однакові відбитки на двох
сторінках дають пари. Пари з однаковим масштабом і зсувом складають
перетворення; коли таких пар багато (≥ MIN_PAIRS), це той самий малюнок.
Переносяться області, що лежать на знайдених контурах; назви й кольори
лишаються.

    # знайти, що з наборів одного буклета лягає на сторінки іншого
    .venv/bin/python ua_build/regions_transfer.py --scan Level0_A12 editor/regions_*.json

    # перенести
    .venv/bin/python ua_build/regions_transfer.py editor/regions_p01_q2_Level1_A12_UA_auto.json \\
        --to Level0_A12 --page 1 --pdf Level0_A12_UA_v01.pdf --out editor/regions_p01_Level0_A12_UA.json
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict

import fitz

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from lessons import EN_DIR                                     # noqa: E402

MIN_PAIRS = 12         # стільки однакових контурів з одним перетворенням = той самий малюнок
MIN_PTS = 4            # контури з меншою кількістю точок не годяться як відбиток


def family(pdfname):
    return re.sub(r'_v\d+$', '', os.path.splitext(os.path.basename(str(pdfname or '')))[0])


def booklet_of(pdfname):
    """`Level1_A12_UA_v28.pdf` -> `Level1_A12`."""
    return re.sub(r'_UA$', '', family(pdfname))


def path_points(d):
    pts = []
    for it in d['items']:
        if it[0] == 'l':
            pts += [it[1], it[2]]
        elif it[0] == 'c':
            pts += [it[1], it[2], it[3], it[4]]
        elif it[0] == 're':
            r = fitz.Rect(it[1]); pts += [r.tl, r.tr, r.br, r.bl]
        elif it[0] == 'qu':
            pts += list(it[1])
    return pts


def signature(d, mirror=False):
    """Відбиток контуру; `mirror` — те саме для віддзеркаленого по x: у
    програмі фігури повторюються й у дзеркалі (Марія на стор. 2 Level0_A12
    — дзеркало Марії з Level1_A12 стор. 3)."""
    pts = path_points(d)
    if len(pts) < MIN_PTS:
        return None
    r = fitz.Rect(d['rect'])
    w = max(r.width, 0.5)
    if mirror:
        key = tuple(sorted((round((r.x1 - p.x) / w, 2), round((p.y - r.y0) / w, 2)) for p in pts))
    else:
        key = tuple(sorted((round((p.x - r.x0) / w, 2), round((p.y - r.y0) / w, 2)) for p in pts))
    return (len(pts), round(r.height / w, 2), key)


def page_sigs(page, mirror=False):
    out = defaultdict(list)
    for d in page.get_drawings():
        s = signature(d, mirror)
        if s:
            out[s].append(fitz.Rect(d['rect']))
    return out


def transforms(src_page, dst_page):
    """-> [(s, tx, ty, n, src_union, mirror)] — перетворення з кількістю пар,
    за спаданням. Дзеркальне: x' = tx - s·x."""
    out = []
    for mirror in (False, True):
        out += _transforms(src_page, dst_page, mirror)
    out.sort(key=lambda t: -t[3])
    return out


def _transforms(src_page, dst_page, mirror):
    a, b = page_sigs(src_page, mirror), page_sigs(dst_page)
    votes = defaultdict(list)
    for sig, srects in a.items():
        drects = b.get(sig)
        if not drects or len(srects) > 3 or len(drects) > 3:
            continue                     # повторювані дрібниці (крапки) не голосують
        for sr in srects:
            for dr in drects:
                if sr.width < 3:
                    continue
                s = dr.width / sr.width
                tx = (dr.x1 + s * sr.x0) if mirror else (dr.x0 - s * sr.x0)
                ty = dr.y0 - s * sr.y0
                # голос іде до найближчого вже знайденого перетворення, а не
                # в жорстку комірку: інакше один малюнок ділився на 2–3
                # «різні» перетворення на межах комірок
                for key in votes:
                    if abs(key[0] - s) < 0.01 and abs(key[1] - tx) < 3 and abs(key[2] - ty) < 3:
                        break
                else:
                    key = (s, tx, ty)
                votes[key].append((sr, s, tx, ty))
    out = []
    for key, items in votes.items():
        if len(items) < MIN_PAIRS:
            continue
        s = sum(i[1] for i in items) / len(items)
        tx = sum(i[2] for i in items) / len(items)
        ty = sum(i[3] for i in items) / len(items)
        u = fitz.Rect(items[0][0])
        for i in items:
            u |= i[0]
        out.append((s, tx, ty, len(items), u, mirror))
    return out


def region_bbox(r):
    xs = [p[0] for path in r['paths'] for p in path]
    ys = [p[1] for path in r['paths'] for p in path]
    return fitz.Rect(min(xs), min(ys), max(xs), max(ys))


def transfer(doc, src_page, dst_page, tr, pad=6.0):
    s, tx, ty, _n, u, mirror = tr
    zone = fitz.Rect(u.x0 - pad, u.y0 - pad, u.x1 + pad, u.y1 + pad)
    out = []
    for r in doc['regions']:
        if not zone.contains(region_bbox(r)):
            continue
        rr = dict(r)
        fx = (lambda x: tx - s * x) if mirror else (lambda x: tx + s * x)
        rr['paths'] = [[[round(fx(p[0]), 2), round(s * p[1] + ty, 2)] for p in path]
                       for path in r['paths']]
        out.append(rr)
    return out


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument('sets', nargs='+', help='набір(и) областей')
    ap.add_argument('--scan', metavar='БУКЛЕТ', help='лише шукати збіги на всіх сторінках буклета')
    ap.add_argument('--to', metavar='БУКЛЕТ', help='цільовий буклет, напр. Level0_A12')
    ap.add_argument('--page', type=int, help='цільова сторінка з 1')
    ap.add_argument('--pdf', help='що писати в поле pdf набору, напр. Level0_A12_UA_v01.pdf')
    ap.add_argument('--task', default='q1')
    ap.add_argument('--out')
    ap.add_argument('--append', help='долити до наявного набору (шлях)')
    a = ap.parse_args(argv)

    target = a.scan or a.to
    tdoc = fitz.open(os.path.join(EN_DIR, target + '.pdf'))
    found = []
    for path in a.sets:
        doc = json.load(open(path, encoding='utf-8'))
        book = booklet_of(doc.get('pdf'))
        try:
            sdoc = fitz.open(os.path.join(EN_DIR, book + '.pdf'))
        except Exception:
            print('?? немає оригіналу для', path); continue
        sp = sdoc[int(doc['page']) - 1]
        pages = [a.page] if a.page else range(1, len(tdoc) + 1)
        for pno in pages:
            for tr in transforms(sp, tdoc[pno - 1]):
                regs = transfer(doc, sp, tdoc[pno - 1], tr)
                if len(regs) < 2:
                    continue
                found.append((path, pno, tr, regs))
                print('%s стор. %d -> %s стор. %d: масштаб %.3f%s, контурів %d, областей %d'
                      % (os.path.basename(path), doc['page'], target, pno, tr[0],
                         ' (дзеркально)' if tr[5] else '', tr[3], len(regs)))
    if a.scan or not found:
        return 0
    # Одна область джерела — один раз, навіть якщо її зачепили два близькі
    # перетворення; id нові й наскрізні, бо в наборі-приймачі можуть лежати
    # області з інших джерел із такими самими id.
    regs, done = [], set()
    for path, _n, _t, rs in found:
        for r in rs:
            key = (path, r.get('id'))
            if key in done:
                continue
            done.add(key)
            regs.append(dict(r, src=os.path.basename(path) + ':' + str(r.get('id'))))
    out_doc = None
    if a.append and os.path.exists(a.append):
        out_doc = json.load(open(a.append, encoding='utf-8'))
        have = {r.get('src') for r in out_doc['regions']}
        regs = [r for r in regs if r['src'] not in have]
        base = len(out_doc['regions'])
        for i, r in enumerate(regs, base + 1):
            r['id'] = 't%02d' % i
        out_doc['regions'] += regs
    else:
        for i, r in enumerate(regs, 1):
            r['id'] = 't%02d' % i
        out_doc = {'format': 'bibletime-regions/1', 'pdf': a.pdf or (target + '_UA_v01.pdf'),
                   'page': a.page, 'page_index': a.page - 1, 'dpi': 150,
                   'pt_per_px': 0.48, 'origin_pt': [0, 0], 'task': a.task, 'opacity': 0.45,
                   'source': 'regions_transfer.py з %s' % ', '.join(os.path.basename(p) for p, *_ in found),
                   'regions': regs}
    out = a.out or a.append
    with open(out, 'w', encoding='utf-8') as fh:
        json.dump(out_doc, fh, ensure_ascii=False, indent=1)
    print('->', out, len(out_doc['regions']), 'областей')
    return 0


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