"""
For internal use only.
"""
from __future__ import annotations

import re
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Dict,
    Iterable,
    Optional,
    Type,
    TypeVar,
    cast,
)

from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import to_formatted_text
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType

if TYPE_CHECKING:
    from jedi import Interpreter

    # See: prompt_toolkit/key_binding/key_bindings.py
    # Annotating these return types as `object` is what works best, because
    # `NotImplemented` is typed `Any`.
    NotImplementedOrNone = object

__all__ = [
    "has_unclosed_brackets",
    "get_jedi_script_from_document",
    "document_is_multiline_python",
    "unindent_code",
]


def has_unclosed_brackets(text: str) -> bool:
    """
    Starting at the end of the string. If we find an opening bracket
    for which we didn't had a closing one yet, return True.
    """
    stack = []

    # Ignore braces inside strings
    text = re.sub(r"""('[^']*'|"[^"]*")""", "", text)  # XXX: handle escaped quotes.!

    for c in reversed(text):
        if c in "])}":
            stack.append(c)

        elif c in "[({":
            if stack:
                if (
                    (c == "[" and stack[-1] == "]")
                    or (c == "{" and stack[-1] == "}")
                    or (c == "(" and stack[-1] == ")")
                ):
                    stack.pop()
            else:
                # Opening bracket for which we didn't had a closing one.
                return True

    return False


def get_jedi_script_from_document(
    document: Document, locals: dict[str, Any], globals: dict[str, Any]
) -> Interpreter:
    import jedi  # We keep this import in-line, to improve start-up time.

    # Importing Jedi is 'slow'.

    try:
        return jedi.Interpreter(
            document.text,
            path="input-text",
            namespaces=[locals, globals],
        )
    except ValueError:
        # Invalid cursor position.
        # ValueError('`column` parameter is not in a valid range.')
        return None
    except AttributeError:
        # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65
        # See also: https://github.com/davidhalter/jedi/issues/508
        return None
    except IndexError:
        # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514
        return None
    except KeyError:
        # Workaroud for a crash when the input is "u'", the start of a unicode string.
        return None
    except Exception:
        # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91
        return None


_multiline_string_delims = re.compile("""[']{3}|["]{3}""")


def document_is_multiline_python(document: Document) -> bool:
    """
    Determine whether this is a multiline Python document.
    """

    def ends_in_multiline_string() -> bool:
        """
        ``True`` if we're inside a multiline string at the end of the text.
        """
        delims = _multiline_string_delims.findall(document.text)
        opening = None
        for delim in delims:
            if opening is None:
                opening = delim
            elif delim == opening:
                opening = None
        return bool(opening)

    if "\n" in document.text or ends_in_multiline_string():
        return True

    def line_ends_with_colon() -> bool:
        return document.current_line.rstrip()[-1:] == ":"

    # If we just typed a colon, or still have open brackets, always insert a real newline.
    if (
        line_ends_with_colon()
        or (
            document.is_cursor_at_the_end
            and has_unclosed_brackets(document.text_before_cursor)
        )
        or document.text.startswith("@")
    ):
        return True

    # If the character before the cursor is a backslash (line continuation
    # char), insert a new line.
    elif document.text_before_cursor[-1:] == "\\":
        return True

    return False


_T = TypeVar("_T", bound=Callable[[MouseEvent], None])


def if_mousedown(handler: _T) -> _T:
    """
    Decorator for mouse handlers.
    Only handle event when the user pressed mouse down.

    (When applied to a token list. Scroll events will bubble up and are handled
    by the Window.)
    """

    def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone:
        if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
            return handler(mouse_event)
        else:
            return NotImplemented

    return cast(_T, handle_if_mouse_down)


_T_type = TypeVar("_T_type", bound=type)


def ptrepr_to_repr(cls: _T_type) -> _T_type:
    """
    Generate a normal `__repr__` method for classes that have a `__pt_repr__`.
    """
    if not hasattr(cls, "__pt_repr__"):
        raise TypeError(
            "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method."
        )

    def __repr__(self: object) -> str:
        assert hasattr(cls, "__pt_repr__")
        return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self)))

    cls.__repr__ = __repr__  # type:ignore
    return cls


def unindent_code(text: str) -> str:
    """
    Remove common leading whitespace when all lines are indented.
    """
    lines = text.splitlines(keepends=True)

    # Look for common prefix.
    common_prefix = _common_whitespace_prefix(lines)

    # Remove indentation.
    lines = [line[len(common_prefix) :] for line in lines]

    return "".join(lines)


def _common_whitespace_prefix(strings: Iterable[str]) -> str:
    """
    Return common prefix for a list of lines.
    This will ignore lines that contain whitespace only.
    """
    # Ignore empty lines and lines that have whitespace only.
    strings = [s for s in strings if not s.isspace() and not len(s) == 0]

    if not strings:
        return ""

    else:
        s1 = min(strings)
        s2 = max(strings)

        for i, c in enumerate(s1):
            if c != s2[i] or c not in " \t":
                return s1[:i]

        return s1
