"""
layer.py
--------
Defines ``Layer``.

A ``Layer`` is described by its ``Soil`` and its ``thickness`` alone — the
caller does not need to know or track absolute depth coordinates.  Once layers
are assembled into a ``SoilProfile``, the profile assigns each layer's ``top``
and ``bottom`` depths based on their stacking order.

Modified and improved by Claude Code.
"""

from __future__ import annotations

from typing import Optional

from .soil import Soil


class Layer:
    """A horizontal soil stratum defined by its material and thickness.

    The user supplies only ``soil`` and ``thickness``.  Absolute depth
    coordinates (``top``, ``bottom``) are assigned automatically by
    ``SoilProfile`` when the layer is placed in a profile.  Attempting to read
    ``top`` or ``bottom`` before placement raises ``RuntimeError``.

    :param soil: Material properties for this stratum.
    :type soil: Soil
    :param thickness: Thickness of the layer. Must be > 0.
    :type thickness: float

    Example::

        >>> sand = Soil("Sand", unit_weight=115, unit_weight_sat=120)
        >>> layer = Layer(soil=sand, thickness=5.0)
        >>> layer.thickness
        5.0
        >>> layer.top       # raises until placed in a profile
        RuntimeError: Layer 'Sand' has not been placed in a SoilProfile yet.

    After adding to a profile the coordinates are available::

        >>> fill = Layer(Soil("Fill", unit_weight=115), thickness=10.0)
        >>> profile = SoilProfile([fill, layer])
        >>> fill.top, fill.bottom
        (0.0, 10.0)
        >>> layer.top, layer.bottom
        (10.0, 15.0)
    """

    __slots__ = ("soil", "thickness", "_top", "_bottom")

    def __init__(self, soil: Soil, thickness: float) -> None:
        if not isinstance(soil, Soil):
            raise TypeError(
                f"soil must be a Soil instance, got {type(soil).__name__!r}."
            )
        if thickness <= 0:
            raise ValueError(f"thickness must be > 0, got {thickness!r}.")

        self.soil = soil
        self.thickness = thickness
        self._top: Optional[float] = None
        self._bottom: Optional[float] = None

    def _set_depths(self, top: float) -> None:
        """Assign absolute depth coordinates.  Called only by ``SoilProfile``."""
        self._top = top
        self._bottom = round(top + self.thickness, 10)

    def _check_placed(self) -> None:
        if self._top is None:
            raise RuntimeError(
                f"Layer {self.soil.name!r} has not been placed in a SoilProfile yet."
                "Add it to a SoilProfile to resolve depths."
            )

    @property
    def top(self) -> float:
        """Depth to the top of the layer from the ground surface.

        :raises RuntimeError: If this layer has not yet been placed in a ``SoilProfile``.
        :rtype: float
        """
        self._check_placed()
        return self._top  # type: ignore

    @property
    def bottom(self) -> float:
        """Depth to the bottom of the layer from the ground surface.

        :raises RuntimeError: If this layer has not yet been placed in a ``SoilProfile``.
        :rtype: float
        """
        self._check_placed()
        return self._bottom  # type: ignore

    @property
    def is_placed(self) -> bool:
        """``True`` once depth coordinates have been assigned by a ``SoilProfile``.

        :rtype: bool
        """
        return self._top is not None

    def contains(self, depth: float) -> bool:
        """Return ``True`` if *depth* lies in the half-open interval ``(top, bottom]``.

        The half-open convention means a depth exactly on a layer interface
        unambiguously belongs to the *upper* layer (the one whose bottom
        equals that depth) — no epsilon hacks needed.  The ground surface
        (depth 0) is not covered by this method; :meth:`SoilProfile.layer_at`
        handles that as a special case.

        :param depth: Absolute depth from ground surface.
        :type depth: float
        :rtype: bool
        """
        return self.top < depth <= self.bottom

    def __repr__(self) -> str:
        if self.is_placed:
            return (
                f"Layer({self.soil.name!r}, thickness={self.thickness}, "
                f"top={self._top}, bottom={self._bottom})"
            )
        return f"Layer({self.soil.name!r}, thickness={self.thickness}, [unplaced])"
