import hutnparser as hp
import symbol_table as st
import sys
import types_mv
from declare_functions_visitor import DeclareFunctionsVisitor
from visitor import Visitor


class SemanticsVisitor(Visitor):
    def __init__(self, args):
        Visitor.__init__(self, args)
        self.symbol_table = st.SymbolTable()

        # there is only one input file, list is for sharing it among visitors
        self.inputfiles = []

        # Count whether we are in a while or not
        self.while_counter = 0

        # inherited attribute, set in funcdecl and used in return,
        # to ensure that (returned type == declared type)
        self.current_funcdecl = None

        self.declare_functions_visitor =\
            DeclareFunctionsVisitor(self.symbol_table, self.inputfiles)

    @staticmethod
    def incompatible_types(l_type, r_type):
        if type(l_type) != type(r_type):
            if types_mv.Void in (type(l_type), type(r_type)):
                return True
            if types_mv.Element in (type(l_type), type(r_type)):
                return False
            if l_type.isNotNumber() or r_type.isNotNumber():
                return True
        return False

    def do_check_binary_ops_arithmetic(self, l, r):
        l_type, r_type = self.get_type(l), self.get_type(r)
        if SemanticsVisitor.incompatible_types(l_type, r_type):
            raise RuntimeError(
                "{}:{}:{}: error: invalid operands to binary operator "
                "(have {} and {})".format(self.inputfiles[0],
                                          l.startpos['line'],
                                          l.startpos['column'],
                                          str(l_type),
                                          str(r_type)))

    def check_binary_ops_arithmetic(self, tree):
        l, r = tree.get_tail()[0], tree.get_tail()[2]
        self.do_check_binary_ops_arithmetic(l, r)

    def generalize_binary_ops_arithmetic(self, tree):
        l, r = tree.get_tail()[0], tree.get_tail()[2]
        l_type, r_type = self.get_type(l), self.get_type(r)
        return types_mv.generalize_arithmetic(l_type, r_type)

    def check_unary_ops_arithmetic(self, tree, operator_name):
        l = tree.get_tail()[1]
        l_type = self.get_type(l)
        if l_type.isNotNumber():
            raise RuntimeError(
                "{}:{}:{}: error: wrong type argument to unary {} "
                "({})".format(self.inputfiles[0],
                              l.startpos['line'],
                              l.startpos['column'],
                              operator_name,
                              str(l_type)))

    def promote_unary_ops_arithmetic(self, tree):
        l = tree.get_tail()[1]
        l_type = self.get_type(l)
        try:
            return types_mv.promote_arithmetic(l_type)
        except RuntimeError:
            raise RuntimeError(
                "Pathological situation in promote_unary_ops_arithmetic: "
                "check_unary_ops_arithmetic has not been executed")

    # if r_type is provided, r is not used
    def do_check_assignment(self, l, r, r_type = None):
        if r_type is not None:
            l_type = self.get_type(l)
        else:
            l_type, r_type = self.get_type(l), self.get_type(r)
        if SemanticsVisitor.incompatible_types(l_type, r_type):
            raise RuntimeError("{}:{}:{}: error: cannot assign a value of "
                               "type '{}' to a variable of type '{}'"
                .format(self.inputfiles[0],
                        l.startpos['line'],
                        l.startpos['column'],
                        str(r_type),
                        str(l_type)))

    def check_assignment(self, tree):
        l, r = tree.get_tail()[0], tree.get_tail()[2]
        self.do_check_assignment(l, r)

    def check_return(self, tree):
        l = self.current_funcdecl

        if len(tree.get_tail()) > 2:
            r = tree.get_tail()[1]
            r_type = None
        else:
            r = None
            r_type = types_mv.Void()

        if l:
            self.do_check_assignment(l, r, r_type)
        else:
            raise RuntimeError(
                "{}:{}:{}: error: 'return' is used outside of a function"
                .format(self.inputfiles[0],
                        tree.startpos['line'],
                        tree.startpos['column']))

    def check_predicate(self, tree):
        if isinstance(self.get_type(tree), types_mv.Element):
            return
        if self.get_type(tree).isNotNumber():
            raise RuntimeError(
                "{}:{}:{}: error: predicates of type '{}' are not allowed"
                .format(self.inputfiles[0],
                        tree.startpos['line'],
                        tree.startpos['column'],
                        self.get_type(tree)))

    def replace_child_binary_op_with_call(self, tree, i=0):
        child = tree.get_tail()[i]
        if len(child.get_tail()) > 1:
            l, op, r = child.get_tail()
            l_type, r_type = self.get_type(l), self.get_type(r)
            if type(l_type) != type(r_type):
                print("Error: " + str(l_type) + " <-> " + str(r_type))
                raise RuntimeError(
                    "{}:{}:{}: error: children were not casted".format(
                        self.inputfiles[0],
                        tree.startpos['line'],
                        tree.startpos['column']
                    ))
            call_name = SemanticsVisitor.call_name_binary(l_type, op)
            call_tree = SemanticsVisitor.func_call(call_name, [l, r], tree)
            try:
                self.visit(call_tree)
            except RuntimeError:
                call_signature = "{0} function {1}({2}, {2})".format(
                    str(types_mv.Boolean()), call_name, l_type)
                raise RuntimeError(
                    "{}:{}:{}: error: cannot perform {}: function '{}' is "
                    "not found".format(
                        self.inputfiles[0],
                        tree.startpos['line'],
                        tree.startpos['column'],
                        child.head,
                        call_signature))
            tree.replace_child(child, call_tree)
        self.set_type(tree, self.get_type(tree.get_tail()[i]))

    def replace_child_unary_op_with_call(self, tree):
        child = tree.get_tail()[0]
        if child.head == "keep_sign":
            tree.replace_child(child, child.get_tail()[1])
        else:
            op, l = child.get_tail()
            l_type = self.get_type(l)
            call_name = SemanticsVisitor.call_name_unary(l_type, op)
            call_tree = SemanticsVisitor.func_call(call_name, [l], tree)
            try:
                self.visit(call_tree)
            except RuntimeError:
                call_signature = "{0} function {1}({2})".format(
                    str(types_mv.Boolean()), call_name, l_type)
                raise RuntimeError(
                    "{}:{}:{}: error: cannot perform {}: function '{}' is "
                    "not found".format(
                        self.inputfiles[0],
                        tree.startpos['line'],
                        tree.startpos['column'],
                        child.head,
                        call_signature))
            tree.replace_child(child, call_tree)
        self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def cast_binary_ops_arithmetic(self, tree):
        l, op, r = tree.get_tail()
        l_type, r_type = self.get_type(l), self.get_type(r)
        if type(l_type) != type(r_type):  # if two different numeric types
            g_type = types_mv.generalize_arithmetic(l_type, r_type)
            self.perform_implicit_cast(tree, l, l_type, g_type)
            self.perform_implicit_cast(tree, r, r_type, g_type)

    def cast_binary_ops_logical(self, tree):
        l, op, r = tree.get_tail()
        l_type, r_type = self.get_type(l), self.get_type(r)
        self.perform_implicit_cast(tree, l, l_type, types_mv.Boolean())
        self.perform_implicit_cast(tree, r, r_type, types_mv.Boolean())

    def cast_unary_ops_arithmetic(self, tree):
        l = tree.get_tail()[1]
        l_type = self.get_type(l)
        p_type = self.promote_unary_ops_arithmetic(tree)
        self.perform_implicit_cast(tree, l, l_type, p_type)

    @staticmethod
    def func_call(name, params, old_tree):
        startpos = old_tree.startpos
        endpos = old_tree.endpos
        inputfile = old_tree.inputfile

        tree = hp.Tree(
            "func_call",
            [
                hp.Tree("rvalue",
                        [
                            hp.Tree("ID", [name], startpos, endpos, inputfile)
                        ],
                        startpos, endpos, inputfile),
                # Tokens have no impact on visit_func_call. So leave them out.
            ],
            startpos, endpos, inputfile)

        params = [hp.Tree("expression", [p], startpos, endpos, inputfile) for p in params]

        tree.tail.extend(params)

        return hp.Tree("expression", [tree], startpos, endpos, inputfile)

    @staticmethod
    def cast_name(from_type, to_type):
        from_t = str(from_type)[0].lower()
        to_t = str(to_type)[0].lower()
        cast_name = "cast_{}2{}".format(from_t, to_t)
        return cast_name

    def raise_implicit_cast_error(self, from_type, to_type, tree):
        cast_name = SemanticsVisitor.cast_name(from_type, to_type)
        cast_signature = "{} function {}({})".format(
            str(to_type), cast_name, str(from_type))
        raise RuntimeError(
            "{}:{}:{}: error: cannot perform implicit cast from '{}'"
            " to '{}': function '{}' is not found".format(
                self.inputfiles[0],
                tree.startpos['line'],
                tree.startpos['column'],
                str(to_type), str(from_type),
                cast_signature))

    def perform_implicit_cast(self, tree, child, from_type, to_type):
        if types_mv.Element in (type(from_type), type(to_type)):
            return
        if type(from_type) == type(to_type):
            return
        cast_name = SemanticsVisitor.cast_name(from_type, to_type)
        cast_tree = \
            SemanticsVisitor.func_call(cast_name, [child], tree)
        try:
            self.visit(cast_tree)
        except RuntimeError:
            self.raise_implicit_cast_error(from_type, to_type, child)
        tree.replace_child(child, cast_tree)

    types = {
        "Integer": "integer",
        "Float": "float",
        "Boolean": "bool",
        "String": "string",
        "Action": "action",
        "Element": "element",
        "Type": "type"
    }

    binary_ops = {
        "OR": "or",
        "AND": "and",
        "EQ": "eq",
        "NEQ": "neq",
        "LT": "lt",
        "GT": "gt",
        "LE": "lte",
        "GE": "gte",
        "PLUS": "addition",
        "MINUS": "subtraction",
        "STAR": "multiplication",
        "SLASH": "division"
    }

    unary_ops = {
        "NOT": "not",
        "MINUS": "neg"
    }

    @staticmethod
    def call_name_binary(operand_type, operator):
        # String joins should also be possible
        if str(operand_type) == "String":
            if operator.head == "PLUS":
                return "string_join"

        if operator.head == "EQ":
            return "value_eq"
        elif operator.head == "NEQ":
            return "value_neq"

        call_name = "{}_{}".format(SemanticsVisitor.types[str(operand_type)],
                                   SemanticsVisitor.binary_ops[operator.head])
        return call_name

    @staticmethod
    def call_name_unary(operand_type, operator):
        call_name = "{}_{}".format(SemanticsVisitor.types[str(operand_type)],
                                   SemanticsVisitor.unary_ops[operator.head])

        return call_name

    def dump(self):
        return self.tree.get_text(with_implicit=True)
        # return "No code generation here"

    # a visit_* method for each non-terminal in the grammar
    def visit_start(self, tree):
        self.symbol_table.open_scope()
        self.inputfiles.append(tree.inputfile)
        for child in tree.get_tail():
            self.inputfiles[0] = child.inputfile
            self.declare_functions_visitor.visit(child)
        for child in tree.get_tail():
            self.inputfiles[0] = child.inputfile
            self.visit(child)
        self.inputfiles.pop()
        self.symbol_table.close_scope()
        self.tree = tree

    def visit_statement(self, tree):
        self.visit_children(tree)

    def visit_definition(self, tree):
        self.visit_vardecl(tree)

    def visit_vardecl(self, tree):
        type_spec = tree.get_child("type_specifier")
        var_id = tree.get_child("ID")

        var_type = types_mv.string_to_type(type_spec.get_text())
        var_name = var_id.get_text()

        symbol = st.Symbol(var_name, var_type,
                        is_global=self.current_funcdecl is None)

        try:
            self.symbol_table.add(symbol)
        except Exception:
            raise RuntimeError(
                "{}:{}:{}: error: redeclaration of '{}'".format(
                    self.inputfiles[0], tree.startpos['line'],
                    tree.startpos['column'], var_name))

        self.set_symbol(tree, symbol)

    def visit_assignment(self, tree):
        self.visit_children(tree)
        self.check_assignment(tree)

    def visit_expression(self, tree):
        self.visit_children(tree)
        self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def visit_binary_operation(self, tree):
        self.visit_children(tree)
        self.replace_child_binary_op_with_call(tree)

    def visit_disjunction(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.replace_child_binary_op_with_call(tree)
        else:
            self.replace_child_binary_op_with_call(tree, 2)
            self.cast_binary_ops_logical(tree)
            self.set_type(tree, types_mv.Boolean())

    def visit_conjunction(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.replace_child_binary_op_with_call(tree)
        else:
            self.replace_child_binary_op_with_call(tree, 2)
            self.cast_binary_ops_logical(tree)
            self.set_type(tree, types_mv.Boolean())

    def visit_comparison(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.replace_child_binary_op_with_call(tree)
        else:
            self.replace_child_binary_op_with_call(tree, 2)
            self.check_binary_ops_arithmetic(tree)
            self.cast_binary_ops_arithmetic(tree)
            self.set_type(tree, types_mv.Boolean())

    def visit_relation(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.replace_child_binary_op_with_call(tree)
        else:
            self.replace_child_binary_op_with_call(tree, 2)
            self.check_binary_ops_arithmetic(tree)
            self.cast_binary_ops_arithmetic(tree)
            self.set_type(tree, types_mv.Boolean())

    def visit_sum(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.replace_child_binary_op_with_call(tree)
        else:
            self.replace_child_binary_op_with_call(tree, 2)
            self.check_binary_ops_arithmetic(tree)
            self.cast_binary_ops_arithmetic(tree)
            # after the cast both parameters have the same (generalized) type:
            self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def visit_term(self, tree):
        self.visit_children(tree)
        if len(tree.get_tail()) == 1:
            self.set_type(tree, self.get_type(tree.get_tail()[0]))
        else:
            self.check_binary_ops_arithmetic(tree)
            self.cast_binary_ops_arithmetic(tree)
            # after the cast both parameters have the same (generalized) type:
            self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def visit_factor(self, tree):
        self.visit_children(tree)
        if tree.get_child("primary") is not None:
            self.set_type(tree, self.get_type(tree.get_tail()[0]))
        else:
            self.replace_child_unary_op_with_call(tree)

    def visit_logical_not(self, tree):
        self.visit_children(tree)

        l = tree.get_tail()[1]
        l_type = self.get_type(l)
        self.perform_implicit_cast(tree, l, l_type, types_mv.Boolean())

        self.set_type(tree, self.get_type(tree.get_tail()[1]))

    def visit_invert_sign(self, tree):
        self.visit_children(tree)

        self.check_unary_ops_arithmetic(tree, "minus")
        self.cast_unary_ops_arithmetic(tree)

        self.set_type(tree, self.get_type(tree.get_tail()[1]))

    def visit_keep_sign(self, tree):
        self.visit_children(tree)

        self.check_unary_ops_arithmetic(tree, "plus")
        self.cast_unary_ops_arithmetic(tree)

        self.set_type(tree, self.get_type(tree.get_tail()[1]))

    def visit_primary(self, tree):
        self.visit_children(tree)
        self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def visit_parenthesized(self, tree):
        self.visit_children(tree)
        self.set_type(tree, self.get_type(tree.get_tail()[1]))

    def visit_atomvalue(self, tree):
        self.visit_children(tree)
        self.set_type(tree, self.get_type(tree.get_tail()[0]))

    def visit_type_specifier(self, tree):
        self.set_type(tree, types_mv.Type())

    def visit_actionname(self, tree):
        self.set_type(tree, types_mv.Action())

    def visit_string(self, tree):
        self.set_type(tree, types_mv.String())

    def visit_integer(self, tree):
        self.set_type(tree, types_mv.Integer())

    def visit_float(self, tree):
        self.set_type(tree, types_mv.Float())

    # there is no such rule in the grammar, we just avoid code duplicates
    def visit_id(self, tree):
        name = tree.get_text()
        #TODO this is set to the function returnvalue, even if we use the function pointer...
        try:
            symbol = self.symbol_table.get(name)
        except KeyError:
            raise RuntimeError("{}:{}:{}: error: '{}' is not declared".format(
                self.inputfiles[0], tree.startpos['line'],
                tree.startpos['column'], name))
        self.set_type(tree, symbol.type)
        self.set_symbol(tree, symbol)

    def visit_rvalue(self, tree):
        if len(tree.get_tail()) > 1:
            # Complex: dict_read operation needed
            child = tree.get_tail()[0]
            node = tree.get_child("rvalue")
            expression = tree.get_child("expression")
            operation = "dict_read"
            call_tree = SemanticsVisitor.func_call(operation, [node, expression], tree)
            self.visit(call_tree)
            tree.head = call_tree.head
            tree._tail = call_tree.tail
            tree.tail = call_tree.tail
            self.set_type(tree, self.get_type(node))
        else:
            # Simple
            self.visit_id(tree)

    def visit_lvalue(self, tree):
        self.visit_id(tree)

    def visit_func_call(self, tree):
        self.visit_children(tree)

        symbol = self.get_symbol(tree.get_tail()[0])
        self.set_type(tree, symbol.type)

        if not symbol.is_func():
            if isinstance(symbol.type, types_mv.Element):
                sys.stderr.write("{}:{}:{}: warning: calling a variable of type "
                                 "'Element'\n".format(self.inputfiles[0],
                                                      tree.startpos['line'],
                                                      tree.startpos['column'],
                                                      symbol.name))
                return  # allow the call without knowing the declaration
            raise RuntimeError(
                "{}:{}:{}: error: '{}' is a variable of type '{}', not a "
                "function".format(self.inputfiles[0],
                                  tree.startpos['line'],
                                  tree.startpos['column'],
                                  symbol.name,
                                  symbol.type))

        expressions = tree.get_children("expression")
        if len(expressions) != len(symbol.params):
            raise RuntimeError(
                "{}:{}:{}: error: wrong number of arguments to "
                "function '{}'".format(self.inputfiles[0],
                                       tree.startpos['line'],
                                       tree.startpos['column'],
                                       symbol.signature()))

        for i in range(len(expressions)):
            arg_type = self.get_type(expressions[i])
            param_type = symbol.params[i]
            if SemanticsVisitor.incompatible_types(arg_type, param_type):
                raise RuntimeError(
                    "{}:{}:{}: error: argument {} has type '{}' instead of "
                    "'{}', calling function '{}'".format(
                            self.inputfiles[0],
                            tree.startpos['line'],
                            tree.startpos['column'],
                            i + 1,
                            str(arg_type),
                            str(param_type),
                            symbol.signature()))
            if type(arg_type) != type(param_type):
                self.perform_implicit_cast(tree, expressions[i], arg_type,
                                           param_type)

        if symbol.name in ["input", "output"]:
            tree.head = symbol.name

    def visit_input(self, tree):
        pass  # no need to visit it again

    def visit_output(self, tree):
        pass  # no need to visit it again

    def visit_dictionary(self, tree):
        self.set_type(tree, types_mv.Element)

    def visit_list(self, tree):
        self.set_type(tree, types_mv.Element)

    def visit_dict_item(self, tree):
        pass

    def visit_ifelse(self, tree):
        self.visit_children(tree)
        expressions = tree.get_children("expression")
        for expression in expressions:
            self.check_predicate(expression)

    def visit_while(self, tree):
        self.while_counter += 1
        self.visit_children(tree)
        self.while_counter -= 1
        expression = tree.get_child("expression")
        self.check_predicate(expression)

    def visit_block(self, tree):
        self.symbol_table.open_scope()
        self.visit_children(tree)
        self.symbol_table.close_scope()

    def visit_func_body(self, tree):
        self.visit_children(tree)

    def visit_funcdecl(self, tree):
        # here we only visit the body cause the declaration is already done
        # by declare_functions_visitor
        if tree.get_child('func_body') is not None:
            self.current_funcdecl = tree

            self.symbol_table.open_scope()
            self.visit_children(tree)
            self.symbol_table.close_scope()

            self.current_funcdecl = None

    def visit_parameter(self, tree):
        param_id = tree.get_child("ID")
        type_spec = tree.get_child("type_specifier")

        param_type = types_mv.string_to_type(type_spec.get_text())
        param_name = param_id.get_text()

        symbol = st.Symbol(param_name, param_type, is_global=False)

        try:
            self.symbol_table.add(symbol)
        except Exception:
            raise RuntimeError(
                "{}:{}:{}: error: redeclaration of '{}'".format(
                    self.inputfiles[0], tree.startpos['line'],
                    tree.startpos['column'], param_name))

        self.set_symbol(tree, symbol)

    def visit_return(self, tree):
        self.visit_children(tree)
        self.check_return(tree)

    def visit_bool(self, tree):
        self.set_type(tree, types_mv.Boolean())

    def visit_break(self, tree):
        if self.while_counter == 0:
            raise RuntimeError(
                "{}:{}:{}: error: break outside of while".format(
                    self.inputfiles[0], tree.startpos['line'],
                    tree.startpos['column']))

    def visit_continue(self, tree):
        if self.while_counter == 0:
            raise RuntimeError(
                "{}:{}:{}: error: continue outside of while".format(
                    self.inputfiles[0], tree.startpos['line'],
                    tree.startpos['column']))
