from collections import defaultdict
from contextlib import contextmanager
from textwrap import indent
from typing import Dict, List, Optional, Tuple
from gersemi.ast_helpers import is_line_comment_in
from gersemi.configuration import Indent, ListExpansion, OutcomeConfiguration, Tabs
from gersemi.types import Nodes, Tree
from gersemi.warnings import FormatterWarnings, UnknownCommandWarning


def get_indent(indent_type: Indent) -> str:
    if isinstance(indent_type, Tabs):
        return "\t"
    return " " * indent_type


class WontFit(Exception):
    pass


class BaseDumper:  # pylint: disable=too-many-instance-attributes
    def __init__(self, configuration: OutcomeConfiguration):
        self.width = configuration.line_length
        self.indent_type = configuration.indent
        self._indent_symbol = get_indent(self.indent_type)
        self.indent_level = 0
        self.favour_expansion = False
        self.unknown_commands_used: Dict[str, List[Tuple[int, int]]] = defaultdict(list)
        self.list_expansion = configuration.list_expansion
        self.sort_order = configuration.sort_order

    def __default__(self, tree: Tree):
        return "".join(self.visit_children(tree))

    def visit(self, tree):
        f = getattr(self, tree.data, self.__default__)
        return f(tree)

    def visit_children(self, tree):
        yield from (
            self.visit(child) if isinstance(child, Tree) else str(child)
            for child in tree.children
        )

    @property
    def indent_symbol(self):
        return self._indent_symbol * self.indent_level

    def _indent(self, text: str):
        return indent(text, self.indent_symbol)

    def _single_line_helper(self, children, offset):
        width = offset
        for c in children:
            if isinstance(c, Tree):
                if is_line_comment_in(c):
                    raise WontFit()

                result = self.visit(c)
            else:
                result = str(c)

            if "\n" in result:
                raise WontFit()

            width += len(result)
            if width > self.width:
                raise WontFit()

            yield result
            width += 1

    def _try_to_format_into_single_line(
        self, children: Nodes, prefix: str = "", postfix: str = ""
    ) -> Optional[str]:
        if self.favour_expansion:
            return None

        try:
            reserved_space = len(prefix) + len(postfix) + len(self.indent_symbol)
            with self.not_indented():
                formatted = " ".join(self._single_line_helper(children, reserved_space))
            return f"{self.indent_symbol}{prefix}{formatted}{postfix}"
        except WontFit:
            return None

    @contextmanager
    def with_indent_level(self, indent_level):
        old_indent_level = self.indent_level
        try:
            self.indent_level = indent_level
            yield self
        finally:
            self.indent_level = old_indent_level

    def dedented(self):
        return self.with_indent_level(self.indent_level - 1)

    def indented(self):
        return self.with_indent_level(self.indent_level + 1)

    def not_indented(self):
        return self.with_indent_level(0)

    @contextmanager
    def select_expansion_strategy(self):
        old = self.favour_expansion
        try:
            self.favour_expansion = self.list_expansion == ListExpansion.FavourExpansion
            yield self
        finally:
            self.favour_expansion = old

    @contextmanager
    def select_inlining_strategy(self):
        old = self.favour_expansion
        try:
            self.favour_expansion = False
            yield self
        finally:
            self.favour_expansion = old

    def format_command_name(self, identifier):
        identifier = str(identifier)
        try:
            canonical_name = self._canonical_name
        except AttributeError:
            if "@" in identifier:
                return identifier

            return identifier.lower()

        if canonical_name.strip().lower() != identifier.strip().lower():
            raise RuntimeError

        return canonical_name

    def _record_unknown_command(self, command):
        self.unknown_commands_used[str(command)].append((command.line, command.column))

    def get_warnings(self) -> FormatterWarnings:
        if len(self.unknown_commands_used) == 0:
            return []

        return [
            UnknownCommandWarning(command_name=name, positions=positions)
            for name, positions in self.unknown_commands_used.items()
        ]
