import logging
import re

from regexploit.found_regex import FoundRegex


def make_token_regex(*token_specification):
    return re.compile("|".join("(?P<%s>%s)" % pair for pair in token_specification))


TOKENS_BASE = make_token_regex(
    ("LINE_COMMENT", r"//"),
    ("MULTILINE_COMMENT", r"/\*"),
    ("INDENT_OR_PREPROCESSOR", r"(?:^|\n)\s*#?"),
    ("SEMI_COLON", r";"),
    ("DOUBLE_QUOTE_CHAR_LITERAL", r"'\\?\"'"),
    ("NEW_REGEX", r"new\s+[\w.]*?Regex\("),
    ("BEGIN_VERBATIM_STRING", r'(\$@|@\$?)"'),
    ("BEGIN_STRING", r'\$?"'),
)
TOKENS_LINE_COMMENT = make_token_regex(("END_COMMENT", "\n"))
TOKENS_MULTILINE_COMMENT = make_token_regex(("END_COMMENT", r"\*/"))
TOKENS_VERBATIM_STRING = make_token_regex(
    ("LITERAL_QUOTE", r'""'),
    ("END_VERBATIM_STRING", r'"'),
)
TOKENS_STRING = make_token_regex(
    ("LITERAL_BACKSLASH", r"\\\\"),
    ("LITERAL_QUOTE", r'\\"'),
    ("END_STRING", '"'),
)
TOKENS_END_NEW_REGEX = make_token_regex(("SEMI_COLON", ";"))


def find_regexes(code):
    code = code.decode("utf-8", "replace")
    cursor: int = 0
    mode: re.Pattern = TOKENS_BASE
    reached_end: bool = False
    inside_new_regex: bool = False
    buffered_regex = None
    interpolated: bool = False  # TODO: interpolated $ strings
    newline_positions = make_lines(code)
    seen_line = 0

    while not reached_end:
        for mo in mode.finditer(code, cursor):
            kind = mo.lastgroup
            value = mo.group()
            # print(kind, value.replace('\n', '\\n'), code[cursor:mo.start()].replace('\n', '\\n'), code[mo.start():mo.end()].replace('\n', '\\n'))
            if kind == "END_COMMENT":
                mode = TOKENS_BASE
                cursor = mo.end()
                break
            elif kind == "LINE_COMMENT":
                mode = TOKENS_LINE_COMMENT
                cursor = mo.end()
                break
            elif kind == "INDENT_OR_PREPROCESSOR":
                if value and value[-1] == "#":  # Preprocessor
                    mode = TOKENS_LINE_COMMENT
                    cursor = mo.end()
                    break
            elif kind == "MULTILINE_COMMENT":
                mode = TOKENS_MULTILINE_COMMENT
                cursor = mo.end()
                break
            elif kind == "SEMI_COLON":
                if inside_new_regex and buffered_regex is not None:
                    char_index, line, string = buffered_regex
                    flag_string = code[char_index : mo.start()]
                    flags = 0
                    # TODO: https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regexoptions
                    if "IgnoreCase" in flag_string:
                        flags |= re.I
                    if "Multiline" in flag_string:
                        flags |= re.M
                    if "IgnorePatternWhitespace" in flag_string:
                        flags |= re.X
                    yield FoundRegex(line, string, flags, True)
                    mode = TOKENS_BASE
                    cursor = mo.end()
                    inside_new_regex = False
                    buffered_regex = None
                    break
                inside_new_regex = False
                buffered_regex = None
            elif kind == "NEW_REGEX":
                inside_new_regex = True
                buffered_regex = None
            elif kind == "BEGIN_VERBATIM_STRING":
                interpolated = "$" in value
                mode = TOKENS_VERBATIM_STRING
                cursor = mo.end()
                break
            elif kind == "BEGIN_STRING":
                interpolated = "$" in value  # noqa: F841
                mode = TOKENS_STRING
                cursor = mo.end()
                break
            elif kind in ["END_VERBATIM_STRING", "END_STRING"]:
                string = code[cursor : mo.start()]
                if kind == "END_STRING":
                    try:
                        string = string.encode().decode("unicode_escape")
                    except UnicodeDecodeError:
                        logging.warning(f"Unable to process: {string}")
                        string = string.encode().decode("utf-8", "replace")
                else:
                    string = string.replace('""', '"')
                line = line_of(cursor, newline_positions, seen_line)
                seen_line = line - 1
                cursor = mo.end()
                if inside_new_regex:
                    buffered_regex = (cursor, line, string)
                    mode = TOKENS_END_NEW_REGEX
                else:
                    flags = (
                        re.X if kind == "END_VERBATIM_STRING" and "\n" in string else 0
                    )
                    yield FoundRegex(line, string, flags, False)
                    mode = TOKENS_BASE
                break
        else:
            reached_end = True


def make_lines(code):
    return [m.start() for m in re.finditer("\n", code)]


def line_of(character_index: int, newline_positions, seen_line: int):
    if not newline_positions:
        return 1
    for line_index, newline_position in enumerate(newline_positions[seen_line:]):
        if character_index < newline_position:
            return line_index + seen_line + 1
    return line_index + seen_line
