#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Крапки по контуру областей Редактора — лише для екранної версії.

Навіщо. У малюнку «з'єднай крапки» (Level0_A12, стор. 2, Ангол) руки в
оригіналі не накреслені — їх домалював би олівцем той, хто з'єднує. На
екрані Редактор обвела руки як області розмальовування (`a10`, `a13`), але
дитина їх не бачить, доки не клацне. Рішення замовника від 14 вересня 2026:
домалювати крапки по периметру цих областей так само, як накреслена решта
Ангола, і тільки в інтерактиві — друк не змінюється.

Як. Викликається з `SCREEN_POST` уроку:

    SCREEN_POST = [lambda doc, only=None: artdots.screen_dots(
        doc, only, 'Level0_A12_UA', {1: ['a10', 'a13']})]

Геометрію областей модуль НЕ копіює в урок, а читає з набору областей
буклета на цю сторінку — того самого, який панель бере для накладання
(набір родини буклета з найбільшою кількістю названих областей, як у
`serve.sibling_regions`). Тож якщо Редактор поправить руки, крапки підуть
за ними при наступній збірці. Якщо ж названої області в наборі більше
немає (перейменована, видалена) — збірка ЗУПИНЯЄТЬСЯ і каже, чого бракує:
вказівку на область тримає файл уроку, і оновлює її розробник, а мовчазний
пропуск сховав би зникнення рук.

Крапка — така сама, як в оригіналі: чорне коло діаметром ≈2.1 pt, крок
≈6.5 pt (виміряно на Анголі). Нова крапка не ставиться ближче ніж на
`MIN_GAP` до вже наявних крапок сторінки або до щойно поставлених (на
спільному ребрі двох областей вони інакше подвоїлися б). Контури, менші за
`MIN_AREA`, пропускаються — це шматочки, що лишаються від розпізнавання.
"""
import glob
import json
import math
import os
import re

import fitz

HERE = os.path.dirname(os.path.abspath(__file__))
EDITOR = os.path.join(os.path.dirname(HERE), 'editor')

DOT_D = 2.114          # діаметр крапки, pt (ширина штриха в оригіналі)
SPACING = 6.5          # крок між крапками вздовж контуру, pt
MIN_GAP = 3.6          # ближче до наявної крапки нову не ставимо, pt
MIN_AREA = 20.0        # контури, менші за це (pt²), пропускаємо
SMOOTH = 3             # вікно згладжування контуру (трасування по пікселях)


def region_set(family, page_index):
    """Набір областей буклета `family` на сторінку `page_index` (з 0).

    Той, у якому найбільше названих областей; None, якщо набору немає.
    """
    best, bestn = None, -1
    for path in sorted(glob.glob(os.path.join(EDITOR, 'regions_p*.json'))):
        try:
            with open(path, encoding='utf-8') as fh:
                doc = json.load(fh)
        except (OSError, ValueError):
            continue
        named = os.path.splitext(os.path.basename(doc.get('pdf') or ''))[0]
        if re.sub(r'_v\d+$', '', named) != family:
            continue
        if doc.get('page_index') != page_index:
            continue
        n = sum(1 for r in (doc.get('regions') or [])
                if (r.get('name') or '').strip())
        if n > bestn:
            best, bestn = doc, n
    return best


def existing_dots(page):
    """Центри крапок, уже накреслених на сторінці (штрих нульової довжини)."""
    out = []
    for dr in page.get_drawings():
        r = fitz.Rect(dr['rect'])
        if r.width < 1 and r.height < 1 and dr.get('width') \
                and abs(dr['width'] - DOT_D) < 0.5:
            out.append(((r.x0 + r.x1) / 2, (r.y0 + r.y1) / 2))
    return out


def _area(poly):
    s = 0.0
    for (x0, y0), (x1, y1) in zip(poly, poly[1:] + poly[:1]):
        s += x0 * y1 - x1 * y0
    return abs(s) / 2


def _smooth(poly, k):
    if k < 2 or len(poly) < k:
        return poly
    h = k // 2
    n = len(poly)
    return [(sum(poly[(i + j) % n][0] for j in range(-h, h + 1)) / k,
             sum(poly[(i + j) % n][1] for j in range(-h, h + 1)) / k)
            for i in range(n)]


def along(poly, spacing):
    """Точки на замкненому контурі через рівні проміжки ≈ `spacing`."""
    pts = [(float(x), float(y)) for x, y in poly]
    segs = list(zip(pts, pts[1:] + pts[:1]))
    lens = [math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in segs]
    total = sum(lens)
    if total <= 0:
        return []
    n = max(3, round(total / spacing))
    step = total / n
    out, want, run = [], 0.0, 0.0
    for (a, b), ln in zip(segs, lens):
        while want <= run + ln and len(out) < n:
            t = (want - run) / ln if ln else 0
            out.append((a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t))
            want += step
        run += ln
    return out


def screen_dots(doc, only, family, spec):
    """`spec`: {індекс сторінки: [назви областей]}. Малює крапки, друкує звіт."""
    for pno, names in spec.items():
        if only is not None and pno not in only:
            continue
        rs = region_set(family, pno)
        if rs is None:
            raise SystemExit('artdots: у %s немає набору областей на стор. %d'
                             % (family, pno + 1))
        byname = {(r.get('name') or '').strip(): r for r in rs['regions']}
        missing = [n for n in names if n not in byname]
        if missing:
            raise SystemExit('artdots: у наборі стор. %d буклета %s немає '
                             'областей %s — перейменовано чи видалено? '
                             'Онови перелік у файлі уроку.'
                             % (pno + 1, family, ', '.join(missing)))
        page = doc[pno]
        taken = existing_dots(page)
        placed = 0
        for n in names:
            for poly in byname[n]['paths']:
                if len(poly) < 3 or _area(poly) < MIN_AREA:
                    continue
                for x, y in along(_smooth(poly, SMOOTH), SPACING):
                    if any(math.hypot(x - tx, y - ty) < MIN_GAP
                           for tx, ty in taken):
                        continue
                    page.draw_circle((x, y), DOT_D / 2, color=None,
                                     fill=(0, 0, 0))
                    taken.append((x, y))
                    placed += 1
        print('  стор. %d (екран): крапки по контуру %s — %d'
              % (pno + 1, ', '.join(names), placed))
