import threading
import io
import traceback
import sys
import time
import ast
from damaris.experiments.Experiment import Quit
from damaris.experiments import Experiment

class StopExperiment(Exception):
    pass

class ExperimentHandling(threading.Thread):
    """
    runs the experiment script in sandbox
    """

    def __init__(self, script, exp_writer, data):
        threading.Thread.__init__(self, name="experiment handler")
        self.script=script
        self.writer=exp_writer
        self.data=data
        self.quit_flag = threading.Event()
        if self.data is not None:
            self.data["__recentexperiment"]=-1

    def synchronize(self, before=0, waitsteps=0.1):
        while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.isSet():
            self.quit_flag.wait(waitsteps)
        if self.quit_flag.isSet():
            raise StopExperiment
        
    def sleep(self, seconds):
        self.quit_flag.wait(seconds)
        if self.quit_flag.isSet():
            raise StopExperiment

    def run(self):
        dataspace={}
        exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
        for name in dir(exp_classes):
            if name[:2]=="__" and name[-2:]=="__": continue
            dataspace[name]=exp_classes.__dict__[name]
        del exp_classes

        dataspace["data"]=self.data
        dataspace["synchronize"]=self.synchronize
        dataspace["sleep"]=self.sleep
        self.raised_exception = None
        self.location = None
        exp_iterator=None

        # check for time.sleep()
        try:
            tree = ast.parse(self.script)
            for node in ast.walk(tree):
                found_blocking_sleep = False
                if isinstance(node, ast.Call):
                    func = node.func
                    if isinstance(func, ast.Attribute) and func.attr == 'sleep':
                        if isinstance(func.value, ast.Name) and func.value.id == 'time':
                            found_blocking_sleep = True
                elif isinstance(node, ast.ImportFrom) and node.module == 'time':
                    for alias in node.names:
                        if alias.name == 'sleep':
                            found_blocking_sleep = True
                
                if found_blocking_sleep:
                    print("Warning: time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
                    break
        except Exception:
            pass

        try:
            exec(self.script, dataspace)
        except StopExperiment:
            return
        except Exception as e:
            self.raised_exception=e
            self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
            traceback_file=io.StringIO()
            traceback.print_tb(sys.exc_info()[2], None, traceback_file)
            self.traceback=traceback_file.getvalue()
            traceback_file=None
            return
        if "experiment" in dataspace:
            try:
                exp_iterator=dataspace["experiment"]()
            except StopExperiment:
                return
            except Exception as e:
                self.raised_exception=e
                self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
                traceback_file=io.StringIO()
                traceback.print_tb(sys.exc_info()[2], None, traceback_file)
                self.traceback=traceback_file.getvalue()
                traceback_file=None
                return
        
        while exp_iterator is not None and not self.quit_flag.isSet():
            # get next experiment from script 
            try:
                job=next(exp_iterator)
            except StopExperiment as se:
                break
            except StopIteration as si:
                break
            except Exception as e:
                self.raised_exception=e
                self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
                traceback_file=io.StringIO()
                traceback.print_tb(sys.exc_info()[2], None, traceback_file)
                self.traceback=traceback_file.getvalue()
                traceback_file=None
                break
            # send it
            self.writer.send_next(job)
            # write a note
            if isinstance(job, Experiment):
                if self.data is not None:
                    self.data["__recentexperiment"]=job.job_id+0
                    
                    # track time when experiment is started. This is placed after executing the script
                    # and getting the iterator so that time for initializing devices etc. is not included
                    if "__experimentstarted" not in self.data:
                        self.data["__experimentstarted"] = time.time()
                    # track time of experiments in queue, the total time and for each experiment:
                    if "__totalexperimentlength" not in self.data:
                        self.data["__totalexperimentlength"] = 0.0
                    if "__experimentlengths" not in self.data:
                        self.data["__experimentlengths"] = {}
                    
                    experimentlength = job.get_length()
                    self.data["__experimentlengths"][job.job_id+0] = experimentlength
                    self.data["__totalexperimentlength"] += experimentlength
                    
                    # relax for a short time
                    if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id:
                        self.quit_flag.wait(0.05)
                if self.quit_flag.isSet():
                    dataspace=None
                    exp_iterator=None
                    break

        self.writer.send_next(Quit(), quit=True)
        # do not count quit job (is this a good idea?)
        dataspace=None
        self.exp_iterator=None
        self.writer=None
