"""Classes for representing and interacting with Tcl syntax trees. """


class Visitor:
    """Abstract base class for Visitors that operate on syntax tree."""

    def visit_script(self, script):
        pass

    def visit_comment(self, comment):
        pass

    def visit_command(self, command):
        pass

    def visit_command_sub(self, command_sub):
        pass

    def visit_bare_word(self, word):
        pass

    def visit_braced_word(self, word):
        pass

    def visit_quoted_word(self, word):
        pass

    def visit_compound_bare_word(self, word):
        pass

    def visit_var_sub(self, var_sub):
        pass

    def visit_arg_expansion(self, arg_expansion):
        pass

    def visit_list(self, list):
        pass

    def visit_expression(self, expression):
        pass

    def visit_braced_expression(self, expression):
        pass

    def visit_paren_expression(self, expression):
        pass

    def visit_unary_op(self, unary_op):
        pass

    def visit_binary_op(self, binary_op):
        pass

    def visit_ternary_op(self, ternary_op):
        pass

    def visit_function(self, function):
        pass


class Node:
    """
    Invariants:
    - self.value is some sort of base Python type
    - self.children is a list of Node types
    """

    def __init__(self, *init, pos=None, end_pos=None):
        """pos: line, column of first character of parsed region (1-indexed)
        end_pos: line, column of first character after parsed region (1-indexed)
        """
        self.line = None
        self.col = None
        if pos is not None:
            self.line, self.col = pos
        self.end_pos = end_pos

        self.value = None
        if len(init) > 0 and not isinstance(init[0], Node):
            self.value = init[0]
            init = init[1:]

        if not all(isinstance(v, Node) for v in init):
            raise TypeError("Children must be Node instances")

        self.children = list(init)

    def add(self, node):
        self.children.append(node)

    @property
    def contents(self):
        """This is overloaded by word Nodes that may have concrete contents.

        TODO: I prefer the name value, but that's currently taken...
        """
        return None

    @property
    def contents_pos(self):
        """This is overloaded by word Nodes that may have concrete contents.

        Returns the position at which the contents start.

        TODO: consider combining with with `contents`?
        """
        return None

    def _pos_str(self):
        start_pos_str = "?"
        if self.pos is not None:
            start_pos_str = f"{self.pos[0]}:{self.pos[1]}"
        end_pos_str = "?"
        if self.end_pos is not None:
            end_pos_str = f"{self.end_pos[0]}:{self.end_pos[1]}"

        return f"  # {start_pos_str}-{end_pos_str}"

    def _make_str(self, indent=None, positions=False):
        if indent is not None:
            s = "  " * indent
        else:
            s = ""

        s += self.__class__.__name__
        s += "("

        if self.value:
            s += repr(self.value)
            if self.children:
                s += ", "

        if positions and self.children:
            s += self._pos_str()

        for i, child in enumerate(self.children):
            if indent is not None:
                s += "\n"
            s += child._make_str(
                indent=None if indent is None else indent + 1, positions=positions
            )
            if i < len(self.children) - 1:
                s += ", "
        s += ")"

        if positions and not self.children:
            s += self._pos_str()

        return s

    def pretty(self, positions=False):
        return self._make_str(indent=0, positions=positions)

    def __str__(self):
        return self._make_str()

    def __eq__(self, other):
        if type(self) is not type(other):
            return False

        if self.value != other.value:
            return False

        if len(self.children) != len(other.children):
            return False
        for my_child, other_child in zip(self.children, other.children):
            if my_child != other_child:
                return False

        return True

    def diff(self, other, indent_depth=0):
        lines = []
        indent = "  " * indent_depth

        my_cls = self.__class__.__name__
        other_cls = other.__class__.__name__

        if my_cls != other_cls:
            lines += [f"{indent}-{my_cls}("]
            lines += [f"{indent}+{other_cls}("]
            return lines

        if self.value != other.value:
            lines += [f'{indent}-{my_cls}("{self.value}"']
            lines += [f'{indent}+{other_cls}("{other.value}"']
            return lines

        if len(self.children) != len(other.children):
            my_children = ",".join([
                child.__class__.__name__ for child in self.children
            ])
            other_children = ",".join([
                child.__class__.__name__ for child in other.children
            ])

            lines += [f"{indent}-{my_cls}({my_children})"]
            lines += [f"{indent}+{other_cls}({other_children})"]
            return lines

        if self.value is not None:
            lines += [f"{indent}{my_cls}({self.value}"]
        else:
            lines += [f"{indent}{my_cls}("]

        for my_child, other_child in zip(self.children, other.children):
            lines += my_child.diff(other_child, indent_depth=indent_depth + 1)

        lines += [f"{indent})"]

        return lines

    @property
    def pos(self):
        if self.line is None or self.col is None:
            return None

        return (self.line, self.col)

    def _recurse(self, visitor):
        for child in self.children:
            child.accept(visitor, recurse=True)


class Script(Node):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # hack for spaces-in-braces check
        self.braced = False

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_script(self)


class Comment(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_comment(self)


class Command(Node):
    def __init__(self, routine, *args, pos=None, end_pos=None):
        self.routine = routine
        self.args = args
        super().__init__(routine, *args, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_command(self)


class CommandSub(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_command_sub(self)


class BareWord(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_bare_word(self)

    @property
    def contents(self):
        return self.value

    @property
    def contents_pos(self):
        return self.pos


class BracedWord(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_braced_word(self)

    @property
    def contents(self):
        return self.value

    @property
    def contents_pos(self):
        return (self.line, self.col + 1)


class QuotedWord(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_quoted_word(self)

    @property
    def contents(self):
        """A QuotedWord with concrete contents is put in the tree as
        QuotedWord(BareWord()), so return contents of its child."""
        if len(self.children) > 1:
            return None
        if len(self.children) == 0:
            # weird special case to handle blank quoted string ("") - it doesn't
            # make sense to put in a child, but setting/returning the value is
            # also inconsistent
            return ""

        return self.children[0].contents

    @property
    def contents_pos(self):
        if self.contents is None:
            return None
        return (self.line, self.col + 1)


class CompoundBareWord(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_compound_bare_word(self)


class VarSub(Node):
    def __init__(self, *args, braced=False, **kwargs):
        self.braced = braced
        return super().__init__(*args, **kwargs)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_var_sub(self)


class ArgExpansion(Node):
    def __init__(self, list, pos=None, end_pos=None):
        self.list = list
        super().__init__(list, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_arg_expansion(self)


class List(Node):
    """This Node currently exists exclusively for implementing the switch
    command in a way that facilitates style checks. Might be nice to find
    another way to handle this that doesn't require a special Node."""

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_list(self)


class Expression(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_expression(self)


class BracedExpression(Node):
    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_braced_expression(self)


class ParenExpression(Node):
    def __init__(self, body: Expression, pos=None, end_pos=None):
        self.body = body
        super().__init__(body, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_paren_expression(self)


class UnaryOp(Node):
    def __init__(self, operator, operand, pos=None, end_pos=None):
        self.operator = operator
        self.operand = operand
        super().__init__(operator, operand, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_unary_op(self)


class BinaryOp(Node):
    def __init__(self, operator1, operand, operator2, pos=None, end_pos=None):
        self.operator1 = operator1
        self.operand = operand
        self.operator2 = operator2
        super().__init__(operator1, operand, operator2, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_binary_op(self)


class TernaryOp(Node):
    def __init__(self, condition, question, true, colon, false, pos=None, end_pos=None):
        self.condition = condition
        self.question = question
        self.true = true
        self.colon = colon
        self.false = false

        super().__init__(
            condition, question, true, colon, false, pos=pos, end_pos=end_pos
        )

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_ternary_op(self)


class Function(Node):
    def __init__(self, name, *args, pos=None, end_pos=None):
        self.name = name
        self.args = args
        super().__init__(name, *args, pos=pos, end_pos=end_pos)

    def accept(self, visitor, recurse=False):
        if recurse:
            self._recurse(visitor)
        visitor.visit_function(self)
