# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

import os
from typing import Optional
from urllib.parse import quote, urlparse, unquote
from uuid import uuid4

from markdown_it import MarkdownIt
from markdown_it.token import Token
from mdit_py_plugins.attrs import attrs_plugin
from mdit_py_plugins.dollarmath import dollarmath_plugin
from markdown_it_modified_tasklists_plugin.mdit_tasklists_with_elements_plugin import (
    tasklists_with_elements_plugin,
)
from markdown_it_img_figures_plugin.mdit_img_figures_plugin import img_figures_plugin

from iotas.attachment import Attachment
from iotas.note import Note


def parse_to_tokens(
    note: Note,
    exporting: bool,
    tex_support: bool,
) -> tuple[MarkdownIt, list[Token]]:
    """Generate parser tokens for note.

    :param Note note: Note
    :param bool exporting: Whether for export
    :param bool tex_support: Whether to support TeX equations
    :return: Parser and list of parser tokens
    :rtype: tuple[MarkdownIt, list[Token]]
    """
    parser = (
        MarkdownIt("gfm-like")
        .use(tasklists_with_elements_plugin, enabled=not note.read_only)
        .use(attrs_plugin, after=["image"], allowed=["height", "width"])
        .enable("table")
    )
    parser.use(img_figures_plugin, lazyload=not exporting)
    if tex_support:
        parser.use(dollarmath_plugin, renderer=__render_tex)

    if exporting:
        content = get_note_export_content(note, prefix_note_id=False)
    else:
        content = note.content
    return (parser, parser.parse(content))


def get_note_export_content(note: Note, prefix_note_id: bool) -> str:
    """Get note content with image paths updated for export.

    :param Note note: Note
    :param bool prefix_note_id: Whether to prefix the note id
    :return: Updated content
    :rtype: str
    """
    attachments = get_image_attachments_from_note_content(note)
    note_id_prefix = f"{note.id}." if prefix_note_id else ""
    content = note.content

    for attachment in attachments:
        old_path = attachment.path
        new_path = f"attachments/{note_id_prefix}{os.path.basename(old_path)}"
        content = update_image_path_in_markup(content, old_path, new_path)

    return content


def get_image_attachments_from_tokens(note: Note, tokens: list[Token]) -> list[Attachment]:
    """Get list of attachments from parser tokens.

    :param Note note: Note
    :param list[Token] tokens: Parser tokens
    :return: Attachments
    :rtype: list[Attachment]
    """
    images = filter_image_tokens(tokens)
    attachments = []
    for img in images:
        src = img.attrs["src"]
        if not urlparse(src).netloc and not src.strip().startswith("/"):
            path = unquote(src)
            attachment = create_attachment_for_markup_path(note, path)
            attachments.append(attachment)

    return attachments


def get_image_attachments_from_note_content(note: Note) -> list[Attachment]:
    """Get list of attachments from note contents.

    :param Note note: Note
    :return: Attachments
    :rtype: list[Attachment]
    """
    _parser, tokens = parse_to_tokens(note, exporting=False, tex_support=False)
    return get_image_attachments_from_tokens(note, tokens)


def filter_image_tokens(tokens: list[Token]) -> list[Token]:
    """Filter list of parser tokens to images.

    :param list[Token] tokens: Parser tokens
    :return: Image tokens
    :rtype: list[Token]
    """
    images = []

    def add_images_from_children(tokens: list[Token]) -> None:
        for token in tokens:
            if token.children:
                add_images_from_children(token.children)

            if token.type == "image" and "src" in token.attrs:
                images.append(token)

    add_images_from_children(tokens)
    return images


def create_attachment_for_markup_path(note: Note, path: str) -> Attachment:
    """Create an attachment with the provided markup path.

    :param Note note: Note
    :param str path: Attachment markup path, unquoted
    :return: Attachment
    :rtype: Attachment
    """
    attachment = Attachment()
    attachment.note_id = note.id
    attachment.path = path
    attachment.note_remote_id = note.remote_id
    return attachment


def update_image_path_in_markup(content: str, old_path: str, new_path: str) -> str:
    """Update image path in note content.

    :param str content: Note content
    :param str old_path: Previous path, unquoted
    :param str new_path: New path, unquoted
    :return: Updated note content
    :rtype: str
    """
    old_path = quote(old_path)
    new_path = quote(new_path)
    search = f"]({old_path}"

    start_index = content.find(search)
    while start_index >= 0:
        path_start = start_index + 2
        path_end = path_start + len(old_path)

        image_end = __get_image_end_index(content, path_end)

        if image_end is not None:
            # Ensure we haven't matched a link. Clunky but in theory a little less brittle than a
            # weak regex.
            element_start = content.rfind("![", 0, start_index)
            if element_start != -1:
                full_markup = content[element_start:image_end]
                if string_is_image(full_markup):
                    content = content[0:path_start] + new_path + content[path_end:]
                    path_end = path_start + len(new_path)

        start_index = content.find(search, path_end)

    return content


def string_is_image(input: str) -> bool:
    """Whether provided string is markdown for an image.

    :param str input: Markdown string
    :return: Whether an image
    :rtype: bool
    """
    parser = MarkdownIt("gfm-like")
    tokens = parser.parseInline(input)
    if len(tokens) != 1:
        return False
    elif len(tokens[0].children) != 1:
        return False
    elif tokens[0].children[0].type != "image":
        return False
    else:
        return True


def __render_tex(equation: str, options: dict) -> str:
    """Render TeX markdown into HTML element.

    :param str equation: Equation
    :param dict options: Render options
    :return: Generated HTML
    :rtype: str
    """
    span_id = f"tex-{uuid4()}"
    equation = equation.replace("\\", "\\\\")
    js_options = f'{{displayMode: {str(options["display_mode"]).lower()}}}'
    katex_cmd = (
        f"var equation = `{equation}`; "
        + f"katex.render(equation, document.getElementById('{span_id}'), {js_options});"
    )
    return f'<span id="{span_id}"><script>{katex_cmd}</script></span>'


def __get_image_end_index(content: str, path_end: int) -> Optional[int]:
    index = path_end
    found = False
    in_title = False
    while not found:
        if index >= len(content):
            break
        char = content[index]
        if char == "\n":
            break
        elif char == "\r":
            break
        elif char == '"':
            if in_title:
                in_title = False
            else:
                in_title = True
        elif char == ")" and not in_title:
            found = True
        index += 1

    if found:
        return index
    else:
        return None
