"""Geometric QA for the re-typeset booklet.

Compares what build.py actually drew (layout.json) against the source layout
(template.json + the original PDF) and reports every place where the translated
text moved onto something it did not touch in the original.

Checks
  GROW      block ink grew far beyond its source bbox
  ART       text now covers an illustration it did not cover before
  TEXT      two blocks now overlap each other
  MARGIN    text runs past the right edge of the content column
  INK       coloured artwork disappeared (a redaction reached too far)
  COVER     old artwork is only partly covered - a fragment stays visible
"""
import json
import os
import sys
from collections import defaultdict

import fitz

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
SRC = os.path.join(ROOT, 'original', 'en', 'Level2_WhatChristiansBelieve.pdf')

CONTENT_RIGHT = 566.0     # hard page limit
GROW_TOL = 6.0            # pt a block may exceed its source bbox unremarked
AREA_TOL = 6.0            # pt^2 of new overlap before it is worth reporting


def rect(v):
    return fitz.Rect(v)


def area(r):
    return max(0.0, r.width) * max(0.0, r.height)


def overlap(a, b):
    return area(a & b)


def illustration_rects(page):
    """Artwork a text block must not land on: images and curved line art.

    Frames, rules and grid cells are straight-line paths and are excluded -
    text is meant to sit inside them.
    """
    out = [rect(im['bbox']) for im in page.get_image_info()]
    for dr in page.get_drawings():
        if any(it[0] == 'c' for it in dr['items']):
            r = rect(dr['rect'])
            if area(r) > 40 and area(r) < 0.5 * area(page.rect):
                out.append(r)
    return out


def union(rects):
    if not rects:
        return None
    u = fitz.Rect(rects[0])
    for r in rects[1:]:
        u |= fitz.Rect(r)
    return u


CELL = 24          # page is compared in CELL x CELL pt tiles
INK_DPI = 60


def red_cells(page, wipes):
    """Count brand-red pixels per tile, blanking regions cleared on purpose."""
    pm = page.get_pixmap(dpi=INK_DPI, colorspace=fitz.csRGB)
    sc = INK_DPI / 72.0
    blank = []
    for r in wipes:
        blank.append((int(r[0] * sc) - 1, int(r[1] * sc) - 1,
                      int(r[2] * sc) + 1, int(r[3] * sc) + 1))
    cols = int(page.rect.width / CELL) + 1
    counts = defaultdict(int)
    data, n, stride = pm.samples, pm.n, pm.stride
    px_per_cell = CELL * sc
    for y in range(pm.height):
        row = y * stride
        for x in range(pm.width):
            i = row + x * n
            r_, g_, b_ = data[i], data[i + 1], data[i + 2]
            if r_ > 150 and r_ - g_ > 55 and r_ - b_ > 45:
                if any(x0 <= x <= x1 and y0 <= y <= y1 for x0, y0, x1, y1 in blank):
                    continue
                counts[(int(y / px_per_cell) * cols + int(x / px_per_cell))] += 1
    return counts, cols


def check_covers(doc_src, findings, step=2.0):
    """Artwork inside a rebuilt area must be covered completely.

    A shape may straddle several cover rectangles, so test the union by
    sampling rather than asking whether one rectangle contains it.
    """
    try:
        sys.path.insert(0, HERE)
        from puzzles import COVERS
    except Exception:
        return
    for pno, rects in sorted(COVERS.items()):
        cov = [fitz.Rect(r) for r in rects]
        for dr in doc_src[pno].get_drawings():
            r = fitz.Rect(dr['rect'])
            if not any(r.intersects(c) for c in cov):
                continue
            outside = 0
            y = r.y0
            while y <= r.y1:
                x = r.x0
                while x <= r.x1:
                    if not any(c.contains(fitz.Point(x, y)) for c in cov):
                        outside += 1
                    x += step
                y += step
            if outside:
                findings.append((pno, 'COVER', '-',
                                 f'фігура [{r.x0:.0f},{r.y0:.0f},{r.x1:.0f},{r.y1:.0f}] '
                                 f'закрита не повністю ({outside} точок поза зоною)'))


def check_ink(doc_src, doc_out, blanks_by_page, findings):
    for pno in range(doc_src.page_count):
        w = blanks_by_page.get(pno, [])
        before, cols = red_cells(doc_src[pno], w)
        after, _ = red_cells(doc_out[pno], w)
        lost = [(k, v) for k, v in before.items() if v >= 25 and after.get(k, 0) < v * 0.35]
        if not lost:
            continue
        total = sum(v for _, v in lost)
        k = max(lost, key=lambda kv: kv[1])[0]
        x, y = (k % cols) * CELL, (k // cols) * CELL
        findings.append((pno, 'INK', '-',
                         f'зникла червона графіка у {len(lost)} клітинках '
                         f'({total} px), найбільша біля [{x},{y}]'))


def main():
    tpl = json.load(open(os.path.join(HERE, 'template.json')))
    drawn = json.load(open(os.path.join(HERE, 'layout.json')))
    doc = fitz.open(SRC)

    src_bbox = {}
    page_of = {}
    for pg in tpl:
        for b in pg['blocks']:
            key = f"p{pg['page']}b{b['i']}"
            src_bbox[key] = rect(b['bbox'])
            page_of[key] = pg['page']

    by_page = defaultdict(list)
    for key, rects in drawn.items():
        by_page[page_of[key]].append((key, [rect(r) for r in rects]))

    findings = []
    for pno, blocks in sorted(by_page.items()):
        page = doc[pno]
        art = illustration_rects(page)
        new_union = {k: union(rs) for k, rs in blocks}

        for key, rects in blocks:
            new = new_union[key]
            old = src_bbox[key]

            # --- MARGIN --------------------------------------------------
            if new.x1 > CONTENT_RIGHT:
                findings.append((pno, 'MARGIN', key,
                                 f'right edge {new.x1:.0f} > {CONTENT_RIGHT:.0f}'))

            # --- GROW ----------------------------------------------------
            # growth to the right is already covered by ART / MARGIN; what the
            # source layout cannot absorb is growth downwards or leftwards
            down, left = new.y1 - old.y1, old.x0 - new.x0
            right = new.x1 - old.x1
            centred = abs(left - right) < 4.0     # grew symmetrically -> centred text
            if max(down, left) > GROW_TOL and not (centred and down <= GROW_TOL):
                side = 'вниз' if down >= left else 'вліво'
                findings.append((pno, 'GROW', key, f'+{max(down, left):.0f} pt {side}'))

            # --- ART -----------------------------------------------------
            for a in art:
                # text that already lived inside this artwork (speech bubble,
                # jigsaw piece) may grow inside it freely
                if area(old) and overlap(old, a) / area(old) > 0.8:
                    continue
                before = overlap(old, a)
                after = sum(overlap(r, a) for r in rects)
                if after - before > AREA_TOL and after > AREA_TOL:
                    findings.append((pno, 'ART', key,
                                     f'+{after - before:.0f} pt² на графіку '
                                     f'[{a.x0:.0f},{a.y0:.0f},{a.x1:.0f},{a.y1:.0f}]'))
                    break

            # --- TEXT ----------------------------------------------------
            for other, orects in blocks:
                if other <= key:
                    continue
                before = overlap(old, src_bbox[other])
                after = sum(overlap(r, o) for r in rects for o in orects)
                if after - before > AREA_TOL:
                    findings.append((pno, 'TEXT', key,
                                     f'накладається на {other} (+{after - before:.0f} pt²)'))

    wipes_by_page = defaultdict(list)
    wpath = os.path.join(HERE, 'wipes.json')
    if os.path.exists(wpath):
        for pno, r in json.load(open(wpath)):
            wipes_by_page[pno].append(r)
    # text is red in this design too, so blank every text area - source and
    # translated alike - before comparing artwork ink
    for pg in tpl:
        for b in pg['blocks']:
            for ln in b['lines']:
                for sp in ln['spans']:
                    wipes_by_page[pg['page']].append(
                        [sp['x'] - 1, ln['bbox'][1] - 1, sp['x'] + sp['w'] + 1, ln['bbox'][3] + 1])
    for key, rects in drawn.items():
        for r in rects:
            wipes_by_page[page_of[key]].append([r[0] - 1, r[1] - 1, r[2] + 1, r[3] + 1])

    marker = os.path.join(HERE, 'last_build.txt')
    out_pdf = open(marker).read().strip() if os.path.exists(marker) else ''
    check_covers(doc, findings)
    if out_pdf and os.path.exists(out_pdf):
        print(f'перевіряю {os.path.basename(out_pdf)}')
        check_ink(doc, fitz.open(out_pdf), wipes_by_page, findings)

    order = {'COVER': 0, 'INK': 1, 'ART': 2, 'TEXT': 3, 'MARGIN': 4, 'GROW': 5}
    findings.sort(key=lambda f: (order[f[1]], f[0]))
    counts = defaultdict(int)
    for f in findings:
        counts[f[1]] += 1
    print(f'{len(findings)} findings: ' +
          ', '.join(f'{k}={v}' for k, v in sorted(counts.items())))
    for pno, kind, key, msg in findings:
        print(f'  стор.{pno + 1:>2}  {kind:<6} {key:<10} {msg}')
    return 1 if any(f[1] in ('INK', 'ART', 'TEXT', 'MARGIN') for f in findings) else 0


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