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

from gi.repository import Gtk, GtkSource

import re
from typing import Callable, Optional

from iotas.ordered_list_utils import (
    check_line_for_ordered_list_item,
    calculate_ordered_list_index,
    format_ordered_list_item,
)

# k is the token, v is the continuation
_BULLET_LIST_TOKENS = {
    "- [ ] ": "- [ ] ",
    "- [x] ": "- [ ] ",
    "+ [ ] ": "+ [ ] ",
    "+ [x] ": "+ [ ] ",
    "* [ ] ": "* [ ] ",
    "* [x] ": "* [ ] ",
    "- ": "- ",
    "+ ": "+ ",
    "* ": "* ",
}


def check_and_extend_list(buffer: GtkSource.Buffer) -> bool:
    """Extend any list after a newline is inserted.

    :param GtkSource.Buffer buffer: Buffer to work on
    """
    handled = False
    if buffer.get_has_selection():
        insert_iter, _unused = buffer.get_selection_bounds()
    else:
        mark = buffer.get_insert()
        insert_iter = buffer.get_iter_at_mark(mark)

    line_start = insert_iter.copy()
    line_start.set_line_offset(0)
    previous_line = line_start.get_text(insert_iter)

    def check_and_delete_selection(insert_iter, line_start) -> tuple[Gtk.TextIter, Gtk.TextIter]:
        if buffer.get_has_selection():
            buffer.delete_selection()
            mark = buffer.get_insert()
            insert_iter = buffer.get_iter_at_mark(mark)
            line_start = insert_iter.copy()
            line_start.set_line_offset(0)
        return insert_iter, line_start

    # TODO look at changing to use the GtkSourceLanguage context?
    regex_match = _check_line_for_bullet_list_item(previous_line)
    if regex_match is not None:
        insert_iter, line_start = check_and_delete_selection(insert_iter, line_start)
        handled = _extend_bullet_list(buffer, regex_match.group(), insert_iter, line_start)
    else:
        regex_match = check_line_for_ordered_list_item(previous_line)
        if regex_match is not None:
            insert_iter, line_start = check_and_delete_selection(insert_iter, line_start)
            handled = _extend_ordered_list(buffer, regex_match.groups(), insert_iter, line_start)

    return handled


def increase_indentation(buffer: GtkSource.Buffer) -> None:
    """Increase indentation on a buffer line or selection.

    :param Gtk.Buffer buffer: Buffer to work on
    """
    _handle_tab(buffer, True)


def decrease_indentation(buffer: GtkSource.Buffer) -> None:
    """Decrease indentation on a buffer line or selection.

    :param Gtk.Buffer buffer: Buffer to work on
    """
    _handle_tab(buffer, False)


def _handle_tab(buffer: GtkSource.Buffer, increase: bool) -> None:
    buffer.begin_user_action()
    if buffer.get_has_selection():
        begin, end = buffer.get_selection_bounds()
        begin.order(end)
        multi_line = "\n" in buffer.get_text(begin, end, False)
        if multi_line:
            line_iter = begin.copy()
            line_mark = buffer.create_mark(None, line_iter)
            end_mark = buffer.create_mark(None, end)
            while line_iter.compare(end) < 0:
                _modify_single_line_indent(buffer, line_iter, increase, multi_line)
                line_iter = buffer.get_iter_at_mark(line_mark)
                line_iter.forward_line()
                buffer.delete_mark(line_mark)
                line_mark = buffer.create_mark(None, line_iter)
                end = buffer.get_iter_at_mark(end_mark)
            buffer.delete_mark(line_mark)
            buffer.delete_mark(end_mark)
        else:
            _modify_single_line_indent(buffer, begin, increase, multi_line)
    else:
        mark = buffer.get_insert()
        begin = buffer.get_iter_at_mark(mark)
        _modify_single_line_indent(buffer, begin, increase, False)
    buffer.end_user_action()


def _extend_bullet_list(
    buffer: GtkSource.Buffer,
    matched_list_item: str,
    insert_iter: Gtk.TextIter,
    line_start: Gtk.TextIter,
) -> bool:
    previous_line = line_start.get_text(insert_iter)

    # If the list already continues after our newline, don't add the next item markup.
    # Caters for accidentally deleting a line break in the middle of a list and then
    # pressing enter to revert that.
    cur_line_end = insert_iter.copy()
    if not cur_line_end.ends_line():
        cur_line_end.forward_to_line_end()
    cur_line_text = insert_iter.get_text(cur_line_end)
    if _check_line_for_bullet_list_item(cur_line_text) is not None:
        return False

    def generate_sequence_previous_item() -> Optional[str]:
        return matched_list_item

    buffer.begin_user_action()
    empty_list_line = _inserted_empty_item_at_end_of_list(
        previous_line, matched_list_item, line_start, generate_sequence_previous_item
    )
    if empty_list_line:
        # An empty list line has been entered, remove it from the list end
        buffer.delete(line_start, insert_iter)
        buffer.insert_at_cursor("\n")
    else:
        dict_key = matched_list_item.lstrip()
        spacing = matched_list_item[0 : len(matched_list_item) - len(dict_key)]
        new_entry = f"\n{spacing}{_BULLET_LIST_TOKENS[dict_key]}"
        buffer.insert_at_cursor(new_entry)
    buffer.end_user_action()
    return True


def _extend_ordered_list(
    buffer: GtkSource.Buffer,
    regex_groups: tuple,
    insert_iter: Gtk.TextIter,
    line_start: Gtk.TextIter,
) -> bool:
    spacing = regex_groups[0]
    index = regex_groups[1]
    marker = regex_groups[2]
    previous_line = line_start.get_text(insert_iter)

    def generate_sequence_previous_item() -> Optional[str]:
        # Verify there's more than one list item. This allows inserting lines with list start
        # tokens and nothing else.
        previous_index = calculate_ordered_list_index(index, -1)
        if previous_index is None:
            return None
        else:
            return format_ordered_list_item(spacing, previous_index, marker)

    buffer.begin_user_action()
    empty_list_line = _inserted_empty_item_at_end_of_list(
        previous_line, "".join(regex_groups), line_start, generate_sequence_previous_item
    )
    if empty_list_line:
        # An empty list line has been entered, remove it from the list end
        buffer.delete(line_start, insert_iter)
        buffer.insert_at_cursor("\n")
        buffer.end_user_action()
    else:
        sequence_next = calculate_ordered_list_index(index, 1)
        # Handle value of Z, ending sequence
        if sequence_next is None:
            buffer.end_user_action()
            return False
        new_entry = "\n" + format_ordered_list_item(spacing, sequence_next, marker)
        buffer.insert(insert_iter, new_entry)

        _increment_any_following_ordered_list_items(
            buffer, insert_iter, sequence_next, marker, spacing
        )

        buffer.end_user_action()

    return True


def _increment_any_following_ordered_list_items(
    buffer: GtkSource.Buffer, insert_iter: Gtk.TextIter, index: str, marker: str, spacing: str
) -> None:
    cursor_mark = buffer.create_mark(None, insert_iter)

    current_entry = format_ordered_list_item(spacing, index, marker)

    while True:
        # Check if the next line continues the list
        if not insert_iter.forward_line():
            break
        line_end = insert_iter.copy()
        line_end.forward_to_line_end()
        text = buffer.get_text(insert_iter, line_end, include_hidden_chars=False)
        if not text.startswith(current_entry):
            break

        next_index = calculate_ordered_list_index(index, 1)
        # Handle value of Z, ending sequence
        if not next_index:
            break
        index = next_index
        entry_next = format_ordered_list_item(spacing, index, marker)

        # Delete existing
        end = insert_iter.copy()
        end.forward_chars(len(current_entry))
        buffer.delete(insert_iter, end)

        # Insert new
        buffer.insert(insert_iter, entry_next)

        current_entry = entry_next

    buffer.place_cursor(buffer.get_iter_at_mark(cursor_mark))
    buffer.delete_mark(cursor_mark)


def _modify_single_line_indent(
    buffer: GtkSource.Buffer, location: Gtk.TextIter, increase: bool, multi_line: bool
) -> None:
    line_start = location.copy()
    line_start.set_line_offset(0)
    line_end = location.copy()
    if not line_end.ends_line():
        line_end.forward_to_line_end()
    line_contents = buffer.get_text(line_start, line_end, False)

    # Don't process empty lines
    if multi_line and line_contents == "":
        return

    list_item = (
        _check_line_for_bullet_list_item(line_contents) is not None
        or check_line_for_ordered_list_item(line_contents) is not None
    )
    indent_chars = "  " if list_item else "\t"

    if increase:
        if multi_line or list_item:
            buffer.insert(line_start, indent_chars)
        else:
            buffer.insert(location, indent_chars)
    else:
        if line_contents.startswith(indent_chars):
            end_deletion = line_start.copy()
            end_deletion.forward_chars(len(indent_chars))
            buffer.delete(line_start, end_deletion)


def _inserted_empty_item_at_end_of_list(
    line_text: str,
    matched_list_item: str,
    previous_line_start: Gtk.TextIter,
    fetch_sequence_previous: Callable[[], Optional[str]],
) -> bool:
    """Determine whether we have inserted an empty item at the end of a list

    The list needs to have at least one previous item.

    :param str line_text: The full line text
    :param str matched_list_item: Our matched list marker
    :param Gtk.TextIter previous_line_start: The start of the previous line
    :param Callable[], Optional[str]] fetch_sequence_previous: A function to fetch a previous
        line's marker
    :return: Whether we've inserted an empty line
    :rtype: bool
    """

    # Check if entered line contains only the list item markup
    if line_text.strip() != matched_list_item.strip():
        return False

    # Fetch what a previous item in the sequence would have been, for the check below. The
    # callable is used to prevent calculating this before we even know if match above will
    # pass (while sharing logic for bullet and ordered lists).
    sequence_previous_item = fetch_sequence_previous()
    if sequence_previous_item is None:
        return False

    # Verify there's more than one list item. This allows inserting lines with list start
    # tokens and nothing else.
    two_prev_start = previous_line_start.copy()
    two_prev_start.backward_line()
    two_prev_start.set_line_offset(0)
    two_prev_line = two_prev_start.get_text(previous_line_start)
    return two_prev_line.startswith(sequence_previous_item)


def _check_line_for_bullet_list_item(linetext: str) -> Optional[re.Match[str]]:
    # Avoid misidentifying our horizontal rule markup as an ordered list line. This should be made
    # to handle other valid horizontal rule forms, and be integrated into the regex below.
    if linetext == "- - -":
        return None

    term = r"^\s*("
    escaped_tokens = []
    for token in _BULLET_LIST_TOKENS.keys():
        escaped_tokens.append(re.escape(token))
    term += "|".join(escaped_tokens)
    term += ")"
    return re.search(term, linetext)
