import logging
import threading
import functools

from concurrent.futures import ThreadPoolExecutor

from PySide6.QtCore import QObject, Signal, QSettings

from pymoku import MokuInfo
from pymoku.moku import Moku, MOKU_CLASS


log = logging.getLogger()

logging.getLogger("matplotlib").setLevel(logging.CRITICAL)
logging.getLogger("PIL").setLevel(logging.CRITICAL)

async_pool = ThreadPoolExecutor(max_workers=2)

settings = QSettings('Liquid Instruments', 'PyMoku')


class doasync(QObject):
    result = Signal(object)
    error = Signal(Exception)
    finished = Signal(None)

    def __init__(self, func):
        functools.update_wrapper(self, func)
        super().__init__()
        self._func = func
        self._inst = None

    def __call__(self, *args, **kwargs):
        self._future = async_pool.submit(self._dly_call, *args, **kwargs)

    def __get__(self, instance, owner):
        return functools.partial(self.__call__, instance)

    def _dly_call(self, *args, **kwargs):
        try:
            self.result.emit(self._func(*args, **kwargs))
        except Exception as e:
            log.exception("Error in async function")
            self.error.emit(e)
        finally:
            self.finished.emit()


class ratelimit(QObject):
    error = Signal(Exception)

    def __init__(self, func):
        super().__init__()
        # TODO ratelimit instances are per class not per instance...
        self._func = func
        self._lock = threading.RLock()
        self._args = None
        self._future = None
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        with self._lock:
            self._args = (args, kwargs)
            if self._future is None:
                self._future = async_pool.submit(self._loop_call)

    def __get__(self, instance, owner):
        return functools.partial(self.__call__, instance)

    def _loop_call(self):
        while True:
            with self._lock:
                args = self._args
                self._args = None
                if args is None:
                    self._future = None
                    return

            try:
                a, k = args
                self._func(*a, **k)
            except Exception as e:
                log.exception("Error in async function")
                self.error.emit(e)


def moku_key(moku):
    if isinstance(moku, Moku):
        return f'{moku.DEV_NAME}/{moku.serial:d}'

    assert isinstance(moku, MokuInfo)
    devname = MOKU_CLASS[str(moku.hwver)].DEV_NAME
    return f'{devname}/{moku.serial:d}'


class SlotFrameSettings(object):
    def _key(self):
        # TODO we should also check if the slot was redeployed
        # TODO delete this key when the slot is cleared
        return f'{moku_key(self.moku)}/{self.slot_inst.slot:d}/{type(self.slot_inst).__name__}'

    def save_state(self):
        settings.beginGroup(self._key())
        settings.setValue("geometry", self.saveGeometry())
        settings.setValue("windowState", self.saveState())
        settings.setValue("deployId", self.slot_inst.timestamp)
        settings.endGroup()

    def restore_state(self):
        """ Restore the state of this instrument frame from local settings
            based on moku and instrument combination
        """
        settings.beginGroup(self._key())
        try:
            if int(settings.value("deployId")) != self.slot_inst.timestamp:
                return
            self.restoreGeometry(settings.value("geometry"))
            self.restoreState(settings.value("windowState"))
        except Exception:
            log.error(f"Couldn't load state for {self}")
        finally:
            settings.endGroup()
