from math import floor, ceil

from neo.core.baseneo import BaseNeo
from neo.core.imagesequence import ImageSequence


class RegionOfInterest(BaseNeo):
    """Abstract base class"""

    _parent_objects = ("Group",)
    _parent_attrs = ("group",)
    _necessary_attrs = (("image_sequence", ("ImageSequence",), 1),)
    is_view = True

    def __init__(self, image_sequence, name=None, description=None, file_origin=None, **annotations):
        super().__init__(name=name, description=description, file_origin=file_origin, **annotations)

        if not (
            isinstance(image_sequence, ImageSequence)
            or (hasattr(image_sequence, "proxy_for") and issubclass(image_sequence.proxy_for, ImageSequence))
        ):
            raise ValueError("Can only take a RegionOfInterest of an ImageSequence")
        self.image_sequence = image_sequence

    def _get_obj(self):
        # for consistency with ChannelView
        return self.image_sequence

    def _set_obj(self, value):
        if not isinstance(value, ImageSequence):
            raise TypeError(f"Value must be ImageSequence, not of type: {type(value)}")
        self.image_sequence = value

    obj = property(fget=_get_obj, fset=_set_obj)

    def resolve(self):
        """
        Return a signal from within this region of the underlying ImageSequence.
        """
        return self.image_sequence.signal_from_region(self)


class CircularRegionOfInterest(RegionOfInterest):
    """Representation of a circular ROI

    *Usage:*

    >>> roi = CircularRegionOfInterest(20.0, 20.0, radius=5.0)
    >>> signal = image_sequence.signal_from_region(roi)

    *Required attributes/properties*:
        :x, y: (integers or floats)
            Pixel coordinates of the centre of the ROI
        :radius: (integer or float)
            Radius of the ROI in pixels
    """

    _necessary_attrs = (("image_sequence", ("ImageSequence",), 1), ("x", int), ("y", int), ("radius", int))

    def __init__(self, image_sequence, x, y, radius, name=None, description=None, file_origin=None, **annotations):
        super().__init__(image_sequence, name, description, file_origin, **annotations)
        self.y = y
        self.x = x
        self.radius = radius

    @property
    def centre(self):
        return (self.x, self.y)

    @property
    def center(self):
        return self.centre

    def is_inside(self, x, y):
        if (x - self.x) * (x - self.x) + (y - self.y) * (y - self.y) <= self.radius * self.radius:
            return True
        else:
            return False

    def pixels_in_region(self):
        """Returns a list of pixels whose *centres* are within the circle"""
        pixel_in_list = []
        for y in range(int(floor(self.y - self.radius)), int(ceil(self.y + self.radius))):
            for x in range(int(floor(self.x - self.radius)), int(ceil(self.x + self.radius))):
                if self.is_inside(x, y):
                    pixel_in_list.append([x, y])

        return pixel_in_list


class RectangularRegionOfInterest(RegionOfInterest):
    """Representation of a rectangular ROI

    *Usage:*

    >>> roi = RectangularRegionOfInterest(20.0, 20.0, width=5.0, height=5.0)
    >>> signal = image_sequence.signal_from_region(roi)

    *Required attributes/properties*:
        :x, y: (integers or floats)
            Pixel coordinates of the centre of the ROI
        :width: (integer or float)
            Width (x-direction) of the ROI in pixels
        :height: (integer or float)
            Height (y-direction) of the ROI in pixels
    """

    _necessary_attrs = (
        ("image_sequence", ("ImageSequence",), 1),
        ("x", int),
        ("y", int),
        ("width", int),
        ("height", int),
    )

    def __init__(
        self, image_sequence, x, y, width, height, name=None, description=None, file_origin=None, **annotations
    ):
        super().__init__(image_sequence, name, description, file_origin, **annotations)
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def is_inside(self, x, y):
        if (
            self.x - self.width / 2.0 <= x < self.x + self.width / 2.0
            and self.y - self.height / 2.0 <= y < self.y + self.height / 2.0
        ):
            return True
        else:
            return False

    def pixels_in_region(self):
        """Returns a list of pixels whose *centres* are within the rectangle"""
        pixel_list = []
        h = self.height
        w = self.width
        for y in range(int(floor(self.y - h / 2.0)), int(ceil(self.y + h / 2.0))):
            for x in range(int(floor(self.x - w / 2.0)), int(ceil(self.x + w / 2.0))):
                if self.is_inside(x, y):
                    pixel_list.append([x, y])
        return pixel_list


class PolygonRegionOfInterest(RegionOfInterest):
    """Representation of a polygonal ROI

    *Usage:*

    >>> roi = PolygonRegionOfInterest(
    ...     (20.0, 20.0),
    ...     (30.0, 20.0),
    ...     (25.0, 25.0)
    ... )
    >>> signal = image_sequence.signal_from_region(roi)

    *Required attributes/properties*:
        :vertices:
            tuples containing the (x, y) coordinates, as integers or floats,
            of the vertices of the polygon
    """

    _necessary_attrs = (
        ("image_sequence", ("ImageSequence",), 1),
        ("vertices", list),
    )

    def __init__(self, image_sequence, *vertices, name=None, description=None, file_origin=None, **annotations):
        super().__init__(image_sequence, name, description, file_origin, **annotations)
        self.vertices = vertices

    def polygon_ray_casting(self, bounding_points, bounding_box_positions):

        # from https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
        # user  Noresourses

        # Arrays containing the x- and y-coordinates of the polygon's vertices.
        vertx = [point[0] for point in bounding_points]
        verty = [point[1] for point in bounding_points]
        # Number of vertices in the polygon
        nvert = len(bounding_points)
        # Points that are inside
        points_inside = []

        # For every candidate position within the bounding box
        for idx, pos in enumerate(bounding_box_positions):
            testx, testy = (pos[0], pos[1])
            c = 0
            for i in range(0, nvert):
                j = i - 1 if i != 0 else nvert - 1
                if ((verty[i] * 1.0 > testy * 1.0) != (verty[j] * 1.0 > testy * 1.0)) and (
                    testx * 1.0
                    < (vertx[j] * 1.0 - vertx[i] * 1.0)
                    * (testy * 1.0 - verty[i] * 1.0)
                    / (verty[j] * 1.0 - verty[i] * 1.0)
                    + vertx[i] * 1.0
                ):
                    c += 1
            # If odd, that means that we are inside the polygon
            if c % 2 == 1:
                points_inside.append(pos)

        return points_inside

    def pixels_in_region(self):

        min_x, max_x, min_y, max_y = (
            self.vertices[0][0],
            self.vertices[0][0],
            self.vertices[0][1],
            self.vertices[0][1],
        )

        for i in self.vertices:
            if i[0] < min_x:
                min_x = i[0]
            if i[0] > max_x:
                max_x = i[0]
            if i[1] < min_y:
                min_y = i[1]
            if i[1] > max_y:
                max_y = i[1]

        list_coord = []
        for y in range(int(floor(min_y)), int(ceil(max_y))):
            for x in range(int(floor(min_x)), int(ceil(max_x))):
                list_coord.append((x, y))

        pixel_list = self.polygon_ray_casting(self.vertices, list_coord)

        return pixel_list
