"""
Contains the support class for managing attributes whose data is all types of numeric values

Exports:
    is_number
    is_number_or_list_of_numbers
    NumericAttributeManager
    values_in_range
"""
from typing import List, Union

from ..utils import ParseError
from .AttributeManager import AttributeManager, PropertySet
from .parsing import KEY_ATTR_MAXIMUM, KEY_ATTR_MINIMUM


# ======================================================================
def is_number(value):
    """Return True if the value is a number, not including booleans"""
    if isinstance(value, float) or (isinstance(value, int) and not isinstance(value, bool)):
        return True
    return False


# ======================================================================
def is_number_or_list_of_numbers(value, type_count: int):
    """Return True if the value is a number or a list of "type_count" numbers, not including booleans"""
    if is_number(value) and type_count == 1:
        return True
    if isinstance(value, list):
        if len(value) != type_count:
            return False
        return all(is_number(single_value) for single_value in value)
    return False


# ======================================================================
def values_in_range(value, min_value: int, max_value: int):
    """Return True if the value is a number or list of numbers in the range [min_value,max_value]"""
    if isinstance(value, list):
        return all(not (single_value < min_value or single_value > max_value) for single_value in value)

    return min_value <= value <= max_value


# ======================================================================
class NumericAttributeManager(AttributeManager):
    """Support class for attributes with simple numeric types

    Attributes:
        minimum: Minimum allowable value of the numeric attribute. None means no minimum.
        maximum: Maximum allowable value of the numeric attribute. None means no maximum.
    """

    # Convenience types for determining numerical type information about these attributes with numerical_type()
    TYPE_OTHER = 0
    TYPE_INTEGER = 1
    TYPE_UNSIGNED_INTEGER = 2
    TYPE_DECIMAL = 3
    TYPE_FLOAT = 4
    TYPE_OBJECT_ID = 5

    def __init__(self, attribute_name: str, attribute_type_name: str):
        """Initialize the numeric attribute information

        Args:
            attribute_name: Name to use for this attribute
            attribute_type_name: Type of this attribute
        """
        super().__init__(attribute_name, attribute_type_name)
        self.minimum = None
        self.maximum = None

    def numerical_type(self) -> int:
        """Returns the numerical TYPE_* value for this attribute's data"""
        return NumericAttributeManager.TYPE_OTHER

    def cpp_element_value(self, value) -> str:
        """Ensure floating point elements have decimal values so that they are properly recognized."""
        if self.numerical_type() == self.TYPE_DECIMAL:
            return f"{value * 1.0}"
        if self.numerical_type() == self.TYPE_FLOAT:
            return f"{value * 1.0}f"
        return f"{int(value)}"

    def validate_value(self, value):
        """Validate that the given data is a matching numeric type, and in range when min/max are specified

        The parse_extra_properties() method should have been called before validating anything.

        Args:
            value: Data value to verify

        Raises:
            ParseError if the min/max range is not respected by the value
        """
        if not is_number_or_list_of_numbers(value, self.tuple_count):
            raise ParseError(f"Value {value} on a double[{self.tuple_count}] attribute is not a matching type")
        self.validate_numbers_in_range(value)

    def validate_numbers_in_range(self, value):
        """Validate that the given value is in the legal numeric range, if any are specified

        Args:
            value: Data value to verify

        Raises:
            ParseError if the min/max range is not respected by the value
        """
        # Convert single values to lists for uniform handling
        values = value if isinstance(value, list) else [value]
        if self.minimum is not None:
            minimums = self.minimum if isinstance(self.minimum, list) else [self.minimum]
            for single_value, minimum in zip(values, minimums):
                if single_value < minimum:
                    raise ParseError(f"Value of {value} is less than the allowed minimum of {self.minimum}")
        if self.maximum is not None:
            maximums = self.maximum if isinstance(self.maximum, list) else [self.maximum]
            for single_value, maximum in zip(values, maximums):
                if single_value > maximum:
                    raise ParseError(f"Value of {value} is greater than the allowed maximum of {self.maximum}")
        super().validate_value(value)

    def parse_extra_properties(self, property_set: dict) -> PropertySet:
        """Parse properties specific to numeric attribute types

        Args:
            property_set: (NAME, VALUE) for properties the attribute type might support

        Raises:
            ParseError: If any of the extra properties are invalid or not recognized
        """
        remaining_properties = self.parse_min_max_properties(property_set)
        unchecked_properties = super().parse_extra_properties(remaining_properties)
        return unchecked_properties

    def parse_min_max_properties(self, property_set: dict) -> PropertySet:
        """Check that the min and max property values in property_set are acceptable values (or not present).

        Args:
            property_set: (KEY:VALUE) set from which the min and max will be extracted

        Returns:
            The subset of property_set not checked by this method
        """
        unchecked_set = {}
        for property_name, property_value in property_set.items():
            if property_name in [KEY_ATTR_MINIMUM, KEY_ATTR_MAXIMUM]:
                try:
                    # Min/Max values are applied to all array members, but elements must be specified individually
                    self.validate_value_nested(property_value, [])
                except ParseError as error:
                    raise ParseError(f"Setting {property_name} on {self.name}") from error
                setattr(self, property_name, property_value)
            else:
                unchecked_set[property_name] = property_value
        return unchecked_set

    def cpp_includes(self) -> List[str]:
        """Numeric tuples can use the core 'tuple' class to provide some helpful support methods"""
        regular_includes = super().cpp_includes()
        if self.tuple_count > 1:
            regular_includes.append("omni/graph/core/tuple.h")
        return regular_includes

    def empty_base_value(self) -> Union[float, int]:
        """Returns an empty value of the current attribute type without tuples or arrays"""
        return 0.0 if self.numerical_type() in [self.TYPE_DECIMAL, self.TYPE_FLOAT] else 0
