//! Provenance back-references (Chapter 7 §"Provenance").
//!
//! Every IR object at every stage carries a [`Provenance`] tracing it back to
//! the score-graph object that originated it. This is what makes selection,
//! error reporting, and — above all — *incremental layout* possible: an edit to
//! a score-graph object invalidates only the IR objects whose provenance lists
//! it (Chapter 7 §"Incremental Layout"). The round-trip harness proves the
//! complete provenance of every object survives the whole pipeline unchanged.

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

/// An IR object's stable identifier across re-layouts where its source is
/// unchanged (Chapter 7 §"Provenance"). Carried unchanged through every stage.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct LayoutObjectId(pub u128);

/// A registry id for an extension-defined [`SynthesisKind::Registered`]
/// (Chapter 7: `SynthesisRegistryId`). v0 hardcodes the core registry and
/// loads no external registries (QUICKSTART "Don't … implement extension
/// registries"); the id is an opaque value.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SynthesisRegistryId(pub u128);

/// A stable semantic key distinguishing multiple synthesized layout objects
/// with the same source and synthesis kind. Callers derive this from the
/// object's role (for example, the affected pitch and cancellation position),
/// never from traversal order.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SynthesisInstanceKey(pub u128);

/// Why an IR object exists without a direct score-graph source (Chapter 7
/// §"Provenance"). Engraver-synthesized objects MUST declare one; the variant
/// set is the spec's normative `SynthesisKind`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SynthesisKind {
    /// An accidental inserted to cancel a prior alteration.
    CancellationAccidental,
    /// A natural sign generated by key-signature rules.
    KeySignatureNatural,
    /// A rest inserted to fill a voice gap.
    GeneratedRest,
    /// A system or page break selected by the engraver.
    EngravedBreak,
    /// A multimeasure rest combining consecutive empty measures.
    MultimeasureRest,
    /// A cautionary key or time signature at a system break.
    Cautionary,
    /// A custom synthesis declared by a layout extension.
    Registered(SynthesisRegistryId),
}

/// The provenance back-reference every layout object carries (Chapter 7
/// §"Provenance"). The pipeline's contract is that this record — `source`,
/// `synthesis`, `dependencies`, and `stable_id` together — is preserved across
/// every stage transition (Chapter 7 §"Stage Contracts").
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Provenance {
    /// The score-graph object this IR object represents or derives from.
    pub source: TypedObjectId,
    /// For engraver-synthesized objects, the synthesis kind; `None` for objects
    /// with a direct score-graph source.
    pub synthesis: Option<SynthesisKind>,
    /// Every score-graph object whose change should invalidate this layout
    /// object (the incremental-layout dependency set, Chapter 7
    /// §"Invalidation Rules").
    pub dependencies: Vec<TypedObjectId>,
    /// Stable across re-layouts where the source is unchanged.
    pub stable_id: LayoutObjectId,
}

impl Provenance {
    /// A provenance for an object projected directly from a score-graph
    /// `source`, with the given invalidation `dependencies` and no synthesis.
    /// The stable id is a pure function of the source — use this for objects
    /// with a single manifestation (a region, a score-level cross-cutting
    /// object). For objects manifested *within* a region, use
    /// [`Provenance::manifested`].
    pub fn projected(source: TypedObjectId, dependencies: Vec<TypedObjectId>) -> Self {
        Provenance {
            stable_id: stable_layout_id(&source),
            source,
            synthesis: None,
            dependencies,
        }
    }

    /// A provenance for an object **manifested within `region`** (Chapter 5
    /// §"Staff-Based Content": a staff instance manifests a globally-identified
    /// staff *for the duration of the region*). The stable id derives from both
    /// the source and the region, so the same score-graph object manifested in
    /// two time-disjoint regions (Chapter 5 §"Region Overlap and Concurrency")
    /// yields two distinct layout objects, not one. The id is still independent
    /// of traversal position, so it is stable across relayouts.
    pub fn manifested(
        source: TypedObjectId,
        region: RegionId,
        dependencies: Vec<TypedObjectId>,
    ) -> Self {
        Provenance {
            stable_id: manifestation_layout_id(&source, region),
            source,
            synthesis: None,
            dependencies,
        }
    }

    /// A provenance for an engraver-synthesized object: it still names a
    /// `source` (the score-graph object it derives from), declares a
    /// [`SynthesisKind`], and lists its invalidation `dependencies`.
    ///
    /// The stable id incorporates the **synthesis kind** and a semantic key, so
    /// distinct synthesized objects from the same source never collide — neither
    /// a cancellation and a cautionary (distinct kinds) nor two cautionaries
    /// (distinct keys). The key must remain stable when unrelated synthesized
    /// objects are inserted, removed, or reordered.
    pub fn synthesized(
        source: TypedObjectId,
        synthesis: SynthesisKind,
        instance: SynthesisInstanceKey,
        dependencies: Vec<TypedObjectId>,
    ) -> Self {
        Provenance {
            stable_id: synthesized_layout_id(&source, synthesis, instance),
            source,
            synthesis: Some(synthesis),
            dependencies,
        }
    }
}

/// Derives the stable layout id of an object from its score-graph **source**
/// alone — never from its traversal position.
///
/// This is what makes the id stable across relayouts (Chapter 7 §"Provenance":
/// stable across re-layouts where the source is unchanged): inserting,
/// removing, or reordering other objects cannot change any object's stable id,
/// because each depends solely on its own source's canonical bytes. This is the
/// ratified derivation of `req:layoutir:object-id-derivation`: domain-separated
/// BLAKE3 truncation under the [`DomainTag::LAYOUT_OBJECT_ID`] (`MUSCLOID`) tag,
/// keyed on `source.canonical_bytes()` for a single manifestation.
pub fn stable_layout_id(source: &TypedObjectId) -> LayoutObjectId {
    LayoutObjectId(
        Preimage::new(DomainTag::LAYOUT_OBJECT_ID)
            .push_bytes(&source.canonical_bytes())
            .finish_trunc128(),
    )
}

/// Derives the stable layout id of an object manifested **within a region**,
/// from its source *and* that region (Chapter 5 §"Region Overlap and
/// Concurrency"). Distinct regions give the same source distinct manifestation
/// ids, so multiple manifestations of one score-graph object are distinct layout
/// objects; the id remains independent of traversal position. The ratified
/// `MUSCLOID` derivation keyed on the pair `(source, region)`
/// (`req:layoutir:object-id-derivation`).
pub fn manifestation_layout_id(source: &TypedObjectId, region: RegionId) -> LayoutObjectId {
    LayoutObjectId(
        Preimage::new(DomainTag::LAYOUT_OBJECT_ID)
            .push_bytes(&source.canonical_bytes())
            .push_bytes(&region.canonical_bytes())
            .finish_trunc128(),
    )
}

/// Derives the stable layout id of an **engraver-synthesized** object from its
/// `source`, its [`SynthesisKind`], and a stable semantic instance key, so
/// distinct synthesis kinds — and distinct instances of one kind — from one
/// source do not collide (Chapter 7 §"Provenance"). The ratified `MUSCLOID`
/// derivation keyed on the triple `(source, synthesis_kind, instance_key)`
/// (`req:layoutir:object-id-derivation`); the instance key is a semantically
/// stable discriminator, never a layout-position ordinal.
pub fn synthesized_layout_id(
    source: &TypedObjectId,
    kind: SynthesisKind,
    instance: SynthesisInstanceKey,
) -> LayoutObjectId {
    let (disc, reg) = match kind {
        SynthesisKind::CancellationAccidental => (0u64, 0u128),
        SynthesisKind::KeySignatureNatural => (1, 0),
        SynthesisKind::GeneratedRest => (2, 0),
        SynthesisKind::EngravedBreak => (3, 0),
        SynthesisKind::MultimeasureRest => (4, 0),
        SynthesisKind::Cautionary => (5, 0),
        SynthesisKind::Registered(id) => (6, id.0),
    };
    let mut p = Preimage::new(DomainTag::LAYOUT_OBJECT_ID);
    p.push_bytes(&source.canonical_bytes());
    p.push_u64_le(disc);
    p.push_u64_le((reg >> 64) as u64);
    p.push_u64_le(reg as u64);
    p.push_u64_le((instance.0 >> 64) as u64);
    p.push_u64_le(instance.0 as u64);
    LayoutObjectId(p.finish_trunc128())
}

#[cfg(test)]
mod tests {
    use super::*;
    use epiphany_core::{EventId, RegionId};

    #[test]
    fn stable_id_is_a_pure_function_of_source() {
        let src = TypedObjectId::Event(EventId::from_raw(0x1234));
        assert_eq!(stable_layout_id(&src), stable_layout_id(&src));
        assert_eq!(
            Provenance::projected(src, vec![]).stable_id,
            stable_layout_id(&src)
        );
    }

    #[test]
    fn distinct_sources_have_distinct_stable_ids() {
        let a = TypedObjectId::Event(EventId::from_raw(1));
        let b = TypedObjectId::Region(RegionId::from_raw(1));
        assert_ne!(stable_layout_id(&a), stable_layout_id(&b));
    }

    #[test]
    fn manifestations_in_distinct_regions_are_distinct() {
        use epiphany_core::StaffId;
        let staff = TypedObjectId::Staff(StaffId::from_raw(1));
        let a = RegionId::from_raw(10);
        let b = RegionId::from_raw(11);
        // Same source, different regions → different manifestation ids.
        assert_ne!(
            manifestation_layout_id(&staff, a),
            manifestation_layout_id(&staff, b)
        );
        // A manifestation id is stable for the same (source, region).
        assert_eq!(
            manifestation_layout_id(&staff, a),
            Provenance::manifested(staff, a, vec![]).stable_id
        );
        // It differs from the context-free source-only id.
        assert_ne!(manifestation_layout_id(&staff, a), stable_layout_id(&staff));
    }

    #[test]
    fn synthesized_objects_from_one_source_do_not_collide() {
        let src = TypedObjectId::Event(EventId::from_raw(1));
        let cancel = Provenance::synthesized(
            src,
            SynthesisKind::CancellationAccidental,
            SynthesisInstanceKey(0),
            vec![],
        );
        let caution = Provenance::synthesized(
            src,
            SynthesisKind::Cautionary,
            SynthesisInstanceKey(0),
            vec![],
        );
        // Same source, different synthesis kinds → distinct ids.
        assert_ne!(cancel.stable_id, caution.stable_id);
        // Same source AND kind, different semantic keys → still distinct ids.
        let caution_b = Provenance::synthesized(
            src,
            SynthesisKind::Cautionary,
            SynthesisInstanceKey(1),
            vec![],
        );
        assert_ne!(caution.stable_id, caution_b.stable_id);
        // And distinct from the plain source-only id.
        assert_ne!(cancel.stable_id, stable_layout_id(&src));
    }

    #[test]
    fn stable_id_uses_the_ratified_muscloid_derivation() {
        use epiphany_determinism::{blake3_256, trunc128};
        let src = TypedObjectId::Event(EventId::from_raw(0x1234));
        // Exactly the MUSCLOID-tagged derivation `req:layoutir:object-id-derivation`
        // pins: domain-separated BLAKE3 over the source's canonical bytes.
        let expected = LayoutObjectId(
            Preimage::new(DomainTag::LAYOUT_OBJECT_ID)
                .push_bytes(&src.canonical_bytes())
                .finish_trunc128(),
        );
        assert_eq!(stable_layout_id(&src), expected);
        // ...and it is genuinely domain-separated — not the old untagged stand-in,
        // so a hash for some other domain over the same bytes cannot alias a layout id.
        let untagged = LayoutObjectId(trunc128(&blake3_256(&src.canonical_bytes())));
        assert_ne!(stable_layout_id(&src), untagged);
    }

    #[test]
    fn the_three_keying_schemes_do_not_collide() {
        use epiphany_core::StaffId;
        // One source, the three manifestation schemes (single / multiply-manifested /
        // synthesized) → three distinct ids, so a single object's id can never alias a
        // manifestation or a synthesized object derived from the same source.
        let src = TypedObjectId::Staff(StaffId::from_raw(7));
        let single = stable_layout_id(&src);
        let manifested = manifestation_layout_id(&src, RegionId::from_raw(3));
        let synthesized =
            synthesized_layout_id(&src, SynthesisKind::GeneratedRest, SynthesisInstanceKey(0));
        assert_ne!(single, manifested);
        assert_ne!(single, synthesized);
        assert_ne!(manifested, synthesized);
    }
}
