import re
from talon import ctrl, ui, Module, Context, actions, clip
import itertools
from typing import Union

ctx = Context()
mod = Module()


text_navigation_max_line_search = mod.setting(
    "text_navigation_max_line_search",
    type=int,
    default=10,
    desc="the maximum number of rows that will be included in the search for the keywords above and below in <user direction>",
)

mod.list(
    "navigation_action",
    desc="actions to perform, for instance move, select, cut, etc",
)
mod.list(
    "before_or_after",
    desc="words to indicate if the cursor should be moved before or after a given reference point",
)
mod.list(
    "navigation_target_name",
    desc="names for regular expressions for common things to navigate to, for instance a word with or without underscores",
)

ctx.lists["self.navigation_action"] = {
    "move": "GO",
    "extend": "EXTEND",
    "select": "SELECT",
    "clear": "DELETE",
    "cut": "CUT",
    "copy": "COPY",
}
ctx.lists["self.before_or_after"] = {
    "before": "BEFORE",
    "after": "AFTER",
    # DEFAULT is also a valid option as input for this capture, but is not directly accessible for the user.
}
navigation_target_names = {
    "word": r"\w+",
    "small": r"[A-Z]?[a-z0-9]+",
    "big": r"[\S]+",
    "parens": r'\((.*?)\)',
    "squares": r'\[(.*?)\]',
    "braces": r'\{(.*?)\}',
    "quotes": r'\"(.*?)\"',
    "angles": r'\<(.*?)\>',
    #"single quotes": r'\'(.*?)\'',
    "all": r'(.+)',
    "method": r'\w+\((.*?)\)',
    "constant": r'[A-Z_][A-Z_]+'
}
ctx.lists["self.navigation_target_name"] = navigation_target_names

@mod.capture(rule="<user.any_alphanumeric_key> | {user.navigation_target_name} | phrase <user.text>")
def navigation_target(m) -> re.Pattern:
    """A target to navigate to. Returns a regular expression."""
    if hasattr(m, 'any_alphanumeric_key'):
        return re.compile(re.escape(m.any_alphanumeric_key), re.IGNORECASE)
    if hasattr(m, 'navigation_target_name'):
        return re.compile(m.navigation_target_name)
    return re.compile(re.escape(m.text), re.IGNORECASE)

@mod.action_class
class Actions:
    def navigation(
            navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
            direction: str,         # up, down, left, right
            navigation_target_name: str,
            before_or_after: str,   # BEFORE, AFTER, DEFAULT
            regex: re.Pattern,
            occurrence_number: int,
    ):
        """Navigate in `direction` to the occurrence_number-th time that `regex` occurs, then execute `navigation_action` at the given `before_or_after` position."""
        direction = direction.upper()
        navigation_target_name = re.compile((navigation_target_names["word"] if (navigation_target_name == "DEFAULT") else navigation_target_name))
        function = navigate_left if direction in ("UP", "LEFT") else navigate_right
        function(navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction)

    def navigation_by_name(
            navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
            direction: str,         # up, down, left, right
            before_or_after: str,   # BEFORE, AFTER, DEFAULT
            navigation_target_name: str, # word, big, small
            occurrence_number: int,
    ):
        """Like user.navigation, but to a named target."""
        r = re.compile(navigation_target_names[navigation_target_name])
        actions.user.navigation(navigation_action, direction, "DEFAULT", before_or_after, r, occurrence_number)

def get_text_left():
    actions.edit.extend_line_start()
    text = actions.edit.selected_text()
    actions.edit.right()
    return text


def get_text_right():
    actions.edit.extend_line_end()
    text = actions.edit.selected_text()
    actions.edit.left()
    return text


def get_text_up():
    actions.edit.up()
    actions.edit.line_end()
    for j in range(0, text_navigation_max_line_search.get()):
        actions.edit.extend_up()
    actions.edit.extend_line_start()
    text = actions.edit.selected_text()
    actions.edit.right()
    return text


def get_text_down():
    actions.edit.down()
    actions.edit.line_start()
    for j in range(0, text_navigation_max_line_search.get()):
        actions.edit.extend_down()
    actions.edit.extend_line_end()
    text = actions.edit.selected_text()
    actions.edit.left()
    return text


def get_current_selection_size():
    return len(actions.edit.selected_text())


def go_right(i):
    for j in range(0, i):
        actions.edit.right()


def go_left(i):
    for j in range(0, i):
        actions.edit.left()


def extend_left(i):
    for j in range(0, i):
        actions.edit.extend_left()


def extend_right(i):
    for j in range(0, i):
        actions.edit.extend_right()


def select(direction, start, end, length):
    if direction == "RIGHT" or direction == "DOWN":
        go_right(start)
        extend_right(end - start)
    else:
        go_left(length - end)
        extend_left(end - start)


def navigate_left(
    navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
):
    current_selection_length = get_current_selection_size()
    if current_selection_length > 0:
        actions.edit.right()
    text = get_text_left() if direction == "LEFT" else get_text_up()
    # only search in the text that was not selected
    subtext = (
        text if current_selection_length <= 0 else text[:-current_selection_length]
    )
    match = match_backwards(regex, occurrence_number, subtext)
    if match == None:
        # put back the old selection, if the search failed
        extend_left(current_selection_length)
        return
    start = match.start()
    end = match.end()
    handle_navigation_action(
        navigation_action, navigation_target_name, before_or_after, direction, text, start, end
    )


def navigate_right(
    navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
):
    current_selection_length = get_current_selection_size()
    if current_selection_length > 0:
        actions.edit.left()
    text = get_text_right() if direction == "RIGHT" else get_text_down()
    # only search in the text that was not selected
    sub_text = text[current_selection_length:]
    # pick the next interrater, Skip n number of occurrences, get an iterator given the Regex
    match = match_forward(regex, occurrence_number, sub_text)
    if match == None:
        # put back the old selection, if the search failed
        extend_right(current_selection_length)
        return
    start = current_selection_length + match.start()
    end = current_selection_length + match.end()
    handle_navigation_action(
        navigation_action, navigation_target_name, before_or_after, direction, text, start, end
    )


def handle_navigation_action(
    navigation_action, navigation_target_name, before_or_after, direction, text, start, end
):
    length = len(text)
    if navigation_action == "GO":
        handle_move(direction, before_or_after, start, end, length)
    elif navigation_action == "SELECT":
        handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
    elif navigation_action == "DELETE":
        handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
        actions.edit.delete()
    elif navigation_action == "CUT":
        handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
        actions.edit.cut()
    elif navigation_action == "COPY":
        handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
        actions.edit.copy()
    elif navigation_action == "EXTEND":
        handle_extend(before_or_after, direction, start, end, length)


def handle_select(navigation_target_name, before_or_after, direction, text, start, end, length):
    if before_or_after == "BEFORE":
        select_left = length - start
        text_left = text[:-select_left]
        match2 = match_backwards(navigation_target_name, 1, text_left)
        if match2 == None:
            end = start
            start = 0
        else:
            start = match2.start()
            end = match2.end()
    elif before_or_after == "AFTER":
        text_right = text[end:]
        match2 = match_forward(navigation_target_name, 1, text_right)
        if match2 == None:
            start = end
            end = length
        else:
            start = end + match2.start()
            end = end + match2.end()
    select(direction, start, end, length)


def handle_move(direction, before_or_after, start, end, length):
    if direction == "RIGHT" or direction == "DOWN":
        if before_or_after == "BEFORE":
            go_right(start)
        else:
            go_right(end)
    else:
        if before_or_after == "AFTER":
            go_left(length - end)
        else:
            go_left(length - start)


def handle_extend(before_or_after, direction, start, end, length):
    if direction == "RIGHT" or direction == "DOWN":
        if before_or_after == "BEFORE":
            extend_right(start)
        else:
            extend_right(end)
    else:
        if before_or_after == "AFTER":
            extend_left(length - end)
        else:
            extend_left(length - start)


def match_backwards(regex, occurrence_number, subtext):
    try:
        match = list(regex.finditer(subtext))[-occurrence_number]
        return match
    except IndexError:
        return


def match_forward(regex, occurrence_number, sub_text):
    try:
        match = next(
            itertools.islice(regex.finditer(sub_text), occurrence_number - 1, None)
        )
        return match
    except StopIteration:
        return None
