#!/usr/bin/env python3
"""Extract dancing-banana.js frames from the classic GIF."""

from __future__ import annotations

from collections import deque
from pathlib import Path

from PIL import Image

OUT = Path(__file__).resolve().parents[1] / 'dancing-banana.js'
BG = (255, 255, 255)
TARGET_W = 24

CHAR = {
    (0, 0, 0): 'B',
    (255, 255, 0): 'Y',
    (206, 206, 0): 'y',
    (156, 156, 0): 'd',
    (255, 0, 0): 'R',
    (255, 255, 255): 'W',
}

JS_TEMPLATE = '''\
// Extracted from the classic dancing banana GIF (Trym Stene, 1999).
// 8 frames, {w}×{h}px — fixed canvas, solid 2-space blocks.

const RESET = '\\x1b[0m'
const FRAME_WIDTH = {w}
const FRAME_HEIGHT = {h}

const COLORS = {{
  B: 236,
  R: 196,
  W: 255,
  E: 255,
  Y: 226,
  d: 136,
  y: 214,
}}

const FRAMES = [
{frames}
]

function renderRow(row) {{
  const padded = row.padEnd(FRAME_WIDTH, '.').slice(0, FRAME_WIDTH)
  let line = ''
  for (const cell of padded) {{
    if (cell === '.') {{
      line += '  '
      continue
    }}
    const code = COLORS[cell]
    if (code === undefined) {{
      line += '  '
      continue
    }}
    const bold = cell === 'E' ? '\\x1b[1m' : ''
    line += `${{bold}}\\x1b[48;5;${{code}}m  ${{RESET}}`
  }}
  return line
}}

export function getBananaFrame(index) {{
  const frame = FRAMES[index % FRAMES.length]
  const lines = frame.map(renderRow)
  while (lines.length < FRAME_HEIGHT) {{
    lines.push(' '.repeat(FRAME_WIDTH * 2))
  }}
  return lines.slice(0, FRAME_HEIGHT).join('\\n')
}}

export function getBananaFrameCount() {{
  return FRAMES.length
}}
'''


def remove_bg(frame: Image.Image) -> Image.Image:
    px = frame.load()
    w, h = frame.size
    seen = [[False] * w for _ in range(h)]
    q: deque[tuple[int, int]] = deque()

    for x in range(w):
        for y in (0, h - 1):
            if px[x, y][:3] == BG:
                q.append((x, y))
                seen[y][x] = True

    for y in range(h):
        for x in (0, w - 1):
            if not seen[y][x] and px[x, y][:3] == BG:
                q.append((x, y))
                seen[y][x] = True

    while q:
        x, y = q.popleft()
        px[x, y] = (255, 255, 255, 0)
        for nx, ny in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
            if 0 <= nx < w and 0 <= ny < h and not seen[ny][nx] and px[nx, ny][:3] == BG:
                seen[ny][nx] = True
                q.append((nx, ny))

    return frame


def color_key(px: tuple[int, int, int, int]) -> str:
    if px[3] < 128:
        return '.'
    rgb = px[:3]
    if rgb in CHAR:
        return CHAR[rgb]
    nearest = min(CHAR, key=lambda c: sum((a - b) ** 2 for a, b in zip(rgb, c)))
    return CHAR[nearest]


def trim_frames(frames: list[list[str]]) -> tuple[int, int, list[list[str]]]:
    h = len(frames[0])
    w = len(frames[0][0])

    while h and all(frames[f][0][c] == '.' for f in range(len(frames)) for c in range(w)):
        for f in range(len(frames)):
            frames[f].pop(0)
        h -= 1

    while h and all(frames[f][h - 1][c] == '.' for f in range(len(frames)) for c in range(w)):
        for f in range(len(frames)):
            frames[f].pop()
        h -= 1

    while w and all(frames[f][r][0] == '.' for f in range(len(frames)) for r in range(h)):
        for f in range(len(frames)):
            frames[f] = [row[1:] for row in frames[f]]
        w -= 1

    while w and all(frames[f][r][w - 1] == '.' for f in range(len(frames)) for r in range(h)):
        for f in range(len(frames)):
            frames[f] = [row[:-1] for row in frames[f]]
        w -= 1

    return w, h, frames


def content_bounds(frames: list[list[str]]) -> tuple[int, int, int, int]:
    top = len(frames[0])
    bottom = 0
    left = len(frames[0][0])
    right = 0

    for frame in frames:
        for y, row in enumerate(frame):
            for x, cell in enumerate(row):
                if cell != '.':
                    top = min(top, y)
                    bottom = max(bottom, y)
                    left = min(left, x)
                    right = max(right, x)

    return top, bottom, left, right


def normalize_canvas(frames: list[list[str]]) -> tuple[int, int, list[list[str]]]:
    top, bottom, left, right = content_bounds(frames)
    canvas_h = bottom - top + 1
    canvas_w = right - left + 1

    normalized: list[list[str]] = []
    for frame in frames:
        rows = [row[left : right + 1] for row in frame[top : bottom + 1]]
        normalized.append([row.ljust(canvas_w, '.')[:canvas_w] for row in rows])

    return canvas_w, canvas_h, normalized


def enhance_eyes(frames: list[list[str]]) -> list[list[str]]:
    h = len(frames[0])
    w = len(frames[0][0])
    out: list[list[list[str]]] = [[list(row) for row in frame] for frame in frames]

    for grid in out:
        for y in range(h):
            for x in range(w):
                if grid[y][x] != 'W':
                    continue
                if y < int(h * 0.42) or y > int(h * 0.75):
                    continue
                neighbors = []
                for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
                    if 0 <= nx < w and 0 <= ny < h:
                        neighbors.append(grid[ny][nx])
                if any(n in 'YyBR' for n in neighbors):
                    grid[y][x] = 'E'

    return [[''.join(row) for row in grid] for grid in out]


def main() -> None:
    gif_path = Path(
        '/Users/Arkasha/.cursor/projects/Volumes-Work-ITCase-itcase-ui/assets/'
        'dancing-banana-gif-809950cd-831b-4c1e-8547-2d3612956da9.gif',
    )
    img = Image.open(gif_path)

    minx, miny, maxx, maxy = 9999, 9999, 0, 0
    frames_raw: list[Image.Image] = []

    for i in range(img.n_frames):
        img.seek(i)
        frame = remove_bg(img.convert('RGBA'))
        frames_raw.append(frame)
        fw, fh = frame.size
        for y in range(fh):
            for x in range(fw):
                if frame.getpixel((x, y))[3] >= 128:
                    minx = min(minx, x)
                    miny = min(miny, y)
                    maxx = max(maxx, x)
                    maxy = max(maxy, y)

    crop_h = maxy - miny + 1
    target_h = max(8, round(crop_h * TARGET_W / (maxx - minx + 1)))

    frames: list[list[str]] = []
    for frame in frames_raw:
        cropped = frame.crop((minx, miny, maxx + 1, maxy + 1))
        small = cropped.resize((TARGET_W, target_h), Image.NEAREST)
        rows = []
        for y in range(target_h):
            rows.append(''.join(color_key(small.getpixel((x, y))) for x in range(TARGET_W)))
        frames.append(rows)

    w, h, frames = trim_frames(frames)
    w, h, frames = normalize_canvas(frames)
    frames = enhance_eyes(frames)

    frame_blocks = []
    for frame in frames:
        frame_blocks.append('  [')
        for row in frame:
            frame_blocks.append(f"    '{row}',")
        frame_blocks.append('  ],')

    OUT.write_text(
        JS_TEMPLATE.format(
            w=w,
            h=h,
            frames='\n'.join(frame_blocks),
        ),
    )


if __name__ == '__main__':
    main()
