from pygame_ui_toolkit import pygame
from pygame_ui_toolkit import utils


class Button:
    """
    The base class that all other button classes inherit from.

    This class should not be used directly. 
    Instead use RectButton, CircleButton, PolygonButton, BorderedRectButton, BorderedCircleButton, or BorderedPolygonButton.

    Attributes
    ----------
    surface : pygame.Surface
        the surface that the button is drawn onto
    x : int
        the x position of the center of the button
    y : int
        the y position of the center of the button
    on_click : callable | None, optional
        the function that is called when the button is clicked. If the function accepts 1 argument, self is passed into it (defaults to None)
    on_hover : callable | None, optional
        the function that is called when the button is hovered over. If the function accepts 1 argument, self is passed into it (defaults to None)
    on_normal : callable | None, optional
        the function that is called when the button is neither clicked or hovered over. If the function accepts 1 argument, self is passed into it (defaults to None)
    click_once : bool, optional
        if True, the on_click and on_hover functions will be called once per click or hover event, otherwise they will be called every ferame the button is clicked or hovered (defaults to True)
    
    Methods
    -------
    update_clicked(currently_clicked: bool)
        update the clicked attribute.
    update_hovered(currently_hovered: bool)
        update the hovered attribute.
    call_func(func: callable)
        call utils.call_func(func, self).
    check_click()
        return whether the button is ciurrently being clicked.
    check_hover()
        return whether the button is being hovered over.
    update()
        draw the button and call the on_click, on_hover and on_normal functions where appropriate.
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the necessary attributes for the Button object"""
        self.surface = surface

        self.x = x
        self.y = y

        self.on_click = on_click
        self.on_hover = on_hover
        self.on_normal = on_normal

        self.click_once = click_once

        self.clicked = False
        self.hovered = False

    def update_clicked(self, currently_clicked: bool) -> bool:
        """
        Update the clicked attribute.
        
        Parameters
        ----------

        currently_clicked : bool
            whether the mouse is currently over the button and the mouse button is pressed

        Returns
        -------
        bool
            whether on_click should be called
        """
        if not self.click_once:
            self.clicked = currently_clicked
            return currently_clicked
        
        if not currently_clicked:
            self.clicked = False
            return False
        
        if self.clicked:
            return False
        else:
            self.clicked = True
            return True
        
    def update_hovered(self, currently_hovered: bool) -> bool:
        """
        Update the hovered attribute.
        
        Parameters
        ----------

        currently_hovered : bool
            whether the mouse is currently over the button

        Returns
        -------
        bool
            whether on_hover should be called
        """
        if not self.click_once:
            self.hovered = currently_hovered
            return currently_hovered
        
        if not currently_hovered:
            self.hovered = False
            return False
        
        if self.hovered:
            return False
        else:
            self.hovered = True
            return True

    get_pos = lambda self: (self.x, self.y)

    def call_func(self, func: callable) -> None:
        """Call utils.call_func(func, self)."""
        if func == None:
            return
        else:
            utils.call_func(func, self)
        
    def check_click(self) -> bool:
        """Return whether the button is ciurrently being clicked."""
        mouse_over = self.mouse_over()
        mouse_down = pygame.mouse.get_pressed(3)[0]

        click = mouse_over and mouse_down

        return self.update_clicked(click)
    
    def check_hover(self) -> bool:
        """Return whether the button is currently being hovered over."""
        mouse_over = self.mouse_over()

        return self.update_hovered(mouse_over)
    
    def update(self) -> None:
        """
        Draw the button and call the on_click, on_hover and on_normal functions where appropriate.

        This should be called once per frame.

        An exception is raised if the button object does not contain the nessesary methods.
        """
        
        if not hasattr(self, "check_click") or not hasattr(self, "check_hover"):
            raise Exception("Base Button class should not be used by itself. Use another class like RectButton instead")

        if self.check_click():
            self.call_func(self.on_click)
        elif self.check_hover():
            self.call_func(self.on_hover)
        else:
            self.call_func(self.on_normal)

        self.draw()


class RectButton(Button):
    """
    A rectangular button.

    This inherits from the Button class.

    Attributes
    ----------
    This class inherits from Button, so contains all the attributes that Button does.
    It also contains these additional attributes:
    background_colour : tuple[int]
        the colour of the button
    width : int
        the width (parallel to the x axis) of the button
    corner_radius : int, optional
        the radius of the circles on the rounded corners of the rectangle where -1 means sharp edges and not rounded corners (defaults to -1)
    
    Methods
    -------
    This class inherits from Button, so contains all the metehods that Button does.
    It also contains these additional methods:
    mouse_over()
        return whether the mouse is colliding with, or within the region of, the button rectangle
    draw()
        draw a rectangle with the appropriate width, height, position and colour
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, background_colour: tuple[int], width: int, height: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, corner_radius: int = -1, click_once: bool = True) -> None:
        """Construct the necessary attributes for the RectButton object"""
        super().__init__(surface, x, y, on_click, on_hover, on_normal, click_once)

        self.width = width
        self.height = height

        self.background_colour = background_colour

        self.corner_radius = corner_radius

    def mouse_over(self) -> bool:
        """Return whether the mouse is colliding with, or within the region of, the button rectangle"""
        x, y = pygame.mouse.get_pos()

        min_x = self.x - self.width // 2
        max_x = self.x + self.width // 2
        min_y = self.y - self.height // 2
        max_y = self.y + self.height // 2

        return min_x <= x <= max_x and min_y <= y <= max_y
    
    def draw(self) -> None:
        """Draw a rectangle with the appropriate width, height, position and colour"""
        rect = pygame.Rect(self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)
        pygame.draw.rect(self.surface, self.background_colour, rect, border_radius=self.corner_radius)

    get_width = lambda self: self.width


class CircleButton(Button):
    """
    A circular button.

    This inherits from the Button class.

    Attributes
    ----------
    This class inherits from Button, so contains all the attributes that Button does.
    It also contains these additional attributes:
    background_colour : tuple[int]
        the colour of the button
    radius : int
        the radius of the circle
        
    Methods
    -------
    This class inherits from Button, so contains all the metehods that Button does.
    It also contains these additional methods:
    mouse_over()
        return whether the mouse position is within the circle
    draw()
        draw a circle with the appropriate radius, position and colour
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, background_colour: tuple[int], radius: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the nessesary attributes for the CircleButton object"""
        super().__init__(surface, x, y, on_click, on_hover, on_normal, click_once)

        self.background_colour = background_colour

        self.radius = radius
        self.radius_sq = radius**2

    def mouse_over(self) -> bool:
        """Return whether the mouse position is within the circle"""
        x, y = pygame.mouse.get_pos()

        dist_sq = (x - self.x)**2 + (y - self.y)**2

        return dist_sq < self.radius_sq
    
    def draw(self) -> None:
        """Draw a circle with the appropriate radius, position and colour"""
        center = self.get_pos()
        pygame.draw.circle(self.surface, self.background_colour, center, self.radius)

    get_width = lambda self: self.radius * 2


class PolygonButton(Button):
    """
    A button in the shape of any convex polygon.
    Convex polygons are **not** supported and will result in unexpected behaviour.

    This inherits from the Button class.

    Attributes
    ----------
    This class inherits from Button, so contains all the attributes that Button does.
    It also contains these additional attributes:
    background_colour: tuple[int]
        the colour of the button
    points : list[tuple[int]]
        a list of x, y coordinates that correspond to each point on the polygon
        
    Methods
    -------
    This class inherits from Button, so contains all the metehods that Button does.
    It also contains these additional methods:
    get_center(points: list[tuple[int]])
        return the center of the polygon by averaging the position of each point
    get_inequalities(points: list[tuple[int]], center_x: int, center_y: int)
        return a list of inequalities that describe the button region
    mouse_over()
        return whether the mouse position is within the polygon using inequalities
    draw()
        draw a polygon with the appropriate points and colour
    """

    def __init__(self, surface: pygame.Surface, points: list[tuple[int]], background_colour: tuple[int], on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the necessary attributes for the PolygonButton object"""
        x, y = self.get_center(points)
        
        super().__init__(surface, x, y, on_click, on_hover, on_normal, click_once)

        self.points = points
        self.background_colour = background_colour

        self.inequalities = self.get_inequalities(points, x, y)

    def get_center(self, points: list[tuple[int]]) -> tuple[int]:
        """
        Return the center of the polygon by averaging the position of each point.

        Parameters
        ----------
        points : list[tuple[int]]
            a list of x, y coordinates that correspond to each point on the polygon

        Returns
        -------
        tuple[int]
            x, y coordinates to describe the center of the polygon
        """
        x_values = [i[0] for i in points]
        y_values = [i[1] for i in points]

        x = sum(x_values) // len(x_values)
        y = sum(y_values) // len(y_values)

        return x, y
    
    def get_inequalities(self, points: list[tuple[int]], center_x: int, center_y: int) -> list[tuple[float, float, bool]]:
        """
        Return a list of inequalities that describe the button region.

        Parameters
        ----------
        points : list[tuple[int]]
            a list of x, y coordinates that correspond to each point on the polygon
        center_x : int
            the x coordinate of the center of the polygon
        center_y : int
            the y coordinate of the center of the polygon

        Returns
        -------
        list[tuple[float, float, bool]]
            a list of linear inequalities in the form: line_gradient, y_intercept, greater_than (each line is interpreted as having the equation y=mx + c)
        """
        inequalities = []
        for i, x in enumerate(points):
            try:
                m = (x[1] - points[i - 1][1]) / (x[0] - points[i - 1][0])
            except ZeroDivisionError:
                m = 1e10

            c = x[1] - m * x[0]
            greater_than = (m * center_x + c) < center_y

            inequalities.append((m, c, greater_than))

        return inequalities

    def mouse_over(self) -> None:
        """Return whether the mouse position is within the polygon using inequalities."""
        x, y = pygame.mouse.get_pos()

        for m, c, greater_than in self.inequalities:
            intersect_y = m * x + c
            actual_greater_than = intersect_y < y

            if actual_greater_than != greater_than:
                return False
            
        return True
    
    def draw(self) -> None:
        """Draw a polygon with the appropriate points and colour."""
        pygame.draw.polygon(self.surface, self.background_colour, self.points)


class ImageButton(RectButton):
    """
    A button as an image.

    This inherits from the RectButton class.

    Attributes
    ----------
    This class inherits from RectButton, so contains all the attributes that RectButton does.
    It also contains these additional attributes:
    image_path : str
        the file path to the desired image
    image : pygame.Surface
        the image of the button in a form useable for pygame

    Methods
    -------
    This class inherits from RectButton, so contains all the methods that RectButton does.
    It also contains these additional methods:
    update_image()
        update the image attribute of the object

    The following methods are overwritten:
    draw()
        blit the image to the screen
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, width: int, height: int, image_path: str, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the necessary attributes for the ImageButton object"""
        super().__init__(surface, x, y, None, width, height, on_click, on_hover, on_normal, -1, click_once)

        self.update_image(image_path)

    def update_image(self, new_image_path: str) -> None:
        """Update the image attribute of the object."""
        image = pygame.image.load(new_image_path)
        scaled_image = pygame.transform.scale(image, (self.width, self.height))

        self.image = scaled_image

    def draw(self) -> None:
        """Blit the image to the screen."""
        image_x = self.x - self.width // 2
        image_y = self.y - self.height // 2

        self.surface.blit(self.image, (image_x, image_y))


class BorderedRectButton(RectButton):
    """
    A rectangluar button with a border.

    This inherits from the RectButton class.

    Attributes
    ----------
    This class inherits from RectButton, so contains all the attributes that RectButton does.
    It also contains these additional attributes:
    border_colour : tuple[int]
        the colour of the border
    border_width : int
        the width, or thickness, of the border

    Methods
    -------
    This class inherits from RectButton, so contains all the methods that RectButton does.
    The following methods are overwritten:
    draw()
        draw a rectangle and border
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, background_colour: tuple[int], border_colour: tuple[int], width: int, height: int, border_width: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, corner_radius: int = -1, click_once: bool = True) -> None:
        """Construct the necessary attributes for the BorderedRectButton object."""
        super().__init__(surface, x, y, background_colour, width, height, on_click, on_hover, on_normal, corner_radius, click_once)

        self.border_colour = border_colour
        self.border_width = border_width

    def draw(self) -> None:
        """Draw a rectangle and border."""
        rect = pygame.Rect(self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)

        pygame.draw.rect(self.surface, self.background_colour, rect, border_radius=self.corner_radius)
        pygame.draw.rect(self.surface, self.border_colour, rect, self.border_width, self.corner_radius)


class BorderedCircleButton(CircleButton):
    """
    A circular button with a border.

    This inherits from the CircleButton class.

    Attributes
    ----------
    This class inherits from CircleButton, so contains all the attributes that CircleButton does.
    It also contains these additional attributes:
    border_colour : tuple[int]
        the colour of the border
    border_width : int
        the width, or thickness, of the border

    Methods
    -------
    This class inherits from CircleButton, so contains all the methods that CircleButton does.
    The following methods are overwritten:
    draw()
        draw a circle and border
    """

    def __init__(self, surface: pygame.Surface, x: int, y: int, background_colour: tuple[int], border_colour: tuple[int], radius: int, border_width: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the necessary attributes for the BorderedCircleButton object."""
        super().__init__(surface, x, y, background_colour, radius, on_click, on_hover, on_normal, click_once)

        self.border_colour = border_colour
        self.border_width = border_width

    def draw(self) -> None:
        """Draw a circle and border."""
        pygame.draw.circle(self.surface, self.background_colour, self.get_pos(), self.radius)
        pygame.draw.circle(self.surface, self.border_colour, self.get_pos(), self.radius, self.border_width)


class BorderedPolygonButton(PolygonButton):
    """
    A polygon button with a border.

    This inherits from the PolygonButton class.

    Attributes
    ----------
    This class inherits from PolygonButton, so contains all the attributes that PolygonButton does.
    It also contains these additional attributes:
    border_colour : tuple[int]
        the colour of the border
    border_width : int
        the width, or thickness, of the border

    Methods
    -------
    This class inherits from PolygonButton, so contains all the methods that PolygonButton does.
    The following methods are overwritten:
    draw()
        draw a polygon and border
    """
    def __init__(self, surface: pygame.Surface, points: list[tuple[int]], background_colour: tuple[int], border_colour: tuple[int], border_width: int, on_click: callable = None, on_hover: callable = None, on_normal: callable = None, click_once: bool = True) -> None:
        """Construct the necessary attributes for the BorderedPolygonButton object."""
        super().__init__(surface, points, background_colour, on_click, on_hover, on_normal, click_once)

        self.border_colour = border_colour
        self.border_width = border_width

    def draw(self) -> None:
        """Draw a polygon and border."""
        pygame.draw.polygon(self.surface, self.background_colour, self.points)
        pygame.draw.polygon(self.surface, self.border_colour, self.points, self.border_width)


class TextWrapper:
    """
    A wrapper object that allows text to be drawn on top of button objects.

    All buttons other than PolygonButton and BorderedPolygonButton are supported.

    Attributes
    ----------
    button_object : RectButton | CircleButton | BorderedCircleButton | BorderedRectButton
        the button object that text will be drawn upon
    text : str
        the text displayed
    font_colour : tuple[int]
        the colour of the displayed text
    font : pygame.font.Font
        the font object used to blit text
    font_name : str | None, optional
        the name of the font used (defaults to None)
    antialias : bool, optional
        whether the text is drawn with antialias (defaults to False)
    text_surface : pygame.Surface
        the surface used to render text
    text_rect : pygame.Rect
        the rect position that the text is drawn to

    Methods
    -------
    get_text()
        return a text surface and rect object to blit text
    blit_text()
        draw text to the screen
    update_text(new_text: str | None = None, new_font_colour: tuple[int] | None = None, new_font_size: int | None = None, new_font_name: str | None = None)
        change the text, font_colour, font attributes of the object
    update()
        update the TextWrapper object
    """

    def __init__(self, button_object: RectButton | CircleButton | BorderedCircleButton | BorderedRectButton, text: str, font_colour: tuple[int], font_size: int, font_name: str | None = None, antialias: bool = False) -> None:
        """Construct necessary attributes for TextWrapper object."""
        self.button_object = button_object
        
        self.text = text
        self.font_colour = font_colour
        self.font = pygame.font.Font(font_name, font_size)

        self.antialias = antialias

        self.text_surface, self.text_rect = self.get_text()

    def get_text(self) -> tuple[pygame.Surface, pygame.Rect]:
        """Return a text surface and rect object to blit text."""
        text_surf = self.font.render(self.text, self.antialias, self.font_colour, self.button_object.background_colour)
        text_rect = text_surf.get_rect()
        text_rect.center = self.button_object.get_pos()

        return text_surf, text_rect
    
    def blit_text(self) -> None:
        """Draw text to the screen."""
        self.button_object.surface.blit(self.text_surface, self.text_rect)

    def update_text(self, new_text: str, new_font_colour: tuple[int], new_font_size: int, new_font_name: str | None) -> None:
        """Change the text, font_colour, font attributes of the object."""
        utils.update_font_attrs(self, new_text, new_font_colour, new_font_name, new_font_size)

        self.text_surface, self.text_rect = self.get_text()

    def draw(self) -> None:
        """Draw button object and blit text on top."""
        self.button_object.update()
        self.blit_text()
        
    def update(self) -> None:
        """
        Update the text wrapper button.
        
        This should be called once per frame.
        """

        self.draw()