__version__ = "5.0.0"

import fractions
import re
from decimal import Decimal
from typing import Any, Union

from djfractions.exceptions import InvalidFractionString, NoHtmlUnicodeEntity

__all__ = [
    "quantity_to_decimal",
    "is_number",
    "is_fraction",
    "get_fraction_parts",
    "get_fraction_unicode_entity",
]

# Aligns with https://docs.python.org/3/library/fractions.html#fractions.Fraction.limit_denominator
DEFAULT_MAX_DENOMINATOR = 1000000

# Enttities from https://dev.w3.org/html5/html-author/charref
HTML_ENTITIES = [
    "&frac12;",
    "&frac13;",
    "&frac23;",
    "&frac14;",
    "&frac34;",
    "&frac15;",
    "&frac25;",
    "&frac35;",
    "&frac45;",
    "&frac16;",
    "&frac56;",
    "&frac17;",
    "&frac18;",
    "&frac38;",
    "&frac58;",
    "&frac78;",
]


def is_number(s: Any) -> bool:
    """
    Determine if the input value is numeric - an int, float, decimal.Decimal,
    or a string such as '1', '1.23', etc.

    :param s: A string value to check to see if it represents a float or int.
    """
    try:
        int(s)
        return True
    except ValueError:
        pass

    try:
        float(s)
        return True
    except ValueError:
        pass

    return False


def is_fraction(s: Any) -> bool:
    """
    Determine if the input string appears to represent a fraction.
    This does not include mixed numbers such as 1 1/3

    :param s: A string value to check if it is formatted as a fraction.
    """
    return bool(re.match(r"^-?\d+/\d+$", s))


def coerce_to_thirds(value: fractions.Fraction) -> fractions.Fraction:
    """
    takes a :class:`fractions.Fraction` and forces it to thirds if it is one that
    is frequently the result of taking a number such as 1/3, converting to decimal/float,
    then back to a fraction.
    """
    temp_decimal = Decimal(value.numerator / value.denominator).quantize(Decimal("0.00"))
    if (
        temp_decimal % 1 == Decimal(".33")
        or temp_decimal % 1 == Decimal(".3")
        or temp_decimal % 1 == Decimal(".67")
        or temp_decimal % 1 == Decimal(".6")
    ):
        value = value.limit_denominator(3)
    return value


def quantity_to_decimal(quantity_string: str) -> Decimal:
    """
    Take a quantity string and return a decimal.

    Handles one hundred, two hundred, three hundred twenty five,
    1, 1 1/4, 1 and 1/4, 1.25, .25.  Negative values should have
    the negative sign first, such as -1/4 or -1 1/4

    :param quantity_string: String to convert to a :class:`decimal.Decimal`
    """

    # get actual fraction-like strings to be N/N with no spaces
    quantity_string = quantity_string.strip()
    quantity_string = re.sub(r"\b(\d+)\s+/\s+(\d+)\b", r"\1/\2", quantity_string)

    if is_number(quantity_string):
        return Decimal(quantity_string)

    if is_fraction(quantity_string):
        return _fraction_string_to_decimal(quantity_string)

    # assume the a hyphen between a whole value and fraction such as 1-1/4
    # is a separator and not a negative fraction.
    # If the negative is first though, then we need to keep it negative.
    positive_or_negative = -1 if quantity_string.startswith("-") else 1
    quantity_string = quantity_string.replace("-", " ")

    parts = quantity_string.split()
    parts_length = len(parts)

    # it must be a mixed number like 1 1/4
    number_stack = []  # for storing the entire number to return in parts
    for part in parts:
        if is_fraction(part):
            number_stack.append(_fraction_string_to_decimal(part))
        elif is_number(part):
            number_stack.append(Decimal(part))

    return Decimal(sum(number_stack)) * positive_or_negative


def quantity_to_fraction(quantity_string: str) -> fractions.Fraction:
    """
    Take a quantity string and return a :class:`fractions.Fraction`.

    Handles one hundred, two hundred, three hundred twenty five,
    1, 1 1/4, 1 and 1/4, 1-1/4, 1.25, .25. Negative values should have
    the negative sign first, such as -1/4 or -1 1/4

    :param quantity_string: String to convert to a :class:`fractions.Fraction`
    """
    # get actual fraction-like strings to be N/N with no spaces
    quantity_string = quantity_string.strip()
    quantity_string = re.sub(r"\b(\d+)\s+/\s+(\d+)\b", r"\1/\2", quantity_string)
    if is_number(quantity_string):
        return fractions.Fraction(quantity_string)

    if is_fraction(quantity_string):
        return _fraction_string_to_fraction(quantity_string)

    # it must be a something like 1 1/4
    # assume that a hyphen between a whole value and fraction such as 1-1/4
    # is a separator and not a negative fraction.
    # If the negative is first though, then we need to keep it negative.
    # Cannot just keep the fraction on the int or we end up subtraction.
    # -1 1/4 becomes -3/4 when what is meant is -5/4
    positive_or_negative = -1 if quantity_string.startswith("-") else 1

    # non-capturing group in the middle handls just a space, hyphen with
    # optional spaces, or the word and.  Examples:
    # 1 1/4, 1-1/4, 1 - 1/4, 1 and 1/4
    parts = re.match(r"^-?(\d+)(?:\s+|\s*-?\s*|\s+and\s+)(\d+\/\d+)", quantity_string)
    if not parts:
        raise InvalidFractionString("%s is not a valid fraction" % quantity_string)
    # parts.group(0) is the entire string, 1 is the whole number bit
    f = fractions.Fraction(parts.group(2))
    f = (f + int(parts.group(1))) * positive_or_negative
    return f


def _fraction_string_to_fraction(fraction: str) -> fractions.Fraction:
    """
    Convert a string representing a fraction to a :class:`fractions.Fraction`
    """
    parts = fraction.split("/")
    numerator = int(parts[0])
    denominator = int(parts[1])
    return fractions.Fraction(numerator, denominator)


def _fraction_string_to_decimal(fraction: str) -> Decimal:
    """
    Convert strings such as '1/4' to a Decimal
    """
    parts = fraction.split("/")
    numerator = int(parts[0])
    denominator = int(parts[1])
    return Decimal(numerator / denominator)


def get_fraction_parts(
    value: Union[fractions.Fraction, float, Decimal, int, str],
    allow_mixed_numbers: bool = True,
    limit_denominator: int = DEFAULT_MAX_DENOMINATOR,
    coerce_thirds: bool = True,
):
    """
    Takes an `int`, `float`, or :class:`decimal.Decimal` and returns
    a tuple of (whole_number, numerator, denominator).  If allow_mixed_numbers
    is not True, then whole_number will be None.

    :param value: The value to convert to parts of a fraction.
    :param bool allow_mixed_numbers: Defaults to True.  If True, then parts for
        mixed numbers will be created, otherwise improper fractions with a
        whole_number of 0 will be created.  In the case where value is a
        whole number such as 4, if allow_mixed_numbers is True, then
        a tuple of (4, 0, 1) would be returned, otherwise
        (0, 4, 1) would be returned.
    :param bool limit_denominator: Limit the denominator to this value.  Defaults to 1000000,
        which is the same as :meth:`fractions.Fraction.limit_denominator()` default max_denominator
    :param bool coerce_thirds:  Defaults to True.  If True
        then .3 repeating is forced to 1/3 rather than 3/10, 33/100, etc.
        and .66 and .67 are forced to 2/3.
    """

    f = fractions.Fraction(value)

    whole_number = 0
    if allow_mixed_numbers and f.numerator >= f.denominator:
        # convert to complex number
        # whole_number = f.numerator // f.denominator
        whole_number, numerator = divmod(f.numerator, f.denominator)
        # f = fractions.Fraction(f.numerator - (whole_number * f.denominator), f.denominator)
        f = fractions.Fraction(numerator, f.denominator)

    if limit_denominator:
        f = f.limit_denominator(limit_denominator)

    if coerce_thirds and (not limit_denominator or limit_denominator > 3):
        # if denominator is limited to less than 3, this would be in opposition to that.
        # if denominator is limited to 3 then this has naturally already been done.
        f = coerce_to_thirds(f)
    return (whole_number, f.numerator, f.denominator)


def get_fraction_unicode_entity(value: Union[fractions.Fraction, float, Decimal, int, str]) -> str:
    """
    Returns the html unicode entity for the fraction if one exists or None

    :param value:  The value to get the entity for.
    """
    if not isinstance(value, fractions.Fraction):
        value = fractions.Fraction(value)

    entity = "&frac%d%d;" % (value.numerator, value.denominator)

    if entity not in HTML_ENTITIES:
        raise NoHtmlUnicodeEntity("No valid HTML entity exists for %s" % value)
    return entity
