import os
from functools import lru_cache
from typing import Optional, Union, Tuple, List, Iterator
from robotframework_ls.impl.protocols import (
    IRobotVariableMatch,
    IRobotToken,
    ICompletionContext,
    AbstractVariablesCollector,
    IVariableFound,
)
from robocorp_ls_core.protocols import Sentinel
from robocorp_ls_core.robotframework_log import get_logger
import threading

log = get_logger(__name__)


def extract_variable_base(text: str) -> str:
    """
    Converts something as: "${S_ome.VAR}[foo]" to "S_ome.VAR".
    """
    variable_match = robot_search_variable(text)
    if variable_match is not None:
        base = variable_match.base
        if base is not None:
            return base

    if len(text) >= 3:
        if text.endswith("}") and text[1] == "{":
            return text[2:-1]
    return text


@lru_cache(maxsize=500)
def normalize_variable_name(text: str) -> str:
    """
    Converts something as: "${S_ome.VAR}[foo]" to "some.var".
    """
    base = extract_variable_base(text)

    return base.lower().replace("_", "").replace(" ", "")


@lru_cache(maxsize=200)
def robot_search_variable(text: str) -> Optional[IRobotVariableMatch]:
    """
    Provides the IRobotVariableMatch from a text such as "${S_ome.VAR}[foo]".
    """
    from robot.variables.search import search_variable  # type:ignore

    try:
        variable_match = search_variable(text, identifiers="$@&%", ignore_errors=True)
        return variable_match
    except:
        pass

    return None


def iter_robot_variable_matches(
    string: str,
) -> Iterator[Tuple[IRobotVariableMatch, int]]:
    """
    Provides the IRobotVariableMatch and the relative index for the match in the string.
    """
    # Based on robot.variables.search.VariableIterator
    remaining = string
    relative_index = 0
    while True:
        robot_match = robot_search_variable(remaining)
        if not robot_match:
            break
        yield robot_match, relative_index
        remaining = robot_match.after
        relative_index += len(robot_match.before) + len(robot_match.match)


def find_split_index(string: str) -> int:
    # Based on: robot.utils.escaping.split_from_equals
    # Based on: robot.utils.escaping._find_split_index

    eq_i = string.find("=")
    if eq_i == -1:
        return -1

    return _find_split_index(string, eq_i)


@lru_cache(maxsize=200)
def _find_split_index(string: str, eq_i: int) -> int:
    try:
        variables = tuple(iter_robot_variable_matches(string))
        if not variables and "\\" not in string:
            return eq_i

        relative_index = 0
        for robot_match, relative_index in variables:
            before, string = (
                robot_match.before,
                robot_match.after,
            )
            try:
                return _find_split_index_from_part(before) + relative_index
            except ValueError:
                pass
        return _find_split_index_from_part(string) + relative_index
    except ValueError:
        return -1


def _find_split_index_from_part(string):
    index = 0
    while "=" in string[index:]:
        index += string[index:].index("=")
        if _not_escaping(string[:index]):
            return index
        index += 1
    raise ValueError()


def _not_escaping(name):
    backslashes = len(name) - len(name.rstrip("\\"))
    return backslashes % 2 == 0


class _VariablesCollector(AbstractVariablesCollector):
    def __init__(self):
        self.var_name_to_var_found = {}

    def accepts(self, variable_name: str) -> bool:
        return True

    def on_variable(self, variable_found: IVariableFound):
        from robotframework_ls.impl.text_utilities import normalize_robot_name

        self.var_name_to_var_found[
            normalize_robot_name(variable_found.variable_name)
        ] = variable_found


class ResolveVariablesContext:
    _thread_local = threading.local()

    def __init__(self, completion_context: ICompletionContext):
        self.config = completion_context.config
        self.completion_context = completion_context

    @property
    def doc_path(self) -> str:
        return self.completion_context.doc.path

    def _resolve_environment_variable(self, var_name, value_if_not_found, log_info):
        ret = os.environ.get(var_name, Sentinel.SENTINEL)
        if ret is Sentinel.SENTINEL:
            log.info(*log_info)
            return value_if_not_found
        return ret

    def _convert_robot_variable(self, var_name, value_if_not_found):
        from robotframework_ls.impl.text_utilities import normalize_robot_name

        normalized = normalize_robot_name(var_name)

        if normalized == "curdir":
            return os.path.dirname(self.doc_path)

        completion_context = self.completion_context
        found = completion_context.get_doc_normalized_var_name_to_var_found().get(
            normalized
        )

        if found is not None:
            return found.variable_value

        found = completion_context.get_settings_normalized_var_name_to_var_found().get(
            normalized
        )
        if found is not None:
            return found.variable_value

        found = completion_context.get_arguments_files_normalized_var_name_to_var_found().get(
            normalized
        )
        if found is not None:
            return found.variable_value

        found = completion_context.get_builtins_normalized_var_name_to_var_found(
            True
        ).get(normalized)
        if found is not None:
            return found.variable_value

        # At this point we have to do a search in the imports to check whether
        # maybe the variable is defined in a dependency. Note that we can get
        # into a recursion here as we may need to resolve other variables
        # in order to get here.
        # It goes something like:
        # Resolve variable -> some variable
        # collect dependencies (which may need to resolve variables again).
        # In this case we have a thread-local variable to know whether this is the
        # case and if it is bail out.
        try:
            resolve_info = self._thread_local.resolve_info
        except:
            resolve_info = self._thread_local.resolve_info = set()

        if var_name in resolve_info:
            log.info("Unable to find robot variable: %s", var_name)
            return value_if_not_found

        else:
            try:
                resolve_info.add(var_name)

                from robotframework_ls.impl.variable_completions import (
                    collect_global_variables_from_document_dependencies,
                )

                collector = _VariablesCollector()

                collect_global_variables_from_document_dependencies(
                    completion_context, collector
                )

                found = collector.var_name_to_var_found.get(normalized)
                if found is not None:
                    return found.variable_value

                log.info("Unable to find robot variable: %s", var_name)
                return value_if_not_found
            finally:
                resolve_info.discard(var_name)

    def _convert_environment_variable(self, var_name, value_if_not_found):
        from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_PYTHON_ENV

        if self.config is None:
            value = self._resolve_environment_variable(
                var_name,
                value_if_not_found,
                (
                    "Config not available while trying to convert environment variable: %s",
                    var_name,
                ),
            )
        else:
            robot_env_vars = self.config.get_setting(OPTION_ROBOT_PYTHON_ENV, dict, {})
            value = robot_env_vars.get(var_name, Sentinel.SENTINEL)
            if value is Sentinel.SENTINEL:
                value = self._resolve_environment_variable(
                    var_name,
                    value_if_not_found,
                    ("Unable to find environment variable: %s", var_name),
                )

        value = str(value)
        return value

    def token_value_resolving_variables(self, token: IRobotToken) -> str:
        return self.token_value_and_unresolved_resolving_variables(token)[0]

    def token_value_and_unresolved_resolving_variables(
        self, token: IRobotToken
    ) -> Tuple[str, Tuple[IRobotToken, ...]]:
        from robotframework_ls.impl import ast_utils

        robot_token: IRobotToken
        if isinstance(token, str):
            robot_token = ast_utils.create_token(token)
        else:
            robot_token = token

        try:
            tokenized_vars = ast_utils.tokenize_variables(robot_token)
        except:
            return robot_token.value, ()  # Unable to tokenize

        unresolved: List[IRobotToken] = []

        parts: List[str] = []
        tok: IRobotToken
        value: str
        for tok in tokenized_vars:
            if tok.type == tok.NAME:
                parts.append(str(tok))

            elif tok.type == tok.VARIABLE:
                value = str(tok)

                convert_with = None
                if value.startswith("${") and value.endswith("}"):
                    convert_with = self._convert_robot_variable

                elif value.startswith("%{") and value.endswith("}"):
                    convert_with = self._convert_environment_variable

                if convert_with is None:
                    log.info("Cannot resolve variable: %s", value)
                    unresolved.append(tok)
                    parts.append(value)  # Leave unresolved.
                else:
                    value = value[2:-1]
                    converted = convert_with(value, None)
                    if converted is None:
                        log.info("Cannot resolve variable: %s", value)
                        unresolved.append(tok)
                        parts.append(value)
                    else:
                        parts.append(converted)

        joined_parts = "".join(parts)
        return joined_parts, tuple(unresolved)
