//! Dense, arena-style container for ASPIC+ arguments.
//!
//! [`ArgumentStore`] serves two complementary roles:
//!
//! 1. **Arena allocation** — arguments are stored in a contiguous `Vec`,
//!    indexed by [`ArgumentId`] (a thin `usize` wrapper). This gives O(1)
//!    random access and cache-friendly iteration, while keeping argument
//!    cross-references cheap (just a `usize` copy rather than `Rc`/`Arc`).
//!
//! 2. **Conclusion index** — a secondary `HashMap<Formula, Vec<ArgumentId>>`
//!    maps each conclusion formula to every argument that derives it. The
//!    forward-chaining constructor and the attack-relation computer both need
//!    to answer "which arguments conclude φ?" efficiently; without this index
//!    they would have to scan the entire store on every query.
//!
//! Arguments must be inserted in ID order (enforced by `debug_assert`), so
//! the `ArgumentId` of any argument always equals its position in the
//! inner `Vec`. This invariant is what makes `get` a simple index lookup.

use std::collections::HashMap;

use crate::framework::argument::{Argument, ArgumentId};
use crate::language::formula::Formula;

/// Dense, arena-style container for ASPIC+ arguments.
///
/// See the [module-level documentation](self) for design rationale.
///
/// # Example
///
/// ```
/// use pickaxe_rs::prelude::*;
///
/// let (system, _) = AspicSystemBuilder::new()
///     .ordinary_premise(formula!(a))
///     .defeasible_rule("d1", vec![formula!(a)], formula!(b))
///     .with_classical_negation()
///     .build();
///
/// let pipeline = AspicPipeline::classical(
///     system,
///     Box::new(FlatOrdering),
/// );
/// let result = pipeline.evaluate(Semantics::Grounded).unwrap();
///
/// // Iterate all arguments and print their conclusions.
/// for (id, arg) in result.store.all_arguments() {
///     println!("A{}: {}", id.0, arg.conclusion());
/// }
///
/// // Look up arguments by conclusion formula.
/// let args_for_b = result.store.arguments_for(&formula!(b));
/// assert!(!args_for_b.is_empty());
/// ```
#[derive(Clone, Debug, Default)]
pub struct ArgumentStore {
    arguments: Vec<Argument>,
    by_conclusion: HashMap<Formula, Vec<ArgumentId>>,
}

impl ArgumentStore {
    /// Create an empty argument store.
    pub fn new() -> Self {
        Self::default()
    }

    /// Insert an argument whose `id` must equal [`next_id`](Self::next_id).
    pub fn add(&mut self, argument: Argument) -> ArgumentId {
        let id = argument.id();
        debug_assert_eq!(
            id,
            self.next_id(),
            "ArgumentStore: inserted argument ID {id:?} does not match expected next ID {:?}",
            self.next_id()
        );
        let conclusion = argument.conclusion().clone();
        self.arguments.push(argument);
        self.by_conclusion.entry(conclusion).or_default().push(id);
        id
    }

    /// O(1) lookup by [`ArgumentId`].
    pub fn get(&self, id: ArgumentId) -> Option<&Argument> {
        self.arguments.get(id.0)
    }

    /// All arguments whose conclusion is `conclusion`, in insertion order.
    #[must_use]
    pub fn arguments_for(&self, conclusion: &Formula) -> &[ArgumentId] {
        self.by_conclusion
            .get(conclusion)
            .map_or(&[], std::vec::Vec::as_slice)
    }

    /// Iterate over every `(ArgumentId, &Argument)` pair in insertion order.
    pub fn all_arguments(&self) -> impl Iterator<Item = (ArgumentId, &Argument)> {
        self.arguments
            .iter()
            .enumerate()
            .map(|(i, a)| (ArgumentId(i), a))
    }

    /// The number of arguments in the store.
    pub fn len(&self) -> usize {
        self.arguments.len()
    }

    /// Returns `true` if the store contains no arguments.
    pub fn is_empty(&self) -> bool {
        self.arguments.is_empty()
    }

    /// The [`ArgumentId`] that the next inserted argument must carry.
    pub fn next_id(&self) -> ArgumentId {
        ArgumentId(self.arguments.len())
    }
}
