from sccd.runtime.statecharts_core import Event
import sccd.runtime.socket2event as socket2event
import modelverse_SCCD as modelverse_SCCD
import time
import threading

import sys
if sys.version_info[0] < 3:
    from urllib2 import urlopen as urlopen
    from urllib2 import Request as Request
    from urllib import urlencode as urlencode
else:
    from urllib.request import urlopen as urlopen
    from urllib.request import Request as Request
    from urllib.parse import urlencode as urlencode

# Exceptions
class ModelverseException(Exception):
    pass

class UnknownError(ModelverseException):
    pass

class UnknownM3(ModelverseException):
    pass

class UnknownIdentifier(ModelverseException):
    pass

class CompilationError(ModelverseException):
    pass

class UnknownAttribute(UnknownIdentifier):
    pass

class UnknownElement(UnknownIdentifier):
    pass

class UnknownModel(UnknownIdentifier):
    pass

class UnknownLocation(UnknownIdentifier):
    pass

class ConnectionError(ModelverseException):
    pass

class ExistsError(ModelverseException):
    pass

class AttributeExists(ExistsError):
    pass

class ElementExists(ExistsError):
    pass

class ModelExists(ExistsError):
    pass

class FolderExists(ExistsError):
    pass

class PermissionDenied(ModelverseException):
    pass

class ReadPermissionDenied(PermissionDenied):
    pass

class WritePermissionDenied(PermissionDenied):
    pass

class ExecutePermissionDenied(PermissionDenied):
    pass

class UserPermissionDenied(PermissionDenied):
    pass

class GroupPermissionDenied(PermissionDenied):
    pass

class AdminPermissionDenied(PermissionDenied):
    pass

class InterfaceMismatch(ModelverseException):
    pass

class UnknownMetamodellingHierarchy(ModelverseException):
    pass

class NotAnAssociation(ModelverseException):
    pass

def run_controller():
    try:
        controller.start()
    finally:
        controller.stop()

def _next_ID():
    global ID
    ID += 1
    return ID

def __run_new_modelverse(address, username, password, callback, model):
    init(address)
    login(username, password)
    callback(model)
    exit_save(model)
    disconnect()

def __run_new_modelverse_activity(address, username, password, taskname, pipe, callback):
    init(address, taskname=taskname)
    controller.username = username
    controller.password = password
    t = OUTPUT()

    if t == "OP":
        model = OUTPUT()
        if callback is not None:
            __invoke(callback, model)
        controller.addInput(Event("data_input", "action_in", [None]))
        time.sleep(2)
    elif t == "SC":
        while 1:
            empty = True

            # Fetch output from the MV
            response = responses.fetch(0)
            if response is not None:
                if response.name == "data_output":
                    # Got output of MV, so forward to SCCD
                    if pipe is not None:
                        pipe.send(("input", response.parameters))
                elif response.name == "result":
                    # Finished execution, so continue and return result
                    if pipe is not None:
                        pipe.send(("terminate", []))
                        pipe.close()
                    return
                else:
                    raise Exception("Unknown data from MV to SC: " + str(response))
                empty = False

            # Fetch output from the SC
            if pipe is not None and pipe.poll():
                response = pipe.recv()

                if response.name == "output":
                    controller.addInput(Event("data_input", "action_in", [response.parameters]))
                else:
                    raise Exception("Unknown data from SC to MV: " + str(response))
                empty = False

            if empty:
                time.sleep(0.05)

def __invoke(callback, model):
    import multiprocessing
    p = multiprocessing.Process(target=__run_new_modelverse, args=[controller.address, controller.username, controller.password, callback, model])
    p.start()
    p.join()

def _process_SC(statechart, port_sc, taskname):
    import multiprocessing
    p2c_pipe, c2p_pipe = multiprocessing.Pipe()
    p = multiprocessing.Process(target=__run_new_modelverse_activity, args=[controller.address, controller.username, controller.password, taskname, c2p_pipe, None])
    p.start()
    while 1:
        empty = True

        if p2c_pipe.poll():
            response = p2c_pipe.recv()
            statechart[0].addInput(Event(response[0], statechart[1], response[1]))
            
            if response[0] == "terminate":
                p2c_pipe.close()
                break
            empty = False

        response = port_sc.fetch(0)
        if response is not None:
            p2c_pipe.send(response)
            empty = False

        if empty:
            time.sleep(0.05)

    p.join()

def _process_OP(callback, taskname):
    import multiprocessing
    p = multiprocessing.Process(target=__run_new_modelverse_activity, args=[controller.address, controller.username, controller.password, taskname, None, callback])
    p.start()
    p.join()

def INPUT(action, parameters):
    controller.addInput(Event("action", "action_in", [action, _next_ID(), parameters]))

def OUTPUT():
    while 1:
        response = responses.fetch(-1)
        if response.name == "result":
            return response.parameters[1]
        elif response.name == "exception":
            try:
                raise eval(response.parameters[1])(*response.parameters[2:])
            except NameError:
                raise UnknownError(response.parameters[1:])

def init(address_param="127.0.0.1:8001", timeout=60.0, taskname=None):
    global controller
    global ID
    global responses

    controller = modelverse_SCCD.Controller(taskname)
    socket2event.boot_translation_service(controller)

    ID = 0

    thrd = threading.Thread(target=run_controller)
    thrd.daemon = True
    thrd.start()

    responses = controller.addOutputListener("action_out")
    controller.addOutputListener("ready").fetch(-1)

    INPUT("init", [address_param, timeout])
    controller.address = address_param
    OUTPUT()
    return

def login(username, password):
    controller.username = username
    controller.password = password
    INPUT("login", [username, password])
    return OUTPUT()

def model_list(location):
    INPUT("model_list", [location])
    return OUTPUT()

def model_add(model_name, metamodel_name, model_code=""):
    INPUT("model_add", [model_name, metamodel_name, model_code])
    return OUTPUT()

def model_move(source_name, target_name):
    INPUT("model_move", [source_name, target_name])
    return OUTPUT()

def model_delete(model_name):
    INPUT("model_delete", [model_name])
    return OUTPUT()

def model_list_full(location):
    INPUT("model_list_full", [location])
    return OUTPUT()

def verify(model_name, metamodel_name, conformance_function=""):
    INPUT("verify", [model_name, metamodel_name, conformance_function])
    return OUTPUT()

def model_overwrite(model_name, new_model):
    INPUT("model_overwrite", [model_name, new_model])
    return OUTPUT()

def disconnect():
    INPUT("disconnect", [])
    return OUTPUT()

def user_logout():
    INPUT("user_logout", [])
    return OUTPUT()

def model_render(model_name, mapper_name, rendered_name):
    INPUT("model_render", [model_name, mapper_name, rendered_name])
    return OUTPUT()

def transformation_between(sources, targets):
    INPUT("transformation_between", [sources, targets])
    return OUTPUT()

def transformation_add_MT(source_metamodels, target_metamodels, operation_name, code, callback=None):
    INPUT("transformation_add_MT", [source_metamodels, target_metamodels, operation_name, code, True])
    model = OUTPUT()

    if callback is not None:
        __invoke(callback, model)
    controller.addInput(Event("data_input", "action_in", [None]))

    return OUTPUT()

def transformation_add_AL(source_metamodels, target_metamodels, operation_name, code, callback=None):
    INPUT("transformation_add_AL", [source_metamodels, target_metamodels, operation_name, code, True])
    model = OUTPUT()

    if model is not None:
        # In case the source and target metamodels are empty, the model will be None, indicating that we are finished already (no callbacks allowed)

        if callback is not None:
            __invoke(callback, model)
        controller.addInput(Event("data_input", "action_in", [None]))

    return OUTPUT()

def transformation_add_MANUAL(source_metamodels, target_metamodels, operation_name, callback=None):
    INPUT("transformation_add_MANUAL", [source_metamodels, target_metamodels, operation_name, True])
    model = OUTPUT()

    if callback is not None:
        __invoke(callback, model)
    controller.addInput(Event("data_input", "action_in", [None]))

    return OUTPUT()

def __transformation_execute(operation_name, input_models_dict, output_models_dict, statechart, tracability_model, fetch_output):
    if statechart is not None:
        port_sc = statechart[0].addOutputListener(statechart[2])

    INPUT("transformation_execute", [operation_name, input_models_dict, output_models_dict, tracability_model, fetch_output])
    taskname = OUTPUT()
    if statechart is not None:
        threading.Thread(target=_process_SC, args=[statechart, port_sc, taskname]).start()
    return OUTPUT()

def transformation_execute_MT(operation_name, input_models_dict, output_models_dict, statechart=None, tracability_model="", fetch_output=True):
    return __transformation_execute(operation_name, input_models_dict, output_models_dict, statechart, tracability_model, fetch_output)

def transformation_execute_AL(operation_name, input_models_dict, output_models_dict, statechart=None, tracability_model="", fetch_output=True):
    return __transformation_execute(operation_name, input_models_dict, output_models_dict, statechart, tracability_model, fetch_output)

def transformation_execute_MANUAL(operation_name, input_models_dict, output_models_dict, callback=None, tracability_model=""):
    INPUT("transformation_execute", [operation_name, input_models_dict, output_models_dict, tracability_model])
    taskname = OUTPUT()

    _process_OP(callback, taskname)

    return OUTPUT()

def transformation_signature(operation_name):
    INPUT("transformation_signature", [operation_name])
    return OUTPUT()

def process_signature(process_name):
    INPUT("process_signature", [process_name])
    return OUTPUT()

def permission_modify(model_name, permissions):
    INPUT("permission_modify", [model_name, permissions])
    return OUTPUT()

def permission_owner(model_name, permission):
    INPUT("permission_owner", [model_name, permission])
    return OUTPUT()

def permission_group(model_name, group):
    INPUT("permission_group", [model_name, group])
    return OUTPUT()

def group_create(group_name):
    INPUT("group_create", [group_name])
    return OUTPUT()

def group_delete(group_name):
    INPUT("group_delete", [group_name])
    return OUTPUT()

def group_owner_add(group_name, user_name):
    INPUT("group_owner_add", [group_name, user_name])
    return OUTPUT()

def group_owner_delete(group_name, user_name):
    INPUT("group_owner_delete", [group_name, user_name])
    return OUTPUT()

def group_join(group_name, user_name):
    INPUT("group_join", [group_name, user_name])
    return OUTPUT()

def group_kick(group_name, user_name):
    INPUT("group_kick", [group_name, user_name])
    return OUTPUT()

def group_list():
    INPUT("group_list", [])
    return OUTPUT()

def admin_promote(user_name):
    INPUT("admin_promote", [user_name])
    return OUTPUT()

def admin_demote(user_name):
    INPUT("admin_demote", [user_name])
    return OUTPUT()

def conformance_delete(model_name, metamodel_name):
    INPUT("conformance_delete", [model_name, metamodel_name])
    return OUTPUT()

def conformance_add(model_name, metamodel_name):
    INPUT("conformance_add", [model_name, metamodel_name])
    return OUTPUT()

def folder_create(folder_name):
    INPUT("folder_create", [folder_name])
    return OUTPUT()

def model_types(model_name):
    INPUT("model_types", [model_name])
    return OUTPUT()

def alter_context(model_name, metamodel_name):
    INPUT("alter_context", [model_name, metamodel_name])

def element_list(model_name):
    INPUT("element_list", [model_name])
    return OUTPUT()

def element_list_nice(model_name):
    INPUT("element_list_nice", [model_name])
    return OUTPUT()

def types(model_name):
    INPUT("types", [model_name])
    return OUTPUT()

def types_full(model_name):
    INPUT("types_full", [model_name])
    return OUTPUT()

def read_info(model_name, ID):
    INPUT("read_info", [model_name, ID])
    return OUTPUT()

def read_attrs(model_name, ID):
    INPUT("read_attrs", [model_name, ID])
    return OUTPUT()

def instantiate(model_name, typename, edge=None, ID=""):
    INPUT("instantiate", [model_name, typename, edge, ID])
    return OUTPUT()

def delete_element(model_name, ID):
    INPUT("delete_element", [model_name, ID])
    return OUTPUT()

def attr_assign(model_name, ID, attr, value):
    INPUT("attr_assign", [model_name, ID, attr, value])
    return OUTPUT()

def attr_assign_code(model_name, ID, attr, code):
    INPUT("attr_assign_code", [model_name, ID, attr, code])
    return OUTPUT()

def attr_delete(model_name, ID, attr):
    INPUT("attr_delete", [model_name, ID, attr])
    return OUTPUT()

def AL_text(code_location):
    INPUT("AL_text", [code_location])
    return OUTPUT()

def read_outgoing(model_name, ID, typename):
    INPUT("read_outgoing", [model_name, ID, typename])
    return OUTPUT()

def read_incoming(model_name, ID, typename):
    INPUT("read_incoming", [model_name, ID, typename])
    return OUTPUT()

def read_association_source(model_name, ID):
    INPUT("read_association_source", [model_name, ID])
    return OUTPUT()

def read_association_destination(model_name, ID):
    INPUT("read_association_destination", [model_name, ID])
    return OUTPUT()

def connections_between(model_name, source, target):
    INPUT("connections_between", [model_name, source, target])
    return OUTPUT()

def define_attribute(model_name, node, attr_name, attr_type):
    INPUT("define_attribute", [model_name, node, attr_name, attr_type])
    return OUTPUT()

def all_instances(model_name, type_name):
    INPUT("all_instances", [model_name, type_name])
    return OUTPUT()

def process_execute(process_name, model_mapping, callbacks={}):
    # for all callbacks to SCs, start up the output port already
    sc_ports = {}
    for k, v in callbacks.items():
        if isinstance(v, (tuple, list)):
            # Is a statechart, so register already
            sc_ports[k] = v[0].addOutputListener(v[2])

    INPUT("process_execute", [process_name, model_mapping])

    while 1:
        result = OUTPUT()
        if result == "Success":
            # Finished
            return None
        else:
            taskname, operation = result

            if (operation in callbacks):
                data = callbacks[operation]

                if isinstance(data, (tuple, list)):
                    # Statechart, so consider like that
                    threading.Thread(target=_process_SC, args=[data, sc_ports[operation], taskname]).start()
                else:
                    # Assume function
                    threading.Thread(target=_process_OP, args=[data, taskname]).start()
            else:
                # Assume empty function
                threading.Thread(target=_process_OP, args=[None, taskname]).start()

def get_taskname():
    """Fetch the taskname of the current connection."""
    return controller.taskname

def exit_save(model_name):
    INPUT("exit_save", [model_name])
    return OUTPUT()

""" Some hardcoded functions... Way easier to express them with code than with statecharts!"""
import json
import urllib
try:
    import urllib2
except ImportError:
    import urllib as urllib2

def service_register(name, function):
    """Register a function as a service with a specific name."""
    INPUT("service_register", [name, function])
    port = OUTPUT()
    return port

def service_stop():
    """Stop the currently executing process."""
    INPUT("service_stop", [])
    return OUTPUT()

def service_get(port):
    """Get the values on the specified port."""

    data = urlencode({"op": "get_output", "taskname": port}).encode()
    val = json.loads(urlopen(Request("http://%s" % controller.address, data), timeout=99999).read())
    return val

def service_set(port, value):
    """Set a value on a specified port."""
    if isinstance(value, type([])):
        value = json.dumps(value)
        data = urlencode({"op": "set_input", "data": value, "taskname": port}).encode()
        urlopen(Request("http://%s" % controller.address, data), timeout=99999).read()
    else:
        value = json.dumps(value)
        data = urlencode({"op": "set_input", "value": value, "taskname": port}).encode()
        urlopen(Request("http://%s" % controller.address, data), timeout=99999).read()

def service_poll(port):
    """Checks whether or not the Modelverse side has any input ready to be processed."""
    raise NotImplementedError()
