#!/usr/bin/env python3
import math
import os
import struct
import subprocess
import zlib


ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
RESOURCES_DIR = os.path.join(ROOT_DIR, "Resources")
ICONSET_DIR = os.path.join(RESOURCES_DIR, "AppIcon.iconset")
MASTER_PNG = os.path.join(RESOURCES_DIR, "AppIcon-1024.png")
ICNS_PATH = os.path.join(RESOURCES_DIR, "AppIcon.icns")


def clamp(value, low=0.0, high=1.0):
    return max(low, min(high, value))


def blend_pixel(pixels, width, x, y, color):
    if x < 0 or y < 0 or x >= width or y >= width:
        return

    r, g, b, a = color
    idx = (y * width + x) * 4
    src_a = clamp(a / 255.0)
    inv_a = 1.0 - src_a

    pixels[idx] = int((r * src_a) + (pixels[idx] * inv_a))
    pixels[idx + 1] = int((g * src_a) + (pixels[idx + 1] * inv_a))
    pixels[idx + 2] = int((b * src_a) + (pixels[idx + 2] * inv_a))
    pixels[idx + 3] = min(255, int((a * src_a) + (pixels[idx + 3] * inv_a)))


def write_png(path, width, height, pixels):
    raw = bytearray()
    stride = width * 4
    for y in range(height):
        raw.append(0)
        raw.extend(pixels[y * stride:(y + 1) * stride])

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

    png = bytearray(b"\x89PNG\r\n\x1a\n")
    png.extend(chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)))
    png.extend(chunk(b"IDAT", zlib.compress(bytes(raw), 9)))
    png.extend(chunk(b"IEND", b""))

    with open(path, "wb") as file:
        file.write(png)


def rounded_rect_mask(x, y, size, radius):
    cx = min(max(x, radius), size - radius)
    cy = min(max(y, radius), size - radius)
    return math.hypot(x - cx, y - cy) <= radius


def point_in_polygon(x, y, points):
    inside = False
    j = len(points) - 1
    for i in range(len(points)):
        xi, yi = points[i]
        xj, yj = points[j]
        crosses = (yi > y) != (yj > y)
        if crosses:
            at_x = (xj - xi) * (y - yi) / max(0.0001, yj - yi) + xi
            if x < at_x:
                inside = not inside
        j = i
    return inside


def draw_polygon(pixels, size, points, color):
    min_x = max(0, int(min(p[0] for p in points)))
    max_x = min(size - 1, int(max(p[0] for p in points)) + 1)
    min_y = max(0, int(min(p[1] for p in points)))
    max_y = min(size - 1, int(max(p[1] for p in points)) + 1)

    for y in range(min_y, max_y + 1):
        for x in range(min_x, max_x + 1):
            if point_in_polygon(x + 0.5, y + 0.5, points):
                blend_pixel(pixels, size, x, y, color)


def draw_line(pixels, size, start, end, width, color):
    x1, y1 = start
    x2, y2 = end
    min_x = max(0, int(min(x1, x2) - width - 2))
    max_x = min(size - 1, int(max(x1, x2) + width + 2))
    min_y = max(0, int(min(y1, y2) - width - 2))
    max_y = min(size - 1, int(max(y1, y2) + width + 2))
    dx = x2 - x1
    dy = y2 - y1
    length_sq = max(0.0001, dx * dx + dy * dy)

    for y in range(min_y, max_y + 1):
        for x in range(min_x, max_x + 1):
            t = clamp(((x - x1) * dx + (y - y1) * dy) / length_sq)
            px = x1 + t * dx
            py = y1 + t * dy
            distance = math.hypot(x - px, y - py)
            edge = clamp(1.0 - ((distance - (width * 0.5)) / max(1.0, width * 0.35)))
            if edge > 0:
                r, g, b, a = color
                blend_pixel(pixels, size, x, y, (r, g, b, int(a * edge)))


def draw_rect(pixels, size, x, y, w, h, color):
    for py in range(max(0, y), min(size, y + h)):
        for px in range(max(0, x), min(size, x + w)):
            blend_pixel(pixels, size, px, py, color)


def draw_bt(pixels, size):
    shadow = (0, 0, 0, 115)
    metal = (215, 222, 232, 242)
    cyan = (84, 231, 255, 220)

    def rects(offset_x, offset_y, color):
        # B
        draw_rect(pixels, size, 325 + offset_x, 690 + offset_y, 34, 190, color)
        draw_rect(pixels, size, 325 + offset_x, 690 + offset_y, 128, 34, color)
        draw_rect(pixels, size, 325 + offset_x, 768 + offset_y, 118, 32, color)
        draw_rect(pixels, size, 325 + offset_x, 846 + offset_y, 132, 34, color)
        draw_rect(pixels, size, 438 + offset_x, 704 + offset_y, 34, 70, color)
        draw_rect(pixels, size, 438 + offset_x, 796 + offset_y, 34, 70, color)

        # T
        draw_rect(pixels, size, 545 + offset_x, 690 + offset_y, 174, 34, color)
        draw_rect(pixels, size, 615 + offset_x, 690 + offset_y, 34, 190, color)

    rects(9, 10, shadow)
    rects(0, 0, metal)
    draw_rect(pixels, size, 325, 889, 394, 8, cyan)


def create_master_icon():
    size = 1024
    radius = 210
    pixels = bytearray([0, 0, 0, 0] * size * size)

    for y in range(size):
        for x in range(size):
            if not rounded_rect_mask(x + 0.5, y + 0.5, size, radius):
                continue

            nx = x / (size - 1)
            ny = y / (size - 1)
            diagonal = clamp((nx + ny) * 0.5)
            r = int(8 + diagonal * 20)
            g = int(10 + diagonal * 24)
            b = int(14 + diagonal * 30)
            idx = (y * size + x) * 4
            pixels[idx:idx + 4] = bytes((r, g, b, 255))

            if (x + y) % 73 == 0:
                blend_pixel(pixels, size, x, y, (70, 78, 86, 22))

    # Subtle technical diagonals.
    for offset in (-280, -120, 90, 260):
        draw_line(pixels, size, (100 + offset, 120), (690 + offset, 690), 3, (95, 108, 120, 55))

    # Metallic material cube.
    top = [(512, 170), (735, 302), (512, 438), (289, 302)]
    left = [(289, 302), (512, 438), (512, 671), (289, 548)]
    right = [(735, 302), (512, 438), (512, 671), (735, 548)]
    draw_polygon(pixels, size, left, (35, 43, 51, 244))
    draw_polygon(pixels, size, right, (54, 65, 75, 246))
    draw_polygon(pixels, size, top, (86, 96, 106, 246))

    for start, end in [
        ((512, 170), (735, 302)),
        ((735, 302), (735, 548)),
        ((735, 548), (512, 671)),
        ((512, 671), (289, 548)),
        ((289, 548), (289, 302)),
        ((289, 302), (512, 170)),
        ((289, 302), (512, 438)),
        ((735, 302), (512, 438)),
        ((512, 438), (512, 671)),
    ]:
        draw_line(pixels, size, start, end, 7, (168, 178, 190, 170))

    # Cyan PBR accent light.
    for width, alpha in [(42, 42), (24, 78), (10, 240)]:
        draw_line(pixels, size, (352, 584), (672, 584), width, (67, 225, 255, alpha))
    draw_line(pixels, size, (622, 414), (677, 382), 10, (67, 225, 255, 220))
    draw_line(pixels, size, (347, 516), (414, 554), 8, (67, 225, 255, 185))

    # Rim highlight.
    draw_line(pixels, size, (194, 928), (830, 928), 4, (120, 132, 145, 100))
    draw_bt(pixels, size)

    os.makedirs(RESOURCES_DIR, exist_ok=True)
    write_png(MASTER_PNG, size, size, pixels)


def build_iconset():
    os.makedirs(ICONSET_DIR, exist_ok=True)
    targets = [
        ("icon_16x16.png", 16),
        ("icon_16x16@2x.png", 32),
        ("icon_32x32.png", 32),
        ("icon_32x32@2x.png", 64),
        ("icon_128x128.png", 128),
        ("icon_128x128@2x.png", 256),
        ("icon_256x256.png", 256),
        ("icon_256x256@2x.png", 512),
        ("icon_512x512.png", 512),
        ("icon_512x512@2x.png", 1024),
    ]

    for filename, pixel_size in targets:
        out_path = os.path.join(ICONSET_DIR, filename)
        if pixel_size == 1024:
            with open(MASTER_PNG, "rb") as src, open(out_path, "wb") as dst:
                dst.write(src.read())
        else:
            subprocess.run(
                ["sips", "-z", str(pixel_size), str(pixel_size), MASTER_PNG, "--out", out_path],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )

    subprocess.run(
        ["iconutil", "-c", "icns", ICONSET_DIR, "-o", ICNS_PATH],
        check=True,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


def main():
    create_master_icon()
    build_iconset()
    print(ICNS_PATH)


if __name__ == "__main__":
    main()
