import hid
import winreg

from .datatypes import *

class FusionDevice():
    VID = 0x1044
    PID = [0x7a39, 0x7a3a]

    #unsure what these devices are for yet
    DEVICES = [RawInputDevice(1, 6, 768), RawInputDevice(1, 2, 256), RawInputDevice(65281, 8713, 256), RawInputDevice(65282, 1, 256), RawInputDevice(65280, 65280, 256)]

    def __init__(self, path: bytes) -> None:
        self.h = hid.device()
        self.h.open_path(path)
        self.path = path

    def compute_simple_checksum(self, buf):
        acc = 0
        for i in range(8):
            acc += buf[i]
            acc %= 0xFF
        buf[8] = 0xFF - acc

    def send_simple_command_and_recv(self, opcode: int, report_num = 0):
        buf = bytearray(9)
        buf[1] = opcode
        self.compute_simple_checksum(buf)
        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())
        buf = self.h.get_feature_report(0, 9)
        return buf

    def get_firmware_version(self) -> bytes:
        return self.send_simple_command_and_recv(0x80)

    def get_version_string(self) -> str:
        buf = self.get_firmware_version()
        minor = list(str(buf[3]))
        return f'{buf[2]}.{"0" if len(minor) == 1 else minor.pop(0)}.{minor.pop(0)}'
    
    def get_light_effect(self) -> FusionLightData:
        buf = self.send_simple_command_and_recv(0x88)
        return FusionLightData(FusionLightEffect(buf[3]), buf[4], buf[5], FusionLightColor(buf[6]), buf[7])
    
    # this is needed to set the current working profile to custom too (set once only is enough)
    def set_simple_light_effect(self, effect: FusionLightData) -> None:
        buf = bytearray(9)
        buf[1] = 0x8
        buf[3] = int(effect.fusion_effect)
        buf[4] = effect.fusion_speed
        buf[5] = effect.fusion_brightness
        buf[6] = int(effect.fusion_color)
        buf[7] = effect.fusion_direction
        self.compute_simple_checksum(buf)
        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())
    
    #aka SetPictureMatrix2Device
    #TODO seems to not be persistent, but also nice since we can just reset by hibernating + devmgmt disable/enable when it breaks
    def set_custom_light_effect(self, profile: int, pixels: PictureMatrix):
        if profile not in range(5):
            raise TypeError('Invalid profile number!')
        buf = bytearray(9)
        buf[1] = 0x12
        buf[3] = profile
        buf[4] = 8   #why?
        self.compute_simple_checksum(buf)
        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())
        
        data = pixels.to_bytes()
        for split in [data[i:i+64] for i in range(0, len(data), 64)]:
            self.h.write(b'\0' + split)

        #writing all at once would just freeze the keyboard
        #self.h.write(b'\0' + b'\0'.join([data[i:i+64] for i in range(0, len(data), 64)]))

        
    #aka LoadPictureMatrixValue
    #use PictureMatrix.from_bytes to convert it back into a usable PictureMatrix
    def get_custom_light_effect(self, profile: int) -> bytes: 
        if profile not in range(5):
            raise TypeError('Invalid profile number!')
        buf = bytearray(9)
        buf[1] = 0x92
        buf[3] = profile
        self.compute_simple_checksum(buf)
        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())
        
        buf = self.h.get_feature_report(0, 9)
        print(buf)

        data = b''
        for i in range(8):
            print(i)
            data += bytes(self.h.read(65))
        return data

    #TODO seems like a no-op? but even in FusionKeyboard.dll it's immediately succeeded by a call to set static white anyways hm
    #maybe it's for clearing custom profiles in firmware memory (or the unimplemented macro stuff)
    def reset(self) -> None:
        buf = bytearray(9)
        buf[1] = 0x13
        buf[2] = 0xFF
        self.compute_simple_checksum(buf)

        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())

#TODO untested classes below

class FusionDeviceIone(FusionDevice):
    PID = [0x7a3c, 0x7a3d, 0x7a3e]  #normal, UK, JP

    def get_version_string(self) -> str:
        buf = bytearray(264)
        buf[0] = 0x7
        buf[1] = 0x17
        report_num = self.h.send_feature_report(buf)
        print(report_num)
        if report_num == -1:
            raise IOError(self.h.error())
        with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "Software\\GigabyteFusion") as key:
            return winreg.QueryValue(key, 'Version')
        
    def get_firmware_version(self) -> bytes:
        #theres actually no raw response directly so just fake it with the version string
        return self.get_version_string().encode()

    #TODO changeIoneAnimationListUI, changeIoneAnimationTrigger, changeIoneColorGroupUI 
    #TODO seems like the way to do complex profile is changeLightEffect_Details for ione   
    

class FusionDeviceIte(FusionDevice):
    PID = [0x7a38, 0x7a3b, 0x7a3f]

class FusionDeviceX9(FusionDevice):    
    VID = 0x4D9
    PID = [0x8008]

    def get_version_string(self) -> str:
        buf = self.get_firmware_version()
        return f'{buf[2]}.{"0" if len(str(buf[3])) == 1 else buf[3] // 16}.{buf[3] % 16}'



def get_device() -> FusionDevice:
    for dev in hid.enumerate():
        for type in [FusionDevice, FusionDeviceX9, FusionDeviceIone, FusionDeviceIte]:
            #print(type, type.VID, type.PID, dev['vendor_id'], dev['product_id'])
            if dev['vendor_id'] == type.VID and dev['product_id'] in type.PID:
                try:
                    obj = type(dev['path'])
                    #there should only be one that we can get the firmware version successfully from
                    obj.get_firmware_version()
                    return obj
                except IOError:
                    continue