"""Draws OpenPose_full-style skeleton images (black background, colored
limbs/joints) and writes them out via maya.api.OpenMaya.MImage.

This is a self-contained software rasterizer (no OpenGL/render-target
dependency) operating on a flat RGBA bytearray, so it can be driven equally
from a batch/offline context or from inside a render override. Confidence-0
keypoints (unmapped OpenPose_full slots) are simply not drawn, and any limb
whose either endpoint is confidence-0 is skipped -- matching how OpenPose
itself omits undetected points/limbs.
"""

import maya.api.OpenMaya as om

from . import constants

BYTES_PER_PIXEL = 4  # RGBA


def _new_buffer(width, height):
    buf = bytearray(width * height * BYTES_PER_PIXEL)
    for i in range(0, len(buf), BYTES_PER_PIXEL):
        buf[i + 3] = 255  # opaque black background
    return buf


def _set_pixel(buffer, width, height, x, y, color):
    if x < 0 or x >= width or y < 0 or y >= height:
        return
    offset = (y * width + x) * BYTES_PER_PIXEL
    buffer[offset] = color[0]
    buffer[offset + 1] = color[1]
    buffer[offset + 2] = color[2]
    buffer[offset + 3] = 255


def _draw_circle(buffer, width, height, cx, cy, radius, color):
    if radius <= 0:
        return
    x_min = max(0, int(cx - radius))
    x_max = min(width - 1, int(cx + radius))
    y_min = max(0, int(cy - radius))
    y_max = min(height - 1, int(cy + radius))
    radius_sq = radius * radius
    for y in range(y_min, y_max + 1):
        dy = y + 0.5 - cy
        for x in range(x_min, x_max + 1):
            dx = x + 0.5 - cx
            if dx * dx + dy * dy <= radius_sq:
                _set_pixel(buffer, width, height, x, y, color)


def _draw_line(buffer, width, height, x0, y0, x1, y1, thickness, color):
    if thickness <= 0:
        return
    half = max(thickness * 0.5, 0.5)
    x_min = max(0, int(min(x0, x1) - half))
    x_max = min(width - 1, int(max(x0, x1) + half))
    y_min = max(0, int(min(y0, y1) - half))
    y_max = min(height - 1, int(max(y0, y1) + half))

    dx = x1 - x0
    dy = y1 - y0
    length_sq = dx * dx + dy * dy
    half_sq = half * half

    if length_sq < 1e-9:
        _draw_circle(buffer, width, height, x0, y0, half, color)
        return

    for y in range(y_min, y_max + 1):
        py = y + 0.5
        for x in range(x_min, x_max + 1):
            px = x + 0.5
            t = ((px - x0) * dx + (py - y0) * dy) / length_sq
            t = max(0.0, min(1.0, t))
            proj_x = x0 + t * dx
            proj_y = y0 + t * dy
            dist_sq = (px - proj_x) ** 2 + (py - proj_y) ** 2
            if dist_sq <= half_sq:
                _set_pixel(buffer, width, height, x, y, color)


def draw_character(buffer, width, height, keypoints, radius_scale=1.0, thickness_scale=1.0,
                    body_model=constants.BODY_MODEL_BODY_25):
    """Draw one character's OpenPose skeleton into an existing RGBA buffer.

    ``keypoints``: list of length constants.TOTAL_KEYPOINTS of
    (pixel_x, pixel_y, confidence), as returned by
    ``projector.project_character``. ``body_model`` selects BODY_25 (all 25
    body/foot points) or COCO (18 points, no MidHip/feet, different limb
    connectivity) -- hands and face are unaffected either way.
    """
    pairs, colors, thickness_list = constants.full_limb_data(body_model)
    for pair_i, (a, b) in enumerate(pairs):
        xa, ya, ca = keypoints[a]
        xb, yb, cb = keypoints[b]
        if ca <= 0.0 or cb <= 0.0:
            continue
        thickness = thickness_list[pair_i] * thickness_scale
        _draw_line(buffer, width, height, xa, ya, xb, yb, thickness, colors[pair_i])

    active_indices = constants.active_point_indices(body_model)
    for index in active_indices:
        x, y, confidence = keypoints[index]
        if confidence <= 0.0:
            continue
        radius = constants.KEYPOINT_POINT_RADIUS[index] * radius_scale
        _draw_circle(buffer, width, height, x, y, radius, constants.KEYPOINT_POINT_COLORS[index])


def render_characters_to_buffer(characters, width, height):
    """``characters``: iterable of (keypoints, radius_scale, thickness_scale, body_model).

    Returns a flat RGBA bytearray of size width*height*4.
    """
    buffer = _new_buffer(width, height)
    for keypoints, radius_scale, thickness_scale, body_model in characters:
        draw_character(buffer, width, height, keypoints, radius_scale, thickness_scale, body_model)
    return buffer


def _flip_rows(buffer, width, height):
    """MImage.setPixels() treats row 0 as the bottom of the image (Maya's
    classic bottom-up convention); everything else here (and every PNG
    reader downstream) treats row 0 as the top. Flip row order once, right
    before handing pixels to MImage, so the written file is a normal
    top-down image.
    """
    row_bytes = width * BYTES_PER_PIXEL
    flipped = bytearray(len(buffer))
    for y in range(height):
        src = y * row_bytes
        dst = (height - 1 - y) * row_bytes
        flipped[dst:dst + row_bytes] = buffer[src:src + row_bytes]
    return flipped


def render_characters_to_file(characters, width, height, file_path, file_format="png"):
    """Render all characters and write the result to ``file_path``."""
    buffer = render_characters_to_buffer(characters, width, height)
    image = om.MImage()
    image.setPixels(_flip_rows(buffer, width, height), width, height)
    image.writeToFile(file_path, outputFormat=file_format)
