from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import QCoreApplication as QC
from PyQt5.QtCore import QRunnable, QObject, QThreadPool
import multiprocessing as mp
import logging, sys, time, os, signal
from datetime import datetime
from Pythonic.record_function import Record, Function, alphabet

# additional declaration: already defined in elements/basicelements
class ExecRBFunction(Function):

    def __init__(self, config, b_debug, row, column):
        super().__init__(config, b_debug, row, column)

    def execute(self, record):
        result = Record(self.getPos(), (self.row +1, self.column), record)
        return result

class ExecRFunction(Function):

    def __init__(self, config, b_debug, row, column):
        super().__init__(config, b_debug, row, column)

    def execute(self, record):
        result = Record(self.getPos(), (self.row, self.column+1), record)
        return result

class WorkerSignals(QObject):

    finished = pyqtSignal(object, name='element_finished' )
    pid_sig = pyqtSignal(object)

class GridOperator(QObject):

    update_logger   = pyqtSignal(name='update_logger')
    exec_pending    = pyqtSignal(name='exec_pending')
    switch_grid     = pyqtSignal('PyQt_PyObject', name='switch_grid')

    def __init__(self, grid, number):
        super().__init__()
        logging.debug('__init__() called on GridOperator')

        #grid: 3D-array [grid][row][col]
        # here is is only 2D: [row][col]
        #grid[0][row][column] = (function, config, self_sync)

        self.grid = grid
        self.number = number # grid number [0 .. 4]
        self.stop_flag = False
        self.fastpath = False # fastpath is active when debug is diasbled
        self.retry_counter = 0
        self.delay = 0
        self.threadpool = QThreadPool()
        self.b_debug_window = False
        self.pending_return = []
        self.pid_register = []
        self.exec_pending.connect(self.checkPending)
        logging.debug('__init__() GridOperator, threadCount: {}'.format(
            self.threadpool.maxThreadCount()))

    def startExec(self, start_pos, record=None):

        logging.debug('startExec() called, start_pos = {}'.format(start_pos))

        #function, config, self_sync = self.grid[start_pos[0]][start_pos[1]]
        function, self_sync = self.grid[start_pos[0]][start_pos[1]]
        logging.debug('GridOperator::startExec() function found: {}'.format(function))
        logging.debug('GridOperator::startExec() function dict: {}'.format(function.__dict__))
        logging.debug('GridOperator::startExec() config: {}'.format(function.config))


        self.update_logger.emit()
        executor = Executor(function, record, self.delay)
        executor.signals.finished.connect(self.execDone)
        executor.signals.pid_sig.connect(self.register_pid)
        self.threadpool.start(executor)

    def register_pid(self, pid):
        # register PID of spawned child process
        self.pid_register.append(pid)
        logging.debug('PID register: {}'.format(self.pid_register))

    def execDone(self, prg_return):

        logging.debug('execDone() called GridOperator from {}'.format(prg_return.source))

        logging.debug('PID returned: {}'.format(prg_return.pid))
        # remove returned pid from register
        try:
            self.pid_register.remove(prg_return.pid)
        except Exception as e:
            logging.error('De-registration of PID failed: {}'.format(e))


        # if an execption occured
        if(issubclass(prg_return.record_0.__class__, BaseException)):
            logging.error('Grid {} Target {}|{} Exception found: {}'.format(
                self.number + 1,
                prg_return.source[0],
                alphabet[prg_return.source[1]],
                prg_return.record_0))

            return

        ### proceed with regular execution ###

        # when the log checkbox is activated
        if prg_return.log:
            if prg_return.log_txt:
                logging.info('Grid {} Message {}|{} : {}'.format(
                            self.number + 1,
                            prg_return.source[0],
                            alphabet[prg_return.source[1]],
                            prg_return.log_txt))
            else:
                logging.info('Grid {} Message {}|{} : {}'.format(
                            self.number + 1,
                            prg_return.source[0],
                            alphabet[prg_return.source[1]],
                            prg_return.record_0))

        self.goNext(prg_return)

    def checkPending(self):

        logging.debug('GridOperator::checkPending() called')
        
        if self.pending_return:
            prg_return = self.pending_return.pop(0)
            self.execDone(prg_return)

    def proceedExec(self, prg_return):

        element = self.grid.itemAtPosition(*prg_return.source).widget()
        element.highlightStop()
        self.b_debug_window = False
        self.exec_pending.emit()
        self.goNext(prg_return)

    def goNext(self, prg_return):

        # check is target_0 includes a diffrent grid 
        # ExecReturn elemenot
        logging.debug('GridOperator::goNext() called, prg_return: {}'.format(prg_return))
        logging.debug('GridOperator::goNext() called, target_0: {}'.format(prg_return.target_0))

        if prg_return.target_0:
            logging.debug('GridOperator::goNext() called with next target_0: {}'.format(prg_return.target_0))
            logging.debug('GridOperator::goNext() called with record_0: {}'.format(prg_return.record_0))

            if len(prg_return.target_0) == 3: # switch grid, go over main
                # fastpath = True
                self.switch_grid.emit(prg_return)
                return
                
            #Proceed as usual
            new_rec = self.fastPath(prg_return.target_0, prg_return.record_0)
            if new_rec: # check for ExecR or ExecRB
                self.goNext(new_rec)
            else: # if nothing found: proceed as usual
                self.startExec(prg_return.target_0, prg_return.record_0)

        if prg_return.target_1:

            logging.debug('GridOperator::goNext() called with additional target_1: {}'.format(
                prg_return.target_1))
            logging.debug('GridOperator::goNext() called with record_1: {}'.format(prg_return.record_1))

            # self_sync is true on basic_sched and binancesched
            #self_sync = self.grid.itemAtPosition(*prg_return.target_1).widget().self_sync
            #function, config, self_sync = self.grid[prg_return.target_1[0]][prg_return.target_1[1]] 
            function, self_sync = self.grid[prg_return.target_1[0]][prg_return.target_1[1]] 

            if not self_sync:
                new_rec = self.fastPath(prg_return.target_1, prg_return.record_1)
                logging.debug('GridOperator::goNext() exception here')
                logging.debug('GridOperator::goNext() new_rec: {}'.format(new_rec))
                self.goNext(new_rec)
            else:
                self.startExec(prg_return.target_1, prg_return.record_1)

    def fastPath(self, target_0, record):
        row, col = target_0
        logging.debug('GridOperator::fastPath() check row: {} col: {}'.format(*target_0))
        #element = self.grid.itemAtPosition(*target).widget()
        #[row][column] = (function, config, self_sync)
        #function, config, self_sync = self.grid[row][col] 
        function, self_sync = self.grid[row][col] 
        logging.debug('GridOperator::fastPath() function: {}'.format(function))
        logging.debug('GridOperator::fastPath() isinstance ExecRB: {}'.format(isinstance(function, ExecRBFunction)))
        logging.debug('GridOperator::fastPath() isinstance ExecRB#####: {} '.format(str(type(function))))
        logging.debug('GridOperator::fastPath() isinstance ExecR: {}'.format(isinstance(function, ExecRFunction)))

        if str(type(function)) == "<class 'Pythonic.elements.basicelements_func.ExecRBFunction'>": # jump to the next target
            # record_1 -> record_0 when goNext() is called recursively
            # returning only target_0 and record_0
            new_rec = Record((row, col-1), (row+1, col), record)
            return new_rec
        elif str(type(function)) == "<class 'Pythonic.elements.basicelements_func.ExecRFunction'>": # jump to the next target
            #hier testen ob target fings
            # record_1 -> record_0 when goNext() is called recursively
            # returning only target_0 and record_0
            new_rec = Record((row, col-1), (row, col+1), record)
            return new_rec
        else:
            return None

            
    def highlightStop(self, position):
        logging.debug('highlightStop() called for position {}'.format(position))
        element = self.grid.itemAtPosition(*position).widget()
        element.highlightStop()

    def stop_execution(self):
        logging.debug('stop_execution() called')
        self.stop_flag = True

    def kill_proc(self):
        logging.debug('kill_proc() called')

        for proc in self.pid_register:
            try:
                os.kill(proc, signal.SIGTERM)
                logging.info('Process killed, PID {}'.format(proc))
            except Exception as e:
                pass

        self.pid_register.clear()


class Executor(QRunnable):


    def __init__(self, function, record, delay):
        super().__init__()
        #BAUSTELLE: element = function
        logging.debug('Executor::__init__() called')
        self.function = function
        self.record = record
        #self.stop_flag = False
        self.retry_counter = 0
        self.delay = delay
        self.signals = WorkerSignals()

    def run(self):

        logging.debug('Executor::run() called -  pid: {} at {}'.format(os.getpid(), datetime.now()))

        self.start_proc(self.function, self.record, self.delay, 1)

        logging.debug('Executor::run() returned - pid: {} returned at {}'.format(os.getpid(), datetime.now()))


    def start_proc(self, function, record, delay, retries):
        # Bug: Sometimes the Exception windows isnt triggered
        logging.debug('Executor::start_proc() called with programm: {}'.format(function))
            
        return_pipe_0, feed_pipe_0 = mp.Pipe(duplex=False)

        p_0 = mp.Process(target=target_0, args=(function, record, feed_pipe_0, ))

        p_0.start()
        self.signals.pid_sig.emit(p_0.pid) 
        time.sleep(delay)
        
        result = return_pipe_0.recv()
        p_0.join()

        self.signals.finished.emit(result)

def target_0(function, record, feed_pipe):

    ret = function.execute_ex(record)
    feed_pipe.send(ret)