################################################################################
#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
# limitations under the License.
################################################################################

import re
from decimal import Decimal as BigDecimal
from decimal import Inexact
from decimal import ROUND_HALF_UP
from decimal import localcontext
from typing import Optional, Tuple

_DECIMAL_PATTERN = re.compile(
    r"^(DECIMAL|NUMERIC|DEC)(?:\((\d+)(?:,\s*(\d+))?\))?$",
    re.IGNORECASE,
)


class Decimal:
    """
    An internal data structure representing data of DecimalType.

    This data structure is immutable and might store decimal values in a
    compact representation (as an integer value) if values are small enough.

    Python implementation note:
        ``decimal_val`` is an instance of Python's standard library
        ``decimal.Decimal`` (imported as ``BigDecimal``), corresponding to
        Java's ``java.math.BigDecimal``.
    """

    # Maximum number of decimal digits a long integer can represent.
    # (1e18 < Long.MAX_VALUE < 1e19)
    MAX_LONG_DIGITS = 18

    MAX_COMPACT_PRECISION = 18

    # Powers of 10 used for compact decimal conversion.
    POW10 = tuple(10 ** i for i in range(MAX_COMPACT_PRECISION + 1))

    # The semantics of the fields are as follows:
    #
    # - precision and scale represent the SQL decimal type.
    # - If decimal_val is set, it stores the complete decimal value.
    # - Otherwise, the decimal value is represented by
    #     long_val / (10 ** scale).
    #
    # Note that (precision, scale) must always be correct.
    #
    # If precision > MAX_COMPACT_PRECISION:
    #     decimal_val stores the value and long_val is undefined.
    # Otherwise:
    #     (long_val, scale) represents the value and decimal_val may be
    #     lazily initialized and cached.
    def __init__(
            self,
            precision: int,
            scale: int,
            long_val: int,
            decimal_val: Optional[BigDecimal],
    ):
        self.precision = precision
        self.scale = scale
        self.long_val = long_val
        self.decimal_val = decimal_val

    # ----------------------------------------------------------------------
    # Public Interfaces
    # ----------------------------------------------------------------------

    def to_big_decimal(self) -> BigDecimal:
        """
        Converts this Decimal into a python builtin-in decimal.Decimal instance.
        """
        if self.decimal_val is None:
            with localcontext() as ctx:
                ctx.prec = max(self.precision + abs(self.scale), 38)
                self.decimal_val = BigDecimal(self.long_val).scaleb(-self.scale)
        return self.decimal_val

    def to_unscaled_long(self) -> int:
        """
        Returns the unscaled integer value of this Decimal.

        Raises:
            ArithmeticError:
                If this Decimal does not exactly fit into a long integer.
        """
        if self.is_compact():
            return self.long_val

        with localcontext() as ctx:
            bd = self.to_big_decimal()
            ctx.prec = max(
                len(bd.as_tuple().digits) + abs(self.scale),
                self.precision,
                38,
            )
            value = bd.scaleb(self.scale)

            if value != value.to_integral_exact():
                raise ArithmeticError("Decimal does not exactly fit in long.")

        int_val = int(value)
        if not (-(1 << 63) <= int_val <= (1 << 63) - 1):
            raise ArithmeticError("BigInteger out of long range")

        return int_val

    def to_unscaled_bytes(self) -> bytes:
        """
        Returns the unscaled value encoded as a signed byte array.
        """

        with localcontext() as ctx:
            bd = self.to_big_decimal()
            ctx.prec = max(
                len(bd.as_tuple().digits) + abs(self.scale),
                self.precision,
                38,
            )
            value = bd.scaleb(self.scale)

            if value != value.to_integral_exact():
                raise ArithmeticError("Decimal does not exactly fit in long.")

        unscaled = int(value)
        length = self._get_compact_byte_length(unscaled)
        return unscaled.to_bytes(length, byteorder="big", signed=True)

    def is_compact(self) -> bool:
        """
        Returns whether the decimal value is small enough to be stored in a long integer.
        """
        return self.precision <= self.MAX_COMPACT_PRECISION

    def copy(self) -> "Decimal":
        """
        Returns a copy of this Decimal.
        """
        return Decimal(
            self.precision,
            self.scale,
            self.long_val,
            self.decimal_val,
        )

    # ----------------------------------------------------------------------
    # Comparison
    # ----------------------------------------------------------------------

    def __eq__(self, other):
        if not isinstance(other, Decimal):
            return False
        return self.compare_to(other) == 0

    def __lt__(self, other):
        return self.compare_to(other) < 0

    def compare_to(self, other: "Decimal") -> int:
        """
        Compares this Decimal with another Decimal.
        """
        if (
                self.is_compact()
                and other.is_compact()
                and self.scale == other.scale
        ):
            if self.long_val < other.long_val:
                return -1
            if self.long_val > other.long_val:
                return 1
            return 0

        a = self.to_big_decimal()
        b = other.to_big_decimal()

        if a < b:
            return -1
        if a > b:
            return 1
        return 0

    def __hash__(self):
        return hash(self.to_big_decimal())

    def __str__(self):
        return format(self.to_big_decimal(), "f")

    # ----------------------------------------------------------------------
    # Constructor Utilities
    # ----------------------------------------------------------------------

    @classmethod
    def from_big_decimal(
            cls,
            value: BigDecimal,
            precision: int,
            scale: int,
    ) -> Optional["Decimal"]:
        """
        Creates a ``Decimal`` from a Python's built-in ``decimal.Decimal`` with the given precision and scale.

        The value is rounded to the requested scale using ROUND_HALF_UP.
        If the resulting precision exceeds the specified precision, None is returned.

        Note:
            Java ``BigDecimal`` provides arbitrary precision. Paimon supports
            DECIMAL precision up to 38 digits, so a temporary context with
            precision >= 38 is used here to match Java semantics without
            modifying the global decimal context.
        """

        if not value.is_finite():
            raise ArithmeticError("NaN and Infinity are not supported.")

        with localcontext() as ctx:
            t = value.as_tuple()
            ctx.prec = max(
                precision + scale,
                len(t.digits) + abs(t.exponent) + scale,
                38,
            )

            value = value.quantize(
                BigDecimal(1).scaleb(-scale),
                rounding=ROUND_HALF_UP,
            )

            digits = len(value.as_tuple().digits)

            if digits > precision:
                return None

            long_val = -1

            if precision <= cls.MAX_COMPACT_PRECISION:
                unscaled = value.scaleb(scale)

                if unscaled != unscaled.to_integral_exact():
                    raise ArithmeticError(
                        "Decimal does not exactly fit in long."
                    )

                long_val = int(unscaled)

            return cls(
                precision,
                scale,
                long_val,
                value,
            )

    @classmethod
    def from_unscaled_long(
            cls,
            unscaled_long: int,
            precision: int,
            scale: int,
    ) -> "Decimal":
        """
        Creates a Decimal from an unscaled integer value with the given precision and scale.
        """
        if precision <= 0 or precision > cls.MAX_LONG_DIGITS:
            raise ValueError(
                "precision must be between 1 and {}".format(
                    cls.MAX_LONG_DIGITS
                )
            )

        return cls(
            precision,
            scale,
            unscaled_long,
            None,
        )

    @classmethod
    def from_unscaled_bytes(
            cls,
            unscaled_bytes: bytes,
            precision: int,
            scale: int,
    ) -> Optional["Decimal"]:
        """
        Creates a Decimal from an unscaled signed byte array.
        """
        value = int.from_bytes(
            unscaled_bytes,
            byteorder="big",
            signed=True,
        )

        with localcontext() as ctx:
            ctx.prec = max(
                len(str(abs(value))) + abs(scale),
                precision,
                38,
            )
            bd = BigDecimal(value).scaleb(-scale)

        return cls.from_big_decimal(
            bd,
            precision,
            scale,
        )

    @classmethod
    def zero(cls, precision: int, scale: int) -> Optional["Decimal"]:
        """
        Creates a Decimal representing zero with the given precision and scale.

        If the precision exceeds the supported range, None is returned.
        """
        if precision <= cls.MAX_COMPACT_PRECISION:
            return cls(
                precision,
                scale,
                0,
                None,
            )

        return cls.from_big_decimal(
            BigDecimal(0),
            precision,
            scale,
        )

    # ----------------------------------------------------------------------
    # Arithmetic Operations
    # ----------------------------------------------------------------------

    @staticmethod
    def add(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal:
        """
        Returns the sum of two decimal values without losing precision due to
        Python's default decimal context.

        The operation is performed in a temporary high-precision context to
        emulate Java BigDecimal arithmetic.
        """
        assert isinstance(
            lhs, BigDecimal
        ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}"

        assert isinstance(
            rhs, BigDecimal
        ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}"

        with localcontext() as ctx:
            ctx.prec = max(
                len(lhs.as_tuple().digits),
                len(rhs.as_tuple().digits),
                precision,
                38,
            ) + 1

            return lhs + rhs

    @staticmethod
    def subtract(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal:
        """
        Returns the difference of two decimal values without losing precision due
        to Python's default decimal context.

        The operation is performed in a temporary high-precision context to
        emulate Java BigDecimal arithmetic.
        """
        assert isinstance(
            lhs, BigDecimal
        ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}"

        assert isinstance(
            rhs, BigDecimal
        ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}"

        with localcontext() as ctx:
            ctx.prec = max(
                len(lhs.as_tuple().digits),
                len(rhs.as_tuple().digits),
                precision,
                38,
            ) + 1

            return lhs - rhs

    @staticmethod
    def multiply(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal:
        """
        Returns the product of two decimal values without losing precision due to
        Python's default decimal context.

        The operation is performed in a temporary high-precision context to
        emulate Java BigDecimal arithmetic.
        """
        assert isinstance(
            lhs, BigDecimal
        ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}"

        assert isinstance(
            rhs, BigDecimal
        ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}"

        with localcontext() as ctx:
            ctx.prec = max(
                len(lhs.as_tuple().digits)
                + len(rhs.as_tuple().digits),
                precision,
                38,
            )

            return lhs * rhs

    @staticmethod
    def divide(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal:
        """
        Returns the exact quotient of two decimal values.

        Raises:
            ArithmeticError:
                If the division has a non-terminating decimal expansion,
                matching the behavior of Java BigDecimal.divide(BigDecimal).
        """
        assert isinstance(
            lhs, BigDecimal
        ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}"

        assert isinstance(
            rhs, BigDecimal
        ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}"

        with localcontext() as ctx:
            ctx.prec = max(precision, 38)
            ctx.traps[Inexact] = True

            try:
                return lhs / rhs
            except Inexact:
                raise ArithmeticError(
                    "Non-terminating decimal expansion; "
                    "no exact representable decimal result."
                )

    # ----------------------------------------------------------------------
    # Utility Methods
    # ----------------------------------------------------------------------

    @staticmethod
    def is_compact_precision(precision: int) -> bool:
        """
        Returns whether the specified precision can be stored in compact form.
        """
        return precision <= Decimal.MAX_COMPACT_PRECISION

    @staticmethod
    def extract_decimal_precision_scale(type_str: str) -> Tuple[int, int]:
        """
        Extracts the precision and scale from a DECIMAL/NUMERIC/DEC type string.

        Returns: (precision, scale)

        Examples:
            DECIMAL -> (10, 0)
            DECIMAL(10) -> (10, 0)
            DECIMAL(10,2) -> (10, 2)
            NUMERIC(20,5) -> (20, 5)
            DEC(18,6) -> (18, 6)
        """
        match = _DECIMAL_PATTERN.fullmatch(type_str.strip())
        if match is None:
            raise ValueError(f"Invalid decimal type: {type_str}")

        precision = match.group(2)
        scale = match.group(3)

        if precision is None:
            return 10, 0

        if scale is None:
            return int(precision), 0

        return int(precision), int(scale)

    @staticmethod
    def _get_compact_byte_length(val: int) -> int:
        if val == 0:
            return 1

        if val > 0:
            bits = val.bit_length()
        else:
            bits = (~val).bit_length()

        return bits // 8 + 1
