import time as python_time
import json
import sys

class InterpretedFunctionFinished(Exception):
    """Exception to indicate the result value of an interpreted function, as a return
       cannot be used."""
    def __init__(self, value):
        Exception.__init__(self)
        self.result = value

class SleepKernel(Exception):
    """Exception to indicate the kernel to sleep for some time."""
    def __init__(self, timeout, interruptable):
        Exception.__init__(self)
        self.timeout = timeout
        self.interruptable = interruptable

# Functions annotated with __exception_return use the JIT's calling convention instead of
# returnvalue is not modified.
#
# ### Rationale for __exception_return
#
# __exception_return is a useful mechanism because it allows us to have a __call_function
# implementation that has O(1) state read overhead. A previous implementation of
# __call_function checked if the caller's frame had been popped whenever
# ModelverseKernel.execute_yield threw a StopIteration exception. However, that incurs O(n) overhead
# _per call,_ where n is the number of StopIteration exceptions that are thrown during the call.
# O(n) is pretty bad, but this actually becomes O(n * m) when m calls to __call_function are
# nested. And that's just not acceptable.
# __exception_return requires kernel support, but I think the complexity gains are well worth it;
# I reckon JIT-to-interpreter switches aren't going to get a whole lot cheaper than this.
EXCEPTION_RETURN_KEY = "__exception_return"
"""A dictionary key for functions which request that the kernel throw a InterpretedFunctionFinished
   exception with the return value instead of injecting the return value in the caller's frame."""

def integer_subtraction(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] - b['value']}])]

def integer_addition(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] + b['value']}])]

def integer_multiplication(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] * b['value']}])]

def integer_division(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': int(a['value']) // b['value']}])]

def integer_lt(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] < b['value']}])]

def bool_and(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] and b['value']}])]

def bool_or(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] or b['value']}])]

def bool_not(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': not a['value']}])]

def float_subtraction(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] - b['value']}])]

def float_addition(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] + b['value']}])]

def float_multiplication(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] * b['value']}])]

def float_division(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': float(a['value']) / b['value']}])]

def float_lt(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] < b['value']}])]

def string_join(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': str(a['value']) + str(b['value'])}])]

def string_split(a, b, **remainder):
    # TODO make non-primitive, though compiled
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    result = a['value'].split(b['value'])
    elems = yield [("CN", [])] + [("CNV", [v]) for v in result]
    new_val = elems[0]
    yield [("CD", [new_val, i, v]) for i, v in enumerate(elems[1:])]
    yield [("RETURN", [{'id': new_val}])]

def string_get(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'][b['value']]}])]

def string_len(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': len(a['value'])}])]

def value_eq(a, b, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if 'value' not in b:
        b['value'], = yield [("RV", [b['id']])]
    yield [("RETURN", [{'value': a['value'] == b['value']}])]

def element_eq(a, b, **remainder):
    if "id" not in a:
        #print("MATERIALIZING A element_eq")
        a['id'], = yield [("CNV", [a['value']])]
    if "id" not in b:
        #print("MATERIALIZING B element_eq")
        b['id'], = yield [("CNV", [b['value']])]
    yield [("RETURN", [{'value': a['id'] == b['id']}])]

def cast_string(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if isinstance(a['value'], dict):
        yield [("RETURN", [{'value': str(a['value']['value'])}])]
    else:
        yield [("RETURN", [{'value': str(a['value'])}])]

def cast_float(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': float(a['value'])}])]

def cast_boolean(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': bool(a['value'])}])]

def cast_integer(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': int(a['value'])}])]

def cast_value(a, **remainder):
    if 'value' not in a:
        a['value'], = yield [("RV", [a['id']])]
    if isinstance(a['value'], dict):
        yield [("RETURN", [{'value': str(a['value']['value'])}])]
    else:
        yield [("RETURN", [{'value': json.dumps(a['value'])}])]

def cast_id(a, **remainder):
    if "id" not in a:
        #print("MATERIALIZING A cast_id")
        a['id'], = yield [("CNV", [a['value']])]
    yield [("RETURN", [{'value': str(a['id'])}])]

def dict_add_fast(a, b, c, **remainder):
    # TODO deprecate, as dict_add is now also efficient
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]
    if "id" not in c:
        #print("MATERIALIZING C dict_add_fast")
        c['id'], = yield [("CNV", [c['value']])]

    yield [("CD", [a['id'], b['value'], c['id']])]
    yield [("RETURN", [a])]

def dict_delete(a, b, **remainder):
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]

    edge, = yield [("RDE", [a['id'], b['value']])]
    if edge is None:
        print("Failed dict_delete for value '%s'!" % b['value'])
        keys, = yield [("RDK", [a['id']])]
        keys = yield [("RV", [i]) for i in keys]
        print("Keys: " + str(keys))
        raise Exception()
    yield [("DE", [edge])]
    yield [("RETURN", [a])]

def dict_delete_node(a, b, **remainder):
    edge, = yield [("RDNE", [a['id'], b['id']])]
    if edge is None:
        print("Failed dict_delete_node!")
    yield [("DE", [edge])]
    yield [("RETURN", [a])]

def dict_read(a, b, **remainder):
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]
    result, = yield [("RD", [a['id'], b['value']])]
    yield [("RETURN", [{'id': result}])]

def dict_read_edge(a, b, **remainder):
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]
    result, = yield [("RDE", [a['id'], b['value']])]
    yield [("RETURN", [{'id': result}])]

def dict_read_node(a, b, **remainder):
    result, = yield [("RDN", [a['id'], b['id']])]
    yield [("RETURN", [{'id': result}])]

def dict_in(a, b, **remainder):
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]
    value, = yield [("RD", [a['id'], b['value']])]
    yield [("RETURN", [{'value': value is not None}])]

def dict_in_node(a, b, **remainder):
    if "id" not in b:
        # Not even allocated the node, so it is certain not to be in the dictionary
        yield [("RETURN", [{'value': False}])]
    value, = yield [("RDN", [a['id'], b['id']])]
    yield [("RETURN", [{'value': value is not None}])]

def dict_keys(a, **remainder):
    keys, result = yield [("RDK", [a['id']]), ("CN", [])]
    edges = yield [("CE", [result, result]) for _ in keys]
    _ = yield [("CE", [edge, key]) for edge, key in zip(edges, keys)]
    yield [("RETURN", [{'id': result}])]

def is_physical_int(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    try:
        yield [("RETURN", [{'value': isinstance(a['value'], int) or isinstance(a['value'], long)}])]
    except NameError:
        yield [("RETURN", [{'value': isinstance(a['value'], int)}])]

def is_physical_string(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    try:
        yield [("RETURN", [{'value': isinstance(a['value'], str) or isinstance(a['value'], unicode)}])]
    except NameError:
        yield [("RETURN", [{'value': isinstance(a['value'], str)}])]

def is_physical_float(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': isinstance(a['value'], float)}])]

def is_physical_boolean(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': isinstance(a['value'], bool)}])]

def is_physical_action(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': isinstance(a['value'], dict) and a['value']["value"] in ["if", "while", "assign", "call", "break", "continue", "return", "resolve", "access", "constant", "global", "declare"]}])]

def is_physical_none(a, **remainder):
    if a['id'] is None:
        yield [("RETURN", [{"value": True}])]
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    elif a['value'] is None:
        yield [("RETURN", [{"value": True}])]
    yield [("RETURN", [{'value': isinstance(a['value'], dict) and a['value']["value"] == "none"}])]

def create_node(**remainder):
    result, = yield [("CN", [])]
    yield [("RETURN", [{'id': result}])]

def create_edge(a, b, **remainder):
    if "id" not in a:
        #print("MATERIALIZING A create_edge")
        a['id'], = yield [("CNV", [a['value']])]
    if "id" not in b:
        #print("MATERIALIZING B create_edge")
        b['id'], = yield [("CNV", [b['value']])]
    result, = yield [("CE", [a['id'], b['id']])]
    yield [("RETURN", [{'id': result}])]

def create_value(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    yield [("RETURN", [{'value': a['value']}])]

def read_nr_out(a, **remainder):
    if "id" not in a:
        yield [("RETURN", [{'value': 0}])]
    else:
        outgoing, = yield [("RO", [a['id']])]
        yield [("RETURN", [{'value': len(outgoing)}])]

def read_out(a, b, root, **remainder):
    if "id" not in a:
        a['id'], = yield [("CNV", [a['value']])]
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]

    outgoing, = yield [("RO", [a['id']])]
    yield [("RETURN", [{'id': sorted(outgoing)[b['value']] if len(outgoing) > b['value'] else root}])]

def read_nr_in(a, **remainder):
    if "id" not in a:
        yield [("RETURN", [{'value': 0}])]
    else:
        incoming, = yield [("RI", [a['id']])]
        yield [("RETURN", [{'value': len(incoming)}])]

def read_in(a, b, root, **remainder):
    if "id" not in a:
        a['id'], = yield [("CNV", [a['value']])]
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]

    incoming, = yield [("RI", [a['id']])]
    yield [("RETURN", [{'id': sorted(incoming)[b['value']] if len(incoming) > b['value'] else root}])]

def read_edge_src(a, **remainder):
    result, = yield [("RE", [a['id']])]
    yield [("RETURN", [{'id': result[0]}])]

def read_edge_dst(a, **remainder):
    result, = yield [("RE", [a['id']])]
    yield [("RETURN", [{'id': result[1]}])]

def delete_element(a, **remainder):
    if "id" not in a:
        yield [("RETURN", [{'value': False}])]

    edge, = yield [("RE", [a['id']])]
    if edge[0] is None:
        # Not an edge:
        yield [("DN", [a['id']])]
        yield [("RETURN", [{'value': False}])]
    else:
        yield [("DE", [a['id']])]
        yield [("RETURN", [{'value': True}])]

def read_root(root, **remainder):
    yield [("RETURN", [{'id': root}])]

def is_edge(a, **remainder):
    if "id" not in a:
        yield [("RETURN", [{'value': False}])]

    edge, = yield [("RE", [a['id']])]
    yield [("RETURN", [{'value': edge[0] is not None}])]

def log(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    print("== LOG == " + str(a['value']))
    yield [("RETURN", [a])]

def read_taskroot(task_root, **remainder):
    yield [("RETURN", [{'id': task_root}])]

def time(**remainder):
    yield [("RETURN", [{'value': python_time.time()}])]

def hash(a, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    import hashlib

    try:
        value = hashlib.sha512(a['value']).hexdigest()
    except TypeError:
        value = hashlib.sha512(a['value'].encode()).hexdigest()
    yield [("RETURN", [{'value': value}])]

def __sleep(a, b, **remainder):
    if "value" not in a:
        a['value'], = yield [("RV", [a['id']])]
    if "value" not in b:
        b['value'], = yield [("RV", [b['id']])]
    timeout = a['value']
    interruptable = b['value']
    yield [("SLEEP", [timeout, interruptable])]
    yield [("RETURN", [a])]

def is_error(a, **remainder):
    if a['id'] is None:
        yield [("RETURN", [{'value': True}])]
    else:
        yield [("RETURN", [{'value': False}])]

def list_sort(a, **remainder):
    elements, = yield [("RO", [a['id']])]
    values = yield [("RD", [a['id'], i]) for i in range(len(elements))]
    values = yield [("RV", [i]) for i in values]
    new_list, = yield [("CN", [])]
    values = yield [("CNV", [v]) for v in sorted(values)]
    yield [("CD", [new_list, i, v]) for i, v in enumerate(values)]

    yield [("RETURN", [{'id': new_list}])]
