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
[((308, 321), 'lpp.object.Boolean', 'Boolean', (['(True)'], {}), '(True)\n', (315, 321), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((330, 344), 'lpp.object.Boolean', 'Boolean', (['(False)'], {}), '(False)\n', (337, 344), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((352, 358), 'lpp.object.Null', 'Null', ([], {}), '()\n', (356, 358), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((4751, 4776), 'lpp.object.Environment', 'Environment', ([], {'outer': 'fn.env'}), '(outer=fn.env)\n', (4762, 4776), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((9122, 9142), 'typing.cast', 'cast', (['Integer', 'right'], {}), '(Integer, right)\n', (9126, 9142), False, 'from typing import Any, cast, List, Optional, Type\n'), ((9155, 9176), 'lpp.object.Integer', 'Integer', (['(-right.value)'], {}), '(-right.value)\n', (9162, 9176), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((777, 800), 'typing.cast', 'cast', (['ast.Program', 'node'], {}), '(ast.Program, node)\n', (781, 800), False, 'from typing import Any, cast, List, Optional, Type\n'), ((3720, 3738), 'typing.cast', 'cast', (['Function', 'fn'], {}), '(Function, fn)\n', (3724, 3738), False, 'from typing import Any, cast, List, Optional, Type\n'), ((7346, 7365), 'typing.cast', 'cast', (['Integer', 'left'], {}), '(Integer, left)\n', (7350, 7365), False, 'from typing import Any, cast, List, Optional, Type\n'), ((7396, 7416), 'typing.cast', 'cast', (['Integer', 'right'], {}), '(Integer, right)\n', (7400, 7416), False, 'from typing import Any, cast, List, Optional, Type\n'), ((7463, 7496), 'lpp.object.Integer', 'Integer', (['(left_value + right_value)'], {}), '(left_value + right_value)\n', (7470, 7496), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((8398, 8416), 'typing.cast', 'cast', (['String', 'left'], {}), '(String, left)\n', (8402, 8416), False, 'from typing import Any, cast, List, Optional, Type\n'), ((8447, 8466), 'typing.cast', 'cast', (['String', 'right'], {}), '(String, right)\n', (8451, 8466), False, 'from typing import Any, cast, List, Optional, Type\n'), ((8513, 8545), 'lpp.object.String', 'String', (['(left_value + right_value)'], {}), '(left_value + right_value)\n', (8519, 8545), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((10165, 10182), 'typing.cast', 'cast', (['Return', 'obj'], {}), '(Return, obj)\n', (10169, 10182), False, 'from typing import Any, cast, List, Optional, Type\n'), ((907, 942), 'typing.cast', 'cast', (['ast.ExpressionStatement', 'node'], {}), '(ast.ExpressionStatement, node)\n', (911, 942), False, 'from typing import Any, cast, List, Optional, Type\n'), ((4005, 4022), 'typing.cast', 'cast', (['Builtin', 'fn'], {}), '(Builtin, fn)\n', (4009, 4022), False, 'from typing import Any, cast, List, Optional, Type\n'), ((7538, 7571), 'lpp.object.Integer', 'Integer', (['(left_value - right_value)'], {}), '(left_value - right_value)\n', (7545, 7571), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((9810, 9830), 'typing.cast', 'cast', (['Return', 'result'], {}), '(Return, result)\n', (9814, 9830), False, 'from typing import Any, cast, List, Optional, Type\n'), ((1087, 1110), 'typing.cast', 'cast', (['ast.Integer', 'node'], {}), '(ast.Integer, node)\n', (1091, 1110), False, 'from typing import Any, cast, List, Optional, Type\n'), ((1164, 1183), 'lpp.object.Integer', 'Integer', (['node.value'], {}), '(node.value)\n', (1171, 1183), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((7613, 7646), 'lpp.object.Integer', 'Integer', (['(left_value * right_value)'], {}), '(left_value * right_value)\n', (7620, 7646), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((1234, 1257), 'typing.cast', 'cast', (['ast.Boolean', 'node'], {}), '(ast.Boolean, node)\n', (1238, 1257), False, 'from typing import Any, cast, List, Optional, Type\n'), ((7688, 7722), 'lpp.object.Integer', 'Integer', (['(left_value // right_value)'], {}), '(left_value // right_value)\n', (7695, 7722), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((1396, 1418), 'typing.cast', 'cast', (['ast.Prefix', 'node'], {}), '(ast.Prefix, node)\n', (1400, 1418), False, 'from typing import Any, cast, List, Optional, Type\n'), ((1673, 1694), 'typing.cast', 'cast', (['ast.Infix', 'node'], {}), '(ast.Infix, node)\n', (1677, 1694), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2040, 2061), 'typing.cast', 'cast', (['ast.Block', 'node'], {}), '(ast.Block, node)\n', (2044, 2061), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2164, 2182), 'typing.cast', 'cast', (['ast.If', 'node'], {}), '(ast.If, node)\n', (2168, 2182), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2296, 2327), 'typing.cast', 'cast', (['ast.ReturnStatement', 'node'], {}), '(ast.ReturnStatement, node)\n', (2300, 2327), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2470, 2483), 'lpp.object.Return', 'Return', (['value'], {}), '(value)\n', (2476, 2483), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((2540, 2568), 'typing.cast', 'cast', (['ast.LetStatement', 'node'], {}), '(ast.LetStatement, node)\n', (2544, 2568), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2781, 2807), 'typing.cast', 'cast', (['ast.Identifier', 'node'], {}), '(ast.Identifier, node)\n', (2785, 2807), False, 'from typing import Any, cast, List, Optional, Type\n'), ((2928, 2952), 'typing.cast', 'cast', (['ast.Function', 'node'], {}), '(ast.Function, node)\n', (2932, 2952), False, 'from typing import Any, cast, List, Optional, Type\n'), ((3005, 3046), 'lpp.object.Function', 'Function', (['node.parameters', 'node.body', 'env'], {}), '(node.parameters, node.body, env)\n', (3013, 3046), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n'), ((3147, 3167), 'typing.cast', 'cast', (['ast.Call', 'node'], {}), '(ast.Call, node)\n', (3151, 3167), False, 'from typing import Any, cast, List, Optional, Type\n'), ((3513, 3542), 'typing.cast', 'cast', (['ast.StringLiteral', 'node'], {}), '(ast.StringLiteral, node)\n', (3517, 3542), False, 'from typing import Any, cast, List, Optional, Type\n'), ((3558, 3576), 'lpp.object.String', 'String', (['node.value'], {}), '(node.value)\n', (3564, 3576), False, 'from lpp.object import Boolean, Builtin, Environment, Error, Function, Integer, Null, Object, ObjectType, String, Return\n')]