"""Reproduce Issueureum's ZIP experiment using Python's standard library.

Download this script, original.png and jpeg-q75.jpg into the same directory.
Run: python reproduce.py
Outputs go into ./reproduced beside this script; published inputs are not changed.
Use --png PATH --jpg PATH --output DIR to choose other locations.
Use --check to verify an existing output directory without writing files.
"""
from __future__ import annotations

import argparse
import hashlib
import io
import json
from pathlib import Path
import platform
from datetime import datetime, timezone, timedelta
import zipfile
import zlib

ROOT = Path(__file__).resolve().parent
LEVEL = 6
EXPECTED = {
    "original.png": "7b320cd043284aa6cc7dca865d82dde0d159460137ff4141e4b762c18d5c5c13",
    "jpeg-q75.jpg": "69773a8ca6309c431587896037f79b75db33ec3dd97916d326a9438466d822b2",
}


def sha(data):
    return hashlib.sha256(data).hexdigest()


def make_text():
    return ("이슈름 ZIP 실험용 가상 작업 기록\n번호,분류,작업,상태\n"
            + "".join(f"{i:04d},문서,자료 정리,완료\n" for i in range(1, 1001))).encode("utf-8")


def pack(name, data):
    buffer = io.BytesIO()
    entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
    entry.create_system = 3
    entry.external_attr = 0o100644 << 16
    entry.extra = b""
    entry.comment = b""
    with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=LEVEL) as archive:
        archive.writestr(entry, data, compress_type=zipfile.ZIP_DEFLATED, compresslevel=LEVEL)
    return buffer.getvalue()


def measure(name, label, data, source_path):
    packed = pack(name, data)
    with zipfile.ZipFile(io.BytesIO(packed), "r") as archive:
        if archive.namelist() != [name] or archive.testzip() is not None:
            raise ValueError(f"ZIP structure or CRC check failed: {name}")
        restored = archive.read(name)
        info = archive.getinfo(name)
        if restored != data or sha(restored) != sha(data):
            raise ValueError(f"Restored input differs: {name}")
    if pack(name, data) != packed:
        raise ValueError(f"Identical inputs did not reproduce identical ZIP bytes: {name}")
    return packed, {
        "input": name, "label": label, "source_path": source_path, "zip_file": name + ".zip",
        "original_bytes": len(data), "zip_bytes": len(packed),
        "compressed_payload_bytes": info.compress_size,
        "zip_metadata_bytes": len(packed) - info.compress_size,
        "saving_percent": round((1 - len(packed) / len(data)) * 100, 4),
        "original_sha256": sha(data), "zip_sha256": sha(packed), "restored_sha256": sha(restored),
        "crc32": f"{info.CRC:08x}", "restored_bytes_identical": True,
        "crc_check_passed": True, "repeated_zip_bytes_identical": True,
    }


def chart(results, measured_at):
    # One shared 0–100% scale: bars show ZIP size, not the saving rate.
    labels = ["반복 텍스트", "PNG 시험 화면", "JPG 시험 화면"]
    rows = []
    for i, (label, result) in enumerate(zip(labels, results)):
        y = 285 + i * 170
        ratio = result["zip_bytes"] / result["original_bytes"]
        rows.append(f'''<text x="72" y="{y}" font-size="30" font-weight="700">{label}</text>
<text x="1128" y="{y}" font-size="28" text-anchor="end">원본의 {ratio * 100:.1f}%</text>
<rect x="72" y="{y + 24}" width="1056" height="44" rx="4" fill="#e0e5ef"/>
<rect x="72" y="{y + 24}" width="{1056 * ratio:.3f}" height="44" rx="4" fill="#455cdb"/>
<text x="72" y="{y + 106}" font-size="24" fill="#52617a">{result['original_bytes']:,} → {result['zip_bytes']:,}바이트 · {result['saving_percent']:.1f}% 감소</text>''')
    date = datetime.fromisoformat(measured_at).astimezone(timezone(timedelta(hours=9))).strftime("%Y.%m.%d %H:%M KST")
    return f'''<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="900" viewBox="0 0 1200 900" role="img" aria-labelledby="title desc">
<title id="title">ZIP으로 묶은 뒤 남은 파일 크기</title>
<desc id="desc">각 원본을 100%로 비교했습니다. 반복 텍스트는 {results[0]["zip_bytes"] / results[0]["original_bytes"] * 100:.1f}%, PNG는 {results[1]["zip_bytes"] / results[1]["original_bytes"] * 100:.1f}%, JPG는 {results[2]["zip_bytes"] / results[2]["original_bytes"] * 100:.1f}% 크기가 남았습니다. 직접 만든 세 파일만 비교한 결과입니다.</desc>
<rect width="1200" height="900" fill="#f6f8fc"/>
<g font-family="Malgun Gothic, Apple SD Gothic Neo, sans-serif" fill="#182b46">
<text x="72" y="90" font-size="24" fill="#455cdb" font-weight="700">이슈름 실험실 · 파일 압축</text>
<text x="72" y="162" font-size="48" font-weight="700">ZIP으로 얼마나 줄어들까?</text>
<text x="72" y="208" font-size="25" fill="#52617a">회색 막대 = 각 원본 100% · 파란 막대 = ZIP 전체 크기</text>
{''.join(rows)}
<text x="72" y="828" font-size="23" fill="#52617a">직접 만든 텍스트·시험 화면 3개 · DEFLATE 수준 6</text>
<text x="72" y="864" font-size="23" fill="#52617a">측정 {date} · ZIP을 풀면 세 원본 모두 바이트까지 동일</text>
</g></svg>'''


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--png", type=Path, default=ROOT / "original.png")
    parser.add_argument("--jpg", type=Path, default=ROOT / "jpeg-q75.jpg")
    parser.add_argument("--output", type=Path, default=ROOT / "reproduced")
    parser.add_argument("--check", action="store_true")
    args = parser.parse_args()
    images = {}
    for name, path in [("original.png", args.png), ("jpeg-q75.jpg", args.jpg)]:
        data = path.read_bytes()
        if sha(data) != EXPECTED[name]:
            raise ValueError(f"Unexpected source SHA-256: {path}; download the experiment's original file.")
        images[name] = data
    text = make_text()
    inputs = [
        ("repeated-text.txt", "반복 텍스트 1,000행", text, "/lab/zip-compression/repeated-text.txt"),
        ("original.png", "PNG 시험 화면", images["original.png"], "/lab/capture-format/original.png"),
        ("jpeg-q75.jpg", "JPG 시험 화면", images["jpeg-q75.jpg"], "/lab/capture-format/jpeg-q75.jpg"),
    ]
    results, outputs = [], {}
    for name, label, data, source in inputs:
        packed, result = measure(name, label, data, source)
        results.append(result)
        outputs[name + ".zip"] = packed
    outputs["repeated-text.txt"] = text
    if args.check:
        recorded = json.loads((args.output / "results.json").read_text(encoding="utf-8"))
        if recorded["results"] != results:
            raise ValueError("Recorded measurements differ; check source files and Python/zlib versions.")
        for name, data in outputs.items():
            if (args.output / name).read_bytes() != data:
                raise ValueError(f"Saved output differs: {name}")
        print("All 3 saved ZIPs reproduce byte-for-byte; restored bytes, SHA-256 and CRC checks pass.")
        return
    measured_at = datetime.now(timezone.utc).isoformat()
    report = {
        "experiment_id": "lab-002-zip-compression", "title": "ZIP으로 묶으면 얼마나 줄어들까?",
        "measured_at_utc": measured_at,
        "environment": {"python": platform.python_version(), "os": platform.platform(),
                        "zlib_compile": zlib.ZLIB_VERSION, "zlib_runtime": zlib.ZLIB_RUNTIME_VERSION},
        "settings": {"compression": "ZIP_DEFLATED", "compresslevel": LEVEL, "entries_per_archive": 1,
                     "timestamp": "1980-01-01T00:00:00 (fixed ZIP metadata, not measurement time)",
                     "create_system": 3, "file_mode_octal": "100644", "extra_and_comment": "empty",
                     "size_includes": "compressed payload and all ZIP headers",
                     "text_encoding": "UTF-8, no BOM, LF", "text_rows": 1000},
        "limitations": ["Synthetic repetitive text and a single text-and-shapes image; no photographic samples.",
                        "Ratios describe these files and these settings, not every TXT, PNG or JPG.",
                        "Compression time, encryption, multi-file archives and other ZIP programs were not compared.",
                        "ZIP bytes can vary with metadata, compression settings and Python/zlib versions."],
        "sources": ["https://docs.python.org/3/library/zipfile.html", "https://www.w3.org/TR/png-3/"],
        "results": results,
    }
    outputs["results.json"] = (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    outputs["summary.svg"] = chart(results, measured_at).encode("utf-8")
    args.output.mkdir(parents=True, exist_ok=True)
    for name, data in outputs.items():
        (args.output / name).write_bytes(data)
    print(json.dumps({"measured_at_utc": measured_at, "results": results}, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
