"""
ASCII Art Primitives Library

ASCII art rendering with per-zone expression evaluation and cell effects.
"""
import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont
from typing import Any, Dict, List, Optional, Callable
import colorsys


# Character sets
CHAR_SETS = {
    "standard": " .:-=+*#%@",
    "blocks": " ░▒▓█",
    "simple": " .:oO@",
    "digits": "0123456789",
    "binary": "01",
    "ascii": " `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@",
}

# Default font
_default_font = None


def _get_font(size: int):
    """Get monospace font at given size."""
    global _default_font
    try:
        return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", size)
    except:
        return ImageFont.load_default()


def _parse_color(color_str: str) -> tuple:
    """Parse color string to RGB tuple."""
    if color_str.startswith('#'):
        hex_color = color_str[1:]
        if len(hex_color) == 3:
            hex_color = ''.join(c*2 for c in hex_color)
        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

    colors = {
        'black': (0, 0, 0), 'white': (255, 255, 255),
        'red': (255, 0, 0), 'green': (0, 255, 0), 'blue': (0, 0, 255),
        'yellow': (255, 255, 0), 'cyan': (0, 255, 255), 'magenta': (255, 0, 255),
        'gray': (128, 128, 128), 'grey': (128, 128, 128),
    }
    return colors.get(color_str.lower(), (0, 0, 0))


def _cell_sample(frame: np.ndarray, cell_size: int):
    """Sample frame into cells, returning colors and luminances.

    Uses cv2.resize with INTER_AREA (pixel-area averaging) which is
    ~25x faster than numpy reshape+mean for block downsampling.
    """
    h, w = frame.shape[:2]
    rows = h // cell_size
    cols = w // cell_size

    # Crop to exact grid then block-average via cv2 area interpolation.
    cropped = frame[:rows * cell_size, :cols * cell_size]
    colors = cv2.resize(cropped, (cols, rows), interpolation=cv2.INTER_AREA)

    luminances = ((0.299 * colors[:, :, 0] +
                    0.587 * colors[:, :, 1] +
                    0.114 * colors[:, :, 2]) / 255.0).astype(np.float32)

    return colors, luminances


def _luminance_to_char(lum: float, alphabet: str, contrast: float) -> str:
    """Map luminance to character."""
    chars = CHAR_SETS.get(alphabet, alphabet)
    lum = ((lum - 0.5) * contrast + 0.5)
    lum = max(0, min(1, lum))
    idx = int(lum * (len(chars) - 1))
    return chars[idx]


def _render_char_cell(char: str, cell_size: int, color: tuple, bg_color: tuple) -> np.ndarray:
    """Render a single character to a cell image."""
    img = Image.new('RGB', (cell_size, cell_size), bg_color)
    draw = ImageDraw.Draw(img)
    font = _get_font(cell_size)

    # Center the character
    bbox = draw.textbbox((0, 0), char, font=font)
    text_w = bbox[2] - bbox[0]
    text_h = bbox[3] - bbox[1]
    x = (cell_size - text_w) // 2
    y = (cell_size - text_h) // 2 - bbox[1]

    draw.text((x, y), char, fill=color, font=font)
    return np.array(img)


def prim_ascii_fx_zone(
    frame: np.ndarray,
    cols: int = 80,
    char_size: int = None,
    alphabet: str = "standard",
    color_mode: str = "color",
    background: str = "black",
    contrast: float = 1.5,
    char_hue = None,
    char_saturation = None,
    char_brightness = None,
    char_scale = None,
    char_rotation = None,
    char_jitter = None,
    cell_effect = None,
    energy: float = None,
    rotation_scale: float = 0,
    _interp = None,
    _env = None,
    **extra_params
) -> np.ndarray:
    """
    Render frame as ASCII art with per-zone effects.

    Args:
        frame: Input image
        cols: Number of character columns
        char_size: Cell size in pixels (overrides cols if set)
        alphabet: Character set name or custom string
        color_mode: "color", "mono", "invert", or color name
        background: Background color name or hex
        contrast: Contrast for character selection
        char_hue/saturation/brightness/scale/rotation/jitter: Per-zone expressions
        cell_effect: Lambda (cell, zone) -> cell for per-cell effects
        energy: Energy value from audio analysis
        rotation_scale: Max rotation degrees
        _interp: Interpreter (auto-injected)
        _env: Environment (auto-injected)
        **extra_params: Additional params passed to zone dict
    """
    h, w = frame.shape[:2]

    # Calculate cell size
    if char_size is None or char_size == 0:
        cell_size = max(4, w // cols)
    else:
        cell_size = max(4, int(char_size))

    # Sample cells
    colors, luminances = _cell_sample(frame, cell_size)
    rows, cols_actual = luminances.shape

    # Parse background color
    bg_color = _parse_color(background)

    # Create output image
    out_h = rows * cell_size
    out_w = cols_actual * cell_size
    output = np.full((out_h, out_w, 3), bg_color, dtype=np.uint8)

    # Check if we have cell_effect
    has_cell_effect = cell_effect is not None

    # Process each cell
    for r in range(rows):
        for c in range(cols_actual):
            lum = luminances[r, c]
            cell_color = tuple(colors[r, c])

            # Build zone context
            zone = {
                'row': r,
                'col': c,
                'row-norm': r / max(1, rows - 1),
                'col-norm': c / max(1, cols_actual - 1),
                'lum': float(lum),
                'r': cell_color[0] / 255,
                'g': cell_color[1] / 255,
                'b': cell_color[2] / 255,
                'cell_size': cell_size,
            }

            # Add HSV
            r_f, g_f, b_f = cell_color[0]/255, cell_color[1]/255, cell_color[2]/255
            hsv = colorsys.rgb_to_hsv(r_f, g_f, b_f)
            zone['hue'] = hsv[0] * 360
            zone['sat'] = hsv[1]

            # Add energy and rotation_scale
            if energy is not None:
                zone['energy'] = energy
            zone['rotation_scale'] = rotation_scale

            # Add extra params
            for k, v in extra_params.items():
                if isinstance(v, (int, float, str, bool)) or v is None:
                    zone[k] = v

            # Get character
            char = _luminance_to_char(lum, alphabet, contrast)
            zone['char'] = char

            # Determine cell color based on mode
            if color_mode == "mono":
                render_color = (255, 255, 255)
            elif color_mode == "invert":
                render_color = tuple(255 - c for c in cell_color)
            elif color_mode == "color":
                render_color = cell_color
            else:
                render_color = _parse_color(color_mode)

            zone['color'] = render_color

            # Render character to cell
            cell_img = _render_char_cell(char, cell_size, render_color, bg_color)

            # Apply cell_effect if provided
            if has_cell_effect and _interp is not None:
                cell_img = _apply_cell_effect(cell_img, zone, cell_effect, _interp, _env, extra_params)

            # Paste cell to output
            y1, y2 = r * cell_size, (r + 1) * cell_size
            x1, x2 = c * cell_size, (c + 1) * cell_size
            output[y1:y2, x1:x2] = cell_img

    # Resize to match input dimensions
    if output.shape[:2] != frame.shape[:2]:
        output = cv2.resize(output, (w, h), interpolation=cv2.INTER_LINEAR)

    return output


def _apply_cell_effect(cell_img, zone, cell_effect, interp, env, extra_params):
    """Apply cell_effect lambda to a cell image.

    cell_effect is a Lambda object with params and body.
    We create a child environment with zone variables and cell,
    then evaluate the lambda body.
    """
    # Get Environment class from the interpreter's module
    Environment = type(env)

    # Create child environment with zone variables
    cell_env = Environment(env)

    # Bind zone variables
    for k, v in zone.items():
        cell_env.set(k, v)

    # Also bind with zone- prefix for consistency
    cell_env.set('zone-row', zone.get('row', 0))
    cell_env.set('zone-col', zone.get('col', 0))
    cell_env.set('zone-row-norm', zone.get('row-norm', 0))
    cell_env.set('zone-col-norm', zone.get('col-norm', 0))
    cell_env.set('zone-lum', zone.get('lum', 0))
    cell_env.set('zone-sat', zone.get('sat', 0))
    cell_env.set('zone-hue', zone.get('hue', 0))
    cell_env.set('zone-r', zone.get('r', 0))
    cell_env.set('zone-g', zone.get('g', 0))
    cell_env.set('zone-b', zone.get('b', 0))

    # Inject loaded effects as callable functions
    if hasattr(interp, 'effects'):
        for effect_name in interp.effects:
            def make_effect_fn(name):
                def effect_fn(frame, *args):
                    params = {}
                    if name == 'blur' and len(args) >= 1:
                        params['radius'] = args[0]
                    elif name == 'rotate' and len(args) >= 1:
                        params['angle'] = args[0]
                    elif name == 'brightness' and len(args) >= 1:
                        params['amount'] = args[0]
                    elif name == 'contrast' and len(args) >= 1:
                        params['amount'] = args[0]
                    elif name == 'saturation' and len(args) >= 1:
                        params['amount'] = args[0]
                    elif name == 'hue_shift' and len(args) >= 1:
                        params['degrees'] = args[0]
                    elif name == 'rgb_split' and len(args) >= 2:
                        params['offset_x'] = args[0]
                        params['offset_y'] = args[1]
                    elif name == 'pixelate' and len(args) >= 1:
                        params['size'] = args[0]
                    elif name == 'invert':
                        pass
                    result, _ = interp.run_effect(name, frame, params, {})
                    return result
                return effect_fn
            cell_env.set(effect_name, make_effect_fn(effect_name))

    # Bind cell image and zone dict
    cell_env.set('cell', cell_img)
    cell_env.set('zone', zone)

    # Evaluate the cell_effect lambda
    # Lambda has params and body - we need to bind the params then evaluate
    if hasattr(cell_effect, 'params') and hasattr(cell_effect, 'body'):
        # Bind lambda parameters: (lambda [cell zone] body)
        if len(cell_effect.params) >= 1:
            cell_env.set(cell_effect.params[0], cell_img)
        if len(cell_effect.params) >= 2:
            cell_env.set(cell_effect.params[1], zone)

        result = interp.eval(cell_effect.body, cell_env)
    elif isinstance(cell_effect, list):
        # Raw S-expression lambda like (lambda [cell zone] body) or (fn [cell zone] body)
        # Check if it's a lambda expression
        head = cell_effect[0] if cell_effect else None
        head_name = head.name if head and hasattr(head, 'name') else str(head) if head else None
        is_lambda = head_name in ('lambda', 'fn')

        if is_lambda:
            # (lambda [params...] body)
            params = cell_effect[1] if len(cell_effect) > 1 else []
            body = cell_effect[2] if len(cell_effect) > 2 else None

            # Bind lambda parameters
            if isinstance(params, list) and len(params) >= 1:
                param_name = params[0].name if hasattr(params[0], 'name') else str(params[0])
                cell_env.set(param_name, cell_img)
            if isinstance(params, list) and len(params) >= 2:
                param_name = params[1].name if hasattr(params[1], 'name') else str(params[1])
                cell_env.set(param_name, zone)

            result = interp.eval(body, cell_env) if body else cell_img
        else:
            # Some other expression - just evaluate it
            result = interp.eval(cell_effect, cell_env)
    elif callable(cell_effect):
        # It's a callable
        result = cell_effect(cell_img, zone)
    else:
        raise ValueError(f"cell_effect must be a Lambda, list, or callable, got {type(cell_effect)}")

    if isinstance(result, np.ndarray) and result.shape == cell_img.shape:
        return result
    elif isinstance(result, np.ndarray):
        # Shape mismatch - resize to fit
        result = cv2.resize(result, (cell_img.shape[1], cell_img.shape[0]))
        return result

    raise ValueError(f"cell_effect must return an image array, got {type(result)}")


def _get_legacy_ascii_primitives():
    """Import ASCII primitives from legacy primitives module.

    These are loaded lazily to avoid import issues during module loading.
    By the time a primitive library is loaded, sexp_effects.primitives
    is already in sys.modules (imported by sexp_effects.__init__).
    """
    from sexp_effects.primitives import (
        prim_cell_sample,
        prim_luminance_to_chars,
        prim_render_char_grid,
        prim_render_char_grid_fx,
        prim_alphabet_char,
        prim_alphabet_length,
        prim_map_char_grid,
        prim_map_colors,
        prim_make_char_grid,
        prim_set_char,
        prim_get_char,
        prim_char_grid_dimensions,
        cell_sample_extended,
    )
    return {
        'cell-sample': prim_cell_sample,
        'cell-sample-extended': cell_sample_extended,
        'luminance-to-chars': prim_luminance_to_chars,
        'render-char-grid': prim_render_char_grid,
        'render-char-grid-fx': prim_render_char_grid_fx,
        'alphabet-char': prim_alphabet_char,
        'alphabet-length': prim_alphabet_length,
        'map-char-grid': prim_map_char_grid,
        'map-colors': prim_map_colors,
        'make-char-grid': prim_make_char_grid,
        'set-char': prim_set_char,
        'get-char': prim_get_char,
        'char-grid-dimensions': prim_char_grid_dimensions,
    }


PRIMITIVES = {
    'ascii-fx-zone': prim_ascii_fx_zone,
    **_get_legacy_ascii_primitives(),
}
