import cv2
import numpy as np
from PIL import Image, ImageDraw
from pathlib import Path
from multiprocessing import Pool, cpu_count
import time

INPUT_DIR  = Path(r"C:\allgifted\mathapi11v2\docs\past year questions\Old Images")
OUTPUT_DIR = Path(r"C:\allgifted\mathapi11v2\docs\past year questions\Only Images")

BORDER      = 3
PAD         = 36
VPAD        = 16
D0ACAC      = (208, 172, 172)
MIN_AREA    = 600
HEADER_SKIP = 880
HEADER_H_GATE = 1500
DILATE_K    = 25          # v1 grouping (reverted)
TEXT_H      = 30          # row-strip: rows whose components are all shorter = text
INNER_FRAC  = 0.20
MIN_DENSITY = 0.008

def extract(image_path):
    pil = Image.open(image_path).convert("RGB")
    img = np.array(pil)
    h, w = img.shape[:2]

    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    _, binary = cv2.threshold(gray, 210, 255, cv2.THRESH_BINARY_INV)

    skip = HEADER_SKIP if h > HEADER_H_GATE else 0
    if skip:
        binary[:skip, :] = 0

    R, G, B = img[:,:,0], img[:,:,1], img[:,:,2]
    yellow = (R.astype(int)>200) & (G.astype(int)>200) & (B.astype(int)<180)
    for y in np.where(yellow.mean(axis=1) > 0.2)[0]:
        binary[max(0,y-2):min(h,y+28), :] = 0

    n_lbl, labels, stats, _ = cv2.connectedComponentsWithStats(binary)
    illus = np.zeros_like(binary)
    for i in range(1, n_lbl):
        if stats[i, cv2.CC_STAT_AREA] >= MIN_AREA:
            illus[labels == i] = 255

    kernel = np.ones((DILATE_K, DILATE_K), np.uint8)
    dilated = cv2.dilate(illus, kernel)
    contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    regions = []
    for c in contours:
        x, y, bw, bh = cv2.boundingRect(c)
        if bh < 40 or bw < 80 or bw * bh < 8000:
            continue
        if bh < 45 and bw > 400:
            continue
        roi = illus[y:y+bh, x:x+bw]
        ys, xs = np.where(roi > 0)
        if len(xs) == 0:
            continue
        x1 = max(0, x + int(xs.min()) - PAD)
        y1 = max(0, y + int(ys.min()) - PAD)
        x2 = min(w,  x + int(xs.max()) + PAD)
        y2 = min(h,  y + int(ys.max()) + PAD)
        regions.append([x1, y1, x2, y2])

    regions.sort(key=lambda b: b[1])

    merged = []
    for b in regions:
        if merged and b[1] <= merged[-1][3] + 15:
            merged[-1][0] = min(merged[-1][0], b[0])
            merged[-1][1] = min(merged[-1][1], b[1])
            merged[-1][2] = max(merged[-1][2], b[2])
            merged[-1][3] = max(merged[-1][3], b[3])
        else:
            merged.append(b[:])

    final = []
    for b in merged:
        bx1, by1, bx2, by2 = b
        sub = binary[by1:by2, bx1:bx2]
        if sub.size == 0:
            continue
        sn, slab, sst, _ = cv2.connectedComponentsWithStats(sub)
        tall_rows = np.zeros(sub.shape[0], dtype=bool)
        for i in range(1, sn):
            if sst[i, cv2.CC_STAT_HEIGHT] >= TEXT_H:
                t = sst[i, cv2.CC_STAT_TOP]; hh = sst[i, cv2.CC_STAT_HEIGHT]
                tall_rows[t:t+hh] = True
        idx = np.where(tall_rows)[0]
        if len(idx) == 0:
            continue
        ty1 = by1 + int(idx[0]);  ty2 = by1 + int(idx[-1]) + 1
        band = illus[ty1:ty2, bx1:bx2]
        ys, xs = np.where(band > 0)
        if len(xs) == 0:
            continue
        x1 = max(0, bx1 + int(xs.min()) - PAD)
        x2 = min(w,  bx1 + int(xs.max()) + PAD)
        y1 = max(0, ty1 - VPAD)
        y2 = min(h,  ty2 + VPAD)
        if (y2-y1) < 40 or (x2-x1) < 60:
            continue
        mx = int((x2-x1)*INNER_FRAC); my = int((y2-y1)*INNER_FRAC)
        inner = binary[y1+my:y2-my, x1+mx:x2-mx]
        if inner.size == 0 or (inner > 0).mean() < MIN_DENSITY:
            continue
        final.append((x1, y1, x2, y2))

    return pil, final

def process_one(img_path_str):
    cv2.setNumThreads(1)
    img_path = Path(img_path_str)
    try:
        pil, regions = extract(img_path)
    except Exception as e:
        return (img_path.name, -1, 0, str(e))
    stem = img_path.stem
    saved = 0
    for i, (x1, y1, x2, y2) in enumerate(regions, 1):
        crop = pil.crop((x1, y1, x2, y2))
        bw, bh = crop.size
        bd = Image.new("RGB", (bw+BORDER*2, bh+BORDER*2), (255,255,255))
        ImageDraw.Draw(bd).rectangle([0,0,bw+BORDER*2-1,bh+BORDER*2-1], outline=D0ACAC, width=BORDER)
        bd.paste(crop, (BORDER, BORDER))
        bd.save(OUTPUT_DIR / f"{stem}_illus_{i:02d}.png")
        saved += 1
    return (img_path.name, len(regions), saved, None)

if __name__ == "__main__":
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    exts = {".png", ".jpg", ".jpeg", ".bmp"}
    files = sorted(str(f) for f in INPUT_DIR.iterdir() if f.suffix.lower() in exts)
    workers = max(1, min(cpu_count() - 2, len(files)))
    print(f"Found {len(files)} paper images. Using {workers} parallel workers.", flush=True)

    t0 = time.time()
    results = []
    with Pool(workers) as pool:
        for res in pool.imap_unordered(process_one, files):
            results.append(res)
            if res[3]:
                print(f"  ERROR {res[0]}: {res[3]}", flush=True)
    elapsed = time.time() - t0

    results.sort(key=lambda r: r[0])
    log_lines, total_saved, errors, zero = [], 0, 0, 0
    for name, nreg, saved, err in results:
        if err:
            log_lines.append(f"{name} | ERROR | {err}"); errors += 1
        else:
            log_lines.append(f"{name} | {nreg} illustrations | {saved} saved")
            total_saved += saved
            if saved == 0: zero += 1
    (OUTPUT_DIR / "_extraction_log.txt").write_text("\n".join(log_lines), encoding="utf-8")

    print(f"\nDONE in {elapsed:.1f}s", flush=True)
    print(f"  images processed : {len(results)}", flush=True)
    print(f"  crops saved      : {total_saved}", flush=True)
    print(f"  images w/ 0 crops: {zero}", flush=True)
    print(f"  errors           : {errors}", flush=True)
