from __future__ import annotations

import json
import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path

from PIL import Image

from translate_evans_ch2 import is_header_or_footer, line_kind


ROOT = Path(__file__).resolve().parent
PDF = ROOT / "Partial Differential Equations, Second Edition (Lawrence C. Evans) (z-library.sk, 1lib.sk, z-lib.sk).pdf"
RAW = ROOT / "evans_chapter2_raw.txt"
XML = ROOT / "evans_chapter2_bbox.xml"
PAGE_DIR = ROOT / "evans_formula_pages_288"
CROP_DIR = ROOT / "evans_formula_crops"
MANIFEST = ROOT / "evans_formula_crops.json"
GROUP_MANIFEST = ROOT / "evans_formula_group_crops.json"
XML_GROUP_MANIFEST = ROOT / "evans_formula_xmlgroup_crops.json"

PDF_FIRST_PAGE = 34
PDF_LAST_PAGE = 105


def run(cmd: list[str]) -> None:
    subprocess.run(cmd, cwd=ROOT, check=True)


def ensure_xml() -> None:
    if XML.exists() and XML.stat().st_size > 1000:
        return
    with XML.open("wb") as f:
        subprocess.run(
            ["pdftohtml", "-xml", "-f", str(PDF_FIRST_PAGE), "-l", str(PDF_LAST_PAGE), "-i", "-stdout", str(PDF)],
            cwd=ROOT,
            check=True,
            stdout=f,
        )


def ensure_pages() -> None:
    PAGE_DIR.mkdir(exist_ok=True)
    marker = PAGE_DIR / f"page-{PDF_FIRST_PAGE}.png"
    if marker.exists():
        return
    run([
        "pdftoppm",
        "-png",
        "-r",
        "288",
        "-f",
        str(PDF_FIRST_PAGE),
        "-l",
        str(PDF_LAST_PAGE),
        str(PDF),
        str(PAGE_DIR / "page"),
    ])


def page_image(page_no: int) -> Path:
    # pdftoppm pads page numbers by the width needed for the requested range.
    candidates = sorted(PAGE_DIR.glob(f"page-{page_no}*.png"))
    if not candidates:
        candidates = sorted(PAGE_DIR.glob(f"page-*{page_no}.png"))
    if not candidates:
        raise FileNotFoundError(page_no)
    return candidates[0]


def raw_formula_positions() -> list[dict[str, int]]:
    positions: list[dict[str, int]] = []
    raw_pages = RAW.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n").split("\f")
    for page_idx, page in enumerate(raw_pages):
        if page_idx > PDF_LAST_PAGE - PDF_FIRST_PAGE:
            break
        line_index = 0
        for raw_line in page.split("\n"):
            line = raw_line.rstrip()
            if is_header_or_footer(line) or not line.strip():
                continue
            kind = line_kind(line)
            if kind == "formula":
                positions.append({"page": PDF_FIRST_PAGE + page_idx, "line_index": line_index})
            line_index += 1
    return positions


def raw_formula_groups() -> list[dict[str, int]]:
    groups: list[dict[str, int]] = []
    raw_pages = RAW.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n").split("\f")
    for page_idx, page in enumerate(raw_pages):
        if page_idx > PDF_LAST_PAGE - PDF_FIRST_PAGE:
            break
        page_no = PDF_FIRST_PAGE + page_idx
        line_index = 0
        current: dict[str, int] | None = None
        for raw_line in page.split("\n"):
            line = raw_line.rstrip()
            if is_header_or_footer(line):
                continue
            if not line.strip():
                continue
            kind = line_kind(line)
            if kind == "formula":
                if current is None:
                    current = {"page": page_no, "start": line_index, "end": line_index}
                else:
                    current["end"] = line_index
            else:
                if current is not None:
                    groups.append(current)
                    current = None
            line_index += 1
        if current is not None:
            groups.append(current)
    return groups


def xml_lines() -> dict[int, list[dict[str, float]]]:
    tree = ET.parse(XML)
    pages: dict[int, list[dict[str, float]]] = {}
    for page_el in tree.findall("page"):
        page_no = int(page_el.attrib["number"])
        groups: dict[int, list[ET.Element]] = {}
        for text_el in page_el.findall("text"):
            txt = "".join(text_el.itertext()).strip()
            if not txt:
                continue
            top = int(float(text_el.attrib["top"]))
            key = round(top / 4) * 4
            groups.setdefault(key, []).append(text_el)
        lines: list[dict[str, float]] = []
        for _, elems in sorted(groups.items()):
            elems.sort(key=lambda e: float(e.attrib["left"]))
            txt = " ".join("".join(e.itertext()).strip() for e in elems).strip()
            if not txt or is_header_or_footer(txt):
                continue
            left = min(float(e.attrib["left"]) for e in elems)
            top = min(float(e.attrib["top"]) for e in elems)
            right = max(float(e.attrib["left"]) + float(e.attrib["width"]) for e in elems)
            bottom = max(float(e.attrib["top"]) + float(e.attrib["height"]) for e in elems)
            lines.append({"text": txt, "left": left, "right": right, "top": top, "bottom": bottom})
        pages[page_no] = lines
    return pages


def crop_formulas() -> None:
    ensure_xml()
    ensure_pages()
    CROP_DIR.mkdir(exist_ok=True)

    positions = raw_formula_positions()
    lines_by_page = xml_lines()
    manifest: list[str] = []
    for i, pos in enumerate(positions):
        page_no = pos["page"]
        lines = lines_by_page.get(page_no, [])
        if pos["line_index"] >= len(lines):
            manifest.append("")
            continue
        line = lines[pos["line_index"]]
        img_path = page_image(page_no)
        with Image.open(img_path) as im:
            sx = im.width / 1188.0
            sy = im.height / 1512.0
            # Keep the full body width so equation numbers, side conditions, and limits survive.
            x0 = int(245 * sx)
            x1 = int(925 * sx)
            y0 = max(0, int((line["top"] - 10) * sy))
            y1 = min(im.height, int((line["bottom"] + 12) * sy))
            crop = im.crop((x0, y0, x1, y1))
            out = CROP_DIR / f"formula_{i:04d}.png"
            crop.save(out)
            manifest.append(out.relative_to(ROOT).as_posix())
    MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")

    group_manifest: list[str] = []
    for i, group in enumerate(raw_formula_groups()):
        page_no = group["page"]
        lines = lines_by_page.get(page_no, [])
        if group["start"] >= len(lines) or group["end"] >= len(lines):
            group_manifest.append("")
            continue
        first = lines[group["start"]]
        last = lines[group["end"]]
        img_path = page_image(page_no)
        with Image.open(img_path) as im:
            sx = im.width / 1188.0
            sy = im.height / 1512.0
            x0 = int(245 * sx)
            x1 = int(925 * sx)
            y0 = max(0, int((first["top"] - 14) * sy))
            y1 = min(im.height, int((last["bottom"] + 16) * sy))
            crop = im.crop((x0, y0, x1, y1))
            out = CROP_DIR / f"formula_group_{i:04d}.png"
            crop.save(out)
            group_manifest.append(out.relative_to(ROOT).as_posix())
    GROUP_MANIFEST.write_text(json.dumps(group_manifest, ensure_ascii=False, indent=2), encoding="utf-8")

    xml_group_manifest: list[str] = []
    xml_groups: list[dict[str, float]] = []
    for page_no, lines in sorted(lines_by_page.items()):
        current: dict[str, float] | None = None
        for line in lines:
            txt = line.get("text", "")
            is_formula = line_kind(txt) == "formula"
            if is_formula:
                if current is None:
                    current = {
                        "page": page_no,
                        "left": line["left"],
                        "right": line["right"],
                        "top": line["top"],
                        "bottom": line["bottom"],
                    }
                else:
                    current["left"] = min(current["left"], line["left"])
                    current["right"] = max(current["right"], line["right"])
                    current["bottom"] = line["bottom"]
            else:
                if current is not None:
                    xml_groups.append(current)
                    current = None
        if current is not None:
            xml_groups.append(current)

    for i, group in enumerate(xml_groups):
        page_no = int(group["page"])
        img_path = page_image(page_no)
        with Image.open(img_path) as im:
            sx = im.width / 1188.0
            sy = im.height / 1512.0
            x0 = int(245 * sx)
            x1 = int(925 * sx)
            y0 = max(0, int((group["top"] - 24) * sy))
            y1 = min(im.height, int((group["bottom"] + 30) * sy))
            crop = im.crop((x0, y0, x1, y1))
            out = CROP_DIR / f"formula_xmlgroup_{i:04d}.png"
            crop.save(out)
            xml_group_manifest.append(out.relative_to(ROOT).as_posix())
    XML_GROUP_MANIFEST.write_text(json.dumps(xml_group_manifest, ensure_ascii=False, indent=2), encoding="utf-8")
    print(
        f"wrote {len(manifest)} formula crops, "
        f"{len(group_manifest)} raw groups, and {len(xml_group_manifest)} xml groups"
    )


if __name__ == "__main__":
    crop_formulas()
