from typing import (
    Any,
    cast,
    List,
    Optional,
    Type
)

import lpp.ast as ast
from lpp.builtins import BUILTINS
from lpp.object import(
    Boolean,
    Builtin,
    Environment,
    Error,
    Function,
    Integer,
    Null,
    Object,
    ObjectType,
    String,
    Return
)

TRUE = Boolean(True)
FALSE = Boolean(False)
NULL = Null()

_NOT_A_FUNCTION = 'No es una funcion: {}'
_TYPE_MISMATCH = 'Discrepancia de tipos: {} {} {}'
_UNKNOWN_PREFIX_OPERATOR = 'Operador desconocido: {}{}'
_UNKNOWN_INFIX_OPERATOR = 'Operador desconocido: {} {} {}'
_UNKNOWN_IDENTIFIER = 'Identificador no encontrado: {}'

def evaluate(node:ast.ASTNode, env: Environment) -> Optional[Object]:
    node_type: Type = type(node)

    if node_type == ast.Program:
        node = cast(ast.Program, node)
        return _evaluate_program(node, env)
    elif node_type == ast.ExpressionStatement:
        node = cast(ast.ExpressionStatement, node)
        assert node.expression is not None
        return evaluate(node.expression, env)
    
    elif node_type == ast.Integer:
        node = cast(ast.Integer, node)
        assert node.value is not None
        return Integer(node.value)
    elif node_type == ast.Boolean:
        node = cast(ast.Boolean, node)
        assert node.value is not None
        return _to_boolean_object(node.value)
    
    elif node_type == ast.Prefix:
        node = cast(ast.Prefix, node)
        assert node.right is not None
        right = evaluate(node.right, env)
        assert right is not None
        return _evaluate_prifix_expression(node.operator, right, node.right.token.line)
    
    elif node_type == ast.Infix:
        node = cast(ast.Infix, node)
        assert node.left is not None and node.right is not None
        left = evaluate(node.left, env)
        right = evaluate(node.right, env)
        assert right is not None and left is not None
        return _evaluate_infix_expression(node.operator, left, right, node.left.token.line)
    
    elif node_type == ast.Block:
        node = cast(ast.Block, node)
        return _evaluate_block_statement(node, env)
    
    elif node_type == ast.If:
        node = cast(ast.If, node)
        return _evaluate_if_expression(node, env)
    
    elif node_type == ast.ReturnStatement:
        node = cast(ast.ReturnStatement, node)
        assert node.return_value is not None
        value = evaluate(node.return_value, env)
        assert value is not None
        return Return(value)

    elif node_type == ast.LetStatement:
        node = cast(ast.LetStatement, node)
        assert node.value is not None
        value = evaluate(node.value, env)
        assert node.name is not None
        env[node.name.value] = value
    
    elif node_type == ast.Identifier:
        node = cast(ast.Identifier, node)
        return _evaluate_identifier(node, env, node.token.line)
    
    elif node_type == ast.Function:
        node = cast(ast.Function, node)
        assert node.body is not None
        return Function(node.parameters,
                        node.body,
                        env)
    
    elif node_type == ast.Call:
        node = cast(ast.Call, node)
        function = evaluate(node.function, env)
        assert function is not None
        assert node.arguments is not None
        args = _evaluate_expression(node.arguments, env)

        assert function is not None
        return _apply_function(function, args, node.token.line)
    
    elif node_type == ast.StringLiteral:
        node = cast(ast.StringLiteral, node)
        return String(node.value)

    return None

def _apply_function(fn: Object, args: List[Object],line_evaluated: int) -> Object:
    if type(fn) == Function:
        fn = cast(Function, fn)

        extended_enviroment = _extended_function_enviroment(fn, args)
        evaluated = evaluate(fn.body, extended_enviroment)
        
        assert evaluated is not None
        return _unwrap_return_value(evaluated)
    elif type(fn) == Builtin:
        fn = cast(Builtin, fn)
        return fn.fn(*args)
    else:
        return _new_error(_NOT_A_FUNCTION, args, line_evaluated)

def _evaluate_bang_operator_expression(right: Object) -> Object:
    if right is TRUE:
        return FALSE
    elif right is FALSE:
        return TRUE
    elif right is NULL:
        return TRUE
    else:
        return FALSE

def _evaluate_expression(expressions: List[ast.Expression], env: Environment) -> List[Object]:
    result: List[Object] = []

    for expression in expressions:
        evaluated = evaluate(expression, env)
        assert evaluated is not None
        result.append(evaluated)
    
    return result

def _extended_function_enviroment(fn: Function, args: List[Object]) -> Environment:
    env = Environment(outer=fn.env)

    for idx, param in enumerate(fn.parameters):
        env[param.value] = args[idx - 1]
    
    return env

def _evaluate_identifier(node: ast.Identifier, env: Environment, line_evaluated:int) -> Object:
    try:
        return env[node.value]
    except KeyError:
        return BUILTINS.get(node.value, 
            _new_error(_UNKNOWN_IDENTIFIER, [node.value], line_evaluated))

def _evaluate_if_expression(if_expression: ast.If, env: Environment) -> Optional[Object]:
    assert if_expression.condition is not None
    condition = evaluate(if_expression.condition, env)

    assert condition is not None
    if _is_truthy(condition):
        assert if_expression.consequence is not None
        return evaluate(if_expression.consequence, env)
    elif if_expression.alternative is not None:
        return evaluate(if_expression.alternative, env)
    else:
        return NULL

def _is_truthy(obj: Object) -> bool:
    if obj is NULL:
        return False
    elif obj is TRUE:
        return True
    elif obj is FALSE:
        return False
    else:
        return True

def _evaluate_block_statement(block: ast.Block, env: Environment) -> Optional[Object]:
    result: Optional[Object] = None

    for statement in block.statements:
        result = evaluate(statement, env)

        if result is not None and \
            (result.type() == ObjectType.RETURN or result.type() == ObjectType.ERROR):
            return result

    return result

def _evaluate_infix_expression(operator:str, left:Object, right:Object, line_evaluated:int) -> Object:
    if left.type() == ObjectType.INTEGER \
        and right.type() == ObjectType.INTEGER:
        return _evaluate_integer_infix_expression(operator, left, right, line_evaluated)
    if left.type() == ObjectType.STRING \
        and right.type() == ObjectType.STRING:
        return _evaluate_string_infix_expression(operator, left, right, line_evaluated)
    elif operator == '==':
        return _to_boolean_object(left is right)
    elif operator == '!=':
        return _to_boolean_object(left is not right)
    elif left.type() != right.type():
        return _new_error(_TYPE_MISMATCH, [left.type().name,
                operator,
                right.type().name
            ], line_evaluated)
    else:
        return _new_error(_UNKNOWN_INFIX_OPERATOR,[left.type().name,
                operator,
                right.type().name
            ], line_evaluated)

def _evaluate_integer_infix_expression(operator: str, left: Object, right: Object, line_evaluated:int) -> Object:
    left_value = int = cast(Integer, left).value
    right_value = int = cast(Integer, right).value

    if operator == '+':
        return Integer(left_value + right_value)
    elif operator == '-':
        return Integer(left_value - right_value)
    elif operator == '*':
        return Integer(left_value * right_value)
    elif operator == '/':
        return Integer(left_value // right_value) #divicio de enteros
    elif operator == '<':
        return _to_boolean_object(left_value < right_value)
    elif operator == '>':
        return _to_boolean_object(left_value > right_value)
    elif operator == '==':
        return _to_boolean_object(left_value == right_value)
    elif operator == '!=':
        return _to_boolean_object(left_value != right_value)
    else:
        return _new_error(_UNKNOWN_INFIX_OPERATOR,[left.type().name,
                operator,
                right.type().name
            ], line_evaluated)

def _evaluate_string_infix_expression(operator: str, left: Object, right: Object, line_evaluated:int) -> Object:
    left_value = str = cast(String, left).value
    right_value = str = cast(String, right).value

    if operator == '+':
        return String(left_value + right_value)
    elif operator == '==':
        return _to_boolean_object(left_value == right_value)
    elif operator == '!=':
        return _to_boolean_object(left_value != right_value)
    else:
        return _new_error(_UNKNOWN_INFIX_OPERATOR,[left.type().name,
                operator,
                right.type().name
            ], line_evaluated)


def _evaluate_minus_operator_expression(right: Object, line_evaluated:int) -> Object:
    if type(right) != Integer:
        return _new_error(_UNKNOWN_PREFIX_OPERATOR, ['-', right.type().name], line_evaluated)
    
    right = cast(Integer, right)

    return Integer(-right.value)

def _evaluate_prifix_expression(operator: str, right: Object,line_evaluated:int) -> Object:
    if operator == '!':
        return _evaluate_bang_operator_expression(right)
    elif operator == '-':
        return _evaluate_minus_operator_expression(right, line_evaluated)
    else:
        return _new_error(_UNKNOWN_PREFIX_OPERATOR,[operator, right.type().name],line_evaluated)

def _evaluate_program(program: ast.Program, env) -> Optional[Object]:
    result: Optional[Object] = None
    
    for statement in program.statements:
        result = evaluate(statement, env)

        if type(result) == Return:
            result = cast(Return, result)
            return result.value
        elif type(result) == Error:
            return result
    
    return result

def _new_error(message: str, args:List[Any], error_line: int) -> Error:
    return Error(message.format(*args), error_line)

def _unwrap_return_value(obj: Object) -> Object:
    if type(obj) == Return:
        obj = cast(Return, obj)
        return obj.value
    
    return obj

def _to_boolean_object(value: bool) -> Boolean:
    return TRUE if value else FALSE
[((25, 7, 25, 20), 'lpp.object.Boolean', 'Boolean', ({(25, 15, 25, 19): 'True'}, {}), '(True)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((26, 8, 26, 22), 'lpp.object.Boolean', 'Boolean', ({(26, 16, 26, 21): 'False'}, {}), '(False)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((27, 7, 27, 13), 'lpp.object.Null', 'Null', ({}, {}), '()', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((155, 10, 155, 35), 'lpp.object.Environment', 'Environment', (), '', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((273, 12, 273, 32), 'typing.cast', 'cast', ({(273, 17, 273, 24): 'Integer', (273, 26, 273, 31): 'right'}, {}), '(Integer, right)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((275, 11, 275, 32), 'lpp.object.Integer', 'Integer', ({(275, 19, 275, 31): '(-right.value)'}, {}), '(-right.value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((39, 15, 39, 38), 'typing.cast', 'cast', ({(39, 20, 39, 31): 'ast.Program', (39, 33, 39, 37): 'node'}, {}), '(ast.Program, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((121, 13, 121, 31), 'typing.cast', 'cast', ({(121, 18, 121, 26): 'Function', (121, 28, 121, 30): 'fn'}, {}), '(Function, fn)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((227, 23, 227, 42), 'typing.cast', 'cast', ({(227, 28, 227, 35): 'Integer', (227, 37, 227, 41): 'left'}, {}), '(Integer, left)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((228, 24, 228, 44), 'typing.cast', 'cast', ({(228, 29, 228, 36): 'Integer', (228, 38, 228, 43): 'right'}, {}), '(Integer, right)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((231, 15, 231, 48), 'lpp.object.Integer', 'Integer', ({(231, 23, 231, 47): '(left_value + right_value)'}, {}), '(left_value + right_value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((253, 23, 253, 41), 'typing.cast', 'cast', ({(253, 28, 253, 34): 'String', (253, 36, 253, 40): 'left'}, {}), '(String, left)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((254, 24, 254, 43), 'typing.cast', 'cast', ({(254, 29, 254, 35): 'String', (254, 37, 254, 42): 'right'}, {}), '(String, right)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((257, 15, 257, 47), 'lpp.object.String', 'String', ({(257, 22, 257, 46): '(left_value + right_value)'}, {}), '(left_value + right_value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((304, 14, 304, 31), 'typing.cast', 'cast', ({(304, 19, 304, 25): 'Return', (304, 27, 304, 30): 'obj'}, {}), '(Return, obj)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((42, 15, 42, 50), 'typing.cast', 'cast', ({(42, 20, 42, 43): 'ast.ExpressionStatement', (42, 45, 42, 49): 'node'}, {}), '(ast.ExpressionStatement, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((129, 13, 129, 30), 'typing.cast', 'cast', ({(129, 18, 129, 25): 'Builtin', (129, 27, 129, 29): 'fn'}, {}), '(Builtin, fn)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((233, 15, 233, 48), 'lpp.object.Integer', 'Integer', ({(233, 23, 233, 47): '(left_value - right_value)'}, {}), '(left_value - right_value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((292, 21, 292, 41), 'typing.cast', 'cast', ({(292, 26, 292, 32): 'Return', (292, 34, 292, 40): 'result'}, {}), '(Return, result)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((47, 15, 47, 38), 'typing.cast', 'cast', ({(47, 20, 47, 31): 'ast.Integer', (47, 33, 47, 37): 'node'}, {}), '(ast.Integer, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((49, 15, 49, 34), 'lpp.object.Integer', 'Integer', ({(49, 23, 49, 33): 'node.value'}, {}), '(node.value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((235, 15, 235, 48), 'lpp.object.Integer', 'Integer', ({(235, 23, 235, 47): '(left_value * right_value)'}, {}), '(left_value * right_value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((51, 15, 51, 38), 'typing.cast', 'cast', ({(51, 20, 51, 31): 'ast.Boolean', (51, 33, 51, 37): 'node'}, {}), '(ast.Boolean, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((237, 15, 237, 49), 'lpp.object.Integer', 'Integer', ({(237, 23, 237, 48): '(left_value // right_value)'}, {}), '(left_value // right_value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((56, 15, 56, 37), 'typing.cast', 'cast', ({(56, 20, 56, 30): 'ast.Prefix', (56, 32, 56, 36): 'node'}, {}), '(ast.Prefix, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((63, 15, 63, 36), 'typing.cast', 'cast', ({(63, 20, 63, 29): 'ast.Infix', (63, 31, 63, 35): 'node'}, {}), '(ast.Infix, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((71, 15, 71, 36), 'typing.cast', 'cast', ({(71, 20, 71, 29): 'ast.Block', (71, 31, 71, 35): 'node'}, {}), '(ast.Block, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((75, 15, 75, 33), 'typing.cast', 'cast', ({(75, 20, 75, 26): 'ast.If', (75, 28, 75, 32): 'node'}, {}), '(ast.If, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((79, 15, 79, 46), 'typing.cast', 'cast', ({(79, 20, 79, 39): 'ast.ReturnStatement', (79, 41, 79, 45): 'node'}, {}), '(ast.ReturnStatement, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((83, 15, 83, 28), 'lpp.object.Return', 'Return', ({(83, 22, 83, 27): 'value'}, {}), '(value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((86, 15, 86, 43), 'typing.cast', 'cast', ({(86, 20, 86, 36): 'ast.LetStatement', (86, 38, 86, 42): 'node'}, {}), '(ast.LetStatement, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((93, 15, 93, 41), 'typing.cast', 'cast', ({(93, 20, 93, 34): 'ast.Identifier', (93, 36, 93, 40): 'node'}, {}), '(ast.Identifier, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((97, 15, 97, 39), 'typing.cast', 'cast', ({(97, 20, 97, 32): 'ast.Function', (97, 34, 97, 38): 'node'}, {}), '(ast.Function, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((99, 15, 101, 28), 'lpp.object.Function', 'Function', ({(99, 24, 99, 39): 'node.parameters', (100, 24, 100, 33): 'node.body', (101, 24, 101, 27): 'env'}, {}), '(node.parameters, node.body, env)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((104, 15, 104, 35), 'typing.cast', 'cast', ({(104, 20, 104, 28): 'ast.Call', (104, 30, 104, 34): 'node'}, {}), '(ast.Call, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((114, 15, 114, 44), 'typing.cast', 'cast', ({(114, 20, 114, 37): 'ast.StringLiteral', (114, 39, 114, 43): 'node'}, {}), '(ast.StringLiteral, node)', False, 'from typing import Any, cast, List, Optional, Type\n'), ((115, 15, 115, 33), 'lpp.object.String', 'String', ({(115, 22, 115, 32): 'node.value'}, {}), '(node.value)', False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n')]