import unittest
import sys
import os

import sys
import time
import json
import urllib
import urllib2
import subprocess
import signal
import random
import pytest

sys.path.append("interface/HUTN")
sys.path.append("scripts")
from hutn_compiler.compiler import main as do_compile
from check_objects import to_recompile

taskname = "test_task"
parallel_push = True

slow = pytest.mark.skipif(
    not pytest.config.getoption("--runslow"),
        reason="need --runslow option to run"
)

ports = set()

def getFreePort():
    """Gets a unique new port."""
    while 1:
        port = random.randint(10000, 20000)
        # Check if this port is in the set of ports.
        if port not in ports:
            # We have found a unique port. Add it to the set and return.
            ports.add(port)
            return port

def execute(scriptname, parameters=[], wait=False):
    if os.name not in ["nt", "posix"]:
        # Stop now, as we would have no clue on how to kill its subtree
        raise Exception("Unknown OS version: " + str(os.name))

    command = [sys.executable, "scripts/%s.py" % scriptname] + parameters

    if wait:
        return subprocess.call(command, shell=False)
    else:
        return subprocess.Popen(command, shell=False)

def kill(process):
    if os.name == "nt":
        subprocess.call(["taskkill", "/F", "/T", "/PID", "%i" % process.pid])
    elif os.name == "posix":
        subprocess.call(["pkill", "-P", "%i" % process.pid])

def flush_data(address, data):
    if data:
        urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "set_input", "data": json.dumps(data), "taskname": taskname})), timeout=10).read()
    return []

def compile_file(address, mod_filename, filename, mode, proc):
    # Load in the file required
    try:
        timeout_val = 240
        import random
        taskname = str(random.random())
        while 1:
            proc2 = execute("compile", [address, mod_filename, taskname, filename, mode], wait=False)

            if proc.returncode is not None:
                # Modelverse has already terminated, which isn't a good sign!
                raise Exception("Modelverse died!")

            while proc2.returncode is None:
                time.sleep(0.01)
                proc2.poll()
                timeout_val -= 0.01
                if timeout_val < 0:
                    kill(proc2)
                    print("Compilation timeout expired!")
                    return False

            if proc2.returncode != 2:
                break

        # Make sure everything stopped correctly
        assert proc2.returncode == 0
        if proc2.returncode != 0:
            return False
    except:
        raise
    finally:
        try:
            kill(proc2)
        except UnboundLocalError:
            pass

def run_file(files, parameters, expected, mode, wait=False):
    # Resolve file
    import os.path

    if wait is True:
        expected = None

    time.sleep(0.01)
    port = getFreePort()
    address = "http://127.0.0.1:%i" % port
    try:
        # Run Modelverse server
        proc = execute("run_local_modelverse", [str(port)], wait=False)

        threads = []
        mod_files = []
        for filename in files:
            if os.path.isfile(filename):
                mod_filename = filename
            elif os.path.isfile("integration/code/%s" % filename):
                mod_filename = "integration/code/%s" % filename
            elif os.path.isfile("bootstrap/%s" % filename):
                mod_filename = "bootstrap/%s" % filename
            else:
                raise Exception("File not found: %s" % filename)
            mod_files.append(mod_filename)

        to_compile = to_recompile(address, mod_files)

        for mod_filename in to_compile:
            if mod_filename.endswith(".mvc"):
                model_mode = "MO"
                mod_files.remove(mod_filename)
            else:
                model_mode = mode
            if parallel_push:
                import threading
                threads.append(threading.Thread(target=compile_file, args=[address, mod_filename, mod_filename, model_mode, proc]))
                threads[-1].start()
            else:
                compile_file(address, mod_filename, mod_filename, model_mode, proc)

        if parallel_push:
            for t in threads:
                t.join()

        if mode[-1] == "O":
            # Fire up the linker
            val = execute("link_and_load", [address, taskname] + mod_files, wait=True)
            if val != 0:
                raise Exception("Linking error")

        # Send the request ...
        flush_data(address, parameters)
        
        # ... and wait for replies
        if expected is None:
            while 1:
                val = urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "get_output", "taskname": taskname})), timeout=240).read()
                val = json.loads(val)
                print(val)
        for e in expected:
            c = len(e) if isinstance(e, set) else 1
            for _ in range(c):
                val = urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "get_output", "taskname": taskname})), timeout=240).read()
                val = json.loads(val)

                if proc.returncode is not None:
                    # Modelverse has already terminated, which isn't a good sign!
                    raise Exception("Modelverse died!")

                print("Got %s, expect %s" % (val, e))
                if isinstance(e, set):
                    assert val in e
                    if val not in e:
                        return False
                elif e is None:
                    # Skip output value
                    pass
                else:
                    assert val == e
                    if val != e:
                        return False

        # All passed!
        return True
    except:
        raise
    finally:
        try:
            kill(proc)
        except UnboundLocalError:
            pass

def run_barebone(parameters, expected, interface="0", timeout=False, wait=False, link=None, inputs=[]):
    port = getFreePort()
    address = "http://127.0.0.1:%i" % port
    try:
        # Run Modelverse server
        proc = execute("run_local_modelverse", [str(port)], wait=False)

        # Create task and set interface
        timeout_val = 15
        start = time.time()
        while 1:
            proc.poll()
            if proc.returncode is not None:
                # Modelverse has already terminated, which isn't a good sign!
                return False

            try:
                urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "set_input", "element_type": "V", "value": '"%s"' % taskname, "taskname": "task_manager"})), timeout=1).read()
                if interface is not None:
                    urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "set_input", "element_type": "V", "value": interface, "taskname": taskname})), timeout=1).read()
                break
            except:
                time.sleep(0.01)

                if time.time() - start > timeout_val:
                    raise

        # Send the request
        flush_data(address, parameters)

        # Now do linking and loading
        if link is not None:
            # Execute linker
            timeout_val = 10
            proc2 = execute("link_and_load", [address, taskname] + link, wait=False)

            while proc2.returncode is None:
                time.sleep(0.01)
                proc2.poll()
                timeout_val -= 0.01
                if timeout_val < 0:
                    kill(proc2)
                    print("Linking timeout expired!")
                    return False
                if proc.returncode is not None:
                    # Modelverse has already terminated, which isn't a good sign!
                    return False

        for inp in inputs:
            urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "set_input", "element_type": "V", "value": inp, "taskname": taskname})), timeout=1).read()
            proc.poll()
            if proc.returncode is not None:
                # Modelverse has already terminated, which isn't a good sign!
                return False

        counter = 0
        for e in expected:
            print("Expect " + str(e))
            c = len(e) if isinstance(e, set) else 1
            for _ in range(c):
                try:
                    proc.poll()
                    if proc.returncode is not None:
                        # Modelverse has already terminated, which isn't a good sign!
                        return False

                    val = urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "get_output", "taskname": taskname})), timeout=240 if not timeout else 20).read()
                    val = json.loads(val)
                except:
                    if timeout:
                        return True
                    else:
                        raise

                print("Got %s, expect %s" % (val, e))
                if isinstance(e, set):
                    assert val in e
                    if val not in e:
                        return False
                elif e is None:
                    # Skip this input
                    pass
                else:
                    assert val == e
                    if val != e:
                        return False

        # All passed!
        return not timeout
    finally:
        kill(proc)

def get_constructor(code):
    with open("__constraint.alc", "w") as f:
        f.write(code)
        f.flush()

    constructors = do_compile("__constraint.alc", "interface/HUTN/grammars/actionlanguage.g", "CS")

    return constructors

def get_model_constructor(code):
    # First change multiple spaces to a tab
    code_fragments = code.split("\n")
    code_fragments = [i for i in code_fragments if i.strip() != ""]
    code_fragments = [i.replace("    ", "\t") for i in code_fragments]
    initial_tabs = min([len(i) - len(i.lstrip("\t")) for i in code_fragments])
    code_fragments = [i[initial_tabs:] for i in code_fragments]
    code = "\n".join(code_fragments)

    with open("__model.mvc", "w") as f:
        f.write(code)
        f.flush()

    constructors = do_compile("__model.mvc", "interface/HUTN/grammars/modelling.g", "M") + ["exit"]

    return constructors
