"""Throwaway placeholder catalog: fetch public-domain originals, forge crude fakes.

The real authoring pipeline is deferred (see do.md). Nothing in the game may
depend on how this ran — it exists so the loop can be played at full length.

The generator emits each piece.json itself: it reads `aspect` off the image and
already knows `diff_rect`, because it chose where to edit. Worth keeping when
the real pipeline is built.

    python tools/make_placeholder_catalog.py
"""

import json
import shutil
import subprocess
import time
import sys
import urllib.parse
from pathlib import Path

from PIL import Image, ImageChops

DATA = Path(__file__).resolve().parent.parent / "data"
I18N = Path(__file__).resolve().parent.parent / "i18n" / "art.csv"
WIDTH = 1024
QUALITY = 92
UA = "masterpiece-or-not-dev/0.1 (placeholder catalog)"
# Colour to fall back on when a canvas has none of its own to shuffle.
TINT = (196, 122, 58)
# A forgery that moves less of the canvas than this is one nobody can see, and
# an invisible fake is a room the player must lose. Rects cover ~6%, so a fully
# worked rect lands near 5%; this only catches forgeries that did nothing.
MIN_CHANGED = 0.30

# Knowledge-only play needs globally famous works; there are only ~25-30 that
# qualify, so this list is close to the ceiling of what the design can use.
PIECES = [
    ("mona-lisa", "Mona Lisa, by Leonardo da Vinci, from C2RMF retouched.jpg", "Mona Lisa", "Leonardo da Vinci"),
    ("starry-night", "Van Gogh - Starry Night - Google Art Project.jpg", "The Starry Night", "Vincent van Gogh"),
    ("the-scream", "Edvard Munch, 1893, The Scream, oil, tempera and pastel on cardboard, 91 x 73 cm, National Gallery of Norway.jpg", "The Scream", "Edvard Munch"),
    ("girl-with-a-pearl-earring", "Meisje met de parel.jpg", "Girl with a Pearl Earring", "Johannes Vermeer"),
    ("birth-of-venus", "Sandro Botticelli - La nascita di Venere - Google Art Project - edited.jpg", "The Birth of Venus", "Sandro Botticelli"),
    ("great-wave", "Tsunami by hokusai 19th century.jpg", "The Great Wave off Kanagawa", "Katsushika Hokusai"),
    ("american-gothic", "Grant Wood - American Gothic - Google Art Project.jpg", "American Gothic", "Grant Wood"),
    ("liberty-leading-the-people", "Eugène Delacroix - Le 28 Juillet. La Liberté guidant le peuple.jpg", "Liberty Leading the People", "Eugène Delacroix"),
    ("the-night-watch", "The Night Watch - HD.jpg", "The Night Watch", "Rembrandt van Rijn"),
    ("the-kiss", "The Kiss - Gustav Klimt - Google Cultural Institute.jpg", "The Kiss", "Gustav Klimt"),
    ("sunflowers", "Vincent Willem van Gogh 127.jpg", "Sunflowers", "Vincent van Gogh"),
    ("the-last-supper", "Leonardo da Vinci (1452-1519) - The Last Supper (1495-1498).jpg", "The Last Supper", "Leonardo da Vinci"),
    # Every filename below was resolved against the Commons API rather than
    # guessed: a near-miss fetches nothing and silently drops the piece. All are
    # public domain — the most recent painter here died in 1926.
    ("las-meninas", "Las Meninas, by Diego Velázquez, from Prado in Google Earth.jpg", "Las Meninas", "Diego Velázquez"),
    ("the-creation-of-adam", "Michelangelo - Creation of Adam (cropped).jpg", "The Creation of Adam", "Michelangelo"),
    ("the-school-of-athens", '"The School of Athens" by Raffaello Sanzio da Urbino.jpg', "The School of Athens", "Raphael"),
    ("the-third-of-may-1808", "El Tres de Mayo, by Francisco de Goya, from Prado thin black margin.jpg", "The Third of May 1808", "Francisco de Goya"),
    ("wanderer-above-the-sea-of-fog", "Caspar David Friedrich - Wanderer above the sea of fog.jpg", "Wanderer above the Sea of Fog", "Caspar David Friedrich"),
    ("the-arnolfini-portrait", "Van Eyck - Arnolfini Portrait.jpg", "The Arnolfini Portrait", "Jan van Eyck"),
    ("la-grande-jatte", "A Sunday on La Grande Jatte, Georges Seurat, 1884.jpg", "A Sunday Afternoon on the Island of La Grande Jatte", "Georges Seurat"),
    ("impression-sunrise", "Monet - Impression, Sunrise.jpg", "Impression, Sunrise", "Claude Monet"),
    ("napoleon-crossing-the-alps", "Jacques Louis David - Bonaparte franchissant le Grand Saint-Bernard, 20 mai 1800 - Google Art ProjectFXD.jpg", "Napoleon Crossing the Alps", "Jacques-Louis David"),
    ("primavera", "Botticelli-primavera.jpg", "Primavera", "Sandro Botticelli"),
    ("ophelia", "John Everett Millais - Ophelia - Google Art Project.jpg", "Ophelia", "John Everett Millais"),
    ("the-milkmaid", "Johannes Vermeer - Het melkmeisje - Google Art Project.jpg", "The Milkmaid", "Johannes Vermeer"),
    ("whistlers-mother", "Whistlers Mother high res.jpg", "Whistler's Mother", "James McNeill Whistler"),
    ("bedroom-in-arles", "Vincent van Gogh - De slaapkamer - Google Art Project.jpg", "Bedroom in Arles", "Vincent van Gogh"),
    ("the-garden-of-earthly-delights", "The Garden of earthly delights.jpg", "The Garden of Earthly Delights", "Hieronymus Bosch"),
]

# The title as its painter would have written it, and when it was made. The
# cartel beside the frame shows these, so they are data on the piece, not UI
# text: nothing here goes through i18n, because a Dutch painting's Dutch title
# is not a translation of anything — it is the thing itself.
NATIVE = {
    "mona-lisa": ("La Gioconda", "1503–1519"),
    "starry-night": ("De sterrennacht", "1889"),
    "the-scream": ("Skrik", "1893"),
    "girl-with-a-pearl-earring": ("Meisje met de parel", "c. 1665"),
    "birth-of-venus": ("Nascita di Venere", "c. 1485"),
    "great-wave": ("Kanagawa-oki nami ura", "c. 1831"),
    "american-gothic": ("American Gothic", "1930"),
    "liberty-leading-the-people": ("La Liberté guidant le peuple", "1830"),
    "the-night-watch": ("De Nachtwacht", "1642"),
    "the-kiss": ("Der Kuss", "1907–1908"),
    "sunflowers": ("Zonnebloemen", "1888"),
    "the-last-supper": ("Il Cenacolo", "1495–1498"),
    "las-meninas": ("Las Meninas", "1656"),
    "the-creation-of-adam": ("Creazione di Adamo", "c. 1512"),
    "the-school-of-athens": ("Scuola di Atene", "1509–1511"),
    "the-third-of-may-1808": ("El tres de mayo de 1808", "1814"),
    "wanderer-above-the-sea-of-fog": ("Der Wanderer über dem Nebelmeer", "c. 1818"),
    "the-arnolfini-portrait": ("Het Arnolfini-portret", "1434"),
    "la-grande-jatte": ("Un dimanche après-midi à l'Île de la Grande Jatte", "1884–1886"),
    "impression-sunrise": ("Impression, soleil levant", "1872"),
    "napoleon-crossing-the-alps": ("Bonaparte franchissant le Grand-Saint-Bernard", "1801"),
    "primavera": ("La Primavera", "c. 1480"),
    "ophelia": ("Ophelia", "1851–1852"),
    "the-milkmaid": ("Het melkmeisje", "c. 1660"),
    "whistlers-mother": ("Arrangement in Grey and Black No. 1", "1871"),
    "bedroom-in-arles": ("De slaapkamer", "1888"),
    "the-garden-of-earthly-delights": ("De tuin der lusten", "c. 1490–1510"),
}

# (suffix, hint, rect as fractions of the image) — crude on purpose. The three
# rects deliberately do not overlap, so which forgery you are looking at is not
# given away by where the difference sits.
FAKES = [
    ("mirrored", "A detail has been flipped.", (0.30, 0.22, 0.28, 0.22)),
    ("recoloured", "A colour is not the one the painter used.", (0.36, 0.55, 0.30, 0.26)),
    ("cloned", "A detail has been painted over with a copy of the canvas beside it.", (0.55, 0.14, 0.24, 0.24)),
]


def fetch(filename: str, target: Path) -> bool:
    # Unencoded spaces make curl fail silently with code 000.
    url = "https://commons.wikimedia.org/wiki/Special:FilePath/%s?width=%d" % (
        urllib.parse.quote(filename),
        WIDTH,
    )
    # Commons rate-limits a burst of fetches with a 429, whose body is an error
    # page that lands here as a broken image. Without a retry that is
    # indistinguishable from a bad filename, and the piece is dropped for good.
    for attempt in range(4):
        if attempt:
            time.sleep(4 * attempt)
        if _try_fetch(url, target):
            return True
    return False


def _try_fetch(url: str, target: Path) -> bool:
    result = subprocess.run(
        ["curl", "-sL", "-A", UA, "-o", str(target), url], capture_output=True
    )
    if result.returncode != 0 or not target.exists():
        return False
    try:
        with Image.open(target) as im:
            normalised = im.convert("RGB")
    except Exception:
        target.unlink(missing_ok=True)
        return False
    # Re-encode the original through the same pipeline the fakes go through.
    # Otherwise the fake carries fresh JPEG artefacts across the whole canvas and
    # differs from the original everywhere, not only where it was forged —
    # manufacturing differences nobody authored. See do.md.
    normalised.save(target, quality=QUALITY)
    return True


def changed_percent(a: Image.Image, b: Image.Image) -> float:
    """Share of pixels that visibly differ, ignoring JPEG-level noise."""
    diff = ImageChops.difference(a.convert("RGB"), b.convert("RGB")).convert("L")
    hist = diff.histogram()
    return 100.0 * sum(hist[13:]) / float(a.size[0] * a.size[1])


def forge(original: Path, out: Path, kind: str, rect) -> float:
    with Image.open(original) as im:
        im = im.convert("RGB")
        w, h = im.size
        box = (
            int(rect[0] * w),
            int(rect[1] * h),
            int((rect[0] + rect[2]) * w),
            int((rect[1] + rect[3]) * h),
        )
        patch = im.crop(box)
        if kind == "mirrored":
            patch = patch.transpose(Image.FLIP_LEFT_RIGHT)
        elif kind == "recoloured":
            r, g, b = patch.split()
            swapped = Image.merge("RGB", (b, r, g))
            # Rotating the channels does nothing to grey, where r == g == b.
            # Whistler's Mother is literally "Arrangement in Grey and Black" and
            # came out pixel-identical to the original — a fake no player could
            # ever call. Where there is no colour to move, add one.
            if changed_percent(patch, swapped) < 20.0:
                swapped = Image.blend(patch, Image.new("RGB", patch.size, TINT), 0.45)
            patch = swapped
        elif kind == "cloned":
            # The forger painting a detail out under a copy of its neighbour,
            # rather than altering it in place. Take the band beside the rect,
            # from whichever side has the room for it.
            span = box[2] - box[0]
            left = box[0] + span if box[2] + span <= w else box[0] - span
            left = max(0, min(left, w - span))
            if left == box[0]:
                # Copying a patch onto itself forges nothing, and an unaltered
                # "fake" is a room the player cannot read. Better to be loud.
                raise ValueError("clone source lands on the rect for %s" % original)
            patch = im.crop((left, box[1], left + span, box[3]))
        else:
            raise ValueError("unknown fake kind: %s" % kind)
        forged = im.copy()
        forged.paste(patch, box)
        forged.save(out, quality=QUALITY)
        # Measure the file as written: the JPEG round-trip is what the game
        # shows, so judge the forgery through it, not before it.
        with Image.open(out) as written:
            return changed_percent(im, written)


def main() -> int:
    DATA.mkdir(exist_ok=True)
    rows = ["keys,en"]
    made, failed, invisible = [], [], []
    weakest = (100.0, "")

    for piece_id, filename, title, artist in PIECES:
        folder = DATA / piece_id
        folder.mkdir(exist_ok=True)
        original = folder / "original.jpg"

        if not original.exists():
            time.sleep(1)
        if not original.exists() and not fetch(filename, original):
            failed.append(piece_id)
            shutil.rmtree(folder, ignore_errors=True)
            continue

        with Image.open(original) as im:
            width, height = im.size

        title_key = "ART_%s_TITLE" % piece_id.upper().replace("-", "_")
        rows.append(f'{title_key},"{title}"')

        fakes = []
        for kind, hint, rect in FAKES:
            out = folder / f"fake-{kind}.jpg"
            moved = forge(original, out, kind, rect)
            if moved < MIN_CHANGED:
                invisible.append(f"{piece_id}/{out.name} ({moved:.2f}%)")
            weakest = min(weakest, (moved, f"{piece_id}/{out.name}"))
            hint_key = "ART_%s_FAKE_%s" % (piece_id.upper().replace("-", "_"), kind.upper())
            rows.append(f'{hint_key},"{hint}"')
            fakes.append({
                "texture": out.name,
                "hint_key": hint_key,
                "diff_rect": [round(v, 4) for v in rect],
            })

        (folder / "piece.json").write_text(
            json.dumps({
                "id": piece_id,
                "title_key": title_key,
                "artist": artist,
                "title_native": NATIVE.get(piece_id, (title, ""))[0],
                "year": NATIVE.get(piece_id, (title, ""))[1],
                "type": "painting",
                "aspect": round(width / height, 4),
                "authentic_texture": original.name,
                "fakes": fakes,
            }, indent=2) + "\n",
            encoding="utf-8",
        )
        made.append(piece_id)
        print(f"  {piece_id}: {width}x{height} aspect={width / height:.3f}")

    I18N.write_text("\n".join(rows) + "\n", encoding="utf-8")
    print(f"\n{len(made)} pieces, {len(made) * len(FAKES)} fakes")
    print("weakest forgery: %s moved %.2f%% of its canvas" % (weakest[1], weakest[0]))
    if failed:
        print(f"FAILED to fetch: {', '.join(failed)}")
    if invisible:
        # A fake identical to its original is a room the player can only lose,
        # so this fails the run rather than shipping it quietly.
        print("INVISIBLE fakes — the player could only lose these rooms:")
        for entry in invisible:
            print(f"  {entry}")
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
