import hid

from .datatypes import *

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

    ADJUST_RGB = None

    #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_simple_light_effect(self) -> FusionLightData:
        buf = self.send_simple_command_and_recv(0x88)
        return FusionLightData(FusionLightEffect(buf[3]), buf[4], buf[5], FusionLightColor(buf[6]), FusionLightDirection(buf[7]))
    
    #this is not strictly needed when setting custom profiles - 
    #sending the payload via set_custom_light_effect will update the profile and set the current keyboard lighting to it
    #but for the profile selection to persist across reboots etc explicitly calling this to set the custom profile is needed
    #(note that just calling this will also not change the current keyboard lighting, only on reboots,
    # unless we are not in any of the custom profiles then regardless of the actual profile chosen
    # the last image will show up right when we do set_simple_light_effect)
    def set_simple_light_effect(self, effect: FusionLightData) -> None:
        #technically the UI only allows setting 1-10 it looks like, but 255 still works
        #but if speed is 0 the firmware would crash when switching to a profile that utilizes speed
        if effect.fusion_speed not in range(1, 256):
            raise TypeError('Invalid speed!')
        buf = bytearray(9)
        buf[1] = 0x8
        buf[3] = int(effect.fusion_effect)
        buf[4] = effect.fusion_speed
        #also technically this should be a range from 0-50 but up to 255 works (even though it behaves the same as 50 afaict?)
        buf[5] = effect.fusion_brightness
        buf[6] = int(effect.fusion_color)
        buf[7] = int(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
    #requires some cooperation from set_simple_light_effect to properly "persist", see above
    def set_custom_light_effect(self, profile: int, pixels: PictureMatrix, adjust: RGB = None):
        #seems like profile can be anything (just that outside of 0-4 it doesnt persist?)
        #edit: so apparently profiles outside of 0-4 (at least up until 255) actually persists and is accessible, just that on a reboot it will assume the data doesnt exist
        #switching to another profile and then switching back would still work
        #aka the profile restriction only exists in set_simple_light_effect
        # if profile not in range(5):
        #     raise TypeError('Invalid profile number!')
        buf = bytearray(9)
        buf[1] = 0x12
        buf[3] = profile
        buf[4] = 8   #why? its also always returned by get_custom_light_effect
        self.compute_simple_checksum(buf)
        sent = self.h.send_feature_report(buf)
        if sent == -1:
            raise IOError(self.h.error())
        
        data = pixels.to_bytes(adjust)
        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)

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

    #clears all data in firmware memory (custom profiles, macros, etc)
    #in FusionKeyboard.dll it's usually immediately succeeded by a call to reset light profile to static white 
    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())
        buf = self.h.read(9)
        return buf
        
    def get_firmware_version(self) -> bytes:
        buf = self.get_version_string()
        if len(buf) != 9 or buf[3] != 0x17:
            raise IOError('Invalid response received from IONE device!')
        return f'{buf[6]}.{buf[7] // 16}.{buf[7] % 16}'

    #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]  #normal, UK, JP

    #Ite requires RGB color adjustment (it's actually a special case even in FusionKeyboard.dll for Ite)
    #this value is guessed by eyeballing though
    ADJUST_RGB = RGB(70, 255, 130)

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