//! The vertical-band model (Chapter 7 §"Vertical Bands").
//!
//! "Vertical layout uses the same spring model. A *vertical band* is a
//! horizontal slice of the canvas (typically a staff or an inter-staff gap)
//! with its own spring parameters." The solver resolves vertical positions by
//! treating bands as springs, exactly as it treats horizontal time slots
//! (Chapter 7 §"Spring-based spacing").
//!
//! All dimensions are staff-space `f32` per the Chapter 7 §7.2 IR-coordinate
//! rule: the elastic spring parameters (`stretch_factor`, `compress_factor`) and
//! the band heights ([`StaffSpace`]) alike are layout-engine inputs, quantized
//! only if and when a band extent is serialized into canonical output.

use epiphany_core::{StaffId, TypedObjectId};
use epiphany_determinism::{DomainTag, Preimage};

use crate::constrained::GlyphObjectId;
use crate::provenance::{stable_layout_id, LayoutObjectId};
use crate::spatial::StaffSpace;

/// A stable identifier for a vertical band (Chapter 7: `VerticalBandId`).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct VerticalBandId(pub u128);

/// Derives an inter-staff gap id in its own type-prefixed preimage namespace.
/// It cannot alias the region's margin id by arithmetic construction.
pub fn inter_staff_gap_id(region: LayoutObjectId, gap_index: usize) -> VerticalBandId {
    let mut preimage = Preimage::new(DomainTag::CONFLICT);
    preimage.push_bytes(b"vertical-band/inter-staff-gap");
    preimage.push_u64_le((region.0 >> 64) as u64);
    preimage.push_u64_le(region.0 as u64);
    preimage.push_u64_le(gap_index as u64);
    VerticalBandId(preimage.finish_trunc128())
}

/// What a vertical band represents (Chapter 7: `VerticalBandKind`).
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum VerticalBandKind {
    /// A staff's own band.
    Staff(StaffId),
    /// The gap between two staves of a system.
    InterStaffGap,
    /// The gap between two systems on a page.
    InterSystemGap,
    /// A page-margin band.
    MarginBand,
}

/// A vertical band: a horizontal slice of the canvas with its own spring
/// parameters (Chapter 7 §"Vertical Bands"). The constraint solver consumes
/// bands uniformly, just like horizontal spring slots.
#[derive(Clone, PartialEq, Debug)]
pub struct VerticalBand {
    pub id: VerticalBandId,
    pub kind: VerticalBandKind,
    /// The smallest the band may compress to, in staff spaces.
    pub min_height: StaffSpace,
    /// The band's natural height under unconstrained spacing, in staff spaces.
    pub preferred_height: StaffSpace,
    /// The largest the band may stretch to, if bounded, in staff spaces.
    pub max_height: Option<StaffSpace>,
    /// How readily the band stretches when more height is available.
    pub stretch_factor: f32,
    /// How readily the band compresses when less height is available.
    pub compress_factor: f32,
    /// The glyphs belonging to this band.
    pub members: Vec<GlyphObjectId>,
}

impl VerticalBand {
    /// A default staff band, with the band id derived from the staff source
    /// alone. Use this for a staff with a single manifestation; for a staff
    /// manifested in a specific region, use [`VerticalBand::staff_manifestation`]
    /// so two manifestations get two distinct band ids.
    pub fn staff(staff: StaffId, members: Vec<GlyphObjectId>) -> Self {
        Self::with_id(
            VerticalBandId(stable_layout_id(&TypedObjectId::Staff(staff)).0),
            staff,
            members,
        )
    }

    /// A staff band for a specific manifestation, identified by the staff
    /// layout object's stable id (`(staff, region)` — see
    /// [`crate::Provenance::manifested`]). Two manifestations of one staff thus
    /// get two distinct band ids, mirroring the two distinct staff layout
    /// objects.
    pub fn staff_manifestation(
        manifestation: LayoutObjectId,
        staff: StaffId,
        members: Vec<GlyphObjectId>,
    ) -> Self {
        Self::with_id(VerticalBandId(manifestation.0), staff, members)
    }

    /// A margin band (for region content with no staff, e.g. a free-graphic
    /// region), identified by `id` (typically the region's layout id).
    pub fn margin(id: LayoutObjectId, members: Vec<GlyphObjectId>) -> Self {
        let four_spaces = StaffSpace(4.0);
        VerticalBand {
            id: VerticalBandId(id.0),
            kind: VerticalBandKind::MarginBand,
            min_height: four_spaces,
            preferred_height: four_spaces,
            max_height: None,
            stretch_factor: 1.0,
            compress_factor: 1.0,
            members,
        }
    }

    /// An inter-staff gap band: the empty (member-less) spacing region between
    /// two staves of a system (Chapter 7 §"Vertical Bands": `InterStaffGap`). Its
    /// height is a spring the solver resolves; it carries no glyphs.
    pub fn inter_staff_gap(id: VerticalBandId) -> Self {
        VerticalBand {
            id,
            kind: VerticalBandKind::InterStaffGap,
            min_height: StaffSpace(1.0),
            preferred_height: StaffSpace(2.0),
            max_height: None,
            stretch_factor: 1.0,
            compress_factor: 1.0,
            members: Vec::new(),
        }
    }

    /// An inter-system gap band used during page casting-off.
    pub fn inter_system_gap(id: VerticalBandId) -> Self {
        VerticalBand {
            id,
            kind: VerticalBandKind::InterSystemGap,
            min_height: StaffSpace(2.0),
            preferred_height: StaffSpace(4.0),
            max_height: None,
            stretch_factor: 1.0,
            compress_factor: 1.0,
            members: Vec::new(),
        }
    }

    fn with_id(id: VerticalBandId, staff: StaffId, members: Vec<GlyphObjectId>) -> Self {
        // 4 staff spaces between the outer lines of a 5-line staff.
        let four_spaces = StaffSpace(4.0);
        VerticalBand {
            id,
            kind: VerticalBandKind::Staff(staff),
            min_height: four_spaces,
            preferred_height: four_spaces,
            max_height: None,
            stretch_factor: 1.0,
            compress_factor: 1.0,
            members,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn staff_band_is_stable_and_carries_members() {
        let staff = StaffId::from_raw(0x42);
        let members = vec![GlyphObjectId(1), GlyphObjectId(2)];
        let a = VerticalBand::staff(staff, members.clone());
        let b = VerticalBand::staff(staff, members);
        assert_eq!(a.id, b.id);
        assert_eq!(a.kind, VerticalBandKind::Staff(staff));
        assert_eq!(a.members.len(), 2);
        // A different staff → a different band id.
        let other = VerticalBand::staff(StaffId::from_raw(0x43), vec![]);
        assert_ne!(a.id, other.id);
    }

    #[test]
    fn inter_staff_gap_ids_are_stable_and_separate_from_the_region() {
        let region = LayoutObjectId(42);
        assert_eq!(inter_staff_gap_id(region, 1), inter_staff_gap_id(region, 1));
        assert_ne!(inter_staff_gap_id(region, 1), inter_staff_gap_id(region, 2));
        assert_ne!(inter_staff_gap_id(region, 1), VerticalBandId(region.0));
    }
}
