"""
This module is used for calculating hit boxes
"""
import pymunk

from PIL.Image import Image
from arcade import Point
from typing import Tuple
from pymunk import autogeometry

from arcade.arcade_types import PointList


def calculate_hit_box_points_simple(image: Image) -> PointList:
    """
    Given an RGBA image, this returns points that make up a hit box around it. Attempts
    to trim out transparent pixels.

    :param Image image:

    :Returns: List of points
    """
    if image.mode != "RGBA":
        raise ValueError("Image mode is not RGBA. image.convert('RGBA') is needed.")

    # Convert the image into one channel alpha since we don't care about RGB values
    image = image.getchannel("A")
    bbox = image.getbbox()
    # If there is no bounding box the image is empty
    if bbox is None:
        return []

    left_border, top_border, right_border, bottom_border = bbox
    right_border -= 1
    bottom_border -= 1

    def _check_corner_offset(start_x: int, start_y: int, x_direction: int, y_direction: int) -> int:
        bad = False
        offset = 0
        while not bad:
            y = start_y + (offset * y_direction)
            x = start_x
            for _ in range(offset + 1):
                alpha = image.getpixel((x, y))
                # print(f"({x}, {y}) = {my_pixel} | ", end="")
                if alpha != 0:
                    bad = True
                    break
                y -= y_direction
                x += x_direction
            # print(f" - {bad}")
            if not bad:
                offset += 1
        # print(f"offset: {offset}")
        return offset

    def _r(point: Tuple[float, float], height: int, width: int) -> Point:
        return point[0] - width / 2, (height - point[1]) - height / 2

    top_left_corner_offset = _check_corner_offset(left_border, top_border, 1, 1)
    top_right_corner_offset = _check_corner_offset(right_border, top_border, -1, 1)
    bottom_left_corner_offset = _check_corner_offset(left_border, bottom_border, 1, -1)
    bottom_right_corner_offset = _check_corner_offset(right_border, bottom_border, -1, -1)

    p1 = left_border + top_left_corner_offset, top_border
    p2 = (right_border + 1) - top_right_corner_offset, top_border
    p3 = (right_border + 1), top_border + top_right_corner_offset
    p4 = (right_border + 1), (bottom_border + 1) - bottom_right_corner_offset
    p5 = (right_border + 1) - bottom_right_corner_offset, (bottom_border + 1)
    p6 = left_border + bottom_left_corner_offset, (bottom_border + 1)
    p7 = left_border, (bottom_border + 1) - bottom_left_corner_offset
    p8 = left_border, top_border + top_left_corner_offset

    result = []

    h = image.height
    w = image.width

    result.append(_r(p7, h, w))
    if bottom_left_corner_offset:
        result.append(_r(p6, h, w))

    result.append(_r(p5, h, w))
    if bottom_right_corner_offset:
        result.append(_r(p4, h, w))

    result.append(_r(p3, h, w))
    if top_right_corner_offset:
        result.append(_r(p2, h, w))

    result.append(_r(p1, h, w))
    if top_left_corner_offset:
        result.append(_r(p8, h, w))

    # Remove duplicates
    return tuple(dict.fromkeys(result))  # type: ignore


def calculate_hit_box_points_detailed(
    image: Image,
    hit_box_detail: float = 4.5,
) -> PointList:
    """
    Given an RGBA image, this returns points that make up a hit box around it. Attempts
    to trim out transparent pixels.

    :param Image image: Image get hit box from.
    :param int hit_box_detail: How detailed to make the hit box. There's a
                               trade-off in number of points vs. accuracy.

    :Returns: List of points
    """
    if image.mode != "RGBA":
        raise ValueError("Image mode is not RGBA. image.convert('RGBA') is needed.")

    def sample_func(sample_point: Point) -> int:
        """ Method used to sample image. """
        if sample_point[0] < 0 \
                or sample_point[1] < 0 \
                or sample_point[0] >= image.width \
                or sample_point[1] >= image.height:
            return 0

        point_tuple = sample_point[0], sample_point[1]
        color = image.getpixel(point_tuple)
        return 255 if color[3] > 0 else 0

    # Do a quick check if it is a full tile
    p1 = 0, 0
    p2 = 0, image.height - 1
    p3 = image.width - 1, image.height - 1
    p4 = image.width - 1, 0

    if sample_func(p1) and sample_func(p2) and sample_func(p3) and sample_func(p4):
        # Do a quick check if it is a full tile
        p1 = (-image.width / 2, -image.height / 2)
        p2 = (image.width / 2, -image.height / 2)
        p3 = (image.width / 2, image.height / 2)
        p4 = (-image.width / 2, image.height / 2)

        return p1, p2, p3, p4

    # Get the bounding box
    logo_bb = pymunk.BB(-1, -1, image.width, image.height)

    # Set of lines that trace the image
    line_set = autogeometry.PolylineSet()

    # How often to sample?
    downres = 1
    horizontal_samples = int(image.width / downres)
    vertical_samples = int(image.height / downres)

    # Run the trace
    # Get back one or more sets of lines covering stuff.
    line_sets = autogeometry.march_soft(
        logo_bb,
        horizontal_samples, vertical_samples,
        99,
        sample_func)

    if len(line_sets) == 0:
        return []

    selected_line_set = line_sets[0]
    selected_range = None
    if len(line_set) > 1:
        # We have more than one line set. Try and find one that covers most of
        # the sprite.
        for line in line_set:
            min_x = None
            min_y = None
            max_x = None
            max_y = None
            for point in line:
                if min_x is None or point.x < min_x:
                    min_x = point.x
                if max_x is None or point.x > max_x:
                    max_x = point.x
                if min_y is None or point.y < min_y:
                    min_y = point.y
                if max_y is None or point.y > max_y:
                    max_y = point.y

            if min_x is None or max_x is None or min_y is None or max_y is None:
                raise ValueError("No points in bounding box.")

            my_range = max_x - min_x + max_y + min_y
            if selected_range is None or my_range > selected_range:
                selected_range = my_range
                selected_line_set = line

    # Reduce number of vertices
    # original_points = len(selected_line_set)
    selected_line_set = autogeometry.simplify_curves(selected_line_set,
                                                     hit_box_detail)
    # downsampled_points = len(selected_line_set)

    # Convert to normal points, offset fo 0,0 is center, flip the y
    hh = image.height / 2
    hw = image.width / 2
    points = []
    for vec2 in selected_line_set:
        point = round(vec2.x - hw), round(image.height - (vec2.y - hh) - image.height)  # type: ignore
        points.append(point)

    if len(points) > 1 and points[0] == points[-1]:
        points.pop()

    # print(f"{sprite.texture.name} Line-sets={len(line_set)}, Original points={original_points}, Downsampled points={downsampled_points}")  # noqa
    return points  # type: ignore
