from __future__ import annotations

import io

from PIL import Image, ImageDraw, ImageFont, ImageOps

from .templates import (
    DEFAULT_FONT,
    TextConfig,
    get_font_path,
    get_template,
    get_template_image_path,
)

DEFAULT_SIZE = (600, 600)
MINIMUM_FONT_SIZE = 7


def render_meme(
    template_id: str,
    lines: list[str],
    *,
    font_name: str = "",
    style: str = "default",
    max_width: int = 800,
    max_height: int = 800,
    extension: str = "png",
) -> bytes:
    template = get_template(template_id)
    if template is None:
        raise ValueError(f"Unknown template: {template_id}")

    image_path = get_template_image_path(template_id, style)
    background = Image.open(image_path).convert("RGBA")
    background = ImageOps.exif_transpose(background)
    image = _resize(background, max_width, max_height)

    for index, text_config in enumerate(template.lines):
        try:
            line = lines[index]
        except IndexError:
            line = ""

        if not line:
            continue

        _draw_text_on_image(image, line, text_config, font_name)

    stream = io.BytesIO()
    if extension == "jpg":
        image.convert("RGB").save(stream, format="JPEG", quality=95)
    else:
        image.save(stream, format="PNG")
    return stream.getvalue()


def _resize(image: Image.Image, max_width: int, max_height: int) -> Image.Image:
    ratio = image.width / image.height
    default_w, default_h = DEFAULT_SIZE

    if max_width and max_height:
        if max_width < max_height * ratio:
            size = (max_width, int(max_width / ratio))
        else:
            size = (int(max_height * ratio), max_height)
    elif max_width:
        size = (max_width, int(max_width / ratio))
    elif max_height:
        size = (int(max_height * ratio), max_height)
    elif ratio < 1.0:
        size = (default_w, int(default_h / ratio))
    else:
        size = (int(default_w * ratio), default_h)

    return image.resize(size, Image.Resampling.LANCZOS)


def _draw_text_on_image(
    image: Image.Image,
    line: str,
    text_config: TextConfig,
    font_name: str,
) -> None:
    img_w, img_h = image.size

    anchor = (int(img_w * text_config.anchor_x), int(img_h * text_config.anchor_y))
    max_text_size = (int(img_w * text_config.scale_x), int(img_h * text_config.scale_y))

    if max_text_size[0] <= 0 or max_text_size[1] <= 0:
        return

    max_font_size = int(img_h / (4 if text_config.angle else 9))

    styled_line = _stylize(line, text_config.style)
    wrapped = _wrap(
        font_name or text_config.font, styled_line, max_text_size, max_font_size
    )
    font = _get_font(
        font_name or text_config.font, wrapped, max_text_size, max_font_size
    )
    offset = _get_text_offset(wrapped, font, max_text_size, text_config.align)

    stroke_width = _get_stroke_width(font)
    stroke_width, stroke_fill = _get_stroke(text_config.color, stroke_width)

    box = Image.new("RGBA", max_text_size)
    draw = ImageDraw.Draw(box)

    rows = wrapped.count("\n") + 1
    draw.text(
        (-offset[0], -offset[1]),
        wrapped,
        text_config.color,
        font,
        spacing=-offset[1] / (rows * 2),
        align=text_config.align,
        stroke_width=stroke_width,
        stroke_fill=stroke_fill,
    )

    if text_config.angle:
        box = box.rotate(
            text_config.angle, resample=Image.Resampling.BICUBIC, expand=True
        )

    image.paste(box, anchor, box)


def _stylize(text: str, style: str) -> str:
    if style == "none":
        return text
    if style == "default":
        if text.islower():
            text = text.capitalize()
        return text
    if style == "upper":
        return text.upper()
    if style == "lower":
        return text.lower()
    if style == "title":
        return text.title()
    if style == "capitalize":
        return text.capitalize()
    return text.upper()


def _wrap(
    font_name: str, line: str, max_text_size: tuple[int, int], max_font_size: int
) -> str:
    lines_1 = line
    lines_2 = _split_2(line)
    lines_3 = _split_3(line)

    font_1 = _get_font(font_name, lines_1, max_text_size, max_font_size)
    font_2 = _get_font(font_name, lines_2, max_text_size, max_font_size)
    font_3 = _get_font(font_name, lines_3, max_text_size, max_font_size)

    if font_1.size == font_2.size and font_2.size <= MINIMUM_FONT_SIZE:
        return lines_2

    if font_1.size >= font_2.size:
        return lines_1

    text_3_width = _get_text_size(lines_3, font_3)[0]
    if text_3_width >= max_text_size[0] * 0.60:
        return lines_3

    text_2_width = _get_text_size(lines_2, font_2)[0]
    if text_2_width >= max_text_size[0] * 0.60:
        return lines_2

    return lines_1


def _split_2(line: str) -> str:
    if " " not in line:
        return line
    midpoint = len(line) // 2 - 1
    for offset in range(0, len(line) // 4):
        for index in [midpoint - offset, midpoint + offset]:
            if 0 <= index < len(line) and line[index] == " ":
                return line[:index].strip() + "\n" + line[index:].strip()
    return line


def _split_3(line: str) -> str:
    max_len = len(line) / 3
    words = line.split(" ")
    result_lines = ["", "", ""]
    index = 0
    for word in words:
        current_len = len(result_lines[index])
        next_len = current_len + len(word) * 0.7
        if next_len > max_len and index < 2:
            index += 1
        result_lines[index] += word + " "
    return "\n".join(result_lines).strip()


def _get_font(
    name: str, text: str, max_text_size: tuple[int, int], max_font_size: int
) -> ImageFont.FreeTypeFont:
    font_path = str(get_font_path(name or DEFAULT_FONT))
    max_text_width = max_text_size[0] - max_text_size[0] / 35
    max_text_height = max_text_size[1] - max_text_size[1] / 10

    for size in range(max(MINIMUM_FONT_SIZE, max_font_size), 6, -1):
        font = ImageFont.truetype(font_path, size=size)
        text_width, text_height = _get_text_size_minus_offset(text, font)
        if text_width <= max_text_width and text_height <= max_text_height:
            break

    return font


def _get_text_size_minus_offset(
    text: str, font: ImageFont.FreeTypeFont
) -> tuple[int, int]:
    text_width, text_height = _get_text_size(text, font)
    x_offset, y_offset, _, _ = font.getbbox(text or " ")
    return int(text_width - x_offset), int(text_height - y_offset)


def _get_text_size(text: str, font: ImageFont.FreeTypeFont) -> tuple[int, int]:
    image = Image.new("RGB", (100, 100))
    draw = ImageDraw.Draw(image)
    _, _, text_width, text_height = draw.textbbox((0, 0), text or " ", font)
    stroke_width = _get_stroke_width(font)
    return int(text_width + stroke_width), int(text_height + stroke_width)


def _get_text_offset(
    text: str,
    font: ImageFont.FreeTypeFont,
    max_text_size: tuple[int, int],
    align: str = "center",
) -> tuple[float, float]:
    text_size = _get_text_size(text, font)
    stroke_width = _get_stroke_width(font)

    x_offset, y_offset, _, _ = font.getbbox(text or " ")
    x_offset -= stroke_width
    y_offset -= stroke_width

    lines_list = text.split("\n")
    rows = len(lines_list)
    if rows >= 3:
        y_adjust = 1.1
    else:
        y_adjust = 1 + (3 - rows) * 0.25

    if align != "left":
        x_offset -= (max_text_size[0] - text_size[0]) / 2
    y_offset -= (max_text_size[1] - text_size[1] / y_adjust) / 2

    if any(letter in lines_list[-1] for letter in "gjpqy"):
        y_offset += text_size[1] // 20

    return x_offset, y_offset


def _get_stroke_width(font: ImageFont.FreeTypeFont) -> int:
    return min(3, max(1, int(font.size / 12)))


def _get_stroke(color: str, width: int) -> tuple[int, str]:
    if color == "black":
        return 1, "#FFFFFF80"
    if "#" in color:
        stroke_color = "#000000"
        if len(color) >= len(stroke_color) + 2:
            stroke_color += color[-2:]
        return 1, stroke_color
    return width, "black"
