#!/usr/bin/env python3
"""Deterministic Discord banner crop study. No third-party packages.

The source is one 1200 x 675 grid image. The script builds Fill and Fit PNG
exports for the three minimum tool canvases, then measures the files, crop or
padding area, and 320-pixel-wide previews. No clock, randomness or network.
"""

import hashlib
import json
import os
import struct
import zlib

HERE = os.path.dirname(os.path.abspath(__file__))
SRC_W, SRC_H = 1200, 675
CANVASES = [
    ("server-960x540", 960, 540),
    ("profile-main-680x240", 680, 240),
    ("profile-server-600x240", 600, 240),
]

BG = (246, 242, 232, 255)
GRID = (207, 220, 213, 255)
GRID_MAJOR = (139, 165, 155, 255)
INK = (12, 36, 50, 255)
CREAM = (255, 250, 238, 255)
ACCENT = (13, 126, 119, 255)
MARKERS = [
    (233, 92, 70, 255),
    (49, 111, 184, 255),
    (225, 166, 45, 255),
    (156, 75, 139, 255),
]
FEATURE_MARKER = 1
FEATURE_LABEL = 2

# 5x7 bitmap glyphs. A capital letter is 35 px at scale 5.
FONT = {
    " ": ["00000"] * 7,
    "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
    "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
    "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
    "3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
    "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    "5": ["11111", "10000", "10000", "11110", "00001", "00001", "11110"],
    "6": ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
    "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
    "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
    "9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
    "A": ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
    "B": ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
    "C": ["01111", "10000", "10000", "10000", "10000", "10000", "01111"],
    "D": ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
    "E": ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
    "F": ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
    "G": ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
    "H": ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
    "I": ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
    "J": ["00111", "00010", "00010", "00010", "10010", "10010", "01100"],
    "K": ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
    "L": ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
    "M": ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
    "N": ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
    "O": ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
    "P": ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
    "Q": ["01110", "10001", "10001", "10001", "10101", "10010", "01101"],
    "R": ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
    "S": ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
    "T": ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
    "U": ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
    "V": ["10001", "10001", "10001", "10001", "10001", "01010", "00100"],
    "W": ["10001", "10001", "10001", "10101", "10101", "11011", "10001"],
    "X": ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
    "Y": ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
    "Z": ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
    ":": ["00000", "00100", "00100", "00000", "00100", "00100", "00000"],
}


def canvas(width, height, colour=BG):
    return [bytearray(bytes(colour) * width) for _ in range(height)]


def rect(pixels, width, height, x0, y0, box_w, box_h, colour):
    row_bytes = bytes(colour)
    for y in range(max(0, y0), min(height, y0 + box_h)):
        row = pixels[y]
        for x in range(max(0, x0), min(width, x0 + box_w)):
            row[x * 4:x * 4 + 4] = row_bytes


def text(pixels, width, height, x, y, value, scale, colour):
    cursor = x
    for char in value:
        glyph = FONT[char]
        for gy, line in enumerate(glyph):
            for gx, bit in enumerate(line):
                if bit == "1":
                    rect(pixels, width, height, cursor + gx * scale, y + gy * scale, scale, scale, colour)
        cursor += 6 * scale


def centered_text(pixels, width, height, y, value, scale, colour):
    x = centered_text_x(width, value, scale)
    text(pixels, width, height, x, y, value, scale, colour)


def centered_text_x(width, value, scale):
    return (width - (len(value) * 6 * scale - scale)) // 2


def mask_canvas(width=SRC_W, height=SRC_H):
    return [bytearray(width) for _ in range(height)]


def mask_rect(mask, width, height, x0, y0, box_w, box_h, value):
    for y in range(max(0, y0), min(height, y0 + box_h)):
        row = mask[y]
        for x in range(max(0, x0), min(width, x0 + box_w)):
            row[x] = value


def mask_text(mask, width, height, x, y, value, scale, feature):
    cursor = x
    for char in value:
        glyph = FONT[char]
        for gy, line in enumerate(glyph):
            for gx, bit in enumerate(line):
                if bit == "1":
                    mask_rect(mask, width, height, cursor + gx * scale, y + gy * scale, scale, scale, feature)
        cursor += 6 * scale


def source_image():
    pixels = canvas(SRC_W, SRC_H)
    features = mask_canvas()
    for x in range(0, SRC_W, 75):
        colour = GRID_MAJOR if x % 300 == 0 else GRID
        rect(pixels, SRC_W, SRC_H, x, 0, 1, SRC_H, colour)
    for y in range(0, SRC_H, 75):
        colour = GRID_MAJOR if y % 300 == 0 else GRID
        rect(pixels, SRC_W, SRC_H, 0, y, SRC_W, 1, colour)

    rect(pixels, SRC_W, SRC_H, 0, 0, SRC_W, 12, INK)
    rect(pixels, SRC_W, SRC_H, 0, SRC_H - 12, SRC_W, 12, INK)
    rect(pixels, SRC_W, SRC_H, 0, 0, 12, SRC_H, INK)
    rect(pixels, SRC_W, SRC_H, SRC_W - 12, 0, 12, SRC_H, INK)

    corners = [(34, 34), (SRC_W - 124, 34), (34, SRC_H - 124), (SRC_W - 124, SRC_H - 124)]
    for (x, y), colour in zip(corners, MARKERS):
        rect(pixels, SRC_W, SRC_H, x, y, 90, 90, colour)
        mask_rect(features, SRC_W, SRC_H, x, y, 90, 90, FEATURE_MARKER)

    rect(pixels, SRC_W, SRC_H, 360, 235, 480, 205, INK)
    rect(pixels, SRC_W, SRC_H, 376, 251, 448, 173, ACCENT)
    centered_text(pixels, SRC_W, SRC_H, 286, "DISCORD SAMPLE", 5, CREAM)
    centered_text(pixels, SRC_W, SRC_H, 350, "1200 X 675", 6, CREAM)
    label = "SMALL TEXT 28 PX"
    label_scale = 4
    label_x = centered_text_x(SRC_W, label, label_scale)
    text(pixels, SRC_W, SRC_H, label_x, 575, label, label_scale, INK)
    mask_text(features, SRC_W, SRC_H, label_x, 575, label, label_scale, FEATURE_LABEL)
    return pixels, features, {
        "x": label_x,
        "top": 575,
        "width": len(label) * 6 * label_scale - label_scale,
        "height": 7 * label_scale,
    }, [
        {"x": x, "y": y, "width": 90, "height": 90}
        for x, y in corners
    ]


def write_png(path, pixels, width, height):
    raw = b"".join(b"\x00" + bytes(row) for row in pixels)

    def chunk(kind, data):
        return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))

    with open(path, "wb") as stream:
        stream.write(b"\x89PNG\r\n\x1a\n")
        stream.write(chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)))
        stream.write(chunk(b"IDAT", zlib.compress(raw, 9)))
        stream.write(chunk(b"IEND", b""))


def resample(source, source_w, source_h, width, height, sx, sy, step_x, step_y, features=None):
    output = canvas(width, height, (0, 0, 0, 0))
    output_features = mask_canvas(width, height) if features is not None else None
    for y in range(height):
        source_y = min(source_h - 1, max(0, int(sy + (y + 0.5) * step_y - 0.5)))
        row = output[y]
        feature_row = output_features[y] if output_features is not None else None
        for x in range(width):
            source_x = min(source_w - 1, max(0, int(sx + (x + 0.5) * step_x - 0.5)))
            row[x * 4:x * 4 + 4] = source[source_y][source_x * 4:source_x * 4 + 4]
            if feature_row is not None:
                feature_row[x] = features[source_y][source_x]
    return (output, output_features) if output_features is not None else output


def fill_export(source, features, width, height):
    scale = max(width / SRC_W, height / SRC_H)
    kept_w, kept_h = width / scale, height / scale
    sx, sy = (SRC_W - kept_w) / 2, (SRC_H - kept_h) / 2
    pixels, output_features = resample(source, SRC_W, SRC_H, width, height, sx, sy, kept_w / width, kept_h / height, features)
    crop_area = SRC_W * SRC_H - kept_w * kept_h
    return pixels, output_features, {
        "scale": scale,
        "source_x": sx,
        "source_y": sy,
        "source_region": [kept_w, kept_h],
        "crop_area_px": crop_area,
        "crop_share": crop_area / (SRC_W * SRC_H),
        "pad_area_px": 0,
        "pad_share": 0,
    }


def fit_export(source, features, width, height):
    scale = min(width / SRC_W, height / SRC_H)
    drawn_w, drawn_h = SRC_W * scale, SRC_H * scale
    offset_x, offset_y = (width - drawn_w) / 2, (height - drawn_h) / 2
    output = canvas(width, height, (0, 0, 0, 0))
    output_features = mask_canvas(width, height)
    drawn_w_px, drawn_h_px = int(round(drawn_w)), int(round(drawn_h))
    drawn, drawn_features = resample(source, SRC_W, SRC_H, drawn_w_px, drawn_h_px, 0, 0, SRC_W / drawn_w, SRC_H / drawn_h, features)
    x0, y0 = int(round(offset_x)), int(round(offset_y))
    for y, row in enumerate(drawn):
        if 0 <= y0 + y < height:
            output[y0 + y][x0 * 4:(x0 + len(row) // 4) * 4] = row
            output_features[y0 + y][x0:x0 + len(drawn_features[y])] = drawn_features[y]
    pad_area = width * height - drawn_w * drawn_h
    return output, output_features, {
        "scale": scale,
        "source_x": 0,
        "source_y": 0,
        "source_region": [SRC_W, SRC_H],
        "drawn_size": [drawn_w, drawn_h],
        "drawn_size_px": [drawn_w_px, drawn_h_px],
        "offset_x": offset_x,
        "offset_y": offset_y,
        "crop_area_px": 0,
        "crop_share": 0,
        "pad_area_px": pad_area,
        "pad_share": pad_area / (width * height),
    }


def verify_png(path, pixels, width, height):
    """Read the written PNG, validate chunks/CRC, and compare the decoded raster."""
    with open(path, "rb") as stream:
        data = stream.read()
    if data[:8] != b"\x89PNG\r\n\x1a\n":
        raise ValueError(f"{path}: bad PNG signature")
    offset, chunks, image_data = 8, 0, bytearray()
    header = None
    while offset < len(data):
        length = struct.unpack(">I", data[offset:offset + 4])[0]
        kind = data[offset + 4:offset + 8]
        chunk_data = data[offset + 8:offset + 8 + length]
        stored_crc = struct.unpack(">I", data[offset + 8 + length:offset + 12 + length])[0]
        if zlib.crc32(kind + chunk_data) != stored_crc:
            raise ValueError(f"{path}: bad CRC in {kind.decode('ascii')}")
        if kind == b"IHDR":
            header = struct.unpack(">IIBBBBB", chunk_data)
        elif kind == b"IDAT":
            image_data.extend(chunk_data)
        chunks += 1
        offset += 12 + length
        if kind == b"IEND":
            break
    if header is None or not image_data:
        raise ValueError(f"{path}: incomplete PNG")
    read_width, read_height, bit_depth, colour_type, compression, filter_method, interlace = header
    if (read_width, read_height, bit_depth, colour_type, compression, filter_method, interlace) != (width, height, 8, 6, 0, 0, 0):
        raise ValueError(f"{path}: unexpected IHDR {header}")
    raw = zlib.decompress(image_data)
    stride = 1 + width * 4
    if len(raw) != height * stride:
        raise ValueError(f"{path}: decompressed raster has {len(raw)} bytes, expected {height * stride}")
    for y in range(height):
        start = y * stride
        if raw[start] != 0:
            raise ValueError(f"{path}: unexpected PNG filter {raw[start]} on row {y}")
        if raw[start + 1:start + stride] != bytes(pixels[y]):
            raise ValueError(f"{path}: decoded row {y} differs from the generated canvas")
    return {
        "width": read_width,
        "height": read_height,
        "bit_depth": bit_depth,
        "colour_type": "RGBA",
        "chunks": chunks,
        "chunk_crc_ok": True,
        "raster_sha256": hashlib.sha256(raw).hexdigest(),
        "file_bytes": len(data),
    }


def feature_rows(features, value):
    return sum(1 for row in features if value in row)


def feature_pixels(features, value):
    return sum(row.count(value) for row in features)


def marker_fragments(features):
    width, height = len(features[0]), len(features)
    mid_x, mid_y = width // 2, height // 2
    quadrants = [(0, 0, mid_x, mid_y), (mid_x, 0, width, mid_y),
                 (0, mid_y, mid_x, height), (mid_x, mid_y, width, height)]
    visible = 0
    for x0, y0, x1, y1 in quadrants:
        if any(FEATURE_MARKER in row[x0:x1] for row in features[y0:y1]):
            visible += 1
    return visible


def transparent_pixels(pixels):
    return sum(1 for row in pixels for offset in range(3, len(row), 4) if row[offset] == 0)


def source_visible_height(geometry, label):
    top = max(label["top"], geometry["source_y"])
    bottom = min(label["top"] + label["height"], geometry["source_y"] + geometry["source_region"][1])
    return max(0, bottom - top)


def digest(path):
    with open(path, "rb") as stream:
        return hashlib.sha256(stream.read()).hexdigest()


def write_top_band_svg(path):
    svg = """<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 960 540" role="img" aria-labelledby="title desc">
<title id="title">Discord server banner top 48 pixel design hint</title>
<desc id="desc">A 960 by 540 rectangle with its top 48 pixels marked. The mark is Discord's design suggestion, not a complete safe area.</desc>
<rect width="960" height="540" fill="#f6f2e8"/>
<path d="M0 90H960M0 180H960M0 270H960M0 360H960M0 450H960M120 0V540M240 0V540M360 0V540M480 0V540M600 0V540M720 0V540M840 0V540" stroke="#d2ddd6" stroke-width="2"/>
<rect x="1" y="1" width="958" height="538" fill="none" stroke="#0c2432" stroke-width="3"/>
<rect width="960" height="48" fill="#0d7e77"/>
<text x="24" y="31" fill="#fffaf0" font-family="Arial, sans-serif" font-size="20" font-weight="700">48 PX DESIGN HINT</text>
<rect x="305" y="195" width="350" height="150" rx="8" fill="#0c2432"/>
<text x="480" y="258" fill="#fffaf0" font-family="Arial, sans-serif" font-size="30" font-weight="700" text-anchor="middle">SERVER BANNER</text>
<text x="480" y="303" fill="#9ddbd2" font-family="Arial, sans-serif" font-size="22" text-anchor="middle">960 x 540 · 16:9</text>
<text x="480" y="510" fill="#0c2432" font-family="Arial, sans-serif" font-size="18" text-anchor="middle">Keep the top area simple. This is not a complete safe-area map.</text>
</svg>
"""
    with open(path, "w", encoding="utf-8") as stream:
        stream.write(svg)


def main():
    exports_dir = os.path.join(HERE, "exports")
    previews_dir = os.path.join(HERE, "previews-320")
    os.makedirs(exports_dir, exist_ok=True)
    os.makedirs(previews_dir, exist_ok=True)

    source, source_features, label_bounds, marker_bounds = source_image()
    source_path = os.path.join(HERE, "source-grid-1200x675.png")
    write_png(source_path, source, SRC_W, SRC_H)
    source_readback = verify_png(source_path, source, SRC_W, SRC_H)
    write_top_band_svg(os.path.join(HERE, "server-top-band.svg"))

    rows = []
    for preset, width, height in CANVASES:
        for mode, build in (("fill", fill_export), ("fit", fit_export)):
            filename = f"{preset}-{mode}.png"
            pixels, output_features, geometry = build(source, source_features, width, height)
            path = os.path.join(exports_dir, filename)
            write_png(path, pixels, width, height)
            readback = verify_png(path, pixels, width, height)
            preview_height = max(1, round(height * 320 / width))
            preview, preview_features = resample(
                pixels, width, height, 320, preview_height,
                0, 0, width / 320, height / preview_height, output_features
            )
            preview_path = os.path.join(previews_dir, filename)
            write_png(preview_path, preview, 320, preview_height)
            preview_readback = verify_png(preview_path, preview, 320, preview_height)
            rows.append({
                "preset": preset,
                "mode": mode,
                "file": f"exports/{filename}",
                "width": readback["width"],
                "height": readback["height"],
                "bytes": readback["file_bytes"],
                "sha256": digest(path),
                **geometry,
                "png_readback": readback,
                "label_bounds_source": label_bounds,
                "marker_bounds_source": marker_bounds,
                "label_visible_source_height_px": round(source_visible_height(geometry, label_bounds), 2),
                "label_projected_full_height_in_export_px": round(28 * geometry["scale"], 2),
                "label_projected_full_height_at_320_wide_px": round(28 * geometry["scale"] * 320 / width, 2),
                "label_visible_mask_rows_in_export": feature_rows(output_features, FEATURE_LABEL),
                "label_visible_mask_rows_at_320_wide": feature_rows(preview_features, FEATURE_LABEL),
                "label_visible_mask_pixels_in_export": feature_pixels(output_features, FEATURE_LABEL),
                "label_visible_mask_pixels_at_320_wide": feature_pixels(preview_features, FEATURE_LABEL),
                "marker_fragments_visible_in_export": marker_fragments(output_features),
                "marker_visible_mask_pixels_in_export": feature_pixels(output_features, FEATURE_MARKER),
                "marker_fragments_visible_at_320_wide": marker_fragments(preview_features),
                "transparent_pixels_in_export": transparent_pixels(pixels),
                "preview": f"previews-320/{filename}",
                "preview_width": preview_readback["width"],
                "preview_height": preview_readback["height"],
                "preview_bytes": preview_readback["file_bytes"],
                "preview_sha256": digest(preview_path),
                "preview_png_readback": preview_readback,
            })

    result = {
        "generator": "make_samples.py",
        "method": "centred nearest-neighbour sample of the browser crop box; browser resampling differs",
        "png_readback_method": "reopen, validate signature and chunk CRCs, decompress IDAT, compare every decoded row with the generated canvas",
        "measurement_notes": {
            "label": "visible mask rows come from the source label mask sampled through the same crop mapping; projected text height is stored separately",
            "markers": "fragment count is the number of image quadrants containing at least one sampled marker pixel",
            "transparency": "transparent pixels count alpha=0 in the raster; geometric padding can differ after drawn dimensions round to integer pixels",
        },
        "source": {
            "file": "source-grid-1200x675.png",
            "width": source_readback["width"],
            "height": source_readback["height"],
            "bytes": source_readback["file_bytes"],
            "sha256": digest(source_path),
            "png_readback": source_readback,
        },
        "fit_background": "transparent PNG",
        "focus": 0.5,
        "exports": rows,
    }
    with open(os.path.join(HERE, "results.json"), "w", encoding="utf-8") as stream:
        json.dump(result, stream, indent=2)
        stream.write("\n")
    print(f"wrote {len(rows)} exports, {len(rows)} previews, results.json and server-top-band.svg")


if __name__ == "__main__":
    main()
