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

import gi

gi.require_version("GtkSource", "5")
from gi.repository import GLib, Gtk, GtkSource

import logging
import re
from typing import NamedTuple, Optional


class ParsedLink(NamedTuple):
    """Parsed general link."""

    link: str
    """Link URL"""

    start_offset: int
    """Start offset"""

    end_offset: int
    """End offset"""


class ParsedInlineLink(NamedTuple):
    """Parsed markdown inline link."""

    link: str = ""
    """Link URL"""

    text: str = ""
    """Link text"""

    source_text: str = ""
    """Source text"""

    start_offset: int = -1
    """Start offset"""

    end_offset: int = -1
    """End offset"""

    url_angle_bracketed: bool = False
    """Angle-bracket wrapped URL"""

    title: str = ""
    """Link title"""


class ParsedAutomaticLink(NamedTuple):
    """Parsed markdown automatic link."""

    link: str
    """Link URL"""

    start_offset: int
    """Start offset"""

    end_offset: int
    """End offset"""

    source_text: str
    """Source text"""


def sanitise_path(path: str) -> str:
    """Sanitise path for filesystem storage.

    :param str path: The path to work on
    :return: The sanitised path
    :rtype: str
    """
    # This is close to a direct translation of what Nextcloud Notes is using.

    # Remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
    # prevents also directory traversal by eliminiating slashes
    for token in ["*", "|", "/", "\\", ":", '"', "<", ">", "?"]:
        path = path.replace(token, "")

    # Prevent file being hidden
    path = re.sub(r"^[\.\s]+", "", path)

    return path.strip()


def has_unicode(input: str) -> bool:
    has_unicode = False
    try:
        input.encode(encoding="ascii")
    except UnicodeEncodeError:
        has_unicode = True
    return has_unicode


def parse_any_url_at_iter(iter: Gtk.TextIter, http_only: bool) -> Optional[ParsedLink]:
    """Check and parse location in buffer to URL.

    :param Gtk.TextIter iter: Iter
    :param bool http_only: Whether only http/s URIs will pass
    :return: Object with link details, optional
    :rtype: ParsedLink
    """
    start = iter.copy()
    end = iter.copy()

    def whitespace_at_iter(itr: Gtk.TextIter) -> bool:
        char_end = itr.copy()
        char_end.forward_char()
        char = itr.get_text(char_end)
        return char in (" ", "\t", "\n")

    # Find preceding whitespace
    moved = False
    while not start.is_start() and not whitespace_at_iter(start):
        moved = True
        start.backward_char()
    if moved and not (start.is_start() and not whitespace_at_iter(start)):
        start.forward_char()

    # Find following whitespace
    while not end.is_end() and not whitespace_at_iter(end):
        end.forward_char()

    # Grab text between linespace breaks and see if GLib thinks it's a URL, populate
    text = start.get_text(end)
    if str_is_url(text, http_only):
        return ParsedLink(text, start.get_offset(), end.get_offset())
    else:
        return None


def parse_markdown_inline_link(inside: Gtk.TextIter) -> Optional[ParsedInlineLink]:
    """Parse link elements from a known inline link.

    :param Gtk.TextIter inside: Iter inside inline link
    :return: Object with link details, optional
    :rtype: ParsedInlineLink
    """
    start, end = get_iters_at_sourceview_context_class_extents("inline-link", inside)
    if not start or not end:
        return None

    source_text = inside.get_buffer().get_text(start, end, include_hidden_chars=False)

    # Parsing logic here is a little brittle, reflecting being in this block is predicated on
    # the GtkSourceView language parsing
    text, url_part = source_text.split("](")
    text = text[1:]
    url_part = url_part[:-1].strip()
    title = ""
    if url_part.endswith('"'):
        # We have a title, parse that out
        url_part, title = url_part[:-1].split('"')
        url_part = url_part.strip()
    if url_part.startswith("<") and url_part.endswith(">"):
        link = url_part[1:-1]
        angle_bracketed = True
    else:
        link = url_part
        angle_bracketed = False

    return ParsedInlineLink(
        link, text, source_text, start.get_offset(), end.get_offset(), angle_bracketed, title
    )


def parse_markdown_automatic_link(inside: Gtk.TextIter) -> Optional[ParsedAutomaticLink]:
    """Parse URL from a known markdown automatic link.

    :param Gtk.TextIter inside: Iter inside inline link
    :return: Object with link details, optional
    :rtype: ParsedAutomaticLink
    """
    start, end = get_iters_at_sourceview_context_class_extents("automatic-link", inside)
    if not start or not end:
        return None
    link = start.get_text(end)
    if link.startswith("<") and link.endswith(">"):
        return ParsedAutomaticLink(link[1:-1], start.get_offset(), end.get_offset(), link)
    else:
        # Failure very unlikely as we depend on the GtkSourceView language parsing to reach
        # here
        logging.warning("Failed to parse automatic-link")
        return None


def get_iters_at_sourceview_context_class_extents(
    cls: str, inside: Gtk.TextIter
) -> tuple[Optional[Gtk.TextIter], Optional[Gtk.TextIter]]:
    """Get text iterators at the extents of a context class.

    :param str cls: Context class name
    :param Gtk.TextIter: Iterator inside class
    :return: The iters
    :rtype: tuple[Optional[Gtk.TextIter], Optional[Gtk.TextIter]]
    """
    start = inside.copy()
    if not iter_backward_to_context_class_start(start, cls):
        logging.warning("Backward to start of '{cls}' failed")
        return (None, None)
    end = start.copy()
    if not iter_forward_to_context_class_removed(end, cls):
        logging.warning("Forward to end of '{cls}' failed")
        return (None, None)
    return (start, end)


def iter_backward_to_context_class_start(location: Gtk.TextIter, cls: str) -> bool:
    """Move text iterator back to start of context class.

    :param Gtk.TextIter location: Iterator inside class
    :param str cls: Context class name
    :return: Success
    :rtype: bool
    """
    buffer: GtkSource.Buffer = location.get_buffer()
    if cls not in buffer.get_context_classes_at_iter(location):
        logging.debug(f"Not in '{cls}' at start to move backward")
        return False

    while not location.is_start():
        location.backward_char()
        if cls not in buffer.get_context_classes_at_iter(location):
            location.forward_char()
            return True
    return location.is_start()


def iter_forward_to_context_class_removed(location: Gtk.TextIter, cls: str) -> bool:
    """Move text iterator forward to end of context class.

    :param Gtk.TextIter location: Iterator inside class
    :param str cls: Context class name
    :return: Success
    :rtype: bool
    """
    buffer: GtkSource.Buffer = location.get_buffer()
    if cls not in buffer.get_context_classes_at_iter(location):
        logging.debug(f"Not in '{cls}' at start to move forward")
        return False

    while not location.is_end():
        location.forward_char()
        if cls not in buffer.get_context_classes_at_iter(location):
            return True
    return location.is_end()


def str_is_url(text: str, http_only: bool) -> bool:
    """Get whether provided text is a URL.

    :param str text: The text to check
    :param bool http_only: Whether only http/s URIs will pass
    :return: Whether a URL
    :rtype: bool
    """
    valid = False
    try:
        valid = GLib.uri_is_valid(text, GLib.UriFlags.NONE)
    except GLib.GError:
        pass
    # GLib's Uri.is_valid appears to allow eg. http:someurl.com
    if valid and http_only:
        scheme = GLib.uri_peek_scheme(text)
        if scheme not in ("http", "https") or not text.startswith(f"{scheme}://"):
            valid = False
    return valid
