#!/usr/bin/env python3
"""Detect candidate interactive zones in the Level2 workbook.
Outputs candidates.json + render/overlay_pNN.png with numbered candidates."""
import fitz, json, math
from collections import defaultdict

SRC = "data/Level2_WhatChristiansBelieve.pdf"
doc = fitz.open(SRC)

def merge_lines(lines, ytol=2.0, xgap=6.0):
    """Merge near-collinear horizontal segments. lines: list of (x0,x1,y)."""
    lines = sorted(lines, key=lambda L: (round(L[2]/ytol), L[0]))
    out = []
    for x0, x1, y in lines:
        placed = False
        for o in out:
            if abs(o[2]-y) <= ytol and not (x1 < o[0]-xgap or x0 > o[1]+xgap):
                o[0] = min(o[0], x0); o[1] = max(o[1], x1)
                o[2] = (o[2]+y)/2; placed = True; break
        if not placed:
            out.append([x0, x1, y])
    return out

def cluster_grid(squares):
    """Group small square rects into grids. squares: list of fitz.Rect."""
    # rows by y center
    used = [False]*len(squares)
    grids = []
    cents = [( (s.x0+s.x1)/2, (s.y0+s.y1)/2, s) for s in squares]
    # simple connected-components by adjacency (cells touching within tol)
    n = len(squares)
    adj = defaultdict(set)
    for i in range(n):
        for j in range(i+1, n):
            a, b = squares[i], squares[j]
            # adjacent if share an edge approx
            dx = abs(((a.x0+a.x1)/2)-((b.x0+b.x1)/2))
            dy = abs(((a.y0+a.y1)/2)-((b.y0+b.y1)/2))
            w = (a.width+b.width)/2; h=(a.height+b.height)/2
            if (dy < h*0.6 and dx < w*1.4) or (dx < w*0.6 and dy < h*1.4):
                adj[i].add(j); adj[j].add(i)
    seen=set()
    for i in range(n):
        if i in seen: continue
        stack=[i]; comp=[]
        while stack:
            k=stack.pop()
            if k in seen: continue
            seen.add(k); comp.append(k)
            stack.extend(adj[k]-seen)
        if len(comp) >= 3:
            grids.append([squares[k] for k in comp])
    return grids

report = {}
for pi, page in enumerate(doc):
    H = page.rect.height
    raw_lines=[]; box_rects=[]; squares=[]
    for dr in page.get_drawings():
        for it in dr["items"]:
            if it[0]=="l":
                p1,p2=it[1],it[2]
                if abs(p1.y-p2.y)<1.5 and abs(p1.x-p2.x)>20:
                    raw_lines.append((min(p1.x,p2.x),max(p1.x,p2.x),(p1.y+p2.y)/2))
            elif it[0]=="re":
                r=it[1]; w,h=r.width,r.height
                if h<2.5 and w>20:                       # thin rect = line
                    raw_lines.append((r.x0,r.x1,(r.y0+r.y1)/2))
                elif 12<=w<=44 and 12<=h<=44 and abs(w-h)<10:  # square-ish = crossword cell
                    squares.append(fitz.Rect(r))
                elif w>=40 and h>=10 and w<560 and h<400:       # box / answer area
                    box_rects.append(fitz.Rect(r))
    lines = merge_lines(raw_lines)
    lines = [L for L in lines if (L[1]-L[0])>=30]   # keep meaningful writing lines
    grids = cluster_grid(squares)
    # squares not in a grid -> treat as small boxes
    grid_ids = set(id(s) for g in grids for s in g)
    lone_sq = [s for s in squares if id(s) not in grid_ids]
    report[pi] = {
        "lines":[[round(a,1),round(b,1),round(y,1)] for a,b,y in lines],
        "grids":[[ [round(s.x0,1),round(s.y0,1),round(s.x1,1),round(s.y1,1)] for s in g] for g in grids],
        "boxes":[[round(r.x0,1),round(r.y0,1),round(r.x1,1),round(r.y1,1)] for r in box_rects],
        "lone_sq":[[round(s.x0,1),round(s.y0,1),round(s.x1,1),round(s.y1,1)] for s in lone_sq],
    }

json.dump(report, open("candidates.json","w"), indent=0)

# summary
for pi in range(doc.page_count):
    r=report[pi]
    gsz=[len(g) for g in r["grids"]]
    print(f"p{pi:2d}: lines={len(r['lines']):2d} grids={gsz} boxes={len(r['boxes']):2d} lone_sq={len(r['lone_sq'])}")