"""
Number manipulation functions.

This module provides functions for working with numbers including:
- Number construction with precision and scale
- Integer operations
- Parsing numbers from strings
- Querying number properties (precision, scale, size)
"""
from __future__ import annotations

from . import NumberValue, StringValue
from ..frontend.base import Library, Concept, MetaRef, NumberConcept, Expression, Field, Variable
from ..frontend.core import Number, String, Integer, cast as core_cast
from decimal import Decimal as PyDecimal
from relationalai.util.docutils import include_in_docs

__include_in_docs__ = True

# the front-end library object
library = Library("numbers")

#--------------------------------------------------
# Constructors
#--------------------------------------------------
_parse_number = library.Relation("parse_number", [Field.input("value", String), Field("result", Number)])

@include_in_docs
def number(value: NumberValue, precision:int=38, scale:int=14) -> Variable:
    """
    Create an expression that represents a number with specified value, precision and scale.

    Parameters
    ----------
    value: NumberValue
        The numeric value.
    precision: int
        The precision (total number of digits).
    scale: int
        The scale (number of decimal places).

    Returns
    -------
    Variable
        A `Variable` representing the number with the specified properties. Returns `Number`.

    Examples
    --------
    Create a number with custom precision and scale:

    >>> numbers.number(123.456, precision=10, scale=3)
    >>> numbers.number(Product.price, precision=12, scale=2)
    """
    if isinstance(value, Variable):
        cast_type = Number.size(precision, scale)
        return core_cast(MetaRef(cast_type), value, cast_type.ref())

    # literals
    if isinstance(value, int) and scale != 0:
        value = PyDecimal(str(value))
    if isinstance(value, float):
        value = PyDecimal(str(value))
    return Number.size(precision, scale)(value)

@include_in_docs
def integer(value: NumberValue) -> Variable:
    """
    Create an expression that represents an integer with this value.

    Parameters
    ----------
    value: NumberValue
        The integer value.

    Returns
    -------
    Variable
        A `Variable` representing the integer value. Returns `Integer`.

    Examples
    --------
    Create integer expressions:

    >>> numbers.integer(42)
    >>> numbers.integer(Employee.age)
    """
    if isinstance(value, Variable):
        return core_cast(MetaRef(Integer), value, Integer.ref())
    return Integer(value)

@include_in_docs
def parse_number(value: StringValue, precision:int=38, scale:int=14) -> Expression:
    """
    Parse a string value as a number with specified precision and scale.

    Parameters
    ----------
    value: StringValue
        The string value to parse.
    precision: int
        The precision (total number of digits).
    scale: int
        The scale (number of decimal places).

    Returns
    -------
    Expression
        An `Expression` computing the parsed number. Returns `Number`.

    Examples
    --------
    Parse string to number:

    >>> numbers.parse_number("123.456")
    >>> numbers.parse_number(Product.price_str, precision=10, scale=2)
    """
    return _parse_number(value, Number.size(precision, scale).ref())

@include_in_docs
def parse(value: str, number: NumberConcept) -> Expression:
    """
    Parse a string value as a number with the precision and scale of the number argument.

    Parameters
    ----------
    value: str
        The string value to parse.
    number: NumberConcept
        The NumberConcept specifying the target precision and scale.

    Returns
    -------
    Expression
        An `Expression` computing the parsed number. Returns `Number`.

    Examples
    --------
    Parse string using another number's format:

    >>> numbers.parse("999.99", Product.price)
    """
    return parse_number(value, precision(number), scale(number))

#--------------------------------------------------
# Number information.
#--------------------------------------------------

@include_in_docs
def is_number(number: Concept) -> bool:
    """
    Check if a concept represents a number.

    Parameters
    ----------
    number: Concept
        The Concept to check.

    Returns
    -------
    bool
        True if the concept is a NumberConcept.

    Examples
    --------
    Check if a concept is a number:

    >>> numbers.is_number(Product.price)
    """
    return isinstance(number, NumberConcept)

@include_in_docs
def precision(number: NumberConcept) -> int:
    """
    Get the precision (total number of digits) of a number concept.

    Parameters
    ----------
    number: NumberConcept
        The NumberConcept to query.

    Returns
    -------
    int
        The precision of the number.

    Examples
    --------
    Get the precision of a number concept:

    >>> numbers.precision(Product.price)
    """
    return number._precision

@include_in_docs
def scale(number: NumberConcept) -> int:
    """
    Get the scale (number of decimal places) of a number concept.

    Parameters
    ----------
    number: NumberConcept
        The NumberConcept to query.

    Returns
    -------
    int
        The scale of the number.

    Examples
    --------
    Get the scale of a number concept:

    >>> numbers.scale(Product.price)
    """
    return number._scale

@include_in_docs
def size(number: NumberConcept) -> int:
    """
    Get the size (number of bits) needed to represent a number concept.

    Parameters
    ----------
    number: NumberConcept
        The NumberConcept to query.

    Returns
    -------
    int
        The size in bits needed to represent the number.

    Examples
    --------
    Get the bit size of a number concept:

    >>> numbers.size(Product.price)
    """
    return digits_to_bits(number._precision)

@include_in_docs
def digits_to_bits(precision: int) -> int:
    """
    Transform from a number of base 10 digits to the number of bits necessary to represent it.

    Parameters
    ----------
    precision: int
        The number of base 10 digits.

    Returns
    -------
    int
        The number of bits required (8, 16, 32, 64, or 128).

    Raises
    ------
    ValueError
        If precision is greater than 38 or invalid.

    Examples
    --------
    A number with 38 digits requires 128 bits:

    >>> numbers.digits_to_bits(38)
    128

    A number with 9 digits requires 32 bits:

    >>> numbers.digits_to_bits(9)
    32
    """
    if precision <= 2:
        return 8
    elif precision <= 4:
        return 16
    elif precision <= 9:
        return 32
    elif precision <= 18:
        return 64
    elif precision <= 38:
        return 128
    raise ValueError(f"Invalid numeric precision '{precision}'")
