#!/usr/bin/env python3
"""Read two source photographs and their untouched browser line-art exports.

Requires Python 3, Pillow and NumPy. Put this script, coffee.png, chelsea.png,
coffee-output.png and chelsea-output.png in one folder, then run it. The repository
layout is also supported. No image is resized, recolored, aligned or rewritten.
"""

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):
    data = path.read_bytes()
    return {
        "file": os.path.relpath(path, directory),
        "bytes": len(data),
        "sha256": hashlib.sha256(data).hexdigest(),
    }


def inspect(path, directory):
    with Image.open(path) as im:
        im.load()
        rgb = np.asarray(im.convert("RGB"))
        alpha = np.asarray(im.convert("RGBA"))[:, :, 3]
        near_white = (rgb >= 250).all(axis=2)
        dark = (rgb <= 64).any(axis=2)
        non_gray = (rgb[:, :, 0] != rgb[:, :, 1]) | (rgb[:, :, 0] != rgb[:, :, 2])
        return {
            **fingerprint(path, directory),
            "width": im.width,
            "height": im.height,
            "mode": im.mode,
            "pixels": im.width * im.height,
            "fullyOpaquePixels": int((alpha == 255).sum()),
            "nearWhitePixels": int(near_white.sum()),
            "nearWhitePercent": round(float(near_white.mean() * 100), 6),
            "anyChannelAtOrBelow64Pixels": int(dark.sum()),
            "anyChannelAtOrBelow64Percent": round(float(dark.mean() * 100), 6),
            "nonGrayPixels": int(non_gray.sum()),
            "nonGrayPercent": round(float(non_gray.mean() * 100), 6),
        }


def expected_size(width, height):
    if width < height:
        return [768, math.floor(768 / width * height / 8) * 8]
    return [math.floor(768 / height * width / 8) * 8, 768]


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"
        output_path = directory / f"{case_id}-output.png"
        source = inspect(source_path, directory)
        output = inspect(output_path, directory)
        predicted = expected_size(source["width"], source["height"])
        actual = [output["width"], output["height"]]
        cases.append({
            "id": case_id,
            "input": source,
            "output": output,
            "dimensionCheck": {
                "observedOutput": actual,
                "predictedFromReviewedPreprocessing": predicted,
                "matchesReviewedRule": actual == predicted,
                "widthScale": round(output["width"] / source["width"], 6),
                "heightScale": round(output["height"] / source["height"], 6),
                "note": "Source and output are not compared pixel by pixel; dimensions differ and this script does not resample either file.",
            },
        })
        artifacts.extend([fingerprint(source_path, directory), fingerprint(output_path, directory)])
    report = {
        "schemaVersion": 1,
        "recordType": "measurements-of-existing-browser-export-files",
        "definitions": {
            "nearWhite": "All three encoded 8-bit RGB channels are at least 250.",
            "darkChannel": "At least one encoded RGB channel is at most 64. This is a threshold statistic, not measured line quality or semantic detail.",
            "nonGray": "The three RGB channel values are not identical. Exact equality is used, not a perceptual color tolerance.",
            "opacity": "All output alpha values equal 255 in the recorded files; white is an opaque background, not transparency.",
            "dimensions": "The actual dimensions are read with Pillow. Predicted dimensions are separately calculated from the reviewed short-edge-768, multiple-of-8 preprocessing rule.",
            "scope": "Two local-browser outputs from two source photographs. These numbers do not establish contour accuracy, vector quality, benchmark performance or processing speed.",
        },
        "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", sources / "sources.json"]:
        if path.exists():
            artifacts.append(fingerprint(path, directory))
    manifest = {
        "schemaVersion": 1,
        "description": "Untouched input/output images, run record, measurements, script 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), "artifacts": len(artifacts), "dimensionRulesMatch": all(case["dimensionCheck"]["matchesReviewedRule"] for case in cases)}))


if __name__ == "__main__":
    main()
