import subprocess
import platform
import dv.Control as Control
import time
import os
import numpy as np


def start_and_initialize(log_file_name):
    # Starting runtime
    interface = dv_interface(log_file_name)

    # Remove all modules if exists
    interface.remove_all_modules()

    return interface


def start_runtime(log_file):
    popen_args = []

    # Determine runtime executable path
    if 'DV_RUNTIME_PATH' in os.environ:
        runtime_exec = os.environ['DV_RUNTIME_PATH']
    else:
        if (platform.system().upper().find("MINGW") != -1) or (platform.system() == "Windows"):
            runtime_exec = "dv-runtime.exe"
        else:
            runtime_exec = "dv-runtime"

    popen_args.append(runtime_exec)

    # Determine modules path, or leave default
    if 'DV_MODULES_PATH' in os.environ:
        modules_path = os.environ['DV_MODULES_PATH']
    else:
        modules_path = 'default'

    runtime = subprocess.Popen(popen_args,
                               stdin=subprocess.DEVNULL,
                               stdout=log_file,
                               stderr=subprocess.STDOUT,
                               bufsize=0)
    runtime.stdout = log_file

    return runtime


def runtime_out_print(runtime, ):
    runtime.stdout.flush()
    runtime.stdout.seek(0)
    output_runtime = runtime.stdout.read()

    if output_runtime:
        print(
            "\n\n\n*********************************************************************************************************\n"
        )
        for line in output_runtime.split('\n'):
            print(line)
        print(
            "\n*********************************************************************************************************\n\n\n"
        )
        return output_runtime
    else:
        return ''


class dv_interface():
    runtime = None
    control = None
    log_file = None

    def __init__(self, log_file_name):
        self.log_file = open(os.environ['DV_TEST_OUTPUT_DIR'] + os.path.sep + log_file_name, "w+")

        self.runtime = start_runtime(self.log_file)
        print("\tRuntime started")

        self.control = Control()

        if self.control is not None:
            print("\tControl object created")

        running = False
        print("\tWait for system to start running")
        while not running:
            try:
                running = self.get("/system/", "running")
            except:
                time.sleep(1)

    def put(self, path, parameter, value):
        return self.control.put(path, parameter, value)

    def get(self, path, parameter):
        return self.control.get(path, parameter)

    def add_module(self, moduleInfo):
        print("\tAdd module {}".format(moduleInfo.name))

        self.control.add_module(moduleInfo.name, moduleInfo.library)

        if moduleInfo.config_options is not None:
            while not self.node_exists("/mainloop/{}/".format(moduleInfo.name)):
                pass

            self.put_config_options(moduleInfo)

        if moduleInfo.inputs is not None:
            while not self.node_exists("/mainloop/{}/".format(moduleInfo.name)):
                pass

            self.connect_inputs(moduleInfo)

    def remove_module(self, module_name):
        print("\tRemove module {}".format(module_name))
        return self.control.remove_module(module_name)

    def add_input_module(self, filename, name="input", outputs=None, parameters=None):
        config_options = [["timeDelay", "0"], ["file", filename]]
        if parameters is not None:
            config_options += parameters
        input = dv_module(name, "dv_input_file", outputs=outputs, config_options=config_options)

        self.add_module(input)

        return input

    def add_output_module(self, inputs, name="output", prefix="dvSave", parameters=None):
        config_options = [["compression", "NONE"], ["directory", os.getenv('DV_TEST_OUTPUT_DIR')], ["prefix", prefix]]
        if parameters is not None:
            config_options += parameters
        output = dv_module(name, "dv_output_file", config_options=config_options, inputs=inputs)

        self.add_module(output)

        return output

    def put_config_options(self, moduleInfo, parameters=None):
        for option in moduleInfo.config_options:
            print("\tSet config parameter {} to value {} for module {}".format(option.parameter, option.value,
                                                                               moduleInfo.name))
            self.put("/mainloop/{}/".format(moduleInfo.name), option.parameter, option.value)

    def connect_inputs(self, moduleInfo):
        ret = 0
        for input in moduleInfo.inputs:
            print("\tConnect input {} to origin {} for module {}".format(input.name, input.origin, moduleInfo.name))
            self.put("/mainloop/{}/inputs/{}/".format(moduleInfo.name, input.name), "from", input.origin)

        return ret

    def module_is_running(self, moduleInfo):
        return self.get("/mainloop/{}/".format(moduleInfo.name), "isRunning")

    def wait_until_running(self, modules, timeout=5):
        for module in modules:
            print("\tWait until module {} is running".format(module.name))
            now = time.time()
            while not self.module_is_running(module) and np.abs(time.time() - now) < timeout:
                time.sleep(0.01)

    def wait_until_stopped(self, modules):
        for module in modules:
            print("\tWait until module {} is stopped".format(module.name))
            while self.module_is_running(module):
                time.sleep(0.01)

    def get_cpu_time(self, module):
        return self.get("/mainloop/{}/".format(module.name), "cpuTime")

    def get_cycle_time_stats(self, module):
        print("\tObtain cycle time stats for module {}".format(module.name))

        mean = self.get("/mainloop/{}/cycleTime/".format(module.name), "mean")
        var = self.get("/mainloop/{}/cycleTime/".format(module.name), "var")
        min = self.get("/mainloop/{}/cycleTime/".format(module.name), "min")
        max = self.get("/mainloop/{}/cycleTime/".format(module.name), "max")

        return {"mean": mean, "var": var, "min": min, "max": max}

    def get_throughput_stats(self, module):
        print("\tObtain throughput stats for module {}".format(module.name))
        stats = {}
        for input in module.inputs:
            mean = self.get("/mainloop/{}/inputs/{}/throughput/".format(module.name, input.name), "mean")
            var = self.get("/mainloop/{}/inputs/{}/throughput/".format(module.name, input.name), "var")
            min = self.get("/mainloop/{}/inputs/{}/throughput/".format(module.name, input.name), "min")
            max = self.get("/mainloop/{}/inputs/{}/throughput/".format(module.name, input.name), "max")

            stats[input.name] = {"mean": mean, "var": var, "min": min, "max": max}
        return stats

    def node_exists(self, path):
        return self.control.node_exists(path)

    def get_children(self, path):
        try:
            return self.control.get_children(path)
        except RuntimeError as e:
            if str(e.args).find("Node has no children") != -1:
                return ""

    def remove_all_modules(self):
        print("\tRemove all modules")

        self.stop_all_modules()

        removed = []
        module_to_remove = self.get_children("/mainloop/")

        if module_to_remove == None or module_to_remove == "":
            return

        for module in module_to_remove:
            self.remove_module(module)
            removed.append(module)

        return removed

    def run_modules(self, modules_to_run):
        print("\tRun all modules")
        if modules_to_run == None or modules_to_run == "":
            return

        for module in modules_to_run:
            self.put("/mainloop/" + module.name + "/", "running", "true")

        self.wait_until_running(modules_to_run)

    def stop_all_modules(self):
        print("\tStop all modules")
        modules_to_stop = self.get_children("/mainloop/")

        if modules_to_stop == None or modules_to_stop == "":
            return

        for module in modules_to_stop:
            self.put("/mainloop/" + module + "/", "running", "false")

        for module in modules_to_stop:
            while self.get("/mainloop/" + module + "/", "isRunning"):
                time.sleep(0.1)

    def close(self, runtime_output=None):
        # Normal shutdown.
        self.put("/system/", "running", "false")

        # Wait for normal shutdown via running=false.
        try:
            self.runtime.wait(30)
        except subprocess.TimeoutExpired:
            # Didn't work, send SIGTERM.
            self.runtime.terminate()

        # Wait for SIGTERM to take effect.
        try:
            self.runtime.wait(30)
        except subprocess.TimeoutExpired:
            # Didn't work, go nuclear.
            self.runtime.kill()

        # Wait for SIGKILL to take effect.
        # Raise exceptions if even this fails.
        self.runtime.wait(10)

        output = runtime_out_print(self.runtime)

        if runtime_output != None:
            runtime_output.append(output)

        # Close log file.
        self.log_file.close()

        return self.runtime.returncode

    def __del__(self):
        try:
            if self.runtime.returncode is None:
                self.close()
        except Exception as e:
            print(e)


class dv_module:
    name = None
    lib = None
    config_options = []
    inputs = []
    outputs = []

    class config_option:
        parameter = None
        value = None

        def __init__(self, parameter, value):
            self.parameter = parameter
            self.value = value

    class input:
        name = None
        origin = None

        def __init__(self, name, origin):
            self.name = name
            self.origin = origin

    class output:
        name = None

        def __init__(self, name):
            self.name = name

    def __init__(self, name, inputs=None, outputs=None, config_options=None, library=None):
        self.name = name
        self.library = library if library is not None else name
        self.config_options = []
        self.inputs = []
        self.outputs = []

        if inputs is not None:
            for input in inputs:
                self.inputs.append(dv_module.input(input[0], input[1]))

        if outputs is not None:
            for output in outputs:
                self.outputs.append(dv_module.output(output))

        if config_options is not None:
            for option in config_options:
                self.config_options.append(dv_module.config_option(option[0], option[1]))
