from pyparsing import ParseResults

from .condition_node import (BinaryConditionalOperator,
                                          BinaryConditionNode, ConditionNode,
                                          IdentityConditionNode,
                                          UnaryConditionalOperator,
                                          UnaryConditionNode)

from .grammars.proto_grammar import ProtoGrammar
from .node import (AssignNode, CrossJoinNode, DefinitionNode, DifferenceNode,
                   FullOuterJoinNode, FunctionalDependencyNode,
                   InclusionEquivalenceNode, InclusionSubsumptionNode,
                   IntersectNode, LeftOuterJoinNode, MultivaluedDependencyNode,
                   NaturalJoinNode, PrimaryKeyNode, ProjectNode, RelationNode,
                   RenameNode, RightOuterJoinNode, SelectNode, ThetaJoinNode,
                   UnionNode)
from .schema import Schema


class TreeBRD:
    """
    A Tree Builder for Relational (Algebra) Data. TreeBRD is a factory class
    that builds forests of relational algebra syntax trees. STARBuilder
    """

    grammar: ProtoGrammar

    def __init__(self, grammar: ProtoGrammar):
        self.grammar = grammar

    def build(self, instring, schema):
        ra = self.grammar.parse(instring)
        _schema = Schema(schema)
        return [self.to_node(statement, _schema) for statement in ra[:]]

    def to_node(self, exp: ParseResults, schema):
        """
        Return a Node that is the root of the parse tree for the the specified
        expression.

        :param exp: A list that represents a relational algebra expression.
        Assumes that this list was generated by pyparsing.
        :param schema: A dictionary of relation names to attribute names used
        for verification and generating attributes.
        :return: A Node.
        """

        # Check if this is a dependency statement
        if self.is_dependency_statement(exp):
            return self.create_dependency_node(exp, schema)

        # A relation node.
        if len(exp) == 1 and isinstance(exp[0], str):
            name = exp[0]
            node = RelationNode(name=name, schema=schema)

        # An expression.
        elif len(exp) == 1 and isinstance(exp[0], ParseResults):
            node = self.to_node(exp[0], schema)

        # Unary operators.
        elif isinstance(exp[0], str) and self.grammar.is_unary(exp[0]):
            child = self.to_node(exp[2:], schema)
            node = self.create_unary_node(
                operator=exp[0], child=child, param=exp[1], schema=schema
            )

        # Definition.
        elif len(exp) == 2 and isinstance(exp[0], str) and isinstance(exp[1], ParseResults):
            name = exp[0]
            attributes = list(exp[1])  # Convert ParseResults to list
            node = DefinitionNode(name=name, attributes=attributes, schema=schema)

        # Assignment.
        elif exp[1] is self.grammar.syntax.assign_op:
            child = self.to_node(exp[2:], schema)
            node = self.create_unary_node(
                operator=exp[1], child=child, param=exp[0], schema=schema
            )

        # Binary operators.
        elif self.grammar.is_binary(exp[1]):
            # Pyparsing will put different operators with the same precedence
            # in the same list. This can be a problem when we mix operators with
            # and without parameters (for example join). We avoid this below
            # and build from right to left, to create the correct syntax tree.
            if isinstance(exp[-2], str):
                # Operator without parameters
                op_pos = -2
                param = None
            else:
                op_pos = -3
                param = exp[-2]

            operator = exp[op_pos]
            left = self.to_node(exp[:op_pos], schema)
            right = self.to_node(exp[-1], schema)
            node = self.create_binary_node(
                operator=operator, left=left, right=right, param=param
            )

        else:
            raise ValueError

        return node

    def create_condition_node(self, conditions: ParseResults) -> ConditionNode:
        if isinstance(conditions, str):
            # Attribute Condition Node
            return IdentityConditionNode(conditions)

        elif len(conditions) == 2:
            # Unary Condition Node
            op, right = conditions
            right_node = self.create_condition_node(right)

            return UnaryConditionNode(
                op=UnaryConditionalOperator.from_syntax(self.grammar.syntax, op),
                child=right_node,
            )

        elif len(conditions) == 3:
            # Binary Condition Node
            left, op, right = conditions
            left_node = self.create_condition_node(left)
            right_node = self.create_condition_node(right)

            return BinaryConditionNode(
                op=BinaryConditionalOperator.from_syntax(self.grammar.syntax, op),
                left=left_node,
                right=right_node,
            )

        raise ValueError

    def create_unary_node(
        self, operator, child, param: ParseResults | None = None, schema=None
    ):
        """
        Return a Unary Node whose type depends on the specified operator.

        :param schema:
        :param child:
        :param operator: A relational algebra operator (see constants.py)
        :param param: A list of parameters for the operator.
        :return: A Unary Node.
        """

        if operator == self.grammar.syntax.select_op:
            condition = self.create_condition_node(param[0])
            node = SelectNode(child, condition)

        elif operator == self.grammar.syntax.project_op:
            node = ProjectNode(child, param)

        elif operator == self.grammar.syntax.rename_op:
            name = None
            attributes = []
            if isinstance(param[0], str):
                name = param.pop(0)
            if param:
                attributes = param[0]
            node = RenameNode(child, name, attributes, schema)

        elif operator == self.grammar.syntax.assign_op:
            name = param[0]
            attributes = [] if len(param) < 2 else param[1]
            node = AssignNode(child, name, attributes, schema)

        else:
            raise ValueError

        return node

    def create_binary_node(
        self, operator, left, right, param: ParseResults | None = None
    ):
        """
        Return a Node whose type depends on the specified operator.

        :param operator: A relational algebra operator (see constants.py)
        :return: A Node.
        """

        # Join operators
        if operator == self.grammar.syntax.join_op:
            node = CrossJoinNode(left, right)

        elif operator == self.grammar.syntax.natural_join_op:
            node = NaturalJoinNode(left, right)

        elif operator == self.grammar.syntax.theta_join_op:
            condition = self.create_condition_node(param[0])
            node = ThetaJoinNode(left, right, condition)

        elif operator == self.grammar.syntax.full_outer_join_op:
            condition = self.create_condition_node(param[0])
            node = FullOuterJoinNode(left, right, condition)

        elif operator == self.grammar.syntax.left_outer_join_op:
            condition = self.create_condition_node(param[0])
            node = LeftOuterJoinNode(left, right, condition)

        elif operator == self.grammar.syntax.right_outer_join_op:
            condition = self.create_condition_node(param[0])
            node = RightOuterJoinNode(left, right, condition)

        # Set operators
        elif operator == self.grammar.syntax.union_op:
            node = UnionNode(left, right)

        elif operator == self.grammar.syntax.difference_op:
            node = DifferenceNode(left, right)

        elif operator == self.grammar.syntax.intersect_op:
            node = IntersectNode(left, right)

        else:
            raise ValueError

        return node

    def is_dependency_statement(self, exp: ParseResults) -> bool:
        """
        Check if the expression is a dependency statement.

        :param exp: A parsed expression
        :return: True if it's a dependency statement
        """
        if not exp or len(exp) < 2:
            return False

        # Check if the syntax has dependency operators
        dependency_ops = []
        if hasattr(self.grammar.syntax, 'pk_op'):
            dependency_ops.append(self.grammar.syntax.pk_op)
        if hasattr(self.grammar.syntax, 'mvd_op'):
            dependency_ops.append(self.grammar.syntax.mvd_op)
        if hasattr(self.grammar.syntax, 'fd_op'):
            dependency_ops.append(self.grammar.syntax.fd_op)
        if hasattr(self.grammar.syntax, 'inc_equiv_op'):
            dependency_ops.append(self.grammar.syntax.inc_equiv_op)
        if hasattr(self.grammar.syntax, 'inc_subset_op'):
            dependency_ops.append(self.grammar.syntax.inc_subset_op)

        return exp[0] in dependency_ops

    def create_dependency_node(self, exp: ParseResults, schema):
        """
        Create a dependency node from a parsed dependency statement.

        :param exp: A parsed dependency expression
        :param schema: A schema object
        :return: A dependency node
        """
        operator = exp[0]

        if operator == self.grammar.syntax.pk_op:
            # pk_{attributes} relation
            attributes = exp[1]
            relation_name = exp[2]
            return PrimaryKeyNode(relation_name, attributes)

        elif operator in (self.grammar.syntax.mvd_op, self.grammar.syntax.fd_op):
            # mvd_{attribute1, attribute2} relation OR mvd_{attribute1, attribute2} \select_{conditions} relation
            # fd_{attribute1, attribute2} relation OR fd_{attribute1, attribute2} \select_{conditions} relation
            return self._create_dependency_with_optional_select(exp, schema, operator)

        elif operator == self.grammar.syntax.inc_equiv_op:
            # inc=_{attributes} (select_or_relation, select_or_relation)
            attributes = exp[1]
            relations_expr = exp[2]
            
            if isinstance(relations_expr[0], str):
                # Simple form: inc=_{attributes} (relation1, relation2)
                relation_names = relations_expr
                left_child = RelationNode(relation_names[0], schema)
                right_child = RelationNode(relation_names[1], schema)
            else:
                # Complex form: inc=_{attributes} (select_or_relation, select_or_relation)
                # relations_expr is [[left_expr], [right_expr]] where each can be relation or [select, conditions, relation]
                left_expr = relations_expr[0]
                right_expr = relations_expr[1]
                left_child = self._create_cond_dep_expr_node(left_expr, schema)
                right_child = self._create_cond_dep_expr_node(right_expr, schema)
                relation_names = [left_child.name, right_child.name]
            return InclusionEquivalenceNode(relation_names, attributes, left_child, right_child)

        elif operator == self.grammar.syntax.inc_subset_op:
            # inc⊆_{attributes} (select_or_relation, select_or_relation)
            attributes = exp[1]
            relations_expr = exp[2]
            
            if isinstance(relations_expr[0], str):
                # Simple form: inc⊆_{attributes} (relation1, relation2)
                relation_names = relations_expr
                left_child = RelationNode(relation_names[0], schema)
                right_child = RelationNode(relation_names[1], schema)
            else:
                # Complex form: inc⊆_{attributes} (select_or_relation, select_or_relation)
                # relations_expr is [[left_expr], [right_expr]] where each can be relation or [select, conditions, relation]
                left_expr = relations_expr[0]
                right_expr = relations_expr[1]
                left_child = self._create_cond_dep_expr_node(left_expr, schema)
                right_child = self._create_cond_dep_expr_node(right_expr, schema)
                relation_names = [left_child.name, right_child.name]
            return InclusionSubsumptionNode(relation_names, attributes, left_child, right_child)

        else:
            raise ValueError(f"Unknown dependency operator: {operator}")

    def _create_dependency_with_optional_select(self, exp, schema, operator):
        """
        Create a dependency node (MVD or FD) with optional select conditions.
        
        :param exp: Parsed expression containing attributes and optional select
        :param schema: Schema for relation validation
        :param operator: The dependency operator (mvd_op or fd_op)
        :return: MultivaluedDependencyNode or FunctionalDependencyNode
        """
        attributes = exp[1]
        if len(exp) == 3:
            # Simple form: {operator}_{attributes} relation
            relation_name = exp[2]
            relation_node = RelationNode(relation_name, schema)
            if operator == self.grammar.syntax.mvd_op:
                return MultivaluedDependencyNode(relation_name, attributes, relation_node)
            else:  # fd_op
                return FunctionalDependencyNode(relation_name, attributes, relation_node)
        else:
            # With conditions: {operator}_{attributes} \select_{conditions} relation
            # exp[2] = "\select", exp[3] = conditions, exp[4] = relation_name
            conditions = exp[3]
            relation_name = exp[4]
            condition_node = self.create_condition_node(conditions[0])
            base_relation = RelationNode(relation_name, schema)
            select_node = SelectNode(base_relation, condition_node)
            if operator == self.grammar.syntax.mvd_op:
                return MultivaluedDependencyNode(relation_name, attributes, select_node)
            else:  # fd_op
                return FunctionalDependencyNode(relation_name, attributes, select_node)

    def _create_cond_dep_expr_node(self, expr, schema):
        """
        Create a node from a cond_dep_expr (either relation_name or select with relation).
        
        :param expr: Parsed expression (either relation_name or [select, conditions, relation_name])
        :param schema: Schema for relation validation
        :return: RelationNode or SelectNode
        """
        if isinstance(expr, str):
            # Simple relation name
            return RelationNode(expr, schema)
        elif len(expr) == 1 and isinstance(expr[0], str):
            # Simple relation name wrapped in a list
            return RelationNode(expr[0], schema)
        else:
            # Select with conditions: [select, conditions, relation_name]
            conditions = expr[1]
            relation_name = expr[2]
            condition_node = self.create_condition_node(conditions[0])
            base_relation = RelationNode(relation_name, schema)
            return SelectNode(base_relation, condition_node)
