# CelerisLab/body/objects.py
"""
SimObject -- container wrapping geometry, state, and control.

Design:
    SimObject is a thin container that holds a Geometry instance (shape),
    an ObjectState (position/velocity), and an ObjectControl (control input).
    Concrete geometry logic lives in ``body/geometry/*.py``.

    The ``get_curved_list()`` / ``get_sensor_list()`` methods exist for
    backward compatibility with ``ObjectManager.build_compact_lists()``.
    They delegate to the Geometry's build methods and the SoA packer.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Tuple

import numpy as np

from .geometry.base import Geometry


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
FALLBACK_CLASS_BOUZIDI = np.uint8(0)
FALLBACK_CLASS_HALFWAY_BOUNCEBACK = np.uint8(1)


# ---------------------------------------------------------------------------
# State / Control containers
# ---------------------------------------------------------------------------
@dataclass
class ObjectState:
    """Position, velocity, orientation (z reserved for 3D layouts)."""
    x: float = 0.0
    y: float = 0.0
    z: float = 0.0
    theta: float = 0.0
    vx: float = 0.0
    vy: float = 0.0
    omega: float = 0.0


@dataclass
class ObjectControl:
    """Control input (force / velocity / displacement target)."""
    ax: float = 0.0
    ay: float = 0.0
    alpha: float = 0.0
    mode: int = 0  # 0=force, 1=velocity, 2=displacement


# ---------------------------------------------------------------------------
# SimObject container
# ---------------------------------------------------------------------------
class SimObject:
    """Container for a simulation object: geometry, state, control.

    The Geometry instance handles shape-specific operations (cut-link
    enumeration, flag mask generation).  SimObject wraps it with runtime
    state and an id for bookkeeping.

    The *is_sensor* flag selects the flag mask variant for sensors
    (``FLUID|SENSOR_FLAG``) vs. obstacle bodies (``SOLID|OBSTACLE|BC_CURVED``).
    """

    obj_type: str = "generic"

    def __init__(
        self,
        obj_id: int,
        geometry: Optional[Geometry] = None,
        center: Optional[Tuple[float, ...]] = None,
        radius: float = 0.0,
        is_sensor: bool = False,
        is_force_region: bool = False,
    ) -> None:
        self.obj_id = obj_id
        self.center = center if center is not None else (0.0, 0.0)
        self.radius = radius
        self._geometry = geometry
        self._is_sensor = is_sensor
        self._is_force_region = is_force_region

        c = self.center
        z0 = float(c[2]) if len(c) > 2 else 0.0
        self.state = ObjectState(
            x=float(c[0]), y=float(c[1]), z=z0,
        )
        self.control = ObjectControl()

    @property
    def geometry(self) -> Geometry:
        if self._geometry is None:
            raise RuntimeError(
                f"SimObject id={self.obj_id} has no geometry assigned.")
        return self._geometry

    @geometry.setter
    def geometry(self, g: Geometry) -> None:
        self._geometry = g

    @property
    def is_sensor(self) -> bool:
        return self._is_sensor

    @property
    def is_force_region(self) -> bool:
        return self._is_force_region

    # ------------------------------------------------------------------
    # Methods consumed by ObjectManager.build_compact_lists() / build_flags()
    # ------------------------------------------------------------------
    def get_flag_mask(self, nx: int, ny: int, nz: int = 1) -> np.ndarray:
        """Return (nx*ny,) uint16 array with flag bits set for this object.

        Sensors produce ``FLUID|SENSOR_FLAG``; bodies produce
        ``SOLID|OBSTACLE|BC_CURVED``; force-region objects produce
        ``FLUID|FRC_REGION``.
        """
        if self._is_force_region:
            return self.geometry.build_force_region_flag_mask(nx, ny)
        if self._is_sensor:
            return self.geometry.build_sensor_flag_mask(nx, ny)
        return self.geometry.build_flag_mask(nx, ny)

    def get_curved_list(
        self, nx: int, ny: int, nq: int,
        nz: int = 1, ex=None, ey=None, ez=None,
        domain_flags=None,
    ):
        """Return 8-column SoA arrays for curved-boundary kernel.

        Delegates to the geometry object for cut-link enumeration and
        to ``pack_cut_links_to_soa`` for SoA packing.

        Returns:
            8-tuple matching CurvedLinkSoA.assign_host().
        """
        from .coupling.soa_packer import pack_cut_links_to_soa

        links = self.geometry.build_cut_links(nx, ny, domain_flags=domain_flags)
        for cl in links:
            cl.body_id = self.obj_id
        return pack_cut_links_to_soa(links)

    def get_sensor_list(self, nx: int, ny: int, nz: int = 1):
        """Return compact sensor list (cells, obj_ids).

        Returns:
            cells (uint32), obj_ids (int32).
        """
        if self._is_force_region:
            from .coupling.soa_packer import pack_force_region_to_soa as packer
        else:
            from .coupling.soa_packer import pack_sensor_to_soa as packer

        cells = self.geometry.build_sensor_cells(nx, ny)
        for sc in cells:
            sc.body_id = self.obj_id
        return packer(cells)
