"""µFrame Image Converter — convert images to character art.

Supports braille, block, ascii, and halfblock rendering modes
with optional dithering and color output.
"""

from __future__ import annotations

import os
from pathlib import Path

from uframe.chars import BRAILLE_BASE, BRAILLE_LEFT, BRAILLE_RIGHT

try:
    from PIL import Image
    HAS_PIL = True
except ImportError:
    HAS_PIL = False


# ASCII brightness ramp (light → dark)
ASCII_RAMP = " .:-=+*#%@"
# Block shade ramp (light → dark)
BLOCK_RAMP = " ░▒▓█"


def _load_image(path: str) -> "Image.Image":
    """Load an image from file path."""
    if not HAS_PIL:
        raise ImportError("Pillow is required for image conversion: pip install Pillow")
    resolved = Path(path).expanduser()
    if not resolved.is_file():
        raise FileNotFoundError(f"Image not found: {path}")
    return Image.open(str(resolved))


def _floyd_steinberg(pixels: list[list[float]], w: int, h: int, levels: int = 2) -> list[list[int]]:
    """Apply Floyd-Steinberg dithering to a grayscale pixel array.

    Returns quantized values in range [0, levels-1].
    """
    result = [[0] * w for _ in range(h)]
    err = [row[:] for row in pixels]  # copy

    for y in range(h):
        for x in range(w):
            old = err[y][x]
            new = round(old * (levels - 1)) / (levels - 1) if levels > 1 else (1.0 if old > 0.5 else 0.0)
            result[y][x] = int(round(new * (levels - 1)))
            quant_err = old - new

            if x + 1 < w:
                err[y][x + 1] += quant_err * 7 / 16
            if y + 1 < h:
                if x - 1 >= 0:
                    err[y + 1][x - 1] += quant_err * 3 / 16
                err[y + 1][x] += quant_err * 5 / 16
                if x + 1 < w:
                    err[y + 1][x + 1] += quant_err * 1 / 16

    return result


def convert_braille(img: "Image.Image", width: int, dither: str = "floyd",
                    invert: bool = False) -> list[str]:
    """Convert image to braille character art.

    Each character encodes a 2×4 pixel block. Resolution: 2x horizontal, 4x vertical.
    """
    # Resize: each output char = 2 pixels wide × 4 pixels tall
    pixel_w = width * 2
    aspect = img.height / img.width
    pixel_h = int(pixel_w * aspect / 2)  # /2 for terminal cell aspect
    pixel_h = max(pixel_h, 4)
    # Round up to multiple of 4
    pixel_h = ((pixel_h + 3) // 4) * 4

    img_resized = img.resize((pixel_w, pixel_h)).convert("L")

    # Get pixel data as 0.0–1.0 floats
    pixels = []
    for y in range(pixel_h):
        row = []
        for x in range(pixel_w):
            v = img_resized.getpixel((x, y)) / 255.0
            if invert:
                v = 1.0 - v
            row.append(v)
        pixels.append(row)

    # Dither to binary
    if dither == "floyd":
        binary = _floyd_steinberg(pixels, pixel_w, pixel_h, levels=2)
    else:
        binary = [[1 if p > 0.5 else 0 for p in row] for row in pixels]

    # Map 2×4 blocks to braille characters
    lines: list[str] = []
    for by in range(0, pixel_h, 4):
        line = ""
        for bx in range(0, pixel_w, 2):
            code = BRAILLE_BASE
            for row in range(4):
                py = by + row
                if py < pixel_h:
                    # Left column
                    px_l = bx
                    if px_l < pixel_w and binary[py][px_l]:
                        code |= BRAILLE_LEFT[row]
                    # Right column
                    px_r = bx + 1
                    if px_r < pixel_w and binary[py][px_r]:
                        code |= BRAILLE_RIGHT[row]
            line += chr(code)
        lines.append(line)

    return lines


def convert_block(img: "Image.Image", width: int, dither: str = "none",
                  invert: bool = False) -> list[str]:
    """Convert image to block shade characters (░▒▓█)."""
    aspect = img.height / img.width
    height = max(1, int(width * aspect / 2))  # /2 for terminal cell aspect

    img_resized = img.resize((width, height)).convert("L")

    pixels = []
    for y in range(height):
        row = []
        for x in range(width):
            v = img_resized.getpixel((x, y)) / 255.0
            if invert:
                v = 1.0 - v
            row.append(v)
        pixels.append(row)

    if dither == "floyd":
        quantized = _floyd_steinberg(pixels, width, height, levels=len(BLOCK_RAMP))
    else:
        quantized = [[int(p * (len(BLOCK_RAMP) - 1)) for p in row] for row in pixels]

    lines: list[str] = []
    for row in quantized:
        line = "".join(BLOCK_RAMP[min(v, len(BLOCK_RAMP) - 1)] for v in row)
        lines.append(line)

    return lines


def convert_ascii(img: "Image.Image", width: int, dither: str = "none",
                  invert: bool = False) -> list[str]:
    """Convert image to classic ASCII art using brightness ramp."""
    aspect = img.height / img.width
    height = max(1, int(width * aspect / 2))

    img_resized = img.resize((width, height)).convert("L")

    lines: list[str] = []
    for y in range(height):
        line = ""
        for x in range(width):
            v = img_resized.getpixel((x, y)) / 255.0
            if invert:
                v = 1.0 - v
            idx = int(v * (len(ASCII_RAMP) - 1))
            line += ASCII_RAMP[min(idx, len(ASCII_RAMP) - 1)]
        lines.append(line)

    return lines


def convert_halfblock(img: "Image.Image", width: int,
                      invert: bool = False, use_color: bool = False) -> list[tuple[str, str | None, str | None]]:
    """Convert image to half-block characters with optional color.

    Uses ▄ with foreground (bottom pixel) and background (top pixel) colors.
    Returns list of (line_text, fg_colors, bg_colors) tuples.
    Each fg/bg color string has one 3-digit hex per character, or None for mono.
    """
    aspect = img.height / img.width
    height = max(2, int(width * aspect / 2))
    # Round up to even
    height = height + (height % 2)

    img_resized = img.resize((width, height))

    if use_color:
        img_rgb = img_resized.convert("RGB")
    img_gray = img_resized.convert("L")

    lines: list[tuple[str, str | None, str | None]] = []
    for y in range(0, height, 2):
        chars = ""
        fgs = "" if use_color else None
        bgs = "" if use_color else None

        for x in range(width):
            top_v = img_gray.getpixel((x, y)) / 255.0
            bot_v = img_gray.getpixel((x, y + 1)) / 255.0 if y + 1 < height else 0

            if invert:
                top_v = 1.0 - top_v
                bot_v = 1.0 - bot_v

            if use_color:
                top_rgb = img_rgb.getpixel((x, y))
                bot_rgb = img_rgb.getpixel((x, y + 1)) if y + 1 < height else (0, 0, 0)
                # Quantize to 3-digit hex
                fg_hex = f"{round(bot_rgb[0]*15/255):x}{round(bot_rgb[1]*15/255):x}{round(bot_rgb[2]*15/255):x}"
                bg_hex = f"{round(top_rgb[0]*15/255):x}{round(top_rgb[1]*15/255):x}{round(top_rgb[2]*15/255):x}"
                fgs += fg_hex
                bgs += bg_hex

            chars += "▄"

        lines.append((chars, fgs, bgs))

    return lines


def convert_image(path: str, mode: str = "braille", width: int = 30,
                  dither: str = "floyd", invert: bool = False,
                  use_color: bool = False) -> list[str]:
    """High-level image conversion — returns list of character art lines.

    Args:
        path: image file path
        mode: "braille", "block", "ascii", "halfblock"
        width: output width in characters
        dither: "floyd", "threshold", "none"
        invert: flip light/dark
        use_color: preserve colors (halfblock only for now)

    Returns:
        List of strings, one per output line.
    """
    img = _load_image(path)

    if mode == "braille":
        return convert_braille(img, width, dither, invert)
    elif mode == "block":
        return convert_block(img, width, dither, invert)
    elif mode == "ascii":
        return convert_ascii(img, width, dither, invert)
    elif mode == "halfblock":
        hb_lines = convert_halfblock(img, width, invert, use_color)
        # For non-color mode, just return the character strings
        return [line[0] for line in hb_lines]
    else:
        return convert_braille(img, width, dither, invert)


def get_image_height(path: str, mode: str = "braille", width: int = 30) -> int:
    """Calculate the output height for an image without full conversion."""
    try:
        img = _load_image(path)
    except (ImportError, FileNotFoundError):
        return 1

    aspect = img.height / img.width

    if mode == "braille":
        pixel_h = int(width * 2 * aspect / 2)
        return max(1, ((pixel_h + 3) // 4))
    else:
        return max(1, int(width * aspect / 2))
