"""Provides data related to numbers."""

import typing as t
from collections import defaultdict
from decimal import Decimal

from mimesis.enums import NumType
from mimesis.providers.base import BaseProvider
from mimesis.types import Matrix

__all__ = ["Numeric"]


class Numeric(BaseProvider):
    """A provider for generating numeric data."""

    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
        super().__init__(*args, **kwargs)
        self.__increment_dict: t.DefaultDict[str, int] = defaultdict(int)
        self.__default_accumulator_value: t.Final[str] = "default"

    class Meta:
        name = "numeric"

    def increment(self, accumulator: str | None = None) -> int:
        """Generates an incrementing number.

        Each call of this method returns an incrementing number (with the step of +1).

        If **accumulator** passed then increments number associated with it.

        Example:
            >>> self.increment()
            1
            >>> self.increment(accumulator="a")
            1
            >>> self.increment()
            2
            >>> self.increment(accumulator="a")
            2
            >>> self.increment(accumulator="b")
            1
            >>> self.increment(accumulator="a")
            3

        :param accumulator: Accumulator (used to create associative incrementation).
        :return: Integer.
        """
        if not accumulator:
            accumulator = self.__default_accumulator_value

        self.__increment_dict[accumulator] += 1
        return self.__increment_dict[accumulator]

    def float_number(
        self, start: float = -1000.0, end: float = 1000.0, precision: int = 15
    ) -> float:
        """Generates a random float number in range [start, end].

        :param start: Start range.
        :param end:  End range.
        :param precision: Round a number to a given
            precision in decimal digits, default is 15.
        :return: Float.
        """
        return self.random.uniform(start, end, precision)

    def floats(
        self, start: float = 0, end: float = 1, n: int = 10, precision: int = 15
    ) -> list[float]:
        """Generates a list of random float numbers.

        :param start: Start range.
        :param end: End range.
        :param n: Length of the list.
        :param precision: Round a number to a given
            precision in decimal digits, default is 15.
        :return: The list of floating-point numbers.
        """
        return [self.float_number(start, end, precision) for _ in range(n)]

    def integer_number(self, start: int = -1000, end: int = 1000) -> int:
        """Generates a random integer from start to end.

        :param start: Start range.
        :param end: End range.
        :return: Integer.
        """
        return self.random.randint(start, end)

    def integers(self, start: int = 0, end: int = 10, n: int = 10) -> list[int]:
        """Generates a list of random integers.

        :param start: Start.
        :param end: End.
        :param n: Length of the list.
        :return: List of integers.

        :Example:
            [-20, -19, -18, -17]
        """
        return self.random.randints(n, start, end)

    def complex_number(
        self,
        start_real: float = 0.0,
        end_real: float = 1.0,
        start_imag: float = 0.0,
        end_imag: float = 1.0,
        precision_real: int = 15,
        precision_imag: int = 15,
    ) -> complex:
        """Generates a random complex number.

        :param start_real: Start real range.
        :param end_real: End real range.
        :param start_imag: Start imaginary range.
        :param end_imag: End imaginary range.
        :param precision_real:  Round a real part of
            number to a given precision.
        :param precision_imag:  Round the imaginary part of
            number to a given precision.
        :return: Complex numbers.
        """
        real_part = self.random.uniform(start_real, end_real, precision_real)
        imag_part = self.random.uniform(start_imag, end_imag, precision_imag)
        return complex(real_part, imag_part)

    def complexes(
        self,
        start_real: float = 0,
        end_real: float = 1,
        start_imag: float = 0,
        end_imag: float = 1,
        precision_real: int = 15,
        precision_imag: int = 15,
        n: int = 10,
    ) -> list[complex]:
        """Generates a list of random complex numbers.

        :param start_real: Start real range.
        :param end_real: End real range.
        :param start_imag: Start imaginary range.
        :param end_imag: End imaginary range.
        :param precision_real:  Round a real part of
            number to a given precision.
        :param precision_imag:  Round the imaginary part of
            number to a given precision.
        :param n: Length of the list.
        :return: A list of random complex numbers.
        """
        numbers = []
        for _ in range(n):
            numbers.append(
                self.complex_number(
                    start_real=start_real,
                    end_real=end_real,
                    start_imag=start_imag,
                    end_imag=end_imag,
                    precision_real=precision_real,
                    precision_imag=precision_imag,
                ),
            )
        return numbers

    def decimal_number(self, start: float = -1000.0, end: float = 1000.0) -> Decimal:
        """Generates a random decimal number.

        :param start:  Start range.
        :param end: End range.
        :return: :py:class:`decimal.Decimal` object.
        """
        return Decimal.from_float(self.float_number(start, end))

    def decimals(
        self, start: float = 0.0, end: float = 1000.0, n: int = 10
    ) -> list[Decimal]:
        """Generates a list of decimal numbers.

        :param start: Start range.
        :param end: End range.
        :param n: Length of the list.
        :return: A list of :py:class:`decimal.Decimal` objects.
        """
        return [self.decimal_number(start, end) for _ in range(n)]

    def matrix(
        self,
        m: int = 10,
        n: int = 10,
        num_type: NumType = NumType.FLOAT,
        **kwargs: t.Any,
    ) -> Matrix:
        """Generates m x n matrix with a random numbers.

        This method works with a variety of types,
        so you can pass method-specific `**kwargs`.

        :param m: Number of rows.
        :param n: Number of columns.
        :param num_type: NumType enum object.
        :param kwargs: Other method-specific arguments.
        :return: A matrix of random numbers.
        """
        key = self.validate_enum(num_type, NumType)
        kwargs.update({"n": n})
        method = getattr(self, key)
        return [method(**kwargs) for _ in range(m)]
