import math
import os
import warnings

from PIL import Image as PIL_Image, ImageDraw, ImageEnhance

from nider.colors import color_to_rgb, get_img_dominant_color, generate_opposite_color, blend
from nider.core import Font, Text, MultilineTextUnit, SingleLineTextUnit
from nider.exceptions import (ImageGeneratorException, ImageSizeFixedWarning,
                              AutoGeneratedUnitColorUsedWarning, AutoGeneratedUnitOutlinecolorUsedWarning)
from nider.utils import get_random_bgcolor, get_random_texture, is_path_creatable


class Header(MultilineTextUnit):
    __doc__ = '''Class that represents a header used in images''' + \
        '\n\n' + MultilineTextUnit.__doc__


class Paragraph(MultilineTextUnit):
    __doc__ = '''Class that represents a paragraph used in images''' + \
        '\n\n' + MultilineTextUnit.__doc__


class Linkback(SingleLineTextUnit):
    '''Class that represents a linkback used in images

    Args:
        text (str): text used for the unit.
        font (nider.core.Font): font object that represents text's font.
        color (str): string that represents a color. Must be compatible with `PIL.ImageColor <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html>`_ `color names <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html#color-names>`_
        outline (nider.core.Outline): outline object that represents text's outline.
        align ('left' or 'center' or 'right'): side with respect to which the text will be aligned.
        bottom_padding (int): linkback’s bottom padding - padding (in pixels) between the bottom of the image and the linkback itself.

    Raises:
        nider.exceptions.InvalidAlignException: if ``align`` is not supported by nider.
        nider.exceptions.DefaultFontWarning: if ``font.path`` is ``None``.
        nider.exceptions.FontNotFoundWarning: if ``font.path`` does not exist.
    '''

    def __init__(self, text, font=None,
                 color=None, outline=None,
                 align='right', bottom_padding=20):
        self.bottom_padding = bottom_padding
        super().__init__(text=text, font=font, color=color, outline=outline,
                         align=align)

    def _set_height(self):
        '''Sets linkback\'s height'''
        super()._set_height()
        self.height += self.bottom_padding


class Watermark(Text):
    '''Class that represents a watermark used in images

    Args:
        text (str): text to use.
        font (nider.core.Font): font object that represents text's font.
        color (str): string that represents a color. Must be compatible with `PIL.ImageColor <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html>`_ `color names <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html#color-names>`_
        outline (nider.core.Outline): outline object that represents text's outline.
        cross (bool): boolean flag that indicates whether watermark has to be drawn with a cross.
        rotate_angle (int): angle to which watermark's text has to be rotated.
        opacity (0 <= float <= 1): opacity level of the watermark (applies to both the text and the cross).

    Raises:
        nider.exceptions.ImageGeneratorException: if ``font`` is the default font.
        nider.exceptions.DefaultFontWarning: if ``font.path`` is ``None``.
        nider.exceptions.FontNotFoundWarning: if ``font.path`` does not exist.

    Note:
        Watermark tries to takes all available width of the image so providing ``font.size`` has no affect on the watermark.
    '''

    def __init__(self, text, font,
                 color=None, outline=None,
                 cross=True, rotate_angle=None,
                 opacity=0.25):
        if font.is_default:
            raise ImageGeneratorException(
                'Watermark cannot be drawn using a default font. Please provide an existing font')
        super().__init__(text=text, font=font, color=color, outline=outline)
        self.cross = cross
        self.rotate_angle = rotate_angle
        self.opacity = opacity

    def _adjust_fontsize(self, bg_image):
        text_width, text_height = self.font.getsize(self.text)
        angle = self.rotate_angle

        # If the width of the image is bigger than the height of the image and we rotate
        # our watermark it may go beyond the bounding box of the original image,
        # that's why we need to take into consideration the actual width of the rotated
        # waterwark inside the original image's bounding box
        bg_image_w, bg_image_h = bg_image.size
        if angle and (bg_image_w > bg_image_h):
            max_wm_w = 2 * \
                abs(bg_image_h / (2 * math.sin(math.radians(angle))))
        else:
            max_wm_w = bg_image_w

        while text_width + text_height < max_wm_w:
            self.font_object = Font(
                self.font_object.path, self.font_object.size + 1)
            self.font = self.font_object.font
            text_width, text_height = self.font.getsize(self.text)


class Content:
    '''Class that aggregates different units into a sigle object

    Args:
        paragraph (nider.models.Paragraph): paragraph used for in the content.
        header (nider.models.Header): header used for in the content.
        linkback (nider.models.Linkback): linkback used for in the content.
        watermark (nider.models.Watermark): watermark used for in the content.
        padding (int): content's padding - padding (in pixels) between the units.

    Raises:
        nider.exceptions.ImageGeneratorException: if neither of ``paragraph``, ``header`` or ``linkback`` is provided.

    Note:
        ``padding`` is taken into account only if image is to get resized. If size allows content to fit freely, pre-calculated paddings will be used.

    Note:
        Content has to consist at least of one unit: header, paragraph or linkback.
    '''

    def __init__(self, paragraph=None, header=None, linkback=None, watermark=None, padding=45):
        if not any((paragraph, header, linkback, watermark)):
            raise ImageGeneratorException(
                'Content has to consist at least of one unit.')
        self.para = paragraph
        self.header = header
        self.linkback = linkback
        self.watermark = watermark
        self.padding = padding
        self.depends_on_opposite_to_bg_color = not all(
            unit.color for unit in [
                self.para, self.header, self.linkback, self.watermark
            ] if unit
        )
        # Variable to check if the content fits into the img. Default is true,
        # but it may changed by in Img._fix_image_size()
        self.fits = True
        self._set_content_height()

    def _set_content_height(self):
        '''Sets content\'s height'''
        self.height = 0
        if self.para:
            self.height += 2 * self.padding + self.para.height
        if self.header:
            self.height += 1 * self.padding + self.header.height
        if self.linkback:
            self.height += self.linkback.height


class Image:
    '''Base class for a text based image

    Args:
        content (nider.models.Content): content object that has units to be rendered.
        fullpath (str): path where the image has to be saved.
        width (int): width of the image to be generated.
        height (int): height of the image to be generated.
        title (str): title of the image. Serves as metadata for latter rendering in html. May be used as alt text of the image. If no title is provided ``content.header.text`` will be set as the value.
        description (str): description of the image. Serves as metadata for latter rendering in html. May be used as description text of the image. If no description is provided ``content.paragraph.text`` will be set as the value.

    Raises:
        nider.exceptions.ImageGeneratorException: if the current user doesn't have sufficient permissions to create the file at passed ``fullpath``.
        AttributeError: if width <= 0 or height <= 0.
    '''

    def __init__(self, content, fullpath, width=1080, height=1080, title=None, description=None):
        self._set_content(content)
        self._set_fullpath(fullpath)
        self._set_image_size(width, height)
        self._set_title(title)
        self._set_description(description)

    def draw_on_texture(self, texture_path=None):
        '''Draws preinitialized image and its attributes on a texture

        If ``texture_path`` is set to ``None``, random texture from ``nider/textures`` will be taken.

        Args:
            texture_path (str): path of the texture to use.

        Raises:
            FileNotFoundError: if texture file at path ``texture_path`` cannot be found.
            nider.exceptions.ImageSizeFixedWarning: if the image size has to be adjusted to the provided content's size because the content takes too much space.
        '''
        if texture_path is None:
            texture_path = get_random_texture()
        elif not os.path.isfile(texture_path):
            raise FileNotFoundError(
                'Can\'t find texture {}. Please, choose an existing texture'.format(texture_path))
        if self.content.depends_on_opposite_to_bg_color:
            self.opposite_to_bg_color = generate_opposite_color(
                get_img_dominant_color(texture_path)
            )
        self._create_image()
        self._create_draw_object()
        self._fill_image_with_texture(texture_path)
        self._draw_content()
        self._save()

    def draw_on_bg(self, bgcolor=None):
        '''Draws preinitialized image and its attributes on a colored background

        If ``bgcolor`` is set to ``None``, random ``nider.colors.colormap.FLAT_UI_COLORS`` will be taken.

        Args:
            bgcolor (str): string that represents a background color. Must be compatible with `PIL.ImageColor <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html>`_ `color names <http://pillow.readthedocs.io/en/latest/reference/ImageColor.html#color-names>`_

        Raises:
            nider.exceptions.ImageSizeFixedWarning: if the image size has to be adjusted to the provided content's size because the content takes too much space.
        '''
        self.bgcolor = color_to_rgb(
            bgcolor) if bgcolor else get_random_bgcolor()
        if self.content.depends_on_opposite_to_bg_color:
            self.opposite_to_bg_color = generate_opposite_color(self.bgcolor)
        self._create_image()
        self._create_draw_object()
        self._fill_image_with_color()
        self._draw_content()
        self._save()

    def draw_on_image(self, image_path, image_enhancements=None, image_filters=None):
        '''Draws preinitialized image and its attributes on an image

        Args:
            image_path (str): path of the image to draw on.
            image_enhancements (itarable): itarable of tuples, each containing a class from ``PIL.ImageEnhance`` that will be applied and factor - a floating point value controlling the enhancement. Check `documentation <http://pillow.readthedocs.io/en/latest/reference/ImageEnhance.html>`_ of ``PIL.ImageEnhance`` for more info about availabe enhancements.
            image_filters (itarable): itarable of filters from ``PIL.ImageFilter`` that will be applied. Check `documentation <http://pillow.readthedocs.io/en/latest/reference/ImageFilter.html>`_ of ``PIL.ImageFilter`` for more info about availabe filters.

        Raises:
            FileNotFoundError: if image file at path ``image_path`` cannot be found.
        '''
        if not os.path.isfile(image_path):
            raise FileNotFoundError(
                'Can\'t find image {}. Please, choose an existing image'.format(image_path))
        self.image = PIL_Image.open(image_path)
        if self.content.depends_on_opposite_to_bg_color:
            self.opposite_to_bg_color = generate_opposite_color(
                get_img_dominant_color(image_path)
            )
        if image_filters:
            for image_filter in image_filters:
                self.image = self.image.filter(image_filter)
        if image_enhancements:
            for enhancement in image_enhancements:
                enhance_method, enhance_factor = enhancement[0], enhancement[1]
                enhancer = enhance_method(self.image)
                self.image = enhancer.enhance(enhance_factor)
        self._create_draw_object()
        self.width, self.height = self.image.size
        self._draw_content()
        self._save()

    def _save(self):
        '''Saves the image'''
        self.image.save(self.fullpath)

    def _set_content(self, content):
        '''Sets content used in the image'''
        self.content = content
        self.header = content.header
        self.para = content.para
        self.linkback = content.linkback
        self.watermark = content.watermark

    def _set_fullpath(self, fullpath):
        '''Sets path where to save the image'''
        if is_path_creatable(fullpath):
            self.fullpath = fullpath
        else:
            raise ImageGeneratorException(
                "Is seems impossible to create a file in path {}".format(fullpath))

    def _set_image_size(self, width, height):
        '''Sets width and height of the image'''
        if width <= 0 or height <= 0:
            raise AttributeError(
                "Width or height of the image have to be positive integers")
        self.width = width
        self.height = height

    def _set_title(self, title):
        '''Sets title of the image'''
        if title:
            self.title = title
        elif self.content.header:
            self.title = self.content.header.text
        else:
            self.title = ''

    def _set_description(self, description):
        '''Sets description of the image'''
        if description:
            self.description = description
        elif self.content.para:
            self.description = self.content.para.text
        else:
            self.description = ''

    def _fix_image_size(self):
        '''Fixes image's size'''

        if self.content.height >= self.height:
            warnings.warn(ImageSizeFixedWarning())
            self.content.fits = False
            self.height = self.content.height

    def _create_image(self):
        '''Creates a basic PIL image

        Creates a basic PIL image previously fixing its size
        '''
        self._fix_image_size()
        self.image = PIL_Image.new("RGBA", (self.width, self.height))

    def _create_draw_object(self):
        '''Creates a basic PIL Draw object'''
        self.draw = ImageDraw.Draw(self.image)

    def _fill_image_with_texture(self, texture_path):
        '''Fills an image with a texture

        Fills an image with a texture by reapiting it necessary number of times

        Attributes:
            texture_path (str): path of the texture to use
        '''
        texture = PIL_Image.open(texture_path, 'r')
        texture_w, texture_h = texture.size
        bg_w, bg_h = self.image.size
        times_for_Ox = math.ceil(bg_w / texture_w)
        times_for_Oy = math.ceil(bg_h / texture_h)
        for y in range(times_for_Oy):
            for x in range(times_for_Ox):
                offset = (x * texture_w, y * texture_h)
                self.image.paste(texture, offset)

    def _fill_image_with_color(self):
        '''Fills an image with a color

        Fills an image with a color by creating a colored rectangle of the image
        size
        '''
        self.draw.rectangle([(0, 0), self.image.size], fill=self.bgcolor)

    def _prepare_content(self):
        '''Prepares content for drawing'''
        content = self.content
        for unit in [content.header, content.para, content.linkback]:
            if unit:
                if not unit.color:
                    color_to_use = self.opposite_to_bg_color
                    # explicitly sets unit's color to disntinc to bg one
                    unit.color = color_to_use
                    warnings.warn(
                        AutoGeneratedUnitColorUsedWarning(unit, color_to_use))
                if unit.outline and not unit.outline.color:
                    color_to_use = blend(unit.color, '#000', 0.2)
                    unit.outline.color = color_to_use
                    warnings.warn(
                        AutoGeneratedUnitOutlinecolorUsedWarning(unit, color_to_use))

    def _draw_content(self):
        '''Draws each unit of the content on the image'''
        self._prepare_content()
        if self.header:
            self._draw_header()
        if self.para:
            self._draw_para()
        if self.linkback:
            self._draw_linkback()
        if self.watermark:
            self._draw_watermark()

    def _draw_header(self):
        '''Draws the header on the image'''
        current_h = self.content.padding
        self._draw_unit(current_h, self.header)

    def _draw_para(self):
        '''Draws the paragraph on the image'''
        if self.content.fits:
            # Trying to center everything
            current_h = math.floor(
                (self.height - self.para.height) / 2)
            self._draw_unit(current_h, self.para)
        else:
            if self.header:
                header_with_padding_height = 2 * self.content.padding + self.header.height
                current_h = header_with_padding_height
            else:
                current_h = self.content.padding
            self._draw_unit(current_h, self.para)

    def _draw_linkback(self):
        '''Draws a linkback on the image'''
        current_h = self.height - self.linkback.height
        self._draw_unit(current_h, self.linkback)

    def _draw_watermark(self):
        '''Draws a watermark on the image'''
        watermark_image = PIL_Image.new('RGBA', self.image.size, (0, 0, 0, 0))
        self.watermark._adjust_fontsize(self.image)
        draw = ImageDraw.Draw(watermark_image, 'RGBA')
        w_width, w_height = self.watermark.font.getsize(self.watermark.text)
        draw.text(((watermark_image.size[0] - w_width) / 2,
                   (watermark_image.size[1] - w_height) / 2),
                  self.watermark.text, font=self.watermark.font,
                  fill=self.watermark.color)

        if self.watermark.rotate_angle:
            watermark_image = watermark_image.rotate(
                self.watermark.rotate_angle, PIL_Image.BICUBIC)

        alpha = watermark_image.split()[3]
        alpha = ImageEnhance.Brightness(alpha).enhance(self.watermark.opacity)
        watermark_image.putalpha(alpha)

        # Because watermark can be rotated we create a separate image for cross
        # so that it doesn't get rotated also. + It's impossible to drawn
        # on a rotated image
        if self.watermark.cross:
            watermark_cross_image = PIL_Image.new(
                'RGBA', self.image.size, (0, 0, 0, 0))
            cross_draw = ImageDraw.Draw(watermark_cross_image, 'RGBA')
            line_width = 1 + int(sum(self.image.size) / 2 * 0.007)
            cross_draw.line(
                (0, 0) + watermark_image.size,
                fill=self.watermark.color,
                width=line_width
            )
            cross_draw.line(
                (0, watermark_image.size[1], watermark_image.size[0], 0),
                fill=self.watermark.color,
                width=line_width
            )
            watermark_cross_alpha = watermark_cross_image.split()[3]
            watermark_cross_alpha = ImageEnhance.Brightness(
                watermark_cross_alpha).enhance(self.watermark.opacity)
            watermark_cross_image.putalpha(watermark_cross_alpha)
            # Adds cross to the watermark
            watermark_image = PIL_Image.composite(
                watermark_cross_image, watermark_image, watermark_cross_image)

        self.image = PIL_Image.composite(
            watermark_image, self.image, watermark_image)

    def _draw_unit(self, start_height, unit):
        '''Draws the text and its outline on the image starting at specific height'''
        current_h = start_height
        try:
            lines = unit.wrapped_lines
        except AttributeError:
            # text is a one-liner. Construct a list out of it for later usage
            lines = [unit.text]
        line_padding = getattr(unit, 'line_padding', None)
        outline = unit.outline
        font = unit.font

        for line in lines:
            w, h = self.draw.textsize(line, font=unit.font)
            if unit.align == "center":
                x = (self.width - w) / 2
            elif unit.align == "left":
                x = self.width * 0.075
            elif unit.align == "right":
                x = 0.925 * self.width - w
            y = current_h

            if outline:
                # thin border
                self.draw.text((x - outline.width, y), line, font=font,
                               fill=outline.color)
                self.draw.text((x + outline.width, y), line, font=font,
                               fill=outline.color)
                self.draw.text((x, y - outline.width), line, font=font,
                               fill=outline.color)
                self.draw.text((x, y + outline.width), line, font=font,
                               fill=outline.color)

                # thicker border
                self.draw.text((x - outline.width, y - outline.width), line,
                               font=font, fill=outline.color)
                self.draw.text((x + outline.width, y - outline.width), line,
                               font=font, fill=outline.color)
                self.draw.text((x - outline.width, y + outline.width), line,
                               font=font, fill=outline.color)
                self.draw.text((x + outline.width, y + outline.width), line,
                               font=font, fill=outline.color)

            self.draw.text((x, y), line, unit.color, font=font)

            if line_padding:
                current_h += h + line_padding


class FacebookSquarePost(Image):
    '''Alias of :class:`nider.models.Image` with ``width=470`` and ``height=470``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 470)
        kwargs['height'] = kwargs.get('height', 470)
        super().__init__(*args, **kwargs)


class FacebookLandscapePost(Image):
    '''Alias of :class:`nider.models.Image` with ``width=1024`` and ``height=512``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 1024)
        kwargs['height'] = kwargs.get('height', 512)
        super().__init__(*args, **kwargs)


TwitterPost = FacebookLandscapePost


class TwitterLargeCard(Image):
    '''Alias of :class:`nider.models.Image` with ``width=506`` and ``height=506``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 506)
        kwargs['height'] = kwargs.get('height', 506)
        super().__init__(*args, **kwargs)


class InstagramSquarePost(Image):
    '''Alias of :class:`nider.models.Image` with ``width=1080`` and ``height=1080``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 1080)
        kwargs['height'] = kwargs.get('height', 1080)
        super().__init__(*args, **kwargs)


class InstagramPortraitPost(Image):
    '''Alias of :class:`nider.models.Image` with ``width=1080`` and ``height=1350``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 1080)
        kwargs['height'] = kwargs.get('height', 1350)
        super().__init__(*args, **kwargs)


class InstagramLandscapePost(Image):
    '''Alias of :class:`nider.models.Image` with ``width=1080`` and ``height=566``'''

    def __init__(self, *args, **kwargs):
        kwargs['width'] = kwargs.get('width', 1080)
        kwargs['height'] = kwargs.get('height', 566)
        super().__init__(*args, **kwargs)
