"""
* Author: Manuel Di Lullo (https://github.com/manueldilullo)
* Description: Convert a number to use the correct SI or Binary unit prefix.

Inspired by prefix_conversion.py file in this repository by lance-pyles

URL: https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes
URL: https://en.wikipedia.org/wiki/Binary_prefix
"""

from __future__ import annotations

from enum import Enum, unique
from typing import TypeVar

# Create a generic variable that can be 'Enum', or any subclass.
T = TypeVar("T", bound="Enum")


@unique
class BinaryUnit(Enum):
    yotta = 80
    zetta = 70
    exa = 60
    peta = 50
    tera = 40
    giga = 30
    mega = 20
    kilo = 10


@unique
class SIUnit(Enum):
    yotta = 24
    zetta = 21
    exa = 18
    peta = 15
    tera = 12
    giga = 9
    mega = 6
    kilo = 3
    hecto = 2
    deca = 1
    deci = -1
    centi = -2
    milli = -3
    micro = -6
    nano = -9
    pico = -12
    femto = -15
    atto = -18
    zepto = -21
    yocto = -24

    @classmethod
    def get_positive(cls: type[T]) -> dict:
        """
        Returns a dictionary with only the elements of this enum
        that has a positive value
        >>> from itertools import islice
        >>> positive = SIUnit.get_positive()
        >>> inc = iter(positive.items())
        >>> dict(islice(inc, len(positive) // 2))
        {'yotta': 24, 'zetta': 21, 'exa': 18, 'peta': 15, 'tera': 12}
        >>> dict(inc)
        {'giga': 9, 'mega': 6, 'kilo': 3, 'hecto': 2, 'deca': 1}
        """
        return {unit.name: unit.value for unit in cls if unit.value > 0}

    @classmethod
    def get_negative(cls: type[T]) -> dict:
        """
        Returns a dictionary with only the elements of this enum
        that has a negative value
        @example
        >>> from itertools import islice
        >>> negative = SIUnit.get_negative()
        >>> inc = iter(negative.items())
        >>> dict(islice(inc, len(negative) // 2))
        {'deci': -1, 'centi': -2, 'milli': -3, 'micro': -6, 'nano': -9}
        >>> dict(inc)
        {'pico': -12, 'femto': -15, 'atto': -18, 'zepto': -21, 'yocto': -24}
        """
        return {unit.name: unit.value for unit in cls if unit.value < 0}


def add_si_prefix(value: float) -> str:
    """
    Function that converts a number to his version with SI prefix
    @input value (an integer)
    @example:
    >>> add_si_prefix(10000)
    '10.0 kilo'
    """
    prefixes = SIUnit.get_positive() if value > 0 else SIUnit.get_negative()
    for name_prefix, value_prefix in prefixes.items():
        numerical_part = value / (10**value_prefix)
        if numerical_part > 1:
            return f"{str(numerical_part)} {name_prefix}"
    return str(value)


def add_binary_prefix(value: float) -> str:
    """
    Function that converts a number to his version with Binary prefix
    @input value (an integer)
    @example:
    >>> add_binary_prefix(65536)
    '64.0 kilo'
    """
    for prefix in BinaryUnit:
        numerical_part = value / (2**prefix.value)
        if numerical_part > 1:
            return f"{str(numerical_part)} {prefix.name}"
    return str(value)


if __name__ == "__main__":
    import doctest

    doctest.testmod()
