"""
Phonological feature data model for sign language linguistics analysis.

This module provides data structures and functions for representing and analyzing
the phonological structure of signs based on the HLMO (Handshape, Location,
Movement, Orientation) parameter model, which is the standard framework in
sign language linguistics.

Key features:
- PhonologicalForm: Represents the phonological structure of a sign
- LinguisticSegment: Extends Segment with linguistic annotations
- Standard inventories for BSL/ASL compatible handshapes, locations, etc.
- Phonological distance calculations for comparing signs
- Minimal pair detection for linguistic research
- Inventory analysis for corpus linguistics

Example:
    >>> from sltk.linguistics import (
    ...     PhonologicalForm, Handshape, Location, Movement,
    ...     PalmOrientation, FingerOrientation
    ... )
    >>> sign_hello = PhonologicalForm(
    ...     handshape_dom=Handshape.B_FLAT,
    ...     location=Location.FOREHEAD,
    ...     movement=Movement.LINEAR_AWAY,
    ...     orientation_palm=PalmOrientation.PALM_OUT,
    ...     orientation_fingers=FingerOrientation.FINGERS_UP,
    ... )
    >>> sign_fine = PhonologicalForm(
    ...     handshape_dom=Handshape.FIVE,
    ...     location=Location.CHEST,
    ...     movement=Movement.LINEAR_AWAY,
    ...     orientation_palm=PalmOrientation.PALM_OUT,
    ...     orientation_fingers=FingerOrientation.FINGERS_UP,
    ... )
    >>> distance = phonological_distance(sign_hello, sign_fine)
    >>> print(distance)  # Returns 2 (handshape and location differ)
    2

References:
    - Stokoe, W. C. (1960). Sign Language Structure.
    - Battison, R. (1978). Lexical Borrowing in American Sign Language.
    - Brentari, D. (1998). A Prosodic Model of Sign Language Phonology.
"""

from __future__ import annotations

import logging
from collections import Counter
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

logger = logging.getLogger(__name__)


# =============================================================================
# Standard Phonological Inventories
# =============================================================================


class Handshape(str, Enum):
    """
    Standard handshape inventory compatible with BSL and ASL.

    This inventory covers the major distinctive handshapes used across
    sign languages. Each handshape is given a descriptive name based on
    its form or the ASL letter/number it represents.

    The naming convention uses underscores to separate descriptors.
    For example, B_FLAT is a B handshape with flat (extended) fingers.
    """

    # Closed/compact handshapes
    A = "A"  # Fist with thumb alongside
    S = "S"  # Fist with thumb over fingers
    T = "T"  # Fist with thumb between index and middle

    # Open handshapes
    B_FLAT = "B_flat"  # Flat hand, fingers together
    B_BENT = "B_bent"  # Flat hand with bent fingers at knuckles
    FIVE = "5"  # Open hand, fingers spread
    FIVE_CLAW = "5_claw"  # Open hand with curved/clawed fingers
    C = "C"  # Curved hand forming C shape
    O = "O"  # Fingertips and thumb touching in circle

    # Index finger handshapes
    ONE = "1"  # Index finger extended (pointing)
    X = "X"  # Index finger bent at middle joint (hook)
    G = "G"  # Index finger and thumb pointing (like holding small object)
    L = "L"  # Index finger and thumb extended at 90 degrees

    # Multiple finger handshapes
    V = "V"  # Index and middle fingers extended and spread
    U = "U"  # Index and middle fingers extended and together
    H = "H"  # Index and middle fingers extended pointing sideways
    K = "K"  # Index and middle extended, thumb between them
    R = "R"  # Index and middle crossed
    W = "W"  # Index, middle, ring extended and spread
    THREE = "3"  # Thumb, index, middle extended
    FOUR = "4"  # Four fingers extended (not thumb)

    # Thumb handshapes
    THUMBS_UP = "thumbs_up"  # A handshape with thumb extended up
    Y = "Y"  # Thumb and pinky extended

    # Pinky handshapes
    I = "I"  # Pinky extended only
    ILY = "ILY"  # I-love-you: thumb, index, pinky extended

    # Specialized handshapes
    D = "D"  # Index pointing up, other fingers curved to thumb
    E = "E"  # Fingers bent at knuckles touching thumb
    F = "F"  # Index and thumb touching in circle, others extended
    M = "M"  # Three fingers over thumb
    N = "N"  # Two fingers over thumb
    P = "P"  # K handshape pointing down
    Q = "Q"  # G handshape pointing down

    # Numeral handshapes (where different from letters)
    SIX = "6"  # Thumb and pinky touch, others extended
    SEVEN = "7"  # Thumb and ring finger touch
    EIGHT = "8"  # Thumb and middle finger touch
    NINE = "9"  # Thumb and index finger touch, others extended

    # Generic/other
    FLAT_O = "flat_O"  # Flattened O, fingers and thumb touching flat
    CLAW = "claw"  # All fingers curved
    BABY_O = "baby_O"  # Small O with just index and thumb

    # Unknown or unspecified
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> Handshape:
        """
        Convert a string to a Handshape enum value.

        Args:
            value: String representation of handshape

        Returns:
            Handshape enum value

        Raises:
            ValueError: If the value is not a valid handshape

        Example:
            >>> Handshape.from_string("B_flat")
            <Handshape.B_FLAT: 'B_flat'>
        """
        # Try direct match first
        for hs in cls:
            if hs.value == value or hs.name == value.upper():
                return hs
        # Try case-insensitive match
        value_lower = value.lower()
        for hs in cls:
            if hs.value.lower() == value_lower:
                return hs
        raise ValueError(f"Unknown handshape: {value}")


class Location(str, Enum):
    """
    Place of articulation inventory for sign production.

    Locations are organized by body region, from head to neutral space.
    This follows the standard phonological description hierarchy used
    in sign language linguistics.
    """

    # Head region
    HEAD_TOP = "head_top"
    FOREHEAD = "forehead"
    TEMPLE = "temple"
    EYE = "eye"
    NOSE = "nose"
    CHEEK = "cheek"
    EAR = "ear"
    MOUTH = "mouth"
    LIPS = "lips"
    CHIN = "chin"
    JAW = "jaw"

    # Neck and torso
    NECK = "neck"
    THROAT = "throat"
    CHEST = "chest"
    SHOULDER = "shoulder"
    STOMACH = "stomach"
    WAIST = "waist"

    # Arm locations (on non-dominant side typically)
    UPPER_ARM = "upper_arm"
    ELBOW = "elbow"
    FOREARM = "forearm"
    WRIST = "wrist"

    # Hand locations (contact points on non-dominant hand)
    PALM_NONDOM = "palm_nondom"
    BACK_HAND_NONDOM = "back_hand_nondom"
    FINGERS_NONDOM = "fingers_nondom"
    FINGERTIPS_NONDOM = "fingertips_nondom"
    THUMB_NONDOM = "thumb_nondom"

    # Neutral space (signing space in front of body)
    NEUTRAL_SPACE = "neutral_space"
    NEUTRAL_HIGH = "neutral_high"  # High neutral space
    NEUTRAL_LOW = "neutral_low"  # Low neutral space
    NEUTRAL_IPSILATERAL = "neutral_ipsilateral"  # Same side as dominant hand
    NEUTRAL_CONTRALATERAL = "neutral_contralateral"  # Opposite side

    # Other
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> Location:
        """Convert a string to a Location enum value."""
        for loc in cls:
            if loc.value == value or loc.name == value.upper():
                return loc
        value_lower = value.lower()
        for loc in cls:
            if loc.value.lower() == value_lower:
                return loc
        raise ValueError(f"Unknown location: {value}")


class Movement(str, Enum):
    """
    Movement types in sign production.

    Movements are categorized into:
    - Path movements: Overall trajectory of the hand
    - Internal movements: Movements within the hand
    - Manner modifications: How the movement is executed
    """

    # Path movements (direction)
    LINEAR_UP = "linear_up"
    LINEAR_DOWN = "linear_down"
    LINEAR_LEFT = "linear_left"
    LINEAR_RIGHT = "linear_right"
    LINEAR_FORWARD = "linear_forward"  # Away from body
    LINEAR_AWAY = "linear_away"  # Alias for forward
    LINEAR_BACK = "linear_back"  # Toward body
    LINEAR_TOWARD = "linear_toward"  # Alias for back
    LINEAR_IPSILATERAL = "linear_ipsilateral"  # Toward dominant side
    LINEAR_CONTRALATERAL = "linear_contralateral"  # Toward non-dominant side

    # Curved path movements
    ARC_UP = "arc_up"
    ARC_DOWN = "arc_down"
    ARC_LEFT = "arc_left"
    ARC_RIGHT = "arc_right"
    ARC_FORWARD = "arc_forward"

    # Circular movements
    CIRCLE_VERTICAL = "circle_vertical"
    CIRCLE_HORIZONTAL = "circle_horizontal"
    CIRCLE_SAGITTAL = "circle_sagittal"  # Forward/back plane

    # Complex path movements
    ZIGZAG = "zigzag"
    WAVY = "wavy"
    SPIRAL = "spiral"
    HOOK = "hook"  # J-shaped movement
    SEVEN_SHAPE = "seven_shape"  # 7-shaped movement
    Z_SHAPE = "z_shape"

    # Contact movements
    CONTACT = "contact"  # Single contact
    CONTACT_REPEATED = "contact_repeated"  # Multiple contacts
    BRUSHING = "brushing"  # Brushing contact

    # Internal hand movements
    WIGGLE_FINGERS = "wiggle_fingers"
    FLUTTER = "flutter"  # Rapid finger wiggling
    OPEN_CLOSE = "open_close"  # Hand opening/closing
    BEND_FINGERS = "bend_fingers"
    SPREAD_FINGERS = "spread_fingers"
    RUB = "rub"  # Finger rubbing (e.g., index on thumb)

    # Wrist/arm movements
    TWIST_WRIST = "twist_wrist"  # Supination/pronation
    NOD_WRIST = "nod_wrist"  # Wrist flexion/extension
    SHAKE = "shake"  # Rapid side-to-side or twist

    # Manner modifications
    REPEATED = "repeated"  # Any movement repeated
    ALTERNATING = "alternating"  # Two hands alternating
    SIMULTANEOUS = "simultaneous"  # Two hands together

    # No movement
    HOLD = "hold"  # Stationary, no movement
    NONE = "none"

    # Other
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> Movement:
        """Convert a string to a Movement enum value."""
        for mov in cls:
            if mov.value == value or mov.name == value.upper():
                return mov
        value_lower = value.lower()
        for mov in cls:
            if mov.value.lower() == value_lower:
                return mov
        raise ValueError(f"Unknown movement: {value}")


class PalmOrientation(str, Enum):
    """
    Palm orientation (where the palm faces).

    Orientations are described relative to the signer's perspective
    and body.
    """

    PALM_UP = "palm_up"  # Palm faces ceiling
    PALM_DOWN = "palm_down"  # Palm faces floor
    PALM_IN = "palm_in"  # Palm faces body/signer
    PALM_OUT = "palm_out"  # Palm faces away from body
    PALM_LEFT = "palm_left"  # Palm faces left (from signer's view)
    PALM_RIGHT = "palm_right"  # Palm faces right
    PALM_IPSILATERAL = "palm_ipsilateral"  # Toward dominant side
    PALM_CONTRALATERAL = "palm_contralateral"  # Toward non-dominant side
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> PalmOrientation:
        """Convert a string to a PalmOrientation enum value."""
        for ori in cls:
            if ori.value == value or ori.name == value.upper():
                return ori
        value_lower = value.lower()
        for ori in cls:
            if ori.value.lower() == value_lower:
                return ori
        raise ValueError(f"Unknown palm orientation: {value}")


class FingerOrientation(str, Enum):
    """
    Finger orientation (where the fingers point).

    This is independent of palm orientation and describes
    the direction the extended fingers are pointing.
    """

    FINGERS_UP = "fingers_up"
    FINGERS_DOWN = "fingers_down"
    FINGERS_FORWARD = "fingers_forward"  # Away from body
    FINGERS_BACK = "fingers_back"  # Toward body
    FINGERS_LEFT = "fingers_left"
    FINGERS_RIGHT = "fingers_right"
    FINGERS_IPSILATERAL = "fingers_ipsilateral"
    FINGERS_CONTRALATERAL = "fingers_contralateral"
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> FingerOrientation:
        """Convert a string to a FingerOrientation enum value."""
        for ori in cls:
            if ori.value == value or ori.name == value.upper():
                return ori
        value_lower = value.lower()
        for ori in cls:
            if ori.value.lower() == value_lower:
                return ori
        raise ValueError(f"Unknown finger orientation: {value}")


class HandArrangement(str, Enum):
    """
    Hand arrangement for two-handed signs.

    Describes the relationship between the two hands
    in signs that use both hands.
    """

    ONE_HANDED = "one_handed"  # Single hand only
    SYMMETRIC = "symmetric"  # Same handshape, same movement
    ALTERNATING = "alternating"  # Same handshape, alternating movement
    DOMINANT_ACTIVE = "dominant_active"  # Dominant hand moves on non-dominant
    INTERLOCKED = "interlocked"  # Hands intertwined
    STACKED = "stacked"  # One hand on top of other
    SIDE_BY_SIDE = "side_by_side"  # Hands next to each other
    MIRRORED = "mirrored"  # Symmetric but mirrored movement
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> HandArrangement:
        """Convert a string to a HandArrangement enum value."""
        for arr in cls:
            if arr.value == value or arr.name == value.upper():
                return arr
        value_lower = value.lower()
        for arr in cls:
            if arr.value.lower() == value_lower:
                return arr
        raise ValueError(f"Unknown hand arrangement: {value}")


class NonManualSignal(str, Enum):
    """
    Non-manual signals (NMS) that co-occur with manual signs.

    Non-manual signals include facial expressions, head movements,
    and body postures that carry grammatical or affective meaning.
    """

    # Facial expressions - brows
    BROW_RAISE = "brow_raise"
    BROW_FURROW = "brow_furrow"
    BROW_FLASH = "brow_flash"  # Quick raise

    # Facial expressions - eyes
    EYE_GAZE_LEFT = "eye_gaze_left"
    EYE_GAZE_RIGHT = "eye_gaze_right"
    EYE_GAZE_UP = "eye_gaze_up"
    EYE_GAZE_DOWN = "eye_gaze_down"
    EYE_SQUINT = "eye_squint"
    EYE_WIDE = "eye_wide"
    EYE_CLOSE = "eye_close"
    EYE_BLINK = "eye_blink"

    # Facial expressions - nose/mouth
    NOSE_WRINKLE = "nose_wrinkle"
    MOUTH_OPEN = "mouth_open"
    MOUTH_ROUND = "mouth_round"
    LIPS_PURSED = "lips_pursed"
    LIPS_SPREAD = "lips_spread"
    TONGUE_OUT = "tongue_out"
    TONGUE_CLICK = "tongue_click"
    PUFF_CHEEKS = "puff_cheeks"
    SUCK_CHEEKS = "suck_cheeks"

    # Head movements
    HEAD_NOD = "head_nod"
    HEAD_SHAKE = "head_shake"
    HEAD_TILT_LEFT = "head_tilt_left"
    HEAD_TILT_RIGHT = "head_tilt_right"
    HEAD_TILT_FORWARD = "head_tilt_forward"
    HEAD_TILT_BACK = "head_tilt_back"
    HEAD_TURN_LEFT = "head_turn_left"
    HEAD_TURN_RIGHT = "head_turn_right"

    # Body movements
    BODY_LEAN_FORWARD = "body_lean_forward"
    BODY_LEAN_BACK = "body_lean_back"
    BODY_LEAN_LEFT = "body_lean_left"
    BODY_LEAN_RIGHT = "body_lean_right"
    SHOULDER_RAISE = "shoulder_raise"
    SHOULDER_SHRUG = "shoulder_shrug"

    # Mouthings (borrowed from spoken language)
    MOUTHING = "mouthing"  # Spoken word mouthed

    # Other
    OTHER = "other"

    @classmethod
    def from_string(cls, value: str) -> NonManualSignal:
        """Convert a string to a NonManualSignal enum value."""
        for nms in cls:
            if nms.value == value or nms.name == value.upper():
                return nms
        value_lower = value.lower()
        for nms in cls:
            if nms.value.lower() == value_lower:
                return nms
        raise ValueError(f"Unknown non-manual signal: {value}")


# =============================================================================
# Phonological Form Data Model
# =============================================================================


@dataclass
class PhonologicalForm:
    """
    Phonological representation of a sign based on HLMO parameters.

    This dataclass captures the phonological structure of a sign according
    to the standard parameters used in sign language phonology:
    - Handshape (dominant and non-dominant)
    - Location (place of articulation)
    - Movement (path and internal)
    - Orientation (palm and finger)

    Additionally, it captures:
    - Hand arrangement (for two-handed signs)
    - Non-manual signals that co-occur with the sign

    Attributes:
        handshape_dom: Handshape of the dominant hand
        handshape_nondom: Handshape of non-dominant hand (None for one-handed)
        location: Place of articulation
        movement: Type of movement
        orientation_palm: Palm orientation
        orientation_fingers: Finger pointing direction
        hand_arrangement: Relationship between hands (for two-handed signs)
        nms_cooccurring: List of non-manual signals that occur with the sign
        metadata: Additional information (e.g., source, confidence, notes)

    Example:
        >>> # Create phonological form for a sign like "HELLO" in ASL
        >>> hello = PhonologicalForm(
        ...     handshape_dom=Handshape.B_FLAT,
        ...     location=Location.FOREHEAD,
        ...     movement=Movement.LINEAR_AWAY,
        ...     orientation_palm=PalmOrientation.PALM_OUT,
        ...     orientation_fingers=FingerOrientation.FINGERS_UP,
        ... )
        >>> hello.is_one_handed
        True
        >>> hello.parameters
        ['handshape_dom', 'location', 'movement', 'orientation_palm', 'orientation_fingers']
    """

    handshape_dom: Handshape | str
    location: Location | str
    movement: Movement | str
    orientation_palm: PalmOrientation | str
    orientation_fingers: FingerOrientation | str
    handshape_nondom: Handshape | str | None = None
    hand_arrangement: HandArrangement | str | None = None
    nms_cooccurring: list[NonManualSignal | str] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        """Validate and normalize enum values."""
        # Convert strings to enums where possible, but allow strings for flexibility
        self.handshape_dom = self._normalize_enum(self.handshape_dom, Handshape)
        self.location = self._normalize_enum(self.location, Location)
        self.movement = self._normalize_enum(self.movement, Movement)
        self.orientation_palm = self._normalize_enum(self.orientation_palm, PalmOrientation)
        self.orientation_fingers = self._normalize_enum(self.orientation_fingers, FingerOrientation)

        if self.handshape_nondom is not None:
            self.handshape_nondom = self._normalize_enum(self.handshape_nondom, Handshape)

        if self.hand_arrangement is not None:
            self.hand_arrangement = self._normalize_enum(self.hand_arrangement, HandArrangement)
        elif self.handshape_nondom is not None:
            # Default to dominant_active if non-dominant hand present
            self.hand_arrangement = HandArrangement.DOMINANT_ACTIVE

        self.nms_cooccurring = [
            self._normalize_enum(nms, NonManualSignal) for nms in self.nms_cooccurring
        ]

    @staticmethod
    def _normalize_enum(value: Any, enum_class: type[Enum]) -> Enum | str:
        """
        Normalize a value to an enum, keeping strings if conversion fails.

        This allows for both strict typing with enums and flexible string input
        for values not in the standard inventory.
        """
        if isinstance(value, enum_class):
            return value
        if isinstance(value, str):
            try:
                return enum_class.from_string(value)  # type: ignore[attr-defined]
            except (ValueError, AttributeError):
                logger.debug(
                    "Could not convert '%s' to %s, keeping as string", value, enum_class.__name__
                )
                return value
        return value

    @property
    def is_one_handed(self) -> bool:
        """Check if this is a one-handed sign."""
        return self.handshape_nondom is None

    @property
    def is_two_handed(self) -> bool:
        """Check if this is a two-handed sign."""
        return self.handshape_nondom is not None

    @property
    def has_nms(self) -> bool:
        """Check if this sign has non-manual signals."""
        return len(self.nms_cooccurring) > 0

    @property
    def parameters(self) -> list[str]:
        """
        List of parameter names that are specified for this sign.

        Returns:
            List of parameter names (excluding None values and empty lists)
        """
        params = [
            "handshape_dom",
            "location",
            "movement",
            "orientation_palm",
            "orientation_fingers",
        ]
        if self.handshape_nondom is not None:
            params.append("handshape_nondom")
        if self.hand_arrangement is not None:
            params.append("hand_arrangement")
        if self.nms_cooccurring:
            params.append("nms_cooccurring")
        return params

    def get_parameter_value(self, param: str) -> Any:
        """
        Get the value of a specific parameter.

        Args:
            param: Parameter name

        Returns:
            Parameter value (enum or string)

        Raises:
            AttributeError: If parameter doesn't exist
        """
        return getattr(self, param)

    def get_parameter_string(self, param: str) -> str:
        """
        Get the string value of a parameter.

        Args:
            param: Parameter name

        Returns:
            String representation of the parameter value
        """
        value = self.get_parameter_value(param)
        if value is None:
            return "none"
        if isinstance(value, Enum):
            return value.value
        if isinstance(value, list):
            return ",".join(v.value if isinstance(v, Enum) else str(v) for v in value)
        return str(value)

    def to_dict(self) -> dict[str, Any]:
        """
        Convert to dictionary for JSON serialization.

        Returns:
            Dictionary representation with enum values as strings

        Example:
            >>> form = PhonologicalForm(
            ...     handshape_dom=Handshape.A,
            ...     location=Location.CHEST,
            ...     movement=Movement.CONTACT,
            ...     orientation_palm=PalmOrientation.PALM_IN,
            ...     orientation_fingers=FingerOrientation.FINGERS_UP,
            ... )
            >>> d = form.to_dict()
            >>> d["handshape_dom"]
            'A'
        """

        def _to_str(val: Any) -> Any:
            if val is None:
                return None
            if isinstance(val, Enum):
                return val.value
            if isinstance(val, list):
                return [_to_str(v) for v in val]
            return val

        return {
            "handshape_dom": _to_str(self.handshape_dom),
            "handshape_nondom": _to_str(self.handshape_nondom),
            "location": _to_str(self.location),
            "movement": _to_str(self.movement),
            "orientation_palm": _to_str(self.orientation_palm),
            "orientation_fingers": _to_str(self.orientation_fingers),
            "hand_arrangement": _to_str(self.hand_arrangement),
            "nms_cooccurring": _to_str(self.nms_cooccurring),
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> PhonologicalForm:
        """
        Create PhonologicalForm from dictionary.

        Args:
            data: Dictionary with phonological parameters

        Returns:
            PhonologicalForm instance

        Example:
            >>> data = {
            ...     "handshape_dom": "A",
            ...     "location": "chest",
            ...     "movement": "contact",
            ...     "orientation_palm": "palm_in",
            ...     "orientation_fingers": "fingers_up",
            ... }
            >>> form = PhonologicalForm.from_dict(data)
            >>> form.handshape_dom
            <Handshape.A: 'A'>
        """
        return cls(
            handshape_dom=data.get("handshape_dom", "other"),
            handshape_nondom=data.get("handshape_nondom"),
            location=data.get("location", "other"),
            movement=data.get("movement", "other"),
            orientation_palm=data.get("orientation_palm", "other"),
            orientation_fingers=data.get("orientation_fingers", "other"),
            hand_arrangement=data.get("hand_arrangement"),
            nms_cooccurring=data.get("nms_cooccurring", []),
            metadata=data.get("metadata", {}),
        )

    def __eq__(self, other: object) -> bool:
        """Check equality based on phonological parameters."""
        if not isinstance(other, PhonologicalForm):
            return NotImplemented
        return (
            self._to_comparable(self.handshape_dom) == self._to_comparable(other.handshape_dom)
            and self._to_comparable(self.handshape_nondom)
            == self._to_comparable(other.handshape_nondom)
            and self._to_comparable(self.location) == self._to_comparable(other.location)
            and self._to_comparable(self.movement) == self._to_comparable(other.movement)
            and self._to_comparable(self.orientation_palm)
            == self._to_comparable(other.orientation_palm)
            and self._to_comparable(self.orientation_fingers)
            == self._to_comparable(other.orientation_fingers)
            and self._to_comparable(self.hand_arrangement)
            == self._to_comparable(other.hand_arrangement)
        )

    @staticmethod
    def _to_comparable(val: Any) -> str | None:
        """Convert a value to a comparable string."""
        if val is None:
            return None
        if isinstance(val, Enum):
            return val.value
        return str(val)

    def __hash__(self) -> int:
        """Hash based on phonological parameters."""
        return hash(
            (
                self._to_comparable(self.handshape_dom),
                self._to_comparable(self.handshape_nondom),
                self._to_comparable(self.location),
                self._to_comparable(self.movement),
                self._to_comparable(self.orientation_palm),
                self._to_comparable(self.orientation_fingers),
                self._to_comparable(self.hand_arrangement),
            )
        )


# =============================================================================
# Linguistic Segment Extension
# =============================================================================


@dataclass
class LinguisticSegment:
    """
    A temporal segment with linguistic annotations.

    Extends the base Segment concept with detailed linguistic information
    including phonological form, morphological analysis, and part of speech.

    This class is designed to work alongside the core Segment class but
    adds sign-language-specific linguistic annotations.

    Attributes:
        start: Start time in seconds
        end: End time in seconds
        label: Primary label (typically gloss)
        tier: ELAN tier name
        confidence: Annotation confidence score
        phonological_form: Phonological representation of the sign
        morphological_analysis: List of morpheme labels
        part_of_speech: Grammatical category (noun, verb, etc.)
        lemma: Base/citation form of the sign
        annotator_id: ID of the person who created this annotation
        metadata: Additional information

    Example:
        >>> segment = LinguisticSegment(
        ...     start=1.0,
        ...     end=1.5,
        ...     label="BOOK",
        ...     phonological_form=PhonologicalForm(
        ...         handshape_dom=Handshape.B_FLAT,
        ...         location=Location.NEUTRAL_SPACE,
        ...         movement=Movement.OPEN_CLOSE,
        ...         orientation_palm=PalmOrientation.PALM_UP,
        ...         orientation_fingers=FingerOrientation.FINGERS_FORWARD,
        ...     ),
        ...     part_of_speech="noun",
        ...     lemma="BOOK",
        ... )
        >>> segment.duration
        0.5
    """

    start: float
    end: float
    label: str | None = None
    tier: str = "default"
    confidence: float | None = None
    phonological_form: PhonologicalForm | None = None
    morphological_analysis: list[str] | None = None
    part_of_speech: str | None = None
    lemma: str | None = None
    annotator_id: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        """Validate segment times."""
        if self.start < 0:
            raise ValueError(f"start must be non-negative, got {self.start}")
        if self.end < self.start:
            raise ValueError(f"end ({self.end}) must be >= start ({self.start})")
        if self.confidence is not None and not (0 <= self.confidence <= 1):
            logger.warning("Confidence outside expected [0, 1] range: %.3f", self.confidence)

    @property
    def duration(self) -> float:
        """Duration in seconds."""
        return self.end - self.start

    @property
    def midpoint(self) -> float:
        """Midpoint time in seconds."""
        return (self.start + self.end) / 2

    @property
    def has_phonology(self) -> bool:
        """Check if phonological form is specified."""
        return self.phonological_form is not None

    @property
    def has_morphology(self) -> bool:
        """Check if morphological analysis is specified."""
        return self.morphological_analysis is not None and len(self.morphological_analysis) > 0

    def to_dict(self) -> dict[str, Any]:
        """
        Convert to dictionary for JSON serialization.

        Returns:
            Dictionary representation
        """
        return {
            "start": self.start,
            "end": self.end,
            "label": self.label,
            "tier": self.tier,
            "confidence": self.confidence,
            "phonological_form": (
                self.phonological_form.to_dict() if self.phonological_form else None
            ),
            "morphological_analysis": self.morphological_analysis,
            "part_of_speech": self.part_of_speech,
            "lemma": self.lemma,
            "annotator_id": self.annotator_id,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> LinguisticSegment:
        """
        Create LinguisticSegment from dictionary.

        Args:
            data: Dictionary with segment data

        Returns:
            LinguisticSegment instance
        """
        phon_data = data.get("phonological_form")
        phonological_form = PhonologicalForm.from_dict(phon_data) if phon_data else None

        return cls(
            start=data["start"],
            end=data["end"],
            label=data.get("label"),
            tier=data.get("tier", "default"),
            confidence=data.get("confidence"),
            phonological_form=phonological_form,
            morphological_analysis=data.get("morphological_analysis"),
            part_of_speech=data.get("part_of_speech"),
            lemma=data.get("lemma"),
            annotator_id=data.get("annotator_id"),
            metadata=data.get("metadata", {}),
        )


# =============================================================================
# Phonological Distance and Analysis Functions
# =============================================================================


# Define which parameters are compared for phonological distance
CORE_PARAMETERS = [
    "handshape_dom",
    "location",
    "movement",
    "orientation_palm",
    "orientation_fingers",
]

EXTENDED_PARAMETERS = CORE_PARAMETERS + ["handshape_nondom", "hand_arrangement"]


def phonological_distance(
    form1: PhonologicalForm,
    form2: PhonologicalForm,
    include_nondom: bool = True,
    include_arrangement: bool = False,
) -> int:
    """
    Calculate Hamming distance across phonological parameters.

    The Hamming distance counts the number of parameters that differ
    between two phonological forms. This is a standard measure used
    in sign language phonology to quantify similarity between signs.

    Args:
        form1: First phonological form
        form2: Second phonological form
        include_nondom: Include non-dominant handshape in comparison
        include_arrangement: Include hand arrangement in comparison

    Returns:
        Number of parameters that differ (0 = identical)

    Example:
        >>> form1 = PhonologicalForm(
        ...     handshape_dom=Handshape.A,
        ...     location=Location.CHEST,
        ...     movement=Movement.CONTACT,
        ...     orientation_palm=PalmOrientation.PALM_IN,
        ...     orientation_fingers=FingerOrientation.FINGERS_UP,
        ... )
        >>> form2 = PhonologicalForm(
        ...     handshape_dom=Handshape.B_FLAT,  # Different
        ...     location=Location.CHEST,
        ...     movement=Movement.CONTACT,
        ...     orientation_palm=PalmOrientation.PALM_IN,
        ...     orientation_fingers=FingerOrientation.FINGERS_UP,
        ... )
        >>> phonological_distance(form1, form2)
        1
    """
    params = list(CORE_PARAMETERS)
    if include_nondom:
        params.append("handshape_nondom")
    if include_arrangement:
        params.append("hand_arrangement")

    distance = 0
    for param in params:
        val1 = form1._to_comparable(form1.get_parameter_value(param))
        val2 = form2._to_comparable(form2.get_parameter_value(param))
        if val1 != val2:
            distance += 1

    return distance


def get_differing_parameters(
    form1: PhonologicalForm,
    form2: PhonologicalForm,
    include_nondom: bool = True,
    include_arrangement: bool = False,
) -> list[str]:
    """
    Get list of parameters that differ between two forms.

    Args:
        form1: First phonological form
        form2: Second phonological form
        include_nondom: Include non-dominant handshape
        include_arrangement: Include hand arrangement

    Returns:
        List of parameter names that differ

    Example:
        >>> # Assuming form1 and form2 differ only in handshape_dom
        >>> diff = get_differing_parameters(form1, form2)
        >>> diff
        ['handshape_dom']
    """
    params = list(CORE_PARAMETERS)
    if include_nondom:
        params.append("handshape_nondom")
    if include_arrangement:
        params.append("hand_arrangement")

    differing = []
    for param in params:
        val1 = form1._to_comparable(form1.get_parameter_value(param))
        val2 = form2._to_comparable(form2.get_parameter_value(param))
        if val1 != val2:
            differing.append(param)

    return differing


def find_minimal_pairs(
    forms: dict[str, PhonologicalForm],
    include_nondom: bool = False,
    include_arrangement: bool = False,
) -> list[tuple[str, str, str]]:
    """
    Find pairs of signs differing in exactly one parameter.

    Minimal pairs are fundamental to phonological analysis as they
    demonstrate that a particular parameter is contrastive (i.e.,
    it distinguishes meaning). For example, if MOTHER and FATHER
    differ only in location (chin vs. forehead), this shows that
    location is a distinctive feature.

    Args:
        forms: Dictionary mapping sign names/glosses to their phonological forms
        include_nondom: Include non-dominant handshape in comparison
        include_arrangement: Include hand arrangement in comparison

    Returns:
        List of tuples (sign1, sign2, differing_parameter)

    Example:
        >>> forms = {
        ...     "MOTHER": PhonologicalForm(..., location=Location.CHIN, ...),
        ...     "FATHER": PhonologicalForm(..., location=Location.FOREHEAD, ...),
        ... }
        >>> pairs = find_minimal_pairs(forms)
        >>> ("MOTHER", "FATHER", "location") in pairs
        True
    """
    minimal_pairs = []
    sign_names = list(forms.keys())

    for i, name1 in enumerate(sign_names):
        for name2 in sign_names[i + 1 :]:
            form1 = forms[name1]
            form2 = forms[name2]

            differing = get_differing_parameters(
                form1, form2, include_nondom=include_nondom, include_arrangement=include_arrangement
            )

            if len(differing) == 1:
                # Sort names alphabetically for consistent ordering
                if name1 < name2:
                    minimal_pairs.append((name1, name2, differing[0]))
                else:
                    minimal_pairs.append((name2, name1, differing[0]))

    return minimal_pairs


def find_near_minimal_pairs(
    forms: dict[str, PhonologicalForm],
    max_distance: int = 2,
    include_nondom: bool = False,
    include_arrangement: bool = False,
) -> list[tuple[str, str, list[str]]]:
    """
    Find pairs of signs differing in up to max_distance parameters.

    This is an extension of minimal pair finding that allows for
    "near-minimal" pairs, useful for studying phonological neighborhoods.

    Args:
        forms: Dictionary mapping sign names to phonological forms
        max_distance: Maximum number of differing parameters
        include_nondom: Include non-dominant handshape
        include_arrangement: Include hand arrangement

    Returns:
        List of tuples (sign1, sign2, list_of_differing_parameters)
    """
    near_pairs = []
    sign_names = list(forms.keys())

    for i, name1 in enumerate(sign_names):
        for name2 in sign_names[i + 1 :]:
            form1 = forms[name1]
            form2 = forms[name2]

            differing = get_differing_parameters(
                form1, form2, include_nondom=include_nondom, include_arrangement=include_arrangement
            )

            if 1 <= len(differing) <= max_distance:
                if name1 < name2:
                    near_pairs.append((name1, name2, differing))
                else:
                    near_pairs.append((name2, name1, differing))

    return near_pairs


# =============================================================================
# Inventory Analysis
# =============================================================================


@dataclass
class InventoryStats:
    """
    Statistics about the phonological inventory of a corpus.

    Provides counts and distributions of phonological features
    across a set of signs.

    Attributes:
        total_forms: Total number of forms analyzed
        handshape_counts: Counter of handshape occurrences
        location_counts: Counter of location occurrences
        movement_counts: Counter of movement occurrences
        palm_orientation_counts: Counter of palm orientation occurrences
        finger_orientation_counts: Counter of finger orientation occurrences
        hand_arrangement_counts: Counter of hand arrangement types
        nms_counts: Counter of non-manual signal occurrences
        one_handed_count: Number of one-handed signs
        two_handed_count: Number of two-handed signs
        unique_handshapes: Number of distinct handshapes
        unique_locations: Number of distinct locations
        unique_movements: Number of distinct movements
    """

    total_forms: int
    handshape_counts: Counter
    location_counts: Counter
    movement_counts: Counter
    palm_orientation_counts: Counter
    finger_orientation_counts: Counter
    hand_arrangement_counts: Counter
    nms_counts: Counter
    one_handed_count: int
    two_handed_count: int

    @property
    def unique_handshapes(self) -> int:
        """Number of distinct handshapes used."""
        return len(self.handshape_counts)

    @property
    def unique_locations(self) -> int:
        """Number of distinct locations used."""
        return len(self.location_counts)

    @property
    def unique_movements(self) -> int:
        """Number of distinct movements used."""
        return len(self.movement_counts)

    @property
    def one_handed_ratio(self) -> float:
        """Proportion of one-handed signs."""
        if self.total_forms == 0:
            return 0.0
        return self.one_handed_count / self.total_forms

    @property
    def two_handed_ratio(self) -> float:
        """Proportion of two-handed signs."""
        if self.total_forms == 0:
            return 0.0
        return self.two_handed_count / self.total_forms

    def most_common_handshapes(self, n: int = 10) -> list[tuple[str, int]]:
        """Get the n most common handshapes."""
        return self.handshape_counts.most_common(n)

    def most_common_locations(self, n: int = 10) -> list[tuple[str, int]]:
        """Get the n most common locations."""
        return self.location_counts.most_common(n)

    def most_common_movements(self, n: int = 10) -> list[tuple[str, int]]:
        """Get the n most common movements."""
        return self.movement_counts.most_common(n)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "total_forms": self.total_forms,
            "handshape_counts": dict(self.handshape_counts),
            "location_counts": dict(self.location_counts),
            "movement_counts": dict(self.movement_counts),
            "palm_orientation_counts": dict(self.palm_orientation_counts),
            "finger_orientation_counts": dict(self.finger_orientation_counts),
            "hand_arrangement_counts": dict(self.hand_arrangement_counts),
            "nms_counts": dict(self.nms_counts),
            "one_handed_count": self.one_handed_count,
            "two_handed_count": self.two_handed_count,
            "unique_handshapes": self.unique_handshapes,
            "unique_locations": self.unique_locations,
            "unique_movements": self.unique_movements,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> InventoryStats:
        """Create InventoryStats from dictionary."""
        return cls(
            total_forms=data["total_forms"],
            handshape_counts=Counter(data["handshape_counts"]),
            location_counts=Counter(data["location_counts"]),
            movement_counts=Counter(data["movement_counts"]),
            palm_orientation_counts=Counter(data["palm_orientation_counts"]),
            finger_orientation_counts=Counter(data["finger_orientation_counts"]),
            hand_arrangement_counts=Counter(data["hand_arrangement_counts"]),
            nms_counts=Counter(data["nms_counts"]),
            one_handed_count=data["one_handed_count"],
            two_handed_count=data["two_handed_count"],
        )


def analyze_inventory(forms: list[PhonologicalForm]) -> InventoryStats:
    """
    Analyze distribution of phonological features in a corpus.

    This function computes comprehensive statistics about the
    phonological inventory represented in a list of forms.

    Args:
        forms: List of PhonologicalForm instances to analyze

    Returns:
        InventoryStats with counts and distributions

    Example:
        >>> forms = [form1, form2, form3, ...]
        >>> stats = analyze_inventory(forms)
        >>> print(f"Unique handshapes: {stats.unique_handshapes}")
        Unique handshapes: 15
        >>> stats.most_common_handshapes(3)
        [('B_flat', 45), ('1', 32), ('5', 28)]
    """
    handshape_counts: Counter = Counter()
    location_counts: Counter = Counter()
    movement_counts: Counter = Counter()
    palm_orientation_counts: Counter = Counter()
    finger_orientation_counts: Counter = Counter()
    hand_arrangement_counts: Counter = Counter()
    nms_counts: Counter = Counter()
    one_handed = 0
    two_handed = 0

    for form in forms:
        # Count handshapes (including non-dominant)
        hs_dom = form._to_comparable(form.handshape_dom)
        handshape_counts[hs_dom] += 1

        if form.handshape_nondom is not None:
            hs_nondom = form._to_comparable(form.handshape_nondom)
            handshape_counts[hs_nondom] += 1
            two_handed += 1
        else:
            one_handed += 1

        # Count other parameters
        location_counts[form._to_comparable(form.location)] += 1
        movement_counts[form._to_comparable(form.movement)] += 1
        palm_orientation_counts[form._to_comparable(form.orientation_palm)] += 1
        finger_orientation_counts[form._to_comparable(form.orientation_fingers)] += 1

        if form.hand_arrangement is not None:
            hand_arrangement_counts[form._to_comparable(form.hand_arrangement)] += 1

        # Count NMS
        for nms in form.nms_cooccurring:
            nms_counts[form._to_comparable(nms)] += 1

    return InventoryStats(
        total_forms=len(forms),
        handshape_counts=handshape_counts,
        location_counts=location_counts,
        movement_counts=movement_counts,
        palm_orientation_counts=palm_orientation_counts,
        finger_orientation_counts=finger_orientation_counts,
        hand_arrangement_counts=hand_arrangement_counts,
        nms_counts=nms_counts,
        one_handed_count=one_handed,
        two_handed_count=two_handed,
    )


def filter_by_parameter(
    forms: dict[str, PhonologicalForm], parameter: str, value: str | Enum
) -> dict[str, PhonologicalForm]:
    """
    Filter forms by a specific parameter value.

    Args:
        forms: Dictionary mapping sign names to phonological forms
        parameter: Parameter name to filter on
        value: Value to match (can be string or enum)

    Returns:
        Filtered dictionary with only matching forms

    Example:
        >>> # Get all signs at the forehead location
        >>> forehead_signs = filter_by_parameter(forms, "location", Location.FOREHEAD)
    """
    target_value = value.value if isinstance(value, Enum) else value
    result = {}

    for name, form in forms.items():
        form_value = form._to_comparable(form.get_parameter_value(parameter))
        if form_value == target_value:
            result[name] = form

    return result


def group_by_parameter(
    forms: dict[str, PhonologicalForm], parameter: str
) -> dict[str, dict[str, PhonologicalForm]]:
    """
    Group forms by a specific parameter value.

    Args:
        forms: Dictionary mapping sign names to phonological forms
        parameter: Parameter name to group by

    Returns:
        Dictionary mapping parameter values to dicts of matching forms

    Example:
        >>> by_location = group_by_parameter(forms, "location")
        >>> forehead_signs = by_location.get("forehead", {})
    """
    groups: dict[str, dict[str, PhonologicalForm]] = {}

    for name, form in forms.items():
        value = form._to_comparable(form.get_parameter_value(parameter))
        if value is None:
            value = "none"
        if value not in groups:
            groups[value] = {}
        groups[value][name] = form

    return groups


def compute_phonological_neighborhood(
    target: PhonologicalForm, forms: dict[str, PhonologicalForm], max_distance: int = 1
) -> dict[str, tuple[PhonologicalForm, int]]:
    """
    Find all forms within a given phonological distance of a target.

    The phonological neighborhood of a sign includes all signs that
    differ by at most max_distance parameters. This is useful for
    studying lexical organization and potential confusion patterns.

    Args:
        target: Target phonological form
        forms: Dictionary of candidate forms to search
        max_distance: Maximum Hamming distance to include

    Returns:
        Dictionary mapping sign names to (form, distance) tuples

    Example:
        >>> neighbors = compute_phonological_neighborhood(target_form, all_forms, max_distance=2)
        >>> for name, (form, dist) in neighbors.items():
        ...     print(f"{name}: distance {dist}")
    """
    neighborhood = {}

    for name, form in forms.items():
        dist = phonological_distance(target, form)
        if dist <= max_distance:
            neighborhood[name] = (form, dist)

    return neighborhood
