from typing import Any

DPID_MAX = 0xFE
DPID_MIN = 0xF2
DPID_MAX_BYTES = 6

class Pid:
    def __init__(self, name: str, pid: int, size: int, decoder=lambda x: x):
        assert pid in range(0xFFFF)
        self.name = name
        self.id = pid
        self.size = size # number of data bytes returned
        self.decoder = decoder

    def __bytes__(self):
        return self.id.to_bytes(2)

    def __eq__(self, other):
        if isinstance(other, Pid):
            return self.id == other.id
        elif isinstance(other, int):
            return self.id == other
        elif isinstance(other, bytes):
            return bytes(self) == other
        else:
            raise NotImplementedError

    def __hash__(self):
        return hash(self.id)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

class Dpid:
    '''diagnostic data packet'''
    def __init__(self, dpid: int):
        assert dpid in range(0xFF)
        self.id = dpid
        self.pids = []

    def __bytes__(self):
        return self.id.to_bytes()

    def __eq__(self, other):
        if isinstance(other, Dpid):
            return self.id == other.id
        elif isinstance(other, int):
            return self.id == other
        elif isinstance(other, bytes):
            return bytes(self) == other
        else:
            raise NotImplementedError

    def __hash__(self):
        return hash(self.id)

    def __contains__(self, key):
        if isinstance(key, Pid):
            return key in self.pids
        elif isinstance(key, int):
            return key in [pid.id for pid in self.pids]
        elif isinstance(key, bytes):
            return key in [bytes(pid) for pid in self.pids]
        else:
            raise NotImplementedError

    def __len__(self):
        '''return length of response data in bytes'''
        return sum([pid.size for pid in self.pids])
    
    @property
    def bytes_free(self):
        return DPID_MAX_BYTES - len(self)

    def unpack(self, data: bytes) -> dict[Pid, bytes]:
        values = dict.fromkeys(self.pids)
        read_byte = 0
        for pid in values:
            values[pid] = data[read_byte: read_byte + pid.size]
            read_byte += pid.size

        return values

class DpidLogger:
    def __init__(self, vehicle):
        self._vehicle = vehicle
        self.pids = {}
        self._dpids = []
        self._avaliable_dpids = [Dpid(i) for i in range(DPID_MIN, DPID_MAX+1)]

    def add_pid(self, pid: Pid):
        '''add PID to data logger'''
        assert pid not in self.pids
        
        for dpid in self._dpids:
            if dpid.bytes_free >= pid.size:
                self._vehicle.define_dpid(dpid.id, pid.id, pid.size, len(dpid)+1)
                dpid.pids.append(pid)
                self.pids.update({pid: dpid})
                return
        
        dpid = self._avaliable_dpids.pop()
        self._vehicle.define_dpid(dpid.id, pid.id, pid.size, 1) # offset 1 is the first data byte
        dpid.pids.append(pid)
        self.pids.update({pid: dpid})
        self._dpids.append(dpid)

    def remove_pid(self, pid: Pid):
        '''remove PID from data logger'''
        dpid = self.pids[pid]
        self._vehicle.define_dpid(dpid.id, pid.id, 0, 0) # size=0 offset=0 removes pid
        dpid.pids.remove(pid)
        self.pids.pop(pid)

    def get_row(self) -> dict[Pid, Any]:
        '''
        query vehicle for all PIDs being logged
        returns dict of PIDs and their corresponding decoded value
        '''
        values = dict.fromkeys(self.pids)
        for request_dpids in [self._dpids[i:i + 6] for i in range(0, len(self._dpids), 6)]: # request groups of <= 6 dpids
            response = self._vehicle.get_dpids([dpid.id for dpid in request_dpids])
            for i, data in enumerate(response): # one respose line per dpid
                values.update(request_dpids[i].unpack(data))

        return {pid: pid.decoder(value) for pid, value in values.items()}