import usb.core
import usb.util
import time
from ..helpers import error_print, debug_print
import threading
from functools import wraps
from kivy.clock import Clock

def parse_response(r):
    return ''.join([chr(x) for x in r]).strip('\x00')

class USBLock():
    def __call__(self, func):
        @wraps(func)
        def inner(selfp, *args, **kwargs):
            if selfp.lock is not None:
                with selfp.lock:
                    return func(selfp, *args, **kwargs)
            else:
                error_print("Cannot run function with lock: device not initialized")
        return inner

class LtRemote():
    vendor_id = 0x1669
    product_id = 0x1026

    def __init__(self, name, **kwargs):
        self.name = name
        self.device = None
        self.lock = None

    def initialize(self, *args):
        self.device = usb.core.find(idVendor=self.vendor_id, idProduct=self.product_id)
        if self.device is None:
            error_print("Could not find LtRemote device, rescheduling in 10s")
            Clock.schedule_once(self.initialize, 10)
            return

        # The kernel driver does bad thing to the remote
        try:
            self.device.detach_kernel_driver(0)
        except:
            pass

        self.device.set_configuration()

        self.output = self.device.get_active_configuration()[(1, 0)][0]
        self.input = self.device.get_active_configuration()[(1, 0)][1]
        for i in range(0, 4):
            success = self._send_and_check_response("minfo")
            if success:
                break
            time.sleep(2)
        if not success:
            raise Exception(f"Could not initialize LtRemote device")

        self.lock = threading.Lock()


    def _read_until_cmd(self):
        response = []
        responseb = usb.util.create_buffer(1024)
        while True:
            self.input.read(responseb)
            response += parse_response(responseb).splitlines()
            if any(r.startswith("CMD>") for r in response):
                break
        return response

    def _send_and_get(self, usb_command, **kwargs):
        debug_print(f"USB LtRemote ({self.name}): Sending {usb_command}")

        try:
            self.output.write(f"{usb_command}\n")
            response = self._read_until_cmd()
        except usb.core.USBTimeoutError:
            error_print(f"USB LtRemote ({self.name}): Failed to read response from command {usb_command}")
            response = []
        except usb.core.USBError as e:
            error_print(f"USB LtRemote ({self.name}): Failed to send command {usb_command}: {e}")
            response = []
        debug_print(f"USB LtRemote ({self.name}): {response}")
        return response

    def _send_and_check_response(self, usb_command, **kwargs):
        response = self._send_and_get(usb_command)

        success = "CMD>OK" in response
        if not success:
            error_print(f"USB LtRemote ({self.name}): Didn't get success response from remote: {response}")
        return success

    @USBLock()
    def is_alive(self):
        debug_print(f"USB LtRemote ({self.name}): Checking liveness")
        if self.device is None:
            return False
        else:
            return self._send_and_check_response("minfo")

    @USBLock()
    def play(self, bank, group=0, delay=None, offset=None, duration=None, brightness=None, **kwargs):
        """
        Syntax:
        splay <slvbmp>,<bank>,[parameters] 

        Description: 
        The function starts playback of a pre-recorded show from memory <bank> for
        units selected using <slvbmp>  (slvbmp can be a bitmap of the units in the
        group or the UID of the unit or 0 in broadcast mode). Run parameters must be
        separated by a comma and each contain one character of the parameter key and a
        value. The key is always a lowercase letter. The parameters can be in any
        order, if one parameter is used multiple times, the last one is used.
        The show parameters are:

        key description
        h   hold off - delay before the show starts (time in [ms])
        o   offset - offset of the start of playback in the show (time in [ms] that is
            skipped from the show)
        d   duration - playback length in [ms]
        b   brightness level for the show being played 0-5
        """
        command = f"splay {group},{bank-1}"
        for (param, key) in  [(delay, "h"), (offset, "o"), (duration, "d"), (brightness, "b")]:

            if param is not None:
                command += f",{key}{param}"

        debug_print(f"USB LtRemote ({self.name}): Playing: {command}")
        self._send_and_check_response(command)

    @USBLock()
    def stop(self, bank, **kwargs):
        debug_print(f"USB LtRemote ({self.name}): Stopping: sstop {bank-1}")
        self._send_and_check_response(f"sstop {bank-1}")

    def parse_kw(self, s):
        if s[3] == ">":
            s = s[4:]
        return {k:v for [k,v] in [kv.split(":", 1) for kv in s.split(";")]}

    @USBLock()
    def get_status(self, **kwargs):
        response = self._send_and_get("glist")
        devices = []
        for line in response:
            if not line.startswith("GRP>R:s"):
                continue
            devices.append(self.parse_kw(line)["DA"])

        devices_status_line = _("LtRemote:   paired: {device_count}  offline: {offline_device_count}")

        off_devices = 0
        devices_status = []
        for device in devices:
            response = self._send_and_get(f"glist {device}")
            if len(response) > 0 and response[-1] == "CMD>OK":
                battery = "X"
                for l in response:
                    if l.startswith("GRP>"):
                        parsed = self.parse_kw(l)
                        battery = parsed.get("BAT", "X")
                response_ginfo = self._send_and_get(f"ginfo {device}")
                name = "No name"
                if len(response_ginfo) > 0 and "CMD>OK" in response_ginfo:
                    for l in response_ginfo:
                        if l.startswith("INF>"):
                            parsed = self.parse_kw(l)
                            #print(parsed)
                            name = parsed.get("NM", "No name")
                devices_status.append(f"        {name}: {battery}%")
                # décrit les show enregistrés dans la massue
                #response_dcmd = self._send_and_get(f"dcmd {device},sd")
            else:
                off_devices += 1

        # chunks of 3
        group_size = 4
        devices_status = [
                devices_status_line.format(device_count=len(devices), offline_device_count=off_devices)
                ] + [
                        "".join(devices_status[i:i + group_size]) for i in range(0, len(devices_status), group_size)
                        ]
        # repeat every 30s
        return (devices_status, 30)
