"""Implementación de HugeNat basada en int para CPU."""

from __future__ import annotations

from typing import Literal, Optional, Tuple, Union

import numpy as np

_INT_LIKE = Union[int, "HugeNat"]


class HugeNat:
    """Numero natural grande con soporte de indexado y slicing de bits."""

    __slots__ = ("_value", "_fixed_nbits", "_limbs_cache")

    def __init__(
            self,
            value: Union[int, np.ndarray, list, tuple, HugeNat],
            bit_length: Optional[int] = None,
    ) -> None:
        """
        Crea un HugeNat a partir de int o de un array 1D de palabras.

        Si `bit_length` se especifica, fija la anchura lógica del número:
        - si `value` tiene más bits, se trunca por arriba;
        - si tiene menos, se consideran ceros a la izquierda;
        - `bit_length()` devuelve la anchura fijada.
        """
        declared_nbits = self._normalize_declared_nbits(bit_length)

        if isinstance(value, HugeNat):
            raw_value = value._value
            if declared_nbits is None:
                declared_nbits = value._fixed_nbits
        elif isinstance(value, int):
            if value < 0:
                raise ValueError("HugeNat solo acepta int no negativos")
            raw_value = value
        elif isinstance(value, float):
            if not value.is_integer():
                raise ValueError("El float debe ser un entero exacto (sin parte decimal)")
            if value < 0:
                raise ValueError("HugeNat solo acepta valores no negativos")
            if value > 2 ** 53:
                raise ValueError("Float demasiado grande para representación exacta (usa int directamente)")
            raw_value = int(value)
        else:
            limbs = self._normalize_limbs(value)
            raw_value = self._limbs_to_int(limbs)

        self._value = self._apply_declared_nbits(raw_value, declared_nbits)
        self._fixed_nbits = declared_nbits
        self._limbs_cache = None

    @staticmethod
    def _normalize_declared_nbits(bit_length: Optional[int]) -> Optional[int]:
        """Valida y normaliza el bit_length declarado por el usuario."""
        if bit_length is None:
            return None
        if isinstance(bit_length, bool) or not isinstance(bit_length, int):
            raise TypeError("bit_length debe ser int")
        if bit_length < 0:
            raise ValueError("bit_length debe ser no negativo")
        return bit_length

    @staticmethod
    def _apply_declared_nbits(value: int, fixed_nbits: Optional[int]) -> int:
        """Aplica truncado si hay un bit_length fijado."""
        if fixed_nbits is None:
            return value
        if fixed_nbits == 0:
            return 0
        if value.bit_length() <= fixed_nbits:
            return value
        return value & ((1 << fixed_nbits) - 1)

    def _declared_bit_length(self) -> int:
        """Devuelve la anchura lógica: fijada por usuario o bit_length natural."""
        if self._fixed_nbits is not None:
            return self._fixed_nbits
        return self._value.bit_length()

    def __int__(self) -> int:
        """Devuelve el entero subyacente."""
        return self._value

    def __float__(self) -> float:
        """Convierte a float (puede perder precisión para valores muy grandes)."""
        return float(self._value)

    def __index__(self) -> int:
        """Permite usarlo en contextos de indice."""
        return self._value

    def __bool__(self) -> bool:
        """Evalua a False si es cero, True en otro caso."""
        return self._value != 0

    def __len__(self) -> int:
        """Devuelve la anchura lógica del valor."""
        return self._declared_bit_length()

    def __hash__(self) -> int:
        """Hash consistente con int."""
        return hash(self._value)

    def __str__(self) -> str:
        """Representación en cadena igual a int."""
        return str(self._value)

    def __repr__(self) -> str:
        """Representación informativa para depuracion."""
        if self._fixed_nbits is not None:
            return f"HugeNat({self._value}, bit_length={self._fixed_nbits})"
        return f"HugeNat({self._value})"

    @staticmethod
    def _coerce_other(value: _INT_LIKE) -> int:
        """Convierte HugeNat o int a int, o delega si no corresponde."""
        if isinstance(value, HugeNat):
            return value._value
        if isinstance(value, int):
            return value
        return NotImplemented  # type: ignore[return-value]

    @staticmethod
    def _coerce_natural_other(value: _INT_LIKE) -> int:
        """Convierte HugeNat o int a int no negativo, o delega si no corresponde."""
        other_val = HugeNat._coerce_other(value)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        if other_val < 0:
            raise ValueError("HugeNat solo opera con int no negativos")
        return other_val

    def bit_length(self) -> int:
        """Devuelve la anchura lógica en bits del valor."""
        return self._declared_bit_length()

    def bit_count(self) -> int:
        """Cuenta los bits a 1 del valor."""
        return self._value.bit_count()

    def to_bytes(self, length: int, byteorder: Literal["little", "big"], signed: bool = False) -> bytes:
        """Convierte a bytes igual que int.to_bytes."""
        return self._value.to_bytes(length=length, byteorder=byteorder, signed=signed)

    @classmethod
    def from_bytes(
            cls,
            data: bytes,
            byteorder: Literal["little", "big"],
            signed: bool = False,
    ) -> "HugeNat":
        """Crea un HugeNat desde bytes igual que int.from_bytes."""
        value = int.from_bytes(data, byteorder=byteorder, signed=signed)
        return cls(value)

    def __eq__(self, other: object) -> bool:
        """Comparación de igualdad con int o HugeNat."""
        if isinstance(other, HugeNat):
            return self._value == other._value
        if isinstance(other, int):
            return self._value == other
        if isinstance(other, (np.ndarray, list, tuple)):
            try:
                limbs = self._normalize_limbs(other)
            except (ValueError, TypeError):
                return False
            return self._value == self._limbs_to_int(limbs)
        return False

    def __lt__(self, other: _INT_LIKE) -> bool:
        """Comparación menor que con int o HugeNat."""
        other_val = self._coerce_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return self._value < other_val

    def __le__(self, other: _INT_LIKE) -> bool:
        """Comparación menor o igual con int o HugeNat."""
        other_val = self._coerce_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return self._value <= other_val

    def __gt__(self, other: _INT_LIKE) -> bool:
        """Comparación mayor que con int o HugeNat."""
        other_val = self._coerce_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return self._value > other_val

    def __ge__(self, other: _INT_LIKE) -> bool:
        """Comparación mayor o igual con int o HugeNat."""
        other_val = self._coerce_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return self._value >= other_val

    def __add__(self, other: _INT_LIKE) -> "HugeNat":
        """Suma con int o HugeNat."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(self._value + other_val)

    def __radd__(self, other: _INT_LIKE) -> "HugeNat":
        """Suma reflejada."""
        return self.__add__(other)

    def __mul__(self, other: _INT_LIKE) -> "HugeNat":
        """Multiplicación con int o HugeNat."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(self._value * other_val)

    def __rmul__(self, other: _INT_LIKE) -> "HugeNat":
        """Multiplicacion reflejada."""
        return self.__mul__(other)

    def __floordiv__(self, other: _INT_LIKE) -> "HugeNat":
        """Division entera con int o HugeNat."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(self._value // other_val)

    def __rfloordiv__(self, other: _INT_LIKE) -> "HugeNat":
        """Division entera reflejada."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(other_val // self._value)

    def __mod__(self, other: _INT_LIKE) -> "HugeNat":
        """Modulo con int o HugeNat."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(self._value % other_val)

    def __rmod__(self, other: _INT_LIKE) -> "HugeNat":
        """Modulo reflejado."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(other_val % self._value)

    def __pow__(self, exponent: _INT_LIKE, modulo: Optional[_INT_LIKE] = None) -> "HugeNat":
        """Potencia con exponente no negativo y opcional modulo."""
        exp_val = self._coerce_natural_other(exponent)
        if exp_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        if modulo is None:
            return HugeNat(pow(self._value, exp_val))
        return HugeNat(pow(self._value, exp_val, modulo))

    def __rpow__(self, other: _INT_LIKE) -> "HugeNat":
        """Potencia reflejada."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(pow(other_val, self._value))

    def __neg__(self) -> "HugeNat":
        """Negación no permitida: HugeNat solo representa naturales."""
        raise ValueError("HugeNat no permite valores negativos")

    def __abs__(self) -> "HugeNat":
        """Valor absoluto (identidad para naturales)."""
        return HugeNat(self._value, bit_length=self._fixed_nbits)

    def __pos__(self) -> "HugeNat":
        """Operador unario + (identidad para naturales)."""
        return HugeNat(self._value, bit_length=self._fixed_nbits)

    def __divmod__(self, other: _INT_LIKE) -> Tuple["HugeNat", "HugeNat"]:
        """Devuelve (cociente, resto)."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        q, r = divmod(self._value, other_val)
        return HugeNat(q), HugeNat(r)

    def __rdivmod__(self, other: _INT_LIKE) -> Tuple["HugeNat", "HugeNat"]:
        """divmod reflejado."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        q, r = divmod(other_val, self._value)
        return HugeNat(q), HugeNat(r)

    def __sub__(self, other: _INT_LIKE) -> "HugeNat":
        """Resta que no permite resultados negativos."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        result = self._value - other_val
        if result < 0:
            raise ValueError("Resultado negativo no permitido")
        return HugeNat(result)

    def __rsub__(self, other: _INT_LIKE) -> "HugeNat":
        """Resta reflejada que no permite negativos."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        result = other_val - self._value
        if result < 0:
            raise ValueError("Resultado negativo no permitido")
        return HugeNat(result)

    def _resolve_fixed_nbits(self, other) -> Optional[int]:
        """Determina el _fixed_nbits del resultado de una operación bitwise binaria.

        - Ambos con _fixed_nbits iguales → ese ancho.
        - Solo self con _fixed_nbits → ese ancho.
        - Si ambos tienen _fixed_nbits distintos → None (sin ancho fijo).
        - Ninguno → None.
        """
        if self._fixed_nbits is not None:
            if isinstance(other, HugeNat) and other._fixed_nbits is not None:
                if self._fixed_nbits == other._fixed_nbits:
                    return self._fixed_nbits
                return None
            return self._fixed_nbits
        return None

    def __and__(self, other: _INT_LIKE) -> "HugeNat":
        """AND bit a bit."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        nbits = self._resolve_fixed_nbits(other)
        return HugeNat(self._value & other_val, bit_length=nbits)

    def __rand__(self, other: _INT_LIKE) -> "HugeNat":
        """AND reflejado."""
        return self.__and__(other)

    def __or__(self, other: _INT_LIKE) -> "HugeNat":
        """OR bit a bit."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        nbits = self._resolve_fixed_nbits(other)
        return HugeNat(self._value | other_val, bit_length=nbits)

    def __ror__(self, other: _INT_LIKE) -> "HugeNat":
        """OR reflejado."""
        return self.__or__(other)

    def __xor__(self, other: _INT_LIKE) -> "HugeNat":
        """XOR bit a bit."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        nbits = self._resolve_fixed_nbits(other)
        return HugeNat(self._value ^ other_val, bit_length=nbits)

    def __rxor__(self, other: _INT_LIKE) -> "HugeNat":
        """XOR reflejado."""
        return self.__xor__(other)

    def __lshift__(self, shift: int) -> "HugeNat":
        """Desplazamiento a la izquierda."""
        if isinstance(shift, bool):
            raise TypeError("shift debe ser int, no bool")
        if not isinstance(shift, int):
            return NotImplemented  # type: ignore[return-value]
        if shift < 0:
            raise ValueError("Desplazamiento negativo no permitido")
        return HugeNat(self._value << shift, bit_length=self._fixed_nbits)

    def __rlshift__(self, other: _INT_LIKE) -> "HugeNat":
        """Desplazamiento reflejado."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(other_val << self._value)

    def __rshift__(self, shift: int) -> "HugeNat":
        """Desplazamiento a la derecha."""
        if isinstance(shift, bool):
            raise TypeError("shift debe ser int, no bool")
        if not isinstance(shift, int):
            return NotImplemented  # type: ignore[return-value]
        if shift < 0:
            raise ValueError("Desplazamiento negativo no permitido")
        return HugeNat(self._value >> shift, bit_length=self._fixed_nbits)

    def __rrshift__(self, other: _INT_LIKE) -> "HugeNat":
        """Desplazamiento reflejado a la derecha."""
        other_val = self._coerce_natural_other(other)
        if other_val is NotImplemented:
            return NotImplemented  # type: ignore[return-value]
        return HugeNat(other_val >> self._value)

    def __invert__(self) -> "HugeNat":
        """NOT bit a bit dentro del ancho lógico del valor.

        Voltea todos los bits en el rango [0, bit_length()-1].
        El resultado tiene el mismo ancho lógico (bit_length fijado).

        Para HugeNat(0) sin ancho declarado (bit_length=0) devuelve HugeNat(0, bit_length=0).
        """
        width = self._declared_bit_length()
        if width == 0:
            return HugeNat(0, bit_length=0)
        mask = (1 << width) - 1
        return HugeNat(self._value ^ mask, bit_length=width)

    def rotl(self, shift: int) -> "HugeNat":
        """Rota a la izquierda usando el ancho lógico de bits."""
        if isinstance(shift, bool) or not isinstance(shift, int):
            raise TypeError("shift debe ser int")
        width = self.bit_length()
        if width == 0:
            return HugeNat(0, bit_length=0)
        shift %= width
        mask = (1 << width) - 1
        v = self._value & mask
        return HugeNat(((v << shift) | (v >> (width - shift))) & mask, bit_length=width)

    def rotr(self, shift: int) -> "HugeNat":
        """Rota a la derecha usando el ancho lógico de bits."""
        if isinstance(shift, bool) or not isinstance(shift, int):
            raise TypeError("shift debe ser int")
        width = self.bit_length()
        if width == 0:
            return HugeNat(0, bit_length=0)
        shift %= width
        mask = (1 << width) - 1
        v = self._value & mask
        return HugeNat(((v >> shift) | (v << (width - shift))) & mask, bit_length=width)

    def __getitem__(self, key: Union[int, slice]) -> Union[int, "HugeNat"]:
        """Acceso por indice o slicing de bits."""
        if isinstance(key, int):
            return self._get_bit(key)
        if isinstance(key, slice):
            return self._get_slice(key)
        raise TypeError("Indice no valido")

    def append(self, *others: "HugeNat") -> "HugeNat":
        """
        Concatena bits de uno o más HugeNat sobre los de este.

        El resultado contiene los bits de `self` en las posiciones bajas,
        seguidos de los bits de cada argumento en orden.  La anchura lógica
        de cada operando (incluidos ceros altos declarados) se respeta.

        Devuelve un nuevo HugeNat con bit_length igual a la suma de los
        anchos de todos los operandos.
        """
        if not others:
            raise TypeError("append requiere al menos un argumento")

        result_value = self._value
        offset = self.bit_length()
        total_bits = offset

        for other in others:
            if not isinstance(other, HugeNat):
                raise TypeError("append solo acepta HugeNat (se necesita bit_length explícito)")
            result_value |= other._value << offset
            other_bits = other.bit_length()
            offset += other_bits
            total_bits += other_bits

        return HugeNat(result_value, bit_length=total_bits)

    def split(self, n: int) -> Tuple["HugeNat", ...]:
        """
        Divide el número en `n` trozos de igual cantidad de bits.

        Cada trozo se devuelve como `HugeNat` con `bit_length` fijado al ancho
        del trozo. El orden de salida es de menor a mayor peso (LSB -> MSB).
        """
        if isinstance(n, bool) or not isinstance(n, int):
            raise TypeError("n debe ser int")
        if n < 1:
            raise ValueError("n debe ser mayor o igual que 1")

        total_bits = self.bit_length()
        if total_bits % n != 0:
            raise ValueError("La cantidad de bits debe ser divisible por n")

        chunk_bits = total_bits // n
        if chunk_bits == 0:
            return tuple(HugeNat(0, bit_length=0) for _ in range(n))

        mask = (1 << chunk_bits) - 1
        return tuple(
            HugeNat((self._value >> (chunk_bits * i)) & mask, bit_length=chunk_bits)
            for i in range(n)
        )

    def split_core(self, parts: int, word_order: str = "le") -> np.ndarray:
        """Divide en ``parts`` trozos iguales y devuelve array 2D de uint64.

        Devuelve array de forma ``(parts, words_per_part)`` donde fila 0
        corresponde al chunk LSB del valor original.

        Parameters
        ----------
        parts : int
            Número de trozos. ``bit_length()`` debe ser divisible por ``parts``.
        word_order : str
            ``"le"`` (default) o ``"be"`` para cada fila.
        """
        if isinstance(parts, bool) or not isinstance(parts, int):
            raise TypeError("parts debe ser int")
        if parts < 1:
            raise ValueError("parts debe ser mayor o igual que 1")
        if word_order not in ("le", "be"):
            raise ValueError("word_order debe ser 'le' o 'be'")

        total_bits = self.bit_length()
        if total_bits % parts != 0:
            raise ValueError("La cantidad de bits debe ser divisible por parts")

        chunk_bits = total_bits // parts
        if chunk_bits == 0:
            return np.empty((parts, 0), dtype=np.uint64)

        words_per_part = (chunk_bits + 63) >> 6
        cached = self._ensure_limbs_cache()

        out = np.zeros((parts, words_per_part), dtype=np.uint64)
        for p in range(parts):
            start_bit = p * chunk_bits
            word0 = start_bit >> 6
            shift = start_bit & 63
            if shift == 0:
                for w in range(words_per_part):
                    s = word0 + w
                    if s < cached.size:
                        out[p, w] = cached[s]
            else:
                for w in range(words_per_part):
                    s0 = word0 + w
                    s1 = s0 + 1
                    lo = int(cached[s0]) if s0 < cached.size else 0
                    hi = int(cached[s1]) if s1 < cached.size else 0
                    out[p, w] = np.uint64(((lo >> shift) | (hi << (64 - shift))) & 0xFFFFFFFFFFFFFFFF)
            # Mask last word
            rem = chunk_bits & 63
            if rem != 0:
                out[p, -1] &= np.uint64((1 << rem) - 1)

        if word_order == "be":
            out = out[:, ::-1].copy()
        return out

    def split_array(self, parts: int, word_order: str = "be") -> np.ndarray:
        """Divide en ``parts`` trozos iguales y devuelve array 2D de uint64.

        Idéntico a ``split_core`` pero con default ``word_order="be"``.

        Parameters
        ----------
        parts : int
            Número de trozos. ``bit_length()`` debe ser divisible por ``parts``.
        word_order : str
            ``"be"`` (default) o ``"le"`` para cada fila.
        """
        return self.split_core(parts, word_order=word_order)

    def split_u64(self, parts: int) -> np.ndarray:
        """Divide en ``parts`` trozos de <= 64 bits cada uno.

        Devuelve array 1D ``(parts,)`` de ``np.uint64``.
        """
        if isinstance(parts, bool) or not isinstance(parts, int):
            raise TypeError("parts debe ser int")
        if parts < 1:
            raise ValueError("parts debe ser mayor o igual que 1")

        total_bits = self.bit_length()
        if total_bits % parts != 0:
            raise ValueError("La cantidad de bits debe ser divisible por parts")

        chunk_bits = total_bits // parts
        if chunk_bits > 64:
            raise ValueError("chunk_bits debe ser <= 64 para split_u64")
        if chunk_bits == 0:
            return np.zeros(parts, dtype=np.uint64)

        mask = (1 << chunk_bits) - 1
        v = self._value
        return np.array(
            [(v >> (chunk_bits * i)) & mask for i in range(parts)],
            dtype=np.uint64,
        )

    def extract_bits_core(self, start: int, width: int, word_order: str = "le") -> np.ndarray:
        """Extrae ``width`` bits desde la posición ``start`` como array 1D de uint64.

        Parameters
        ----------
        start : int
            Posición del bit menos significativo del rango a extraer (LSB=0).
        width : int
            Número de bits a extraer. Si 0, devuelve array vacío.
        word_order : str
            ``"le"`` (default) o ``"be"``.
        """
        if width < 0:
            raise ValueError("width debe ser no negativo")
        if start < 0:
            raise ValueError("start debe ser no negativo")
        if word_order not in ("le", "be"):
            raise ValueError("word_order debe ser 'le' o 'be'")
        if width == 0:
            return np.empty(0, dtype=np.uint64)

        n_words = (width + 63) >> 6
        cached = self._ensure_limbs_cache()

        out = np.zeros(n_words, dtype=np.uint64)
        word0 = start >> 6
        shift = start & 63

        if shift == 0:
            for w in range(n_words):
                s = word0 + w
                if s < cached.size:
                    out[w] = cached[s]
        else:
            for w in range(n_words):
                s0 = word0 + w
                s1 = s0 + 1
                lo = int(cached[s0]) if s0 < cached.size else 0
                hi = int(cached[s1]) if s1 < cached.size else 0
                out[w] = np.uint64(((lo >> shift) | (hi << (64 - shift))) & 0xFFFFFFFFFFFFFFFF)

        rem = width & 63
        if rem != 0:
            out[-1] &= np.uint64((1 << rem) - 1)

        if word_order == "be":
            return out[::-1].copy()
        return out

    def extract_bits_u64(self, start: int, width: int) -> np.uint64:
        """Extrae ``width`` bits (máx 64) desde ``start`` como un solo ``np.uint64``."""
        if width < 0:
            raise ValueError("width debe ser no negativo")
        if width > 64:
            raise ValueError("width debe ser <= 64 para extract_bits_u64")
        if start < 0:
            raise ValueError("start debe ser no negativo")
        if width == 0:
            return np.uint64(0)
        mask = (1 << width) - 1
        return np.uint64((self._value >> start) & mask)

    def _get_bit(self, i: int) -> int:
        """Obtiene un bit por indice con LSB=0, devolviendo 0 fuera de rango."""
        nbits = self.bit_length()
        if i < 0:
            i += nbits
        if i < 0 or i >= nbits:
            return 0
        return (self._value >> i) & 1

    def _get_slice(self, sl: slice) -> "HugeNat":
        """Obtiene un slice de bits con semantica Python."""
        if sl.step == 0:
            raise ValueError("step no puede ser 0")
        if sl.step is None or sl.step == 1:
            return self._slice_fast(sl.start, sl.stop)
        return self._slice_slow(sl)

    def _slice_fast(self, start: Optional[int], stop: Optional[int]) -> "HugeNat":
        """Slice rapido para step None o 1, con relleno implícito de ceros superiores."""
        nbits = self.bit_length()
        if start is None:
            start = 0
        if stop is None:
            stop = nbits
        if start < 0:
            start += nbits
        if stop < 0:
            stop += nbits
        start = max(0, start)
        stop = max(0, stop)
        if start >= stop:
            return HugeNat(0)
        if start >= nbits:
            return HugeNat(0)

        width = stop - start
        shifted = self._value >> start
        if width >= shifted.bit_length():
            return HugeNat(shifted)

        mask = (1 << width) - 1
        return HugeNat(shifted & mask)

    def _slice_slow(self, sl: slice) -> "HugeNat":
        """Slice general con step arbitrario y repack LSB-first."""
        nbits = self.bit_length()
        step = sl.step if sl.step is not None else 1

        if step > 0:
            start, stop = self._normalize_slice_bounds_positive(nbits, sl.start, sl.stop)
        else:
            start, stop = self._normalize_slice_bounds_negative(nbits, sl.start, sl.stop)

        out = 0
        for j, i in enumerate(range(start, stop, step)):
            if i < nbits:
                out |= ((self._value >> i) & 1) << j
        return HugeNat(out)

    @staticmethod
    def _normalize_slice_bounds_positive(
            nbits: int,
            start: Optional[int],
            stop: Optional[int],
    ) -> Tuple[int, int]:
        """Normaliza límites de slice para `step > 0`."""
        start_i = 0 if start is None else start
        stop_i = nbits if stop is None else stop

        if start_i < 0:
            start_i += nbits
        if stop_i < 0:
            stop_i += nbits

        return max(0, start_i), max(0, stop_i)

    @staticmethod
    def _normalize_slice_bounds_negative(
            nbits: int,
            start: Optional[int],
            stop: Optional[int],
    ) -> Tuple[int, int]:
        """Normaliza límites de slice para `step < 0`."""
        if start is None:
            start_i = nbits - 1
        else:
            start_i = start
            if start_i < 0:
                start_i += nbits
            if start_i < 0:
                start_i = -1

        if stop is None:
            stop_i = -1
        else:
            stop_i = stop
            if stop_i < 0:
                stop_i += nbits
            if stop_i < 0:
                stop_i = -1

        return start_i, stop_i

    def bits(self, order: str = "msb->lsb", length: Optional[int] = None) -> np.ndarray:
        """Devuelve una vista 1D de bits como uint8."""
        if length is not None and length < 0:
            raise ValueError("length debe ser no negativo")
        nbits = self.bit_length()
        if nbits == 0 and length is None:
            return np.empty(0, dtype=np.uint8)

        if order not in ("msb->lsb", "lsb->msb"):
            raise ValueError("order no valido")

        # Ruta rápida: to_bytes + unpackbits (bucle en C)
        if nbits == 0:
            arr_msb = np.empty(0, dtype=np.uint8)
        else:
            n_bytes = (nbits + 7) // 8
            raw = self._value.to_bytes(n_bytes, byteorder="big")
            arr_msb = np.unpackbits(np.frombuffer(raw, dtype=np.uint8))
            # Recortar padding de alineamiento de byte (bits extra a la izquierda)
            arr_msb = arr_msb[len(arr_msb) - nbits:]

        if order == "lsb->msb":
            arr = arr_msb[::-1].copy()
        else:
            arr = arr_msb

        if length is not None:
            cur = len(arr)
            if length > cur:
                pad = np.zeros(length - cur, dtype=np.uint8)
                if order == "msb->lsb":
                    arr = np.concatenate((pad, arr))
                else:
                    arr = np.concatenate((arr, pad))
            elif length < cur:
                if order == "msb->lsb":
                    arr = arr[cur - length:]
                else:
                    arr = arr[:length]

        return np.ascontiguousarray(arr, dtype=np.uint8)

    def bits_str(
            self,
            order: str = "msb->lsb",
            group: int = 64,
            sep: str = " ",
    ) -> str:
        """Devuelve una representacion en string con bits agrupados."""
        if group <= 0:
            raise ValueError("group debe ser mayor que 0")
        if order not in ("msb->lsb", "lsb->msb"):
            raise ValueError("order no valido")

        nbits = self.bit_length()
        if nbits == 0:
            return "0"

        arr = self.bits(order=order)
        chars = "".join(str(b) for b in arr)
        groups = [chars[i:i + group] for i in range(0, len(chars), group)]
        return sep.join(groups)

    def _ensure_limbs_cache(self) -> np.ndarray:
        """Calcula y cachea los limbs LE si aún no existen."""
        if self._limbs_cache is not None:
            return self._limbs_cache
        nbits = self.bit_length()
        if nbits == 0:
            arr = np.empty(0, dtype=np.uint64)
        else:
            n_words = (nbits + 63) >> 6
            n_bytes = n_words * 8
            raw = self._value.to_bytes(n_bytes, byteorder="little")
            arr = np.frombuffer(raw, dtype=np.uint64).copy()
        arr.flags.writeable = False
        object.__setattr__(self, "_limbs_cache", arr)
        return arr

    def to_core(self, copy: bool = True, word_order: str = "le") -> Tuple[np.ndarray, np.int64]:
        """Devuelve (limbs, nbits) en formato compatible con Numba.

        Parameters
        ----------
        copy : bool
            Si True (default), devuelve una copia. Si False, devuelve vista
            read-only de la cache interna.
        word_order : str
            ``"le"`` (default) little-endian, ``"be"`` big-endian (MSB primero).
        """
        if word_order not in ("le", "be"):
            raise ValueError("word_order debe ser 'le' o 'be'")
        cached = self._ensure_limbs_cache()
        nbits = self.bit_length()
        if word_order == "be":
            limbs = cached[::-1].copy()
            if not copy:
                limbs.flags.writeable = False
            return limbs, np.int64(nbits)
        if copy:
            return cached.copy(), np.int64(nbits)
        return cached, np.int64(nbits)

    @classmethod
    def from_core(cls, limbs: np.ndarray, nbits: Union[np.integer, int],
                  word_order: str = "le") -> "HugeNat":
        """Construye un HugeNat desde (limbs, nbits) con anchura lógica explícita.

        Parameters
        ----------
        word_order : str
            ``"le"`` (default) limbs en little-endian, ``"be"`` en big-endian.
        """
        if word_order not in ("le", "be"):
            raise ValueError("word_order debe ser 'le' o 'be'")
        if not isinstance(limbs, np.ndarray):
            raise TypeError("limbs debe ser np.ndarray")
        if limbs.ndim != 1:
            raise ValueError("limbs debe ser 1D")
        if limbs.dtype != np.uint64:
            raise ValueError("limbs debe tener dtype np.uint64")
        if not limbs.flags["C_CONTIGUOUS"]:
            raise ValueError("limbs debe ser contiguo en memoria")
        if isinstance(nbits, bool) or not isinstance(nbits, (int, np.integer)):
            raise TypeError("nbits debe ser int")
        if word_order == "be":
            limbs = np.ascontiguousarray(limbs[::-1])

        nbits_int = int(nbits)
        if nbits_int < 0:
            raise ValueError("nbits debe ser no negativo")
        if nbits_int == 0:
            if limbs.size != 0:
                raise ValueError("limbs debe ser vacio cuando nbits es 0")
            return cls(0, bit_length=0)

        expected_words = (nbits_int + 63) >> 6
        if limbs.size != expected_words:
            raise ValueError("limbs y nbits no son consistentes")

        last_bits = (nbits_int - 1) & 63
        last_mask = (1 << (last_bits + 1)) - 1
        if (int(limbs[-1]) & ~last_mask) != 0:
            raise ValueError("El limb mas significativo contiene bits fuera de nbits")

        value = int.from_bytes(limbs.tobytes(), byteorder="little")

        return cls(value, bit_length=nbits_int)

    def to_words_be(self) -> np.ndarray:
        """Devuelve uint64[] con MSW primero (big-endian word order)."""
        return self._ensure_limbs_cache()[::-1].copy()

    @classmethod
    def from_words_be(cls, words_be: np.ndarray, bit_length: int) -> "HugeNat":
        """Construye un HugeNat desde uint64[] BE + bit_length obligatorio.

        Parameters
        ----------
        words_be : np.ndarray
            Array 1D de uint64 en big-endian (MSW primero).
        bit_length : int
            Anchura lógica en bits.
        """
        if not isinstance(words_be, np.ndarray):
            raise TypeError("words_be debe ser np.ndarray")
        if words_be.ndim != 1:
            raise ValueError("words_be debe ser 1D")
        if words_be.dtype != np.uint64:
            raise ValueError("words_be debe tener dtype np.uint64")
        limbs_le = np.ascontiguousarray(words_be[::-1])
        return cls.from_core(limbs_le, bit_length, word_order="le")

    @staticmethod
    def bitwise_or(a_be: np.ndarray, b_be: np.ndarray, bit_length: int) -> np.ndarray:
        """OR element-wise de dos arrays BE uint64. Devuelve array BE.

        Parameters
        ----------
        a_be, b_be : np.ndarray[uint64]
            Arrays 1D de uint64 en big-endian (MSW primero).
        bit_length : int
            Anchura lógica del resultado en bits.
        """
        from hugenat.numba_core import bitwise_or_core as _or_core
        a_le = np.ascontiguousarray(a_be[::-1])
        b_le = np.ascontiguousarray(b_be[::-1])
        out_le, _ = _or_core(a_le, b_le)
        # Pad/trim to expected word count
        n_words = (bit_length + 63) >> 6
        result = np.zeros(n_words, dtype=np.uint64)
        copy_len = min(out_le.size, n_words)
        result[:copy_len] = out_le[:copy_len]
        # Mask last word
        rem = bit_length & 63
        if rem != 0 and n_words > 0:
            result[-1] &= np.uint64((1 << rem) - 1)
        return result[::-1].copy()

    @staticmethod
    def shift_left(a_be: np.ndarray, k: int, bit_length: int) -> np.ndarray:
        """Shift left por k bits sobre array BE uint64. Devuelve array BE.

        Parameters
        ----------
        a_be : np.ndarray[uint64]
            Array 1D de uint64 en big-endian (MSW primero).
        k : int
            Cantidad de bits a desplazar a la izquierda.
        bit_length : int
            Anchura lógica del resultado en bits (se trunca al ancho dado).
        """
        from hugenat.numba_core import shift_left_core as _shl_core
        a_le = np.ascontiguousarray(a_be[::-1])
        out_le, _ = _shl_core(a_le, k)
        # Pad/trim to expected word count
        n_words = (bit_length + 63) >> 6
        result = np.zeros(n_words, dtype=np.uint64)
        copy_len = min(out_le.size, n_words)
        result[:copy_len] = out_le[:copy_len]
        # Mask last word
        rem = bit_length & 63
        if rem != 0 and n_words > 0:
            result[-1] &= np.uint64((1 << rem) - 1)
        return result[::-1].copy()

    @staticmethod
    def extract_bits(a_be: np.ndarray, start: int, width: int) -> np.ndarray:
        """Extrae ``width`` bits desde posición ``start`` de array BE. Devuelve BE.

        Parameters
        ----------
        a_be : np.ndarray[uint64]
            Array 1D de uint64 en big-endian (MSW primero).
        start : int
            Posición del bit menos significativo del rango a extraer (LSB=0).
        width : int
            Número de bits a extraer.
        """
        from hugenat.numba_core import extract_range_core as _ext_core
        a_le = np.ascontiguousarray(a_be[::-1])
        nbits = a_be.size * 64
        out_le, _ = _ext_core(a_le, nbits, start, width)
        # Pad to expected word count for the width
        n_words = (width + 63) >> 6
        result = np.zeros(n_words, dtype=np.uint64)
        copy_len = min(out_le.size, n_words)
        result[:copy_len] = out_le[:copy_len]
        return result[::-1].copy()

    @staticmethod
    def concat_array(parts_2d: np.ndarray, part_bits: int,
                     word_order: str = "be") -> np.ndarray:
        """Concatena partes 2D de vuelta en un array 1D. Inversa de ``split_array``.

        Parameters
        ----------
        parts_2d : np.ndarray[uint64]
            Array 2D de forma ``(n_parts, words_per_part)``.
            Fila 0 = chunk LSB del valor original.
        part_bits : int
            Bits por cada parte.
        word_order : str
            ``"be"`` (default) o ``"le"`` — orden de las palabras en cada fila
            y en el resultado.
        """
        if word_order not in ("le", "be"):
            raise ValueError("word_order debe ser 'le' o 'be'")
        if not isinstance(parts_2d, np.ndarray):
            raise TypeError("parts_2d debe ser np.ndarray")
        if parts_2d.ndim != 2:
            raise ValueError("parts_2d debe ser 2D")
        if parts_2d.dtype != np.uint64:
            raise ValueError("parts_2d debe tener dtype np.uint64")

        from hugenat.numba_core import concat_parts_core as _concat_core

        if word_order == "be":
            # Revertir cada fila a LE para el kernel
            parts_le = parts_2d[:, ::-1].copy()
        else:
            parts_le = np.ascontiguousarray(parts_2d)

        out_le, out_nbits = _concat_core(parts_le, part_bits)

        # Pad to total expected words
        total_bits = parts_2d.shape[0] * part_bits
        n_words = (total_bits + 63) >> 6
        result = np.zeros(n_words, dtype=np.uint64)
        copy_len = min(out_le.size, n_words)
        result[:copy_len] = out_le[:copy_len]

        if word_order == "be":
            return result[::-1].copy()
        return result

    @staticmethod
    def _normalize_limbs(value) -> np.ndarray:
        """
        Normaliza limbs a np.uint64 1D little-endian y recorta ceros líderes (MS).
        Acepta floats solo si son enteros exactos y no negativos.
        """
        if isinstance(value, (list, tuple)):
            arr = np.asarray(value, dtype=object)
        else:
            arr = np.asarray(value)

        if arr.ndim != 1:
            raise ValueError("El array de limbs debe ser 1D")

        if arr.size == 0:
            return np.empty(0, dtype=np.uint64)

        # Floats: aceptar solo si son enteros exactos, no negativos y precisos (<= 2**53)
        if arr.dtype.kind == "f":
            if not np.all(np.isfinite(arr)):
                raise ValueError("El array debe contener valores finitos")
            if not np.all(arr == np.floor(arr)):
                raise ValueError("El array debe contener enteros")
            if np.any(arr < 0):
                raise ValueError("El array debe contener enteros no negativos")
            if np.any(arr > 2**53):
                raise ValueError("Floats demasiado grandes para representacion exacta")
            arr_u64 = arr.astype(np.uint64, copy=False)
            return HugeNat._trim_high_zero_limbs(arr_u64)

        # Object: validar elemento a elemento (exactitud)
        if arr.dtype == object:
            out = np.empty(arr.size, dtype=np.uint64)
            for i, v in enumerate(arr):
                if not isinstance(v, (int, np.integer, float, np.floating)):
                    raise ValueError("El array debe contener enteros")
                if isinstance(v, (float, np.floating)) and v != int(v):
                    raise ValueError("El array debe contener enteros")
                if isinstance(v, (float, np.floating)) and v > 2**53:
                    raise ValueError("Floats demasiado grandes para representacion exacta")
                iv = int(v)
                if iv < 0:
                    raise ValueError("El array debe contener enteros no negativos")
                if iv > 0xFFFFFFFFFFFFFFFF:
                    raise ValueError("Cada limb debe caber en 64 bits")
                out[i] = np.uint64(iv)
            arr_u64 = out
        else:
            # Enteros (signed/unsigned)
            if arr.dtype.kind not in ("i", "u"):
                raise ValueError("El array debe contener enteros")
            if arr.dtype.kind == "i" and np.any(arr < 0):
                raise ValueError("El array debe contener enteros no negativos")
            # Cast seguro (no oculta signo; ya validado)
            arr_u64 = arr.astype(np.uint64, copy=False)

        return HugeNat._trim_high_zero_limbs(arr_u64)

    @staticmethod
    def _trim_high_zero_limbs(arr_u64: np.ndarray) -> np.ndarray:
        """Recorta limbs de mayor peso en cero y asegura contigüidad."""
        j = arr_u64.size
        while j > 0 and arr_u64[j - 1] == 0:
            j -= 1
        if j == 0:
            return np.empty(0, dtype=np.uint64)
        return np.ascontiguousarray(arr_u64[:j])

    @staticmethod
    def _limbs_to_int(limbs: np.ndarray) -> int:
        """Reconstruye el entero a partir de limbs little-endian."""
        value = 0
        for i, limb in enumerate(limbs):
            value |= int(limb) << (64 * i)
        return value


__all__ = ["HugeNat"]
