import time
import modelverse_jit.jit as jit
import modelverse_jit.tree_ir as tree_ir
import modelverse_jit.cfg_ir as cfg_ir
import modelverse_jit.runtime as jit_runtime

BINARY_INTRINSICS = {
    'value_eq' : '==',
    'value_neq' : '!=',

    'bool_and' : 'and',
    'bool_or' : 'or',

    'integer_addition' : '+',
    'integer_subtraction' : '-',
    'integer_multiplication' : '*',
    'integer_division' : '/',
    'integer_gt' : '>',
    'integer_gte' : '>=',
    'integer_lt' : '<',
    'integer_lte' : '<=',

    'float_addition' : '+',
    'float_subtraction' : '-',
    'float_multiplication' : '*',
    'float_division' : '/',
    'float_gt' : '>',
    'float_gte' : '>=',
    'float_lt' : '<',
    'float_lte' : '<='
}

UNARY_INTRINSICS = {
    'bool_not' : 'not',
    'integer_neg' : '-',
    'float_neg' : '-'
}

CAST_INTRINSICS = {
    'cast_i2f' : float,
    'cast_i2s' : str,
    'cast_i2b' : bool,
    'cast_f2i' : int,
    'cast_f2s' : str,
    'cast_f2b' : bool,
    'cast_s2i' : int,
    'cast_s2f' : float,
    'cast_s2b' : bool,
    'cast_b2i' : int,
    'cast_b2f' : float,
    'cast_b2s' : str
}

def create_get_length(expression):
    """Creates an expression that evaluates the given expression, and then
       computes the length of its result."""
    return tree_ir.CallInstruction(
        tree_ir.LoadGlobalInstruction('len'),
        [expression])

# Don't compain about the variable names, pylint. It's important that we
# get them right.
# pylint: disable=I0011,C0103
def __set_add(a, b):
    store_a, load_a = tree_ir.evaluate_and_load(a)
    return tree_ir.create_block(
        store_a,
        tree_ir.CreateEdgeInstruction(load_a, b),
        load_a)

def __dict_add(a, b, c):
    store_a, load_a = tree_ir.evaluate_and_load(a)
    store_b, load_b = tree_ir.evaluate_and_load(b)
    return tree_ir.create_block(
        store_a,
        store_b,
        tree_ir.CreateEdgeInstruction(
            tree_ir.CreateEdgeInstruction(load_a, c),
            load_b),
        load_a)

def __list_read(a, b):
    # The statements in this function generate the following code:
    #
    # a_tmp = a # To make sure a is evaluated before b.
    # b_value, = yield [("RV", [b])]
    # result, = yield [("RD", [a_tmp, b_value])]
    # if result is None:
    #     raise Exception("List read out of bounds: %s" % b_value)
    # result

    store_a, load_a = tree_ir.evaluate_and_load(a)
    b_val = tree_ir.StoreLocalInstruction(
        None,
        tree_ir.ReadValueInstruction(b))
    result = tree_ir.StoreLocalInstruction(
        None,
        tree_ir.ReadDictionaryValueInstruction(
            load_a.create_load(), b_val.create_load()))

    return tree_ir.create_block(
        store_a,
        b_val,
        result,
        tree_ir.SelectInstruction(
            tree_ir.BinaryInstruction(
                result.create_load(),
                'is',
                tree_ir.LiteralInstruction(None)),
            tree_ir.RaiseInstruction(
                tree_ir.CallInstruction(
                    tree_ir.LoadGlobalInstruction('Exception'),
                    [tree_ir.BinaryInstruction(
                        tree_ir.LiteralInstruction('List read out of bounds: %s'),
                        '%',
                        b_val.create_load())])),
            tree_ir.EmptyInstruction()),
        result.create_load())

def __list_append(a, b):
    # We want to generate code that is more or less equivalent to:
    #
    # a_tmp = a
    # b_tmp = b
    # a_outgoing, = yield [("RO", [a_tmp])]
    # _ = yield [("CD", [a_tmp, len(a_outgoing), b_tmp])]
    # a

    store_a, load_a = tree_ir.evaluate_and_load(a)
    store_b, load_b = tree_ir.evaluate_and_load(b)
    return tree_ir.create_block(
        store_a,
        store_b,
        tree_ir.CreateDictionaryEdgeInstruction(
            load_a,
            create_get_length(
                tree_ir.ReadOutgoingEdgesInstruction(
                    load_a)),
            load_b),
        load_a)

def __log(a):
    # Original definition:
    #
    # def log(a, **remainder):
    #     a_value, = yield [("RV", [a])]
    #     print("== LOG == " + str(a_value))
    #     raise PrimitiveFinished(a)

    store_a, load_a = tree_ir.evaluate_and_load(a)
    return tree_ir.CompoundInstruction(
        tree_ir.create_block(
            store_a,
            tree_ir.PrintInstruction(
                tree_ir.BinaryInstruction(
                    tree_ir.LiteralInstruction("== LOG == "),
                    '+',
                    tree_ir.CallInstruction(
                        tree_ir.LoadGlobalInstruction('str'),
                        [tree_ir.ReadValueInstruction(load_a)])))),
        load_a)

def __read_nr_out(a):
    # Original definition:
    #
    # def read_nr_out(a, **remainder):
    #     outgoing, = yield [("RO", [a])]
    #     result, = yield [("CNV", [len(outgoing)])]
    #     raise PrimitiveFinished(result)

    return tree_ir.CreateNodeWithValueInstruction(
        create_get_length(tree_ir.ReadOutgoingEdgesInstruction(a)))

MISC_INTRINSICS = {
    # Reference equality
    'element_eq' :
        lambda a, b:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.BinaryInstruction(a, '==', b)),
    'element_neq' :
        lambda a, b:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.BinaryInstruction(a, '!=', b)),

    # Strings
    'string_get' :
        lambda a, b:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.LoadIndexInstruction(
                tree_ir.ReadValueInstruction(a),
                tree_ir.ReadValueInstruction(b))),
    'string_len' :
        lambda a:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.CallInstruction(
                tree_ir.LoadGlobalInstruction('len'),
                [tree_ir.ReadValueInstruction(a)])),
    'string_join' :
        lambda a, b:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.BinaryInstruction(
                tree_ir.CallInstruction(
                    tree_ir.LoadGlobalInstruction('str'),
                    [tree_ir.ReadValueInstruction(a)]),
                '+',
                tree_ir.CallInstruction(
                    tree_ir.LoadGlobalInstruction('str'),
                    [tree_ir.ReadValueInstruction(b)]))),
    'string_startswith' :
        lambda a, b:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.CallInstruction(
                tree_ir.LoadMemberInstruction(
                    tree_ir.ReadValueInstruction(a),
                    'startswith'),
                [tree_ir.ReadValueInstruction(b)])),

    # State creation
    'create_node' : tree_ir.CreateNodeInstruction,
    'create_edge' :
        # Lambda is totally necessary here, pylint.
        # You totally dropped the ball on this one.
        # pylint: disable=I0011,W0108
        lambda a, b:
        tree_ir.CreateEdgeInstruction(a, b),
    'create_value' :
        lambda a:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.ReadValueInstruction(a)),

    # State reads
    'read_edge_src' :
        lambda a:
        tree_ir.LoadIndexInstruction(
            tree_ir.ReadEdgeInstruction(a),
            tree_ir.LiteralInstruction(0)),
    'read_edge_dst' :
        lambda a:
        tree_ir.LoadIndexInstruction(
            tree_ir.ReadEdgeInstruction(a),
            tree_ir.LiteralInstruction(1)),
    'is_edge' :
        lambda a:
        tree_ir.CreateNodeWithValueInstruction(
            tree_ir.BinaryInstruction(
                tree_ir.LoadIndexInstruction(
                    tree_ir.ReadEdgeInstruction(a),
                    tree_ir.LiteralInstruction(0)),
                'is not',
                tree_ir.LiteralInstruction(None))),

    'read_nr_out' : __read_nr_out,

    # read_root
    'read_root' :
        lambda:
        tree_ir.LoadIndexInstruction(
            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
            tree_ir.LiteralInstruction('root')),

    # read_taskroot
    'read_taskroot' :
        lambda:
        tree_ir.LoadIndexInstruction(
            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
            tree_ir.LiteralInstruction('task_root')),

    # Dictionary operations
    'dict_read' :
        lambda a, b:
        tree_ir.ReadDictionaryValueInstruction(
            a, tree_ir.ReadValueInstruction(b)),

    'dict_read_edge' :
        lambda a, b:
        tree_ir.ReadDictionaryEdgeInstruction(
            a, tree_ir.ReadValueInstruction(b)),

    'dict_add' : __dict_add,
    'dict_len' : __read_nr_out,

    # Set operations
    'set_add' : __set_add,

    # List operations
    'list_len' : __read_nr_out,

    'list_read' : __list_read,
    'list_append' : __list_append,

    # log
    'log' : __log
}

def __read_nr_out_cfg(original_def, a):
    # Original definition:
    #
    # def read_nr_out(a, **remainder):
    #     outgoing, = yield [("RO", [a])]
    #     result, = yield [("CNV", [len(outgoing)])]
    #     raise PrimitiveFinished(result)

    original_def.redefine(
        cfg_ir.CreateNode(
            original_def.insert_before(
                cfg_ir.create_pure_simple_call(
                    'len',
                    original_def.insert_before(
                        cfg_ir.create_read_outgoing_edges(a))))))

def __dict_in_cfg(original_def, a, b):
    # Original definition:
    #
    # def dict_in(a, b, **remainder):
    #     b_value, = yield [("RV", [b])]
    #     value, = yield [("RD", [a, b_value])]
    #     is_in = value is not None
    #     result, = yield [("CNV", [is_in])]
    #     raise PrimitiveFinished(result)

    original_def.redefine(
        cfg_ir.CreateNode(
            original_def.insert_before(
                cfg_ir.Binary(
                    original_def.insert_before(
                        cfg_ir.create_read_dict_value(
                            a, original_def.insert_before(cfg_ir.Read(b)))),
                    'is not',
                    original_def.insert_before(cfg_ir.Literal(None))))))

def __dict_in_node_cfg(original_def, a, b):
    # Original definition:
    #
    # def dict_in_node(a, b, **remainder):
    #     value, = yield [("RDN", [a, b])]
    #     result, = yield [("CNV", [value is not None])]
    #     raise PrimitiveFinished(result)

    original_def.redefine(
        cfg_ir.CreateNode(
            original_def.insert_before(
                cfg_ir.Binary(
                    original_def.insert_before(cfg_ir.create_read_dict_node(a, b)),
                    'is not',
                    original_def.insert_before(cfg_ir.Literal(None))))))

def __dict_read_cfg(original_def, a, b):
    # Original definition:
    #
    # def dict_read(a, b, **remainder):
    #     b_value, = yield [("RV", [b])]
    #     result, = yield [("RD", [a, b_value])]
    #     raise PrimitiveFinished(result)

    original_def.redefine(
        cfg_ir.create_read_dict_value(
            a,
            original_def.insert_before(cfg_ir.Read(b))))

MISC_CFG_INTRINSICS = {
    # Reference equality
    'element_eq' :
        lambda original_def, a, b:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.Binary(a, '==', b)))),
    'element_neq' :
        lambda original_def, a, b:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.Binary(a, '!=', b)))),


    # String operations
    'string_get' :
        lambda original_def, a, b:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.create_index(
                        original_def.insert_before(cfg_ir.Read(a)),
                        original_def.insert_before(cfg_ir.Read(b)))))),
    'string_len' :
        lambda original_def, a:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.create_pure_simple_call(
                        'len',
                        original_def.insert_before(cfg_ir.Read(a)))))),
    'string_join' :
        lambda original_def, a, b:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.Binary(
                        original_def.insert_before(
                            cfg_ir.create_pure_simple_call(
                                'str',
                                original_def.insert_before(cfg_ir.Read(a)))),
                        '+',
                        original_def.insert_before(
                            cfg_ir.create_pure_simple_call(
                                'str',
                                original_def.insert_before(cfg_ir.Read(b)))))))),
    'string_startswith' :
        lambda original_def, a, b:
        original_def.redefine(
            cfg_ir.CreateNode(
                original_def.insert_before(
                    cfg_ir.DirectFunctionCall(
                        'startswith',
                        [('self', original_def.insert_before(cfg_ir.Read(a))),
                         ('substring', original_def.insert_before(cfg_ir.Read(b)))],
                        calling_convention=cfg_ir.SELF_POSITIONAL_CALLING_CONVENTION,
                        has_value=True, has_side_effects=False)))),

    # State creation
    'create_node' :
        lambda original_def:
        original_def.redefine(
            cfg_ir.CreateNode(original_def.insert_before(cfg_ir.Literal(None)))),
    'create_value' :
        lambda original_def, a:
        original_def.redefine(
            cfg_ir.CreateNode(original_def.insert_before(cfg_ir.Read(a)))),

    # State reads
    'read_nr_out' : __read_nr_out_cfg,

    # Dictionary operations
    'dict_len' : __read_nr_out_cfg,
    'dict_read' : __dict_read_cfg,
    'dict_in' : __dict_in_cfg,
    'dict_in_node' : __dict_in_node_cfg,

    # List operations
    'list_len' : __read_nr_out_cfg
}

def register_time_intrinsic(target_jit):
    """Registers the time() intrinsic with the given JIT."""
    import_name = target_jit.import_value(time.time, 'time')
    target_jit.register_intrinsic(
        'time',
        lambda: tree_ir.CreateNodeWithValueInstruction(
            tree_ir.CallInstruction(
                tree_ir.LoadGlobalInstruction(import_name),
                [])))

def register_intrinsics(target_jit):
    """Registers all intrinsics in the module with the given JIT."""
    for (key, value) in BINARY_INTRINSICS.items():
        target_jit.register_binary_intrinsic(key, value)
    for (key, value) in UNARY_INTRINSICS.items():
        target_jit.register_unary_intrinsic(key, value)
    for (key, value) in CAST_INTRINSICS.items():
        target_jit.register_cast_intrinsic(key, value)
    for (key, value) in MISC_INTRINSICS.items():
        target_jit.register_intrinsic(key, value)
    for (key, value) in MISC_CFG_INTRINSICS.items():
        target_jit.register_cfg_intrinsic(key, value)

    register_time_intrinsic(target_jit)
