#!/usr/bin/env python3
"""Measure original photographs and before/after browser anime exports.

Requires Python 3, Pillow and NumPy. Put the script, coffee.png, chelsea.png,
and the four named browser exports in one directory. Run this file to write
measurements.json and manifest.json. Existing image files are never modified.
The repository directory layout is also supported.
"""

import argparse
import hashlib
import json
import math
import os
from pathlib import Path

import numpy as np
from PIL import Image


def fingerprint(path, directory):
    content = path.read_bytes()
    return {"file": os.path.relpath(path, directory), "bytes": len(content), "sha256": hashlib.sha256(content).hexdigest()}


def inspect(path, directory):
    with Image.open(path) as im:
        im.load()
        rgb = np.array(im.convert("RGB"), dtype=np.int16)
        alpha = np.array(im.convert("RGBA"), dtype=np.uint8)[:, :, 3]
        record = {
            **fingerprint(path, directory),
            "width": im.width,
            "height": im.height,
            "mode": im.mode,
            "pixels": im.width * im.height,
            "fullyOpaquePixels": int((alpha == 255).sum()),
            "meanRgbCodeValues": [round(float(value), 6) for value in rgb.mean(axis=(0, 1))],
        }
    return rgb, record


def expected_size(width, height, multiplier=3):
    if width > height:
        w, h = 512 * multiplier, math.floor(height * 512 / width) * multiplier
    else:
        w, h = math.floor(width * 512 / height) * multiplier, 512 * multiplier
    return [math.floor(w / 8) * 8, math.floor(h / 8) * 8]


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--directory", type=Path, default=Path(__file__).resolve().parent)
    parser.add_argument("--sources-dir", type=Path)
    args = parser.parse_args()
    directory = args.directory.resolve()
    sources = args.sources_dir
    if sources is None:
        sources = directory if (directory / "coffee.png").exists() else directory / "../../images"
    sources = sources.resolve()
    cases, artifacts = [], []
    for case_id in ["coffee", "chelsea"]:
        source_path = sources / f"{case_id}.png"
        before_path = directory / f"{case_id}-before-layout-fix.png"
        after_path = directory / f"{case_id}-output.png"
        _, source = inspect(source_path, directory)
        before_rgb, before = inspect(before_path, directory)
        after_rgb, after = inspect(after_path, directory)
        if before_rgb.shape != after_rgb.shape:
            raise ValueError(f"Before/after export dimensions differ: {case_id}")
        delta = after_rgb - before_rgb
        changed = (delta != 0).any(axis=2)
        predicted = expected_size(source["width"], source["height"])
        actual = [after["width"], after["height"]]
        cases.append({
            "id": case_id,
            "input": source,
            "beforeLayoutFix": before,
            "afterLayoutFix": after,
            "outputComparison": {
                "changedPixels": int(changed.sum()),
                "changedPixelPercent": round(float(changed.mean() * 100), 6),
                "meanAbsoluteRgbCodeDifference": round(float(np.abs(delta).mean()), 6),
                "definition": "Direct comparison of equal-size old/new exports: average absolute difference over every encoded 8-bit RGB component. Alpha is measured separately. This is not a quality or fidelity score.",
            },
            "dimensionCheck": {
                "observedOutput": actual,
                "predictedFromDefaultPreprocessing": predicted,
                "matchesReviewedRule": actual == predicted,
                "note": "Default multiplier 3, no HD. The short dimension is floored before multiplication, then both dimensions are rounded down to multiples of eight.",
            },
        })
        artifacts.extend(fingerprint(path, directory) for path in [source_path, before_path, after_path])
    report = {
        "schemaVersion": 1,
        "recordType": "measurements-of-existing-browser-export-files",
        "scope": {
            "imagesUnmodified": True,
            "sourceOutputPixelScoreComputed": False,
            "whyNoSourcePixelScore": "Source and output dimensions differ, and the workflow intentionally stylizes the photograph. No source resampling, registration or reference reconstruction is performed.",
            "colorBoundary": "Means and differences use encoded RGB code values, not physical light, perceptual color distance or a ranking of image quality.",
            "browserBoundary": "The script only reads existing files. Browser execution and patch chronology are recorded separately in run.json; source-level layout tests remain separately scoped.",
        },
        "cases": cases,
    }
    (directory / "measurements.json").write_text(json.dumps(report, indent=2) + "\n")
    for path in [Path(__file__).resolve(), directory / "run.json", directory / "measurements.json", directory / "layout-review.json", directory / "layout-test-report.json", directory / "inspect-model-layout.mjs", sources / "sources.json"]:
        if path.exists():
            artifacts.append(fingerprint(path, directory))
    manifest = {
        "schemaVersion": 1,
        "description": "Original sources, four untouched browser exports, run record, measurements, script, source-layout evidence and source provenance when available. The manifest excludes itself to avoid recursive hashing.",
        "artifacts": artifacts,
    }
    (directory / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
    print(json.dumps({"cases": len(cases), "browserExports": 4, "artifacts": len(artifacts), "dimensionRulesMatch": all(case["dimensionCheck"]["matchesReviewedRule"] for case in cases)}))


if __name__ == "__main__":
    main()
