"""
MIT License

Copyright (c) 2021-2022 shahriyardx
Copyright (c) 2022-present Guddi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from __future__ import annotations

from io import BytesIO
from typing import List, Tuple, Union

from PIL import Image, ImageDraw, ImageFilter, ImageFont
from typing_extensions import Literal

from .canvas import Canvas
from .font import Font
from .text import Text
from .utils import run_in_executor


class Editor:
    """Editor class. It does all the editing operations.

    Parameters
    ----------
    image : Union[Image.Image, str, Editor, Canvas]
        Image or Canvas to edit.
    """

    def __init__(self, image: Union[Image.Image, str, BytesIO, Editor, Canvas]) -> None:
        if isinstance(image, str) or isinstance(image, BytesIO):
            self.image: Image.Image = Image.open(image)
        elif isinstance(image, Canvas) or isinstance(image, Editor):
            self.image: Image.Image = image.image
        else:
            self.image: Image.Image = image
        self.image: Image.Image = self.image.convert("RGBA")


    @property
    def image_bytes(self) -> BytesIO:
        """Return image bytes

        Returns
        -------
        BytesIO
            Bytes from the image of Editor
        """
        _bytes = BytesIO()
        self.image.save(_bytes, "png")
        _bytes.seek(0)

        return _bytes


    async def resize(self, size: Tuple[float, float], crop: bool = False) -> Editor:
        return await run_in_executor(self.__resize, size, crop=crop)

    def __resize(self, size: Tuple[float, float], crop=False) -> Editor:
        """Resize image

        Parameters
        ----------
        size : Tuple[float, float]
            New Size of image
        crop : bool, optional
            Crop the image to bypass distortion, by default False
        """
        if not crop:
            self.image = self.image.resize(size, Image.ANTIALIAS)

        else:
            width, height = self.image.size
            ideal_width, ideal_height = size

            aspect = width / float(height)
            ideal_aspect = ideal_width / float(ideal_height)

            if aspect > ideal_aspect:
                new_width = int(ideal_aspect * height)
                offset = (width - new_width) / 2
                resize = (offset, 0, width - offset, height)
            else:
                new_height = int(width / ideal_aspect)
                offset = (height - new_height) / 2
                resize = (0, offset, width, height - offset)

            self.image = self.image.crop(resize).resize(
                (ideal_width, ideal_height), Image.ANTIALIAS
            )

        return self


    async def rounded_corners(self, radius: int = 10, offset: int = 2) -> Editor:
        return await run_in_executor(self.__rounded_corners, radius, offset)

    def __rounded_corners(self, radius: int = 10, offset: int = 2) -> Editor:
        """Make image rounded corners

        Parameters
        ----------
        radius : int, optional
            Radius of roundness, by default 10
        offset : int, optional
            Offset pixel while making rounded, by default 2
        """
        background = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        holder = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        mask = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        mask_draw = ImageDraw.Draw(mask)
        mask_draw.rounded_rectangle(
            (offset, offset) + (self.image.size[0] - offset, self.image.size[1] - offset),
            radius=radius,
            fill="black",
        )
        holder.paste(self.image, (0, 0))
        self.image = Image.composite(holder, background, mask)

        return self


    async def circle_image(self) -> Editor:
        return await run_in_executor(self.__circle_image)

    def __circle_image(self) -> Editor:
        """Make image circle"""
        background = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        holder = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        mask = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))
        mask_draw = ImageDraw.Draw(mask)
        mask_draw.ellipse((0, 0) + self.image.size, fill="black")
        holder.paste(self.image, (0, 0))
        self.image = Image.composite(holder, background, mask)

        return self


    async def rotate(self, deg: float = 0, expand: bool = False) -> Editor:
        return await run_in_executor(self.__rotate, deg, expand)

    def __rotate(self, deg: float = 0, expand: bool = False) -> Editor:
        """Rotate image

        Parameters
        ----------
        deg : float, optional
            Degrees to rotate, by default 0
        expand : bool, optional
            Expand while rotating, by default False
        """
        self.image = self.image.rotate(angle=deg, expand=expand)
        return self


    async def blur(self, mode: Literal["box", "gaussian"] = "gaussian", amount: float = 1) -> Editor:
        return await run_in_executor(self.__blur, mode, amount)

    def __blur(self, mode: Literal["box", "gaussian"] = "gaussian", amount: float = 1) -> Editor:
        """Blur image

        Parameters
        ----------
        mode : Literal["box", "gaussian"], optional
            Blur mode, by default "gaussian"
        amount : float, optional
            Amount of blur, by default 1
        """
        if mode == "box":
            self.image = self.image.filter(ImageFilter.BoxBlur(radius=amount))
        if mode == "gaussian":
            self.image = self.image.filter(ImageFilter.GaussianBlur(radius=amount))

        return self


    async def blend(
            self, image: Union[Image.Image, Editor, Canvas], alpha: float = 0.0, on_top: bool = False
    ) -> Editor:
        return await run_in_executor(self.__blend, image, alpha, on_top)

    def __blend(
            self, image: Union[Image.Image, Editor, Canvas], alpha: float = 0.0, on_top: bool = False
    ) -> Editor:
        """Blend image into editor image

        Parameters
        ----------
        image : Union[Image.Image, Editor, Canvas]
            Image to blend
        alpha : float, optional
            Alpha amount, by default 0.0
        on_top : bool, optional
            Places image on top, by default False
        """
        if isinstance(image, Editor) or isinstance(image, Canvas):
            image = image.image

        if image.size != self.image.size:
            image = Editor(image).resize(self.image.size, crop=True).image

        if on_top:
            self.image = Image.blend(self.image, image, alpha=alpha)
        else:
            self.image = Image.blend(image, self.image, alpha=alpha)

        return self


    async def paste(
        self, image: Union[Image.Image, Editor, Canvas], position: Tuple[float, float]
    ) -> Editor:
        return await run_in_executor(self.__paste, image, position)

    def __paste(
        self, image: Union[Image.Image, Editor, Canvas], position: Tuple[float, float]
    ) -> Editor:
        """Paste image into editor

        Parameters
        ----------
        image : Union[Image.Image, Editor, Canvas]
            Image to paste
        position : Tuple[float, float]
            Position to paste
        """
        blank = Image.new("RGBA", size=self.image.size, color=(255, 255, 255, 0))

        if isinstance(image, Editor) or isinstance(image, Canvas):
            image = image.image

        blank.paste(image, position)
        self.image = Image.alpha_composite(self.image, blank)

        return self


    async def text(
        self,
        position: Tuple[float, float],
        text: str,
        font: Union[ImageFont.FreeTypeFont, Font] = None,
        color: Union[Tuple[int, int, int], str, int] = "black",
        align: Literal["left", "center", "right"] = "left",
    ) -> Editor:
        return await run_in_executor(self.__text, position, text, font, color, align)

    def __text(
        self,
        position: Tuple[float, float],
        text: str,
        font: Union[ImageFont.FreeTypeFont, Font] = None,
        color: Union[Tuple[int, int, int], str, int] = "black",
        align: Literal["left", "center", "right"] = "left",
    ) -> Editor:
        """Draw text into image

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw text
        text : str
            Text to draw
        font : Union[ImageFont.FreeTypeFont, Font], optional
            Font used for text, by default None
        color : Union[Tuple[int, int, int], str, int], optional
            Color of the font, by default "black"
        align : Literal["left", "center", "right"], optional
            Align text, by default "left"
        """
        if isinstance(font, Font):
            font = font.font

        anchors = {"left": "lt", "center": "mt", "right": "rt"}

        draw = ImageDraw.Draw(self.image)
        draw.text(position, text, color, font=font, anchor=anchors[align])

        return self


    async def multicolor_text(
        self,
        position: Tuple[float, float],
        texts: List[Text],
        space_separated: bool = True,
        align: Literal["left", "center", "right"] = "left",
    ) -> Editor:
        return await run_in_executor(self.__multicolor_text, position, texts, space_separated, align)

    def __multicolor_text(
        self,
        position: Tuple[float, float],
        texts: List[Text],
        space_separated: bool = True,
        align: Literal["left", "center", "right"] = "left",
    ) -> Editor:
        """Draw multicolor text

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw text
        texts : List[Text]
            List of texts
        space_separated : bool, optional
            Separate texts with space, by default True
        align : Literal["left", "center", "right"], optional
            Align texts, by default "left"
        """
        draw = ImageDraw.Draw(self.image)

        if align == "left":
            position = position

        if align == "right":
            total_width = 0

            for text in texts:
                total_width += text.font.getsize(text.text)[0]

            position = (position[0] - total_width, position[1])

        if align == "center":
            total_width = 0

            for text in texts:
                total_width += text.font.getsize(text.text)[0]

            position = (position[0] - (total_width / 2), position[1])

        for text in texts:
            sentence = text.text
            font = text.font
            color = text.color

            if space_separated:
                width, _ = (
                    font.getsize(sentence)[0] + font.getsize(" ")[0],
                    font.getsize(sentence)[1],
                )
            else:
                width, _ = font.getsize(sentence)

            draw.text(position, sentence, color, font=font)
            position = (position[0] + width, position[1])

        return self


    async def rectangle(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
        radius: int = 0,
    ) -> Editor:
        return await run_in_executor(self.__rectangle, position, width, height, fill, color, outline, stroke_width, radius)

    def __rectangle(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
        radius: int = 0,
    ) -> Editor:
        """Draw rectangle into image

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw rectangle
        width : float
            Width of rectangle
        height : float
            Height of rectangle
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of fill, by default None
        outline : Union[str, int, Tuple[int, int, int]], optional
            Outline color, by default None
        stroke_width : float, optional
            Stroke width, by default 1
        radius : int, optional
            Radius of rectangle, by default 0
        """
        draw = ImageDraw.Draw(self.image)

        to_width = width + position[0]
        to_height = height + position[1]

        if color:
            fill = color

        if radius <= 0:
            draw.rectangle(
                position + (to_width, to_height),
                fill=fill,
                outline=outline,
                width=stroke_width,
            )
        else:
            draw.rounded_rectangle(
                position + (to_width, to_height),
                radius=radius,
                fill=fill,
                outline=outline,
                width=stroke_width,
            )

        return self


    async def bar(
        self,
        position: Tuple[float, float],
        max_width: Union[int, float],
        height: Union[int, float],
        percentage: int = 1,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
        radius: int = 0,
    ) -> Editor:
        return await run_in_executor(self.__bar, position, max_width, height, percentage, fill, color, outline, stroke_width, radius)

    def __bar(
        self,
        position: Tuple[float, float],
        max_width: Union[int, float],
        height: Union[int, float],
        percentage: int = 1,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
        radius: int = 0,
    ) -> Editor:
        """Draw a progress bar

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw bar
        max_width : Union[int, float]
            Max width of the bar
        height : Union[int, float]
            Height of the bar
        percentage : int, optional
            Percentage to fill of the bar, by default 1
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of fill, by default None
        outline : Union[str, int, Tuple[int, int, int]], optional
            Outline color, by default None
        stroke_width : float, optional
            Stroke width, by default 1
        radius : int, optional
            Radius of the bar, by default 0
        """
        draw = ImageDraw.Draw(self.image)

        if color:
            fill = color

        ratio = max_width / 100
        to_width = ratio * percentage + position[0]

        height = height + position[1]

        if radius <= 0:
            draw.rectangle(
                position + (to_width, height),
                fill=fill,
                outline=outline,
                width=stroke_width,
            )
        else:
            draw.rounded_rectangle(
                position + (to_width, height),
                radius=radius,
                fill=fill,
                outline=outline,
                width=stroke_width,
            )

        return self


    async def rounded_bar(
        self,
        position: Tuple[float, float],
        width: Union[int, float],
        height: Union[int, float],
        percentage: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        return await run_in_executor(self.__rounded_bar, position, width, height, percentage, fill, color, stroke_width)

    def __rounded_bar(
        self,
        position: Tuple[float, float],
        width: Union[int, float],
        height: Union[int, float],
        percentage: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        """Draw a rounded bar

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw rounded bar
        width : Union[int, float]
            Width of the bar
        height : Union[int, float]
            Height of the bar
        percentage : float
            Percentage to fill
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of color, by default None
        stroke_width : float, optional
            Stroke width, by default 1
        """
        draw = ImageDraw.Draw(self.image)

        if color:
            fill = color

        start = -90
        end = (percentage * 3.6) - 90

        draw.arc(
            position + (position[0] + width, position[1] + height),
            start,
            end,
            fill,
            width=stroke_width,
        )

        return self


    async def ellipse(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        return await run_in_executor(self.__ellipse, position, width, height, fill, color, outline, stroke_width)

    def __ellipse(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        """Draw an ellipse

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw ellipse
        width : float
            Width of ellipse
        height : float
            Height of ellipse
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of fill, by default None
        outline : Union[str, int, Tuple[int, int, int]], optional
            Outline color, by default None
        stroke_width : float, optional
            Stroke width, by default 1
        """
        draw = ImageDraw.Draw(self.image)
        to_width = width + position[0]
        to_height = height + position[1]

        if color:
            fill = color

        draw.ellipse(
            position + (to_width, to_height),
            outline=outline,
            fill=fill,
            width=stroke_width,
        )

        return self


    async def polygon(
        self,
        coordinates: list,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
    ) -> Editor:
        return await run_in_executor(self.__polygon, coordinates, fill, color, outline)

    def __polygon(
        self,
        coordinates: list,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        outline: Union[str, int, Tuple[int, int, int]] = None,
    ) -> Editor:
        """Draw a polygon

        Parameters
        ----------
        coordinates : list
            Coordinates to draw
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of fill, by default None
        outline : Union[str, int, Tuple[int, int, int]], optional
            Outline color, by default None
        """
        if color:
            fill = color

        draw = ImageDraw.Draw(self.image)
        draw.polygon(coordinates, fill=fill, outline=outline)

        return self


    async def arc(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        start: float,
        rotation: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        return await run_in_executor(self.__arc, position, width, height, start, rotation, fill, color, stroke_width)

    def __arc(
        self,
        position: Tuple[float, float],
        width: float,
        height: float,
        start: float,
        rotation: float,
        fill: Union[str, int, Tuple[int, int, int]] = None,
        color: Union[str, int, Tuple[int, int, int]] = None,
        stroke_width: float = 1,
    ) -> Editor:
        """Draw arc

        Parameters
        ----------
        position : Tuple[float, float]
            Position to draw arc
        width : float
            Width or arc
        height : float
            Height of arch
        start : float
            Start position of arch
        rotation : float
            Rotation in degree
        fill : Union[str, int, Tuple[int, int, int]], optional
            Fill color, by default None
        color : Union[str, int, Tuple[int, int, int]], optional
            Alias of fill, by default None
        stroke_width : float, optional
            Stroke width, by default 1
        """
        draw = ImageDraw.Draw(self.image)

        start = start - 90
        end = rotation - 90

        if color:
            fill = color

        draw.arc(
            position + (position[0] + width, position[1] + height),
            start,
            end,
            fill,
            width=stroke_width,
        )

        return self


    async def show(self):
        return await run_in_executor(self.__show)

    def __show(self):
        """Show the image."""
        self.image.show()


    async def save(self, fp, format: str = None, **params):
        return await run_in_executor(self.__save, fp, format, **params)

    def __save(self, fp, format: str = None, **params):
        """Save the image

        Parameters
        ----------
        fp : str
            File path
        format : str, optional
            File format, by default None
        """
        self.image.save(fp, format, **params)
