""" Generate social media preview images for twitter/mastodon/etc """
import math
import os
import textwrap
from io import BytesIO
from uuid import uuid4
import logging

import colorsys
from colorthief import ColorThief
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor

from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.storage import default_storage
from django.db.models import Avg

from bookwyrm import models, settings
from bookwyrm.tasks import app

logger = logging.getLogger(__name__)

IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
BG_COLOR = settings.PREVIEW_BG_COLOR
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
TRANSPARENT_COLOR = (0, 0, 0, 0)

margin = math.floor(IMG_HEIGHT / 10)
gutter = math.floor(margin / 2)
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
inner_img_width = math.floor(inner_img_height * 0.7)


def get_imagefont(name, size):
    """Loads an ImageFont based on config"""
    try:
        config = settings.FONTS[name]
        path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
        return ImageFont.truetype(path, size)
    except KeyError:
        logger.error("Font %s not found in config", name)
    except OSError:
        logger.error("Could not load font %s from file", name)

    return ImageFont.load_default()


def get_font(weight, size=28):
    """Gets a custom font with the given weight and size"""
    font = get_imagefont(DEFAULT_FONT, size)

    try:
        if weight == "light":
            font.set_variation_by_name("Light")
        if weight == "bold":
            font.set_variation_by_name("Bold")
        if weight == "regular":
            font.set_variation_by_name("Regular")
    except AttributeError:
        pass

    return font


def get_wrapped_text(text, font, content_width):
    """text wrap length depends on the max width of the content"""

    low = 0
    high = len(text)

    try:
        # ideal length is determined via binary search
        while low < high:
            mid = math.floor(low + high)
            wrapped_text = textwrap.fill(text, width=mid)
            width = font.getsize_multiline(wrapped_text)[0]
            if width < content_width:
                low = mid
            else:
                high = mid - 1
    except AttributeError:
        wrapped_text = text

    return wrapped_text


def generate_texts_layer(texts, content_width):
    """Adds text for images"""
    font_text_zero = get_font("bold", size=20)
    font_text_one = get_font("bold", size=48)
    font_text_two = get_font("bold", size=40)
    font_text_three = get_font("regular", size=40)

    text_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR)
    text_layer_draw = ImageDraw.Draw(text_layer)

    text_y = 0

    if "text_zero" in texts and texts["text_zero"]:
        # Text one (Book title)
        text_zero = get_wrapped_text(texts["text_zero"], font_text_zero, content_width)

        text_layer_draw.multiline_text(
            (0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR
        )

        try:
            text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16
        except (AttributeError, IndexError):
            text_y = text_y + 26

    if "text_one" in texts and texts["text_one"]:
        # Text one (Book title)
        text_one = get_wrapped_text(texts["text_one"], font_text_one, content_width)

        text_layer_draw.multiline_text(
            (0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR
        )

        try:
            text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16
        except (AttributeError, IndexError):
            text_y = text_y + 26

    if "text_two" in texts and texts["text_two"]:
        # Text one (Book subtitle)
        text_two = get_wrapped_text(texts["text_two"], font_text_two, content_width)

        text_layer_draw.multiline_text(
            (0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR
        )

        try:
            text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16
        except (AttributeError, IndexError):
            text_y = text_y + 26

    if "text_three" in texts and texts["text_three"]:
        # Text three (Book authors)
        text_three = get_wrapped_text(
            texts["text_three"], font_text_three, content_width
        )

        text_layer_draw.multiline_text(
            (0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR
        )

    text_layer_box = text_layer.getbbox()
    return text_layer.crop(text_layer_box)


def generate_instance_layer(content_width):
    """Places components for instance preview"""
    font_instance = get_font("light", size=28)

    site = models.SiteSettings.objects.get()

    if site.logo_small:
        logo_img = Image.open(site.logo_small)
    else:
        try:
            static_path = os.path.join(settings.STATIC_ROOT, "images/logo-small.png")
            logo_img = Image.open(static_path)
        except FileNotFoundError:
            logo_img = None

    instance_layer = Image.new("RGBA", (content_width, 62), color=TRANSPARENT_COLOR)

    instance_text_x = 0

    if logo_img:
        logo_img.thumbnail((50, 50), Image.ANTIALIAS)

        instance_layer.paste(logo_img, (0, 0))

        instance_text_x = instance_text_x + 60

    instance_layer_draw = ImageDraw.Draw(instance_layer)
    instance_layer_draw.text(
        (instance_text_x, 10), site.name, font=font_instance, fill=TEXT_COLOR
    )

    line_width = 50 + 10 + font_instance.getsize(site.name)[0]

    line_layer = Image.new(
        "RGBA", (line_width, 2), color=(*(ImageColor.getrgb(TEXT_COLOR)), 50)
    )
    instance_layer.alpha_composite(line_layer, (0, 60))

    return instance_layer


def generate_rating_layer(rating, content_width):
    """Places components for rating preview"""
    try:
        icon_star_full = Image.open(
            os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png")
        )
        icon_star_empty = Image.open(
            os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png")
        )
        icon_star_half = Image.open(
            os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png")
        )
    except FileNotFoundError:
        return None

    icon_size = 64
    icon_margin = 10

    rating_layer_base = Image.new(
        "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
    )
    rating_layer_color = Image.new("RGBA", (content_width, icon_size), color=TEXT_COLOR)
    rating_layer_mask = Image.new(
        "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
    )

    position_x = 0

    for _ in range(math.floor(rating)):
        rating_layer_mask.alpha_composite(icon_star_full, (position_x, 0))
        position_x = position_x + icon_size + icon_margin

    if math.floor(rating) != math.ceil(rating):
        rating_layer_mask.alpha_composite(icon_star_half, (position_x, 0))
        position_x = position_x + icon_size + icon_margin

    for _ in range(5 - math.ceil(rating)):
        rating_layer_mask.alpha_composite(icon_star_empty, (position_x, 0))
        position_x = position_x + icon_size + icon_margin

    rating_layer_mask = rating_layer_mask.getchannel("A")
    rating_layer_mask = ImageOps.invert(rating_layer_mask)

    rating_layer_composite = Image.composite(
        rating_layer_base, rating_layer_color, rating_layer_mask
    )

    return rating_layer_composite


def generate_default_inner_img():
    """Adds cover image"""
    font_cover = get_font("light", size=28)

    default_cover = Image.new(
        "RGB", (inner_img_width, inner_img_height), color=DEFAULT_COVER_COLOR
    )
    default_cover_draw = ImageDraw.Draw(default_cover)

    text = "no image :("
    text_dimensions = font_cover.getsize(text)
    text_coords = (
        math.floor((inner_img_width - text_dimensions[0]) / 2),
        math.floor((inner_img_height - text_dimensions[1]) / 2),
    )
    default_cover_draw.text(text_coords, text, font=font_cover, fill="white")

    return default_cover


# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
def generate_preview_image(
    texts=None, picture=None, rating=None, show_instance_layer=True
):
    """Puts everything together"""
    texts = texts or {}
    # Cover
    try:
        inner_img_layer = Image.open(picture)
        inner_img_layer.thumbnail((inner_img_width, inner_img_height), Image.ANTIALIAS)
        color_thief = ColorThief(picture)
        dominant_color = color_thief.get_color(quality=1)
    except:  # pylint: disable=bare-except
        inner_img_layer = generate_default_inner_img()
        dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR)

    # Color
    if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]:
        red, green, blue = dominant_color
        image_bg_color = f"rgb({red}, {green}, {blue})"

        # Adjust color
        image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)]
        image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb)

        if BG_COLOR == "use_dominant_color_light":
            lightness = max(0.9, image_bg_color_hls[1])
        else:
            lightness = min(0.15, image_bg_color_hls[1])

        image_bg_color_hls = (
            image_bg_color_hls[0],
            lightness,
            image_bg_color_hls[2],
        )
        image_bg_color = tuple(
            math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)
        )
    else:
        image_bg_color = BG_COLOR

    # Background (using the color)
    img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=image_bg_color)

    # Contents
    inner_img_x = margin + inner_img_width - inner_img_layer.width
    inner_img_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2)
    content_x = margin + inner_img_width + gutter
    content_width = IMG_WIDTH - content_x - margin

    contents_layer = Image.new(
        "RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR
    )
    contents_composite_y = 0

    if show_instance_layer:
        instance_layer = generate_instance_layer(content_width)
        contents_layer.alpha_composite(instance_layer, (0, contents_composite_y))
        contents_composite_y = contents_composite_y + instance_layer.height + gutter

    texts_layer = generate_texts_layer(texts, content_width)
    contents_layer.alpha_composite(texts_layer, (0, contents_composite_y))
    contents_composite_y = contents_composite_y + texts_layer.height + gutter

    if rating:
        # Add some more margin
        contents_composite_y = contents_composite_y + gutter
        rating_layer = generate_rating_layer(rating, content_width)

        if rating_layer:
            contents_layer.alpha_composite(rating_layer, (0, contents_composite_y))
            contents_composite_y = contents_composite_y + rating_layer.height + gutter

    contents_layer_box = contents_layer.getbbox()
    contents_layer_height = contents_layer_box[3] - contents_layer_box[1]

    contents_y = math.floor((IMG_HEIGHT - contents_layer_height) / 2)

    if show_instance_layer:
        # Remove Instance Layer from centering calculations
        contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2)

    contents_y = max(contents_y, margin)

    # Composite layers
    img.paste(
        inner_img_layer, (inner_img_x, inner_img_y), inner_img_layer.convert("RGBA")
    )
    img.alpha_composite(contents_layer, (content_x, contents_y))

    return img.convert("RGB")


def save_and_cleanup(image, instance=None):
    """Save and close the file"""
    if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
        return False
    image_buffer = BytesIO()

    try:
        try:
            file_name = instance.preview_image.name
        except ValueError:
            file_name = None

        if not file_name or file_name == "":
            uuid = uuid4()
            file_name = f"{instance.id}-{uuid}.jpg"

        # Clean up old file before saving
        if file_name and default_storage.exists(file_name):
            default_storage.delete(file_name)

        # Save
        image.save(image_buffer, format="jpeg", quality=75)

        instance.preview_image = InMemoryUploadedFile(
            ContentFile(image_buffer.getvalue()),
            "preview_image",
            file_name,
            "image/jpg",
            image_buffer.tell(),
            None,
        )

        save_without_broadcast = isinstance(instance, (models.Book, models.User))
        if save_without_broadcast:
            instance.save(broadcast=False, update_fields=["preview_image"])
        else:
            instance.save(update_fields=["preview_image"])

    finally:
        image_buffer.close()
    return True


# pylint: disable=invalid-name
@app.task(queue="low_priority")
def generate_site_preview_image_task():
    """generate preview_image for the website"""
    if not settings.ENABLE_PREVIEW_IMAGES:
        return

    site = models.SiteSettings.objects.get()

    if site.logo:
        logo = site.logo
    else:
        logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")

    texts = {
        "text_zero": settings.DOMAIN,
        "text_one": site.name,
        "text_three": site.instance_tagline,
    }

    image = generate_preview_image(texts=texts, picture=logo, show_instance_layer=False)

    save_and_cleanup(image, instance=site)


# pylint: disable=invalid-name
@app.task(queue="low_priority")
def generate_edition_preview_image_task(book_id):
    """generate preview_image for a book"""
    if not settings.ENABLE_PREVIEW_IMAGES:
        return

    book = models.Book.objects.select_subclasses().get(id=book_id)

    rating = models.Review.objects.filter(
        privacy="public",
        deleted=False,
        book__in=[book_id],
    ).aggregate(Avg("rating"))["rating__avg"]

    texts = {
        "text_one": book.title,
        "text_two": book.subtitle,
        "text_three": book.author_text,
    }

    image = generate_preview_image(texts=texts, picture=book.cover, rating=rating)

    save_and_cleanup(image, instance=book)


@app.task(queue="low_priority")
def generate_user_preview_image_task(user_id):
    """generate preview_image for a book"""
    if not settings.ENABLE_PREVIEW_IMAGES:
        return

    user = models.User.objects.get(id=user_id)

    texts = {
        "text_one": user.display_name,
        "text_three": f"@{user.localname}@{settings.DOMAIN}",
    }

    if user.avatar:
        avatar = user.avatar
    else:
        avatar = os.path.join(settings.STATIC_ROOT, "images/default_avi.jpg")

    image = generate_preview_image(texts=texts, picture=avatar)

    save_and_cleanup(image, instance=user)
