import math
import sys
import os
from enum import IntEnum
from PIL import Image

from ctypes import c_int, c_uint8, c_uint16, c_uint32, c_float, c_char_p, c_bool

from UniTAP.libs.lib_uicl.uicl import UICL_GetRequiredBufferSize, UICL_Convert
from UniTAP.libs.lib_uicl.uicl_types import UICL_Image, UICL_ImageParameters, UICL_Colorspace, UICL_Packing, \
    UICL_Sampling, UICL_Colorimetry, UICL_ComponentOrder, UICL_Alignment, UICL_SUCCESS
from .test_utils import search_bit


class DataType(IntEnum):
    Int = 0
    Double = 1
    String = 2
    Bool = 3
    Uint32 = 4
    ArrayU8 = 5
    ArrayU16 = 6
    ArrayU32 = 7
    PHYAddress = 8
    BitFlag = 9
    ArrayFromFilePath = 10
    FolderPath = 11
    BitList = 12


dict_types = {
    DataType.Int: c_int,
    DataType.Double: c_float,
    DataType.String: c_char_p,
    DataType.Bool: c_bool,
    DataType.Uint32: c_uint32,
    DataType.ArrayU8: c_uint8,
    DataType.ArrayU16: c_uint16,
    DataType.ArrayU32: c_uint32,
    DataType.PHYAddress: c_uint32,
    DataType.BitFlag: c_bool,
    DataType.ArrayFromFilePath: c_uint8,
    DataType.FolderPath: c_char_p,
    DataType.BitList: c_uint32
}


class EnumVar:

    def __init__(self, enumeration_variants: str):
        self.__available_values = []
        self.__str_present = []
        if enumeration_variants.find("#") != -1:
            available_values = enumeration_variants.split("\n")
            for item in available_values:
                self.__available_values.append(item.split(" # ")[0])
                self.__str_present.append(item.split(" # ")[1])
        else:
            self.__available_values = enumeration_variants.split("\n")

    @property
    def available_values(self) -> list:
        return self.__available_values

    @property
    def str_presentation(self) -> list:
        return self.__str_present


class BitField:

    def __init__(self, json_obj):
        self.__description = json_obj.get('description')
        self.__mask = int(json_obj.get('mask'), 16)
        if 'enumerationVariants' in json_obj:
            self.__enumeration_variants = EnumVar(json_obj.get('enumerationVariants'))
        else:
            self.__enumeration_variants = None
        if 'defaultValue' in json_obj:
            self.__default_value = json_obj.get('defaultValue')
        else:
            self.__default_value = None

    @property
    def description(self):
        return self.__description

    @property
    def mask(self):
        return self.__mask

    @property
    def enumeration_variants(self):
        return self.__enumeration_variants

    @description.setter
    def description(self, value: str):
        self.__description = value

    @mask.setter
    def mask(self, value: int):
        self.__mask = value

    @enumeration_variants.setter
    def enumeration_variants(self, value: list):
        self.__enumeration_variants = value


class Param:

    def __init__(self, json_obj):
        self.__min_value = sys.float_info.min
        self.__max_value = sys.float_info.max
        self.__type = DataType.Int
        self.__config_id = 0
        self.__config_id_name = ""
        self.__name = ""
        self.__default_value = None
        self.__enumeration_variants = None
        self.__bit_field_list = []

        self.__process_value(json_obj)

    def _set_by_bitmask(self, value, bitmask_index):
        if len(self.__bit_field_list) > bitmask_index:
            mask = self.__bit_field_list[bitmask_index].mask
            self.__default_value &= ~mask
            self.__default_value |= mask & (value << search_bit(mask))

    def _get_by_bitmask(self, bitmask_index: int, value_type=int):
        if len(self.__bit_field_list) > bitmask_index:
            mask = self.__bit_field_list[bitmask_index].mask
            masked_value = self.__default_value & mask
            return value_type(masked_value >> search_bit(mask))
        return 0

    def _set_by_bit_number(self, state, value, bit_number):
        if state:
            self.__default_value |= (value << bit_number)
        else:
            self.__default_value &= ~(value << bit_number)

    def _get_by_bit_number(self, bit_number) -> bool:
        return (self.__default_value >> bit_number) & 0x1

    def clear(self):
        self.__default_value = 0

    def direct_set_def_value(self, default_value):
        self.__default_value = default_value

    @property
    def default_value(self):
        return self.__default_value

    @default_value.setter
    def default_value(self, default_value):
        if self.__enumeration_variants is not None:
            if str(default_value) in self.__enumeration_variants.available_values:
                self.__default_value = default_value
            else:
                available_variants = '\n'.join(self.__enumeration_variants.available_values)
                raise ValueError(f"Value {default_value} is not in available variants:\n {available_variants}.")
        else:
            self.__default_value = default_value

    @property
    def min_value(self):
        return self.__min_value

    @property
    def max_value(self):
        return self.__max_value

    @property
    def config_id(self):
        return self.__config_id

    @property
    def config_id_name(self):
        return self.__config_id_name

    @property
    def name(self):
        return self.__name

    @property
    def value_type(self):
        return self.__type

    @property
    def enumeration_variants(self):
        return self.__enumeration_variants

    @property
    def bit_field_list(self):
        return self.__bit_field_list

    @property
    def data_length(self):
        if isinstance(self.__default_value, (list, tuple, bytes, bytearray, str)):
            return len(self.__default_value)
        else:
            return 1

    def __str__(self):
        return f"{self.default_value if self.data_length <= 50 else self.default_value[:50]}"

    def __process_value(self, json_obj):
        if 'minValue' in json_obj:
            self.__min_value = int(json_obj.get('minValue'))
        if 'maxValue' in json_obj:
            self.__max_value = int(json_obj.get('maxValue'))
        if 'enumerationVariants' in json_obj:
            self.__enumeration_variants = EnumVar(json_obj.get('enumerationVariants'))
        if 'bitList' in json_obj:
            bit_list = json_obj.get('bitList')
            for item in bit_list:
                self.__bit_field_list.append(BitField(item))

        self.__config_id = int(json_obj.get('configId'), 16)
        self.__type = DataType(json_obj.get('type'))
        self.__name = json_obj.get('name')
        self.__config_id_name = json_obj.get('id')
        if self.__type in [DataType.Int, DataType.Uint32]:
            self.__default_value = int(json_obj.get('defaultValue'), 16) \
                                   if json_obj.get('defaultValue').find("0x") != -1 \
                                   else int(json_obj.get('defaultValue'))
        elif self.__type == DataType.BitList:
            self.__default_value = int(json_obj.get('defaultValue'), 16)
        elif self.__type == DataType.Bool:
            self.__default_value = bool(json_obj.get('defaultValue'))
        elif self.__type == DataType.Double:
            self.__default_value = float(json_obj.get('defaultValue'))
        else:
            self.__default_value = json_obj.get('defaultValue')

    def __eq__(self, other):
        return self.default_value == other.default_value


def check_have_dict_attribute(obj) -> bool:
    try:
        vars(obj)
        return True
    except TypeError:
        return False


def get_param_list(param_obj) -> list:
    param_list = []
    if check_have_dict_attribute(param_obj):
        var_list = vars(param_obj)
    else:
        return param_list

    for item in var_list.keys():
        if isinstance(var_list.get(item), Param) or issubclass(type(var_list.get(item)), Param):
            param_list.append(var_list.get(item))
        elif isinstance(var_list.get(item), list):
            for elem in var_list.get(item):
                if isinstance(elem, Param):
                    param_list.append(elem)
        else:
            param_list.extend(get_param_list(var_list.get(item)))

    return param_list


def param_by_ci_name(json_obj, name: str) -> Param:
    for obj in json_obj:
        if obj.get('id') == name:
            return Param(obj)
    return Param({})


def read_image_file(full_filename: str) -> bytearray:
    file_name, file_extension = os.path.splitext(full_filename)
    if file_extension.lower() == '.bin':
        with open(full_filename, 'rb') as file:
            image_data = bytearray(file.read())
        return image_data
    elif file_extension.lower() in [".png", '.jpg', ".ppm", '.bmp', '.tif', '.tiff']:
        image = Image.open(full_filename)
        image = image.convert('RGB')
        image_data = bytearray(image.tobytes())
        return image_data
    else:
        return bytearray()


def uicl_image_preparation(data: bytearray, params):
    ref_image = UICL_Image()
    ref_param = UICL_ImageParameters()

    ref_param.Width = params.image_width
    ref_param.Height = params.image_height
    # VideoPixelTestElementFormat: RGB_16BPC = 20
    ref_param.Colorspace = UICL_Colorspace.Colorspace_RGB
    ref_param.BitsPerColor = 8
    ref_param.ComponentOrder = UICL_ComponentOrder.Order_RGB
    ref_param.Packing = UICL_Packing.Packing_Packed
    ref_param.Alignment = UICL_Alignment.Alignment_MSB
    # VideoPixelTestElementFormat: YCbCr444_161616 = 257
    ref_param.Sampling = UICL_Sampling.Sampling_444
    ref_param.IsFullRange = False
    ref_param.Colorimetry = UICL_Colorimetry.Colorimetry_Unknown
    ref_param.IsMonochrome = False
    ref_image.Parameters = ref_param
    ref_image.DataSize = UICL_GetRequiredBufferSize(ref_image)
    ref_image.DataPtr = (c_uint8 * ref_image.DataSize)(*data)

    _dest_params = UICL_ImageParameters()
    _dest_params.Width = params.image_width
    _dest_params.Height = params.image_height
    _dest_params.BitsPerColor = params.color_depth
    _dest_params.Colorspace = UICL_Colorspace.Colorspace_RGB if params.element_format <= 20 else UICL_Colorspace.Colorspace_YCbCr
    _dest_params.ComponentOrder = UICL_ComponentOrder.Order_RGB if params.element_format <= 20 else UICL_ComponentOrder.Order_YCbCr
    _dest_params.Sampling = UICL_Sampling.Sampling_444 if params.element_format <= 257 else UICL_Sampling.Sampling_420
    _dest_params.Packing = UICL_Packing.Packing_Packed if params.element_format <= 257 else UICL_Packing.Packing_Planar
    _dest_params.Alignment = UICL_Alignment.Alignment_LSB
    _dest_params.IsFullRange = False
    _dest_params.Colorimetry = UICL_Colorimetry.Colorimetry_Unknown if params.element_format <= 20 else UICL_Colorimetry.Colorimetry_ITU_R_BT601
    _dest_params.IsMonochrome = False

    _dest_image = UICL_Image()
    _dest_image.Parameters = _dest_params
    _dest_image.DataSize = UICL_GetRequiredBufferSize(_dest_image)
    _dest_image.DataPtr = (c_uint8 * _dest_image.DataSize)()

    result = UICL_Convert(ref_image, _dest_image)

    del ref_image
    del data

    if result < UICL_SUCCESS:
        print(f"UICL Error {result}")

    _data = []

    for i in range(_dest_image.DataSize):
        _data.append(_dest_image.DataPtr[i])

    del _dest_image

    return _data
