"""ADC voltage reading configuration and utility methods"""


import logging

from bitstring import BitArray
from edgepi.adc.adc_constants import ADCReadInfo
from edgepi.calibration.calibration_constants import CalibParam
from edgepi.utilities.utilities import bitstring_from_list
from edgepi.adc.adc_crc_8_atm import CRC_8_ATM_LUT


# TODO: retrieve these values from EEPROM once added
STEP_DOWN_RESISTOR_1 = 19.1
STEP_DOWN_RESISTOR_2 = 4.99
REFERENCE_VOLTAGE = 2.5


_logger = logging.getLogger(__name__)


def _is_negative_voltage(code: BitArray):
    """
    Determines if voltage code is negative value
    """
    return code[0] == 1


def _code_to_input_voltage(code: int, v_ref: float, num_bits: int):
    """
    Converts digital code obtained from ADC voltage read to
    ADC input voltage (i.e. voltage measured at ADC) based on voltage range
    provided by reference voltage

    Args:
        `code` (int): uint value of ADC voltage read bytes

        `v_ref` (float): ADC reference voltage in Volts

        `num_bits` (int): number of bits in ADC voltage read (24 or 32)
    """
    voltage_range = v_ref / 2 ** (num_bits - 1)
    return float(code) * voltage_range


def _adc_voltage_to_input_voltage(v_in: float, gain: float, offset: float):
    """
    Converts ADC input voltage (i.e. voltage measured at ADC) to
    ADC output voltage (i.e. voltage measured at terminal block)
    """
    step_up_ratio = (STEP_DOWN_RESISTOR_1 + STEP_DOWN_RESISTOR_2) / STEP_DOWN_RESISTOR_2
    return v_in * step_up_ratio * gain - offset


def code_to_voltage(code: list[int], adc_info: ADCReadInfo, calibs: CalibParam) -> float:
    """
    Converts ADC voltage read digital code to output voltage (voltage measured at terminal block)

    Args:
        `code` (list[int]): code bytes retrieved from ADC voltage read

        `adc_info` (ADCReadInfo): data about this adc's voltage reading configuration

        `calibs` (CalibParam): voltage reading gain and offset calibration values

    Returns:
        `float`: voltage value (V) corresponding to `code`
    """
    code_bits = bitstring_from_list(code)
    num_bits = adc_info.num_data_bytes * 8

    # code is given in 2's complement, remove leading 1
    if _is_negative_voltage(code_bits):
        code_bits[0] = 0

    v_in = _code_to_input_voltage(code_bits.uint, REFERENCE_VOLTAGE, num_bits)

    v_out = _adc_voltage_to_input_voltage(v_in, calibs.gain, calibs.offset)

    return v_out


def code_to_temperature(
    code: list[int],
    ref_resistance: float,
    rtd_sensor_resistance: float,
    rtd_sensor_resistance_variation: float
    ) -> float:
    """
    Converts ADC voltage read digital code to temperature. Intended for use in RTD sampling.

    Args:
        `code` (list[int]): code bytes retrieved from ADC voltage read

       `ref_resistance` (float): EdgePi-specific RTD reference resistance (Ohms)

       `rtd_sensor_resistance` (float): RTD material-dependent resistance value (Ohms)

       `rtd_sensor_resistance_variation` (float): RTD model-dependent resistance variation (Ohms/°C)

    Returns:
        `float`: temperature value (°C) corresponding to `code`
    """
    code_bits = bitstring_from_list(code)

    # refer to https://github.com/osensa/edgepi-python-sdk/issues/159 for computation details
    r_rtd = code_bits.uint / (2 ** 30) * ref_resistance

    temperature = (r_rtd - rtd_sensor_resistance) / rtd_sensor_resistance_variation
    _logger.debug(f"computed rtd temperature = {temperature}, from code = {code_bits.uint}")

    return temperature


class CRCCheckError(Exception):
    """Raised if CRC check fails to match CRC code generated by ADS1263"""


# use this with `generate_crc_8_table` below to get LUT for CRC-8-ATM polynomial
CRC_8_ATM_GEN = 0x07


def generate_crc_8_table(crc_polynomial: int):
    """
    Generates LUT containing CRC-8 values for all possible byte (8 bit) values, i.e.
    every entry is the CRC-8 value for the ith byte value from 0,...,255.

    Adapted from:
    https://github.com/osensa/rtu-adc/blob/6fac17efb955c2e8bb40740dd46e48ae51e1d5ff/crc8atm.py

    Args:
        `crc_polynomial`: the CRC-8 polynomial used to generate this LUT

    Returns:
        `list`: the LUT corresponding to this CRC-8 polynomial
    """
    table = list(range(256))

    # for every byte value from 0 to 255
    for index, crc in enumerate(table):
        for _ in range(8):
            # scan bitwise through byte to align leading 1 with CRC polynomial
            # perform divison (XOR) operation once aligned
            crc = (crc << 1) ^ (crc_polynomial if (crc & 0x80) else 0)

        # store only low byte of computed value to account for left shifting
        table[index] = crc & 0xFF

    return table


def check_crc(voltage_bytes: list, crc_code: int):
    """
    Performs CRC-8 check on a list of data bytes. By default uses
    CRC-8-ATM polynomial table

    Adapted from:
    https://github.com/osensa/rtu-adc/blob/6fac17efb955c2e8bb40740dd46e48ae51e1d5ff/crc8atm.py

    Args:
        `data_bytes` (list): a list of bytes ordered as
            [data_1, data_2,..., data_n, crc_8]

        `crc_table`: a previously generated LUT for the CRC-8-ATM
            polynomial
    """
    crc = 0

    data_bytes = voltage_bytes + [crc_code]

    # compute CRC value of data bytes byte wise using LUT
    # which holds CRC values for all possible byte values
    for byte in data_bytes:
        index = crc ^ byte
        crc = CRC_8_ATM_LUT[index]

    if crc != 0:
        raise CRCCheckError(
            f"CRC check failed with codes: computed={hex(crc)}, expected={hex(crc_code)}"
        )
