import unittest
import sys
import os

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

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

username = "test_user"
parallel_push = True

ports = []

def getFreePort():
    while 1:
        port = random.randint(10000, 20000)
        ports.append(port)

        exists = False
        for p in ports:
            if p == port:
                if not exists:
                    # We have hopefully found our own
                    exists = True
                else:
                    # We seem to be the second entry, so chose another one
                    ports.remove(port)
                    break
        else:
            # Didn't find a duplicate
            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), "username": username})), timeout=10).read()
    return []

def compile_file(address, mod_filename, filename, mode, proc):
    # Load in the file required
    try:
        timeout_val = 240
        import random
        username = str(random.random())
        while 1:
            proc2 = execute("compile", [address, mod_filename, username, 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):
    # Resolve file
    import os.path

    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 = []
        for filename in files:
            if 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)
            print("Found file " + str(mod_filename))

            if parallel_push:
                import threading
                threads.append(threading.Thread(target=compile_file, args=[address, mod_filename, filename, mode, proc]))
                threads[-1].start()
            else:
                compile_file(address, mod_filename, filename, mode, proc)

            # After the first file, which can be whatever we want, we just have to compile dependencies, all of which are PO for efficiency
            mode = "PO"

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

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

        # Send in the actual request and wait for replies
        var_list = {}
        data = []
        got_output = []
        for p in parameters:
            if isinstance(p, int):
                if p not in var_list:
                    data = flush_data(address, data)
                    
                    proc.poll()
                    if proc.returncode is not None:
                        # Modelverse has already terminated, which isn't a good sign!
                        return False

                    while 1:
                        val = urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "get_output", "username": username})), timeout=120).read()
                        l, r = val.split("&", 1)
                        if l.startswith("id"):
                            id_str = l
                            val_str = r
                        else:
                            id_str = r
                            val_str = l
                        id_val = id_str.split("=", 1)[1]
                        val_val = val_str.split("=", 1)[1]

                        if val_val == "None":
                            var_list[p] = id_val
                            break
                        else:
                            got_output.append(val)
                    continue
                else:
                    val = var_list[p]
                    t = "R"
            else:
                val = p
                t = "V"
            data.append([t, val])
        data = flush_data(address, data)
        flush_data(address, data)
        
        for e in expected:
            c = len(e) if isinstance(e, set) else 1
            for _ in range(c):
                if got_output:
                    val = got_output.pop(0)
                else:
                    val = urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "get_output", "username": username})), timeout=240).read()

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

                val = val.split("=", 2)[2]
                print("Got %s, expect %s" % (val, e))
                if isinstance(e, set):
                    assert str(val) in e
                    if str(val) not in e:
                        return False
                else:
                    assert str(val) == e
                    if str(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 user 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"' % username, "username": "user_manager"})), timeout=1).read()
                if interface is not None:
                    urllib2.urlopen(urllib2.Request(address, urllib.urlencode({"op": "set_input", "element_type": "V", "value": interface, "username": username})), timeout=1).read()
                break
            except:
                time.sleep(0.01)

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

        # Send in the actual request and wait for replies
        var_list = {}
        data = []
        for p in parameters:
            if isinstance(p, int):
                if p not in var_list:
                    data = flush_data(address, data)

                    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", "username": username})), timeout=10).read()
                    val = val.split("=", 2)[1].split("&", 1)[0]
                    var_list[p] = val
                    continue
                else:
                    val = var_list[p]
                    t = "R"
            else:
                val = p
                t = "V"
            data.append([t, val])
        data = flush_data(address, data)

        # Now do linking and loading
        if link is not None:
            # Execute linker
            timeout_val = 10
            proc2 = execute("link_and_load", [address, username] + 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, "username": username})), 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", "username": username})), timeout=240 if not timeout else 20).read()
                except:
                    if timeout:
                        return True
                    else:
                        raise
                val = val.split("=", 2)[2]

                print("Got %s, expect %s" % (val, e))
                if isinstance(e, set):
                    assert str(val) in e
                    if str(val) not in e:
                        return False
                else:
                    assert str(val) == e
                    if str(val) != e:
                        return False

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

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

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

    return constructors
