//! Four-hand deal containers.
//!
//! Seats and the bitset over them live in [`crate::seat`]; a [`Seat`] is the
//! indexing key for every container in this module.
//!
//! Deals come in three flavors, distinguished by their invariants:
//!
//! * [`Builder`] — a mutable four-hand scratchpad with no invariants.  The
//!   only deal type exposing [`IndexMut`](core::ops::IndexMut).
//! * [`PartialDeal`] — validated and read-only: each hand holds at most 13
//!   cards and the four hands are pairwise disjoint.
//! * [`FullDeal`] — validated and read-only: exactly 13 cards per hand, all
//!   52 cards accounted for.
//!
//! Build a validated deal via [`Builder::build_partial`] or
//! [`Builder::build_full`]; both return the original `Builder` unchanged as
//! the error on validation failure.  To mutate an already-validated deal,
//! widen it back to a [`Builder`] and re-validate.
//!
//! All three deal types parse the [PBN] deal format —
//! `<dealer>:<hand> <hand> <hand> <hand>` — with holdings ordered spades,
//! hearts, diamonds, clubs.  `PartialDeal` additionally accepts relaxed hand
//! sizes and `x` for unknown ranks.
//!
//! [PBN]: https://www.tistis.nl/pbn/

use crate::hand::{Hand, ParseHandError};
use crate::seat::Seat;
use core::fmt::{self, Write as _};
use core::ops;
use core::str::FromStr;
use thiserror::Error;

/// An error which can be returned when parsing a [`PartialDeal`] or [`FullDeal`]
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParseDealError {
    /// Invalid dealer tag
    #[error("Invalid dealer tag for a deal")]
    InvalidDealer,

    /// Error in a hand
    #[error(transparent)]
    Hand(#[from] ParseHandError),

    /// The deal does not contain 4 hands
    #[error("The deal does not contain 4 hands")]
    NotFourHands,

    /// The deal is not a valid [`PartialDeal`]: some hand has more than 13 cards or
    /// two hands share a card
    #[error("The deal is not a valid subset (>13 cards per hand or overlapping hands)")]
    InvalidPartialDeal,

    /// The deal is not a [`FullDeal`]: some hand does not have exactly 13 cards
    #[error("The deal is not a full deal (each hand must have exactly 13 cards)")]
    NotFullDeal,
}

/// A loose deal builder — any combination of four hands, no invariants
///
/// Use `Builder` to construct a deal incrementally.  Convert it into a
/// [`PartialDeal`] or [`FullDeal`] (via the inherent [`build_partial`] /
/// [`build_full`] methods, or via [`TryFrom`]) once the hands are finalized.
/// `Builder` is the only deal type that exposes [`IndexMut`](ops::IndexMut)
/// for in-place mutation.
///
/// [`build_partial`]: Builder::build_partial
/// [`build_full`]: Builder::build_full
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Builder([Hand; 4]);

impl IntoIterator for Builder {
    type Item = Hand;
    type IntoIter = core::array::IntoIter<Hand, 4>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl ops::Index<Seat> for Builder {
    type Output = Hand;

    #[inline]
    fn index(&self, seat: Seat) -> &Hand {
        &self.0[seat as usize]
    }
}

impl ops::IndexMut<Seat> for Builder {
    #[inline]
    fn index_mut(&mut self, seat: Seat) -> &mut Hand {
        &mut self.0[seat as usize]
    }
}

impl Builder {
    /// Construct an empty builder — all four hands empty
    #[must_use]
    pub const fn new() -> Self {
        Self([Hand::EMPTY; 4])
    }

    /// Set the hand at [`Seat::North`]
    #[must_use]
    pub const fn north(mut self, hand: Hand) -> Self {
        self.0[Seat::North as usize] = hand;
        self
    }

    /// Set the hand at [`Seat::East`]
    #[must_use]
    pub const fn east(mut self, hand: Hand) -> Self {
        self.0[Seat::East as usize] = hand;
        self
    }

    /// Set the hand at [`Seat::South`]
    #[must_use]
    pub const fn south(mut self, hand: Hand) -> Self {
        self.0[Seat::South as usize] = hand;
        self
    }

    /// Set the hand at [`Seat::West`]
    #[must_use]
    pub const fn west(mut self, hand: Hand) -> Self {
        self.0[Seat::West as usize] = hand;
        self
    }

    /// Try to convert this builder into a [`PartialDeal`], validating that each
    /// hand has at most 13 cards and the hands are pairwise disjoint.  On
    /// failure the input is returned unchanged as the error.
    ///
    /// # Errors
    ///
    /// Returns `self` unchanged if the builder is not a valid subset.
    pub fn build_partial(self) -> Result<PartialDeal, Self> {
        let mut seen = Hand::EMPTY;
        for hand in self.0 {
            if hand.len() > 13 || hand & seen != Hand::EMPTY {
                return Err(self);
            }
            seen |= hand;
        }
        Ok(PartialDeal(self))
    }

    /// Try to convert this builder into a [`FullDeal`], validating that each
    /// hand has exactly 13 cards and the hands are pairwise disjoint.  On
    /// failure the input is returned unchanged as the error.
    ///
    /// # Errors
    ///
    /// Returns `self` unchanged if the builder is not a valid full deal.
    pub fn build_full(self) -> Result<FullDeal, Self> {
        match self.build_partial() {
            Ok(subset) if subset.len() == 52 => Ok(FullDeal(subset.0)),
            Ok(subset) => Err(subset.0),
            Err(builder) => Err(builder),
        }
    }
}

/// A validated subset of a bridge deal
///
/// Invariants: each hand holds at most 13 cards, and the four hands are
/// pairwise disjoint.  Construct via [`Builder::build_partial`],
/// [`TryFrom<Builder>`], the infallible widening from a [`FullDeal`], or by
/// parsing a PBN-ish string.
///
/// `PartialDeal` is read-only: it exposes [`Index<Seat>`](ops::Index) but not
/// [`IndexMut`](ops::IndexMut).  To mutate, widen back to a [`Builder`].
///
/// Parses the [PBN] deal format with relaxed per-hand size —
/// `<dealer>:<hand> <hand> <hand> <hand>` — where each hand is four
/// dot-separated holdings ordered spades, hearts, diamonds, clubs.  Holdings
/// may be empty or contain `x` spot cards for unknown ranks.  Hands are
/// listed clockwise starting from the dealer.
///
/// [PBN]: https://www.tistis.nl/pbn/
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
    feature = "serde",
    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
)]
pub struct PartialDeal(Builder);

impl ops::Index<Seat> for PartialDeal {
    type Output = Hand;

    #[inline]
    fn index(&self, seat: Seat) -> &Hand {
        &self.0[seat]
    }
}

impl PartialDeal {
    /// Empty subset — all four hands empty
    pub const EMPTY: Self = Self(Builder::new());

    /// Collect all cards in the subset into a single hand
    #[must_use]
    pub fn collected(&self) -> Hand {
        self.0.into_iter().fold(Hand::EMPTY, |a, h| a | h)
    }

    /// Total number of cards across the four hands
    #[must_use]
    pub fn len(&self) -> usize {
        self.collected().len()
    }

    /// Whether the subset has no cards at all
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.collected().is_empty()
    }

    /// PBN-compatible display from a seat's perspective
    #[must_use]
    pub fn display(&self, seat: Seat) -> impl fmt::Display + use<> {
        DisplayAt {
            builder: self.0,
            seat,
        }
    }
}

impl From<PartialDeal> for Builder {
    #[inline]
    fn from(subset: PartialDeal) -> Self {
        subset.0
    }
}

impl TryFrom<Builder> for PartialDeal {
    type Error = Builder;

    #[inline]
    fn try_from(builder: Builder) -> Result<Self, Self::Error> {
        builder.build_partial()
    }
}

impl FromStr for PartialDeal {
    type Err = ParseDealError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_pbn(s)?
            .build_partial()
            .map_err(|_| ParseDealError::InvalidPartialDeal)
    }
}

impl fmt::Display for PartialDeal {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.display(Seat::North).fmt(f)
    }
}

/// A full bridge deal — exactly 13 cards per hand, 52 total
///
/// Invariants: each of the four hands contains exactly 13 cards, and the
/// hands partition the full 52-card deck.  Construct via
/// [`Builder::build_full`], [`TryFrom<Builder>`], [`TryFrom<PartialDeal>`], or by
/// parsing a PBN string.
///
/// `FullDeal` is read-only.  Parses the [PBN] deal format:
/// `<dealer>:<hand> <hand> <hand> <hand>`, where each hand is four
/// dot-separated holdings ordered spades, hearts, diamonds, clubs.  Hands
/// are listed clockwise starting from the dealer.
///
/// # Examples
///
/// ```
/// use dds_bridge::{FullDeal, Rank, Seat, Suit};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let deal: FullDeal = "N:.63.AKQ987.A9732 A8654.KQ5.T.QJT6 \
///                       J973.J98742.3.K4 KQT2.AT.J6542.85".parse()?;
/// assert!(deal[Seat::East][Suit::Spades].contains(Rank::A));
/// # Ok(())
/// # }
/// ```
///
/// [PBN]: https://www.tistis.nl/pbn/
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
    feature = "serde",
    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
)]
pub struct FullDeal(Builder);

impl ops::Index<Seat> for FullDeal {
    type Output = Hand;

    #[inline]
    fn index(&self, seat: Seat) -> &Hand {
        &self.0[seat]
    }
}

impl IntoIterator for FullDeal {
    type Item = Hand;
    type IntoIter = core::array::IntoIter<Hand, 4>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl FullDeal {
    /// PBN-compatible display from a seat's perspective
    #[must_use]
    pub fn display(&self, seat: Seat) -> impl fmt::Display + use<> {
        DisplayAt {
            builder: self.0,
            seat,
        }
    }
}

impl From<FullDeal> for Builder {
    #[inline]
    fn from(deal: FullDeal) -> Self {
        deal.0
    }
}

impl From<FullDeal> for PartialDeal {
    #[inline]
    fn from(deal: FullDeal) -> Self {
        Self(deal.0)
    }
}

impl TryFrom<Builder> for FullDeal {
    type Error = Builder;

    #[inline]
    fn try_from(builder: Builder) -> Result<Self, Self::Error> {
        builder.build_full()
    }
}

impl TryFrom<PartialDeal> for FullDeal {
    type Error = PartialDeal;

    #[inline]
    fn try_from(subset: PartialDeal) -> Result<Self, Self::Error> {
        match subset.0.build_full() {
            Ok(full) => Ok(full),
            Err(builder) => Err(PartialDeal(builder)),
        }
    }
}

impl FromStr for FullDeal {
    type Err = ParseDealError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_pbn(s)?
            .build_full()
            .map_err(|_| ParseDealError::NotFullDeal)
    }
}

impl fmt::Display for FullDeal {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.display(Seat::North).fmt(f)
    }
}

/// Shared PBN deal parser: reads `<dealer>:<hand> <hand> <hand> <hand>` and
/// returns a `Builder` with hands rotated so seat index 0 is North.
fn parse_pbn(s: &str) -> Result<Builder, ParseDealError> {
    let bytes = s.as_bytes();

    let dealer = match bytes.first().map(u8::to_ascii_uppercase) {
        Some(b'N') => Seat::North,
        Some(b'E') => Seat::East,
        Some(b'S') => Seat::South,
        Some(b'W') => Seat::West,
        _ => return Err(ParseDealError::InvalidDealer),
    };

    if bytes.get(1) != Some(&b':') {
        return Err(ParseDealError::InvalidDealer);
    }

    let hands: Result<Vec<_>, _> = s[2..].split_whitespace().map(Hand::from_str).collect();

    let mut builder = Builder(
        hands?
            .try_into()
            .map_err(|_| ParseDealError::NotFourHands)?,
    );
    builder.0.rotate_right(dealer as usize);
    Ok(builder)
}

/// Shared PBN-compatible `Display` helper for [`PartialDeal`] and [`FullDeal`]
struct DisplayAt {
    builder: Builder,
    seat: Seat,
}

impl fmt::Display for DisplayAt {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_char(self.seat.letter())?;
        f.write_char(':')?;

        self.builder[self.seat].fmt(f)?;
        f.write_char(' ')?;

        self.builder[self.seat.lho()].fmt(f)?;
        f.write_char(' ')?;

        self.builder[self.seat.partner()].fmt(f)?;
        f.write_char(' ')?;

        self.builder[self.seat.rho()].fmt(f)
    }
}
