//! `gate!` proc-macro: a small DSL that lowers to the Predicate IR.
//!
//! Supported forms (v1):
//!
//! - Boolean composition: `&&`, `||`, `!`, parens.
//! - Atoms:
//!   - `pool!(<entity_ref>, <pool_id_expr>) <op> <int>`        — Base mode
//!   - `pool_eff!(<entity_ref>, <pool_id_expr>) <op> <int>`    — Effective mode
//!   - `type_eq(<entity_ref>, <type_id_expr>)`                 — Cmp(TypeOf, Eq, ConstType)
//!   - `exists(<entity_ref>)`                                  — Predicate::Exists
//!   - `any_attached(<entity_ref>) where { <expr> }`           — AnyOf(AttachedTo, _)
//!   - `all_attached(<entity_ref>) where { <expr> }`           — AllOf(AttachedTo, _)
//!   - `any_contained(<entity_ref>) where { <expr> }`          — AnyOf(ContainedIn, _)
//!   - `all_contained(<entity_ref>) where { <expr> }`          — AllOf(ContainedIn, _)
//!   - `distance(<from>, <to>, <kind_expr>) <op> <int>`         — Cmp(Value::Distance, _, _)
//!   - `is_self()`                                              — RefEq(TARGET, SELF)
//!   - `in_range(<int>)`                                        — Cmp(Distance{OBSERVER, TARGET, MotionLimited}, Le, _)
//!
//! Entity-ref symbols (uppercase keywords): `SELF`, `OWNER`, `OBSERVER`,
//! `TARGET`, `IT`. Anything else in entity-ref position is treated as a
//! Rust expression evaluating to an `EntityId` and wrapped as
//! `EntityRef::ById(<expr>)`.

use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
    parse::{Parse, ParseStream},
    token, Expr, Ident, LitInt, Result, Token,
};

/// Top-level expression in the DSL.
pub enum GateExpr {
    /// `<lhs> && <rhs>`
    And(Box<GateExpr>, Box<GateExpr>),
    /// `<lhs> || <rhs>`
    Or(Box<GateExpr>, Box<GateExpr>),
    /// `!<expr>`
    Not(Box<GateExpr>),
    /// An atom (any non-boolean primary). Boxed — Atom is ~544 bytes
    /// (PoolCmp/PredCallExpr carry user expressions) while the other
    /// variants are ~16 bytes, so storing Atom inline triples the
    /// size of every GateExpr node.
    Atom(Box<Atom>),
}

pub enum Atom {
    /// `pool!(<eref>, <key>) <op> <int>` or `pool_eff!(...) <op> <int>`
    PoolCmp {
        entity: ERef,
        key: Expr,
        mode_effective: bool,
        op: CmpOp,
        rhs: LitInt,
    },
    /// `type_eq(<eref>, <type_id_expr>)`
    TypeEq {
        entity: ERef,
        ty: Expr,
    },
    /// `exists(<eref>)`
    Exists(ERef),
    /// `any_attached(<eref>) where { <body> }` (etc.)
    SetQuant {
        kind: SetQuantKind,
        set: SetKind,
        entity: ERef,
        body: Box<GateExpr>,
    },
    /// `distance(<from>, <to>, <kind_expr>) <op> <int>`
    DistanceCmp {
        from: ERef,
        to: ERef,
        kind: Expr,
        op: CmpOp,
        rhs: LitInt,
    },
    /// `is_self()` — sugar for `RefEq(TARGET, SELF)`. True iff the
    /// current evaluation target equals the entity carrying the
    /// predicate (or whatever the composer has set as SELF for the
    /// evaluation context — for WI1 PickEntities, SELF is the
    /// activator/caster, making `!is_self()` exclude the actor from
    /// the candidate list).
    IsSelf,
    /// `in_range(<int>)` — sugar for
    /// `Cmp(Distance{ from: OBSERVER, to: TARGET, kind: MotionLimited }, Le, n)`.
    /// Distance from the active observer (typically the caster) to
    /// the candidate, respecting motion blockers.
    InRange { rhs: LitInt },
    /// WI1-FU3: `visible_to(<observer_eref>)` — sugar for
    /// `Predicate::VisibleTo { observer: <eref>, target: <implicit> }`.
    /// The implicit target is `EntityRef::It` when this atom appears
    /// inside a `SetQuant` body (any_of / all_of), otherwise
    /// `EntityRef::Target`. The lowering walker tracks the binding
    /// state and chooses accordingly.
    VisibleTo { observer: ERef },
    /// O3 (paradigm audit 2026-05-21): `raw(<expr>)` — embed an
    /// arbitrary Rust expression evaluating to a `Predicate` as a
    /// gate atom. Bridges `gate!` to plugin-side helper macros and
    /// functions that produce `Predicate` directly (e.g. dnd5e's
    /// `has_class!`, `ability_at_least!`, `source_loaded(...)`).
    /// Use sparingly — prefer extending the gate atom grammar for
    /// patterns that recur across plugins.
    Raw { expr: Expr },
}

#[derive(Copy, Clone)]
pub enum SetQuantKind {
    Any,
    All,
}

#[derive(Copy, Clone)]
pub enum SetKind {
    Attached,
    Contained,
}

#[derive(Copy, Clone)]
pub enum CmpOp {
    Lt,
    Le,
    Eq,
    Ne,
    Ge,
    Gt,
}

pub enum ERef {
    Self_,
    Owner,
    Observer,
    Target,
    It,
    /// Arbitrary Rust expression yielding an `EntityId`.
    ById(Expr),
}

// --- Parsing ---------------------------------------------------------------

impl Parse for GateExpr {
    fn parse(input: ParseStream) -> Result<Self> {
        parse_or(input)
    }
}

/// Lowest precedence: `||`.
fn parse_or(input: ParseStream) -> Result<GateExpr> {
    let mut lhs = parse_and(input)?;
    while input.peek(Token![||]) {
        let _: Token![||] = input.parse()?;
        let rhs = parse_and(input)?;
        lhs = GateExpr::Or(Box::new(lhs), Box::new(rhs));
    }
    Ok(lhs)
}

/// Mid precedence: `&&`.
fn parse_and(input: ParseStream) -> Result<GateExpr> {
    let mut lhs = parse_unary(input)?;
    while input.peek(Token![&&]) {
        let _: Token![&&] = input.parse()?;
        let rhs = parse_unary(input)?;
        // Flatten: A && B && C → All([A, B, C]) at lowering time.
        // Tree shape doesn't matter — evaluator and lowering produce
        // identical semantics for nested All. We keep nested for now;
        // lowering walks and flattens.
        lhs = GateExpr::And(Box::new(lhs), Box::new(rhs));
    }
    Ok(lhs)
}

/// Highest precedence: unary `!` and primary.
fn parse_unary(input: ParseStream) -> Result<GateExpr> {
    // A leading `!` could be either unary-not OR the bang in a `pool!` /
    // `pool_eff!` macro call. Disambiguate by lookahead: macro calls have
    // form `<ident> ! ( ... )`. Unary not is followed by something else.
    if input.peek(Token![!]) && !input.peek2(token::Paren) {
        // It's truly unary not — the `!` stands on its own (anything but
        // an immediate `(` after it).
        let _: Token![!] = input.parse()?;
        let inner = parse_unary(input)?;
        return Ok(GateExpr::Not(Box::new(inner)));
    }
    parse_primary(input)
}

fn parse_primary(input: ParseStream) -> Result<GateExpr> {
    // Parenthesized group: `( <expr> )`
    if input.peek(token::Paren) {
        let content;
        syn::parenthesized!(content in input);
        return content.parse::<GateExpr>();
    }

    // Otherwise an atom by leading identifier.
    let ident: Ident = input.fork().parse().map_err(|_| {
        input.error("expected atom: pool!(..), pool_eff!(..), type_eq(..), exists(..), any_attached/all_attached/any_contained/all_contained(..) where {..}, distance(..) <op> <int>, is_self(), in_range(<int>), or `(<expr>)`")
    })?;
    let name = ident.to_string();

    match name.as_str() {
        "pool" | "pool_eff" => parse_pool_cmp(input),
        "type_eq" => parse_type_eq(input),
        "exists" => parse_exists(input),
        "any_attached" | "all_attached" | "any_contained" | "all_contained" => {
            parse_set_quant(input)
        }
        "distance" => parse_distance_cmp(input),
        "is_self" => parse_is_self(input),
        "in_range" => parse_in_range(input),
        "visible_to" => parse_visible_to(input),
        "raw" => parse_raw(input),
        _ => Err(syn::Error::new(
            ident.span(),
            format!("unknown gate! atom `{name}`"),
        )),
    }
}

fn parse_raw(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "raw"
    let content;
    syn::parenthesized!(content in input);
    let expr: Expr = content.parse()?;
    if !content.is_empty() {
        return Err(content.error(
            "raw(<expr>) takes exactly one Rust expression evaluating to a Predicate",
        ));
    }
    Ok(GateExpr::Atom(Box::new(Atom::Raw { expr })))
}

fn parse_pool_cmp(input: ParseStream) -> Result<GateExpr> {
    let kw: Ident = input.parse()?;
    let mode_effective = match kw.to_string().as_str() {
        "pool" => false,
        "pool_eff" => true,
        _ => return Err(syn::Error::new(kw.span(), "expected `pool` or `pool_eff`")),
    };
    let _bang: Token![!] = input.parse()?;
    let content;
    syn::parenthesized!(content in input);
    let entity = parse_eref(&content)?;
    let _comma: Token![,] = content.parse()?;
    let key: Expr = content.parse()?;
    if !content.is_empty() {
        return Err(content.error("expected `)` after pool key"));
    }
    let op = parse_cmp_op(input)?;
    let rhs: LitInt = input.parse()?;
    Ok(GateExpr::Atom(Box::new(Atom::PoolCmp {
        entity,
        key,
        mode_effective,
        op,
        rhs,
    })))
}

fn parse_type_eq(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "type_eq"
    let content;
    syn::parenthesized!(content in input);
    let entity = parse_eref(&content)?;
    let _comma: Token![,] = content.parse()?;
    let ty: Expr = content.parse()?;
    if !content.is_empty() {
        return Err(content.error("expected `)` after type_id expression"));
    }
    Ok(GateExpr::Atom(Box::new(Atom::TypeEq { entity, ty })))
}

fn parse_exists(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "exists"
    let content;
    syn::parenthesized!(content in input);
    let entity = parse_eref(&content)?;
    if !content.is_empty() {
        return Err(content.error("expected `)` after entity ref"));
    }
    Ok(GateExpr::Atom(Box::new(Atom::Exists(entity))))
}

fn parse_set_quant(input: ParseStream) -> Result<GateExpr> {
    let kw: Ident = input.parse()?;
    let (kind, set) = match kw.to_string().as_str() {
        "any_attached" => (SetQuantKind::Any, SetKind::Attached),
        "all_attached" => (SetQuantKind::All, SetKind::Attached),
        "any_contained" => (SetQuantKind::Any, SetKind::Contained),
        "all_contained" => (SetQuantKind::All, SetKind::Contained),
        _ => unreachable!(),
    };
    let content;
    syn::parenthesized!(content in input);
    let entity = parse_eref(&content)?;
    if !content.is_empty() {
        return Err(content.error("expected `)` after entity ref"));
    }
    // `where { <body> }`
    let _where: Token![where] = input.parse()?;
    let body_content;
    syn::braced!(body_content in input);
    let body: GateExpr = body_content.parse()?;
    Ok(GateExpr::Atom(Box::new(Atom::SetQuant {
        kind,
        set,
        entity,
        body: Box::new(body),
    })))
}

fn parse_distance_cmp(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "distance"
    let content;
    syn::parenthesized!(content in input);
    let from = parse_eref(&content)?;
    let _comma1: Token![,] = content.parse()?;
    let to = parse_eref(&content)?;
    let _comma2: Token![,] = content.parse()?;
    let kind: Expr = content.parse()?;
    if !content.is_empty() {
        return Err(content.error("expected `)` after distance kind expression"));
    }
    let op = parse_cmp_op(input)?;
    let rhs: LitInt = input.parse()?;
    Ok(GateExpr::Atom(Box::new(Atom::DistanceCmp {
        from,
        to,
        kind,
        op,
        rhs,
    })))
}

fn parse_is_self(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "is_self"
    let content;
    syn::parenthesized!(content in input);
    if !content.is_empty() {
        return Err(content.error("is_self() takes no arguments"));
    }
    Ok(GateExpr::Atom(Box::new(Atom::IsSelf)))
}

fn parse_in_range(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "in_range"
    let content;
    syn::parenthesized!(content in input);
    let rhs: LitInt = content.parse()?;
    if !content.is_empty() {
        return Err(content.error("in_range(n) takes exactly one integer literal"));
    }
    Ok(GateExpr::Atom(Box::new(Atom::InRange { rhs })))
}

fn parse_visible_to(input: ParseStream) -> Result<GateExpr> {
    let _kw: Ident = input.parse()?; // "visible_to"
    let content;
    syn::parenthesized!(content in input);
    let observer = parse_eref(&content)?;
    if !content.is_empty() {
        return Err(content.error("visible_to(<eref>) takes exactly one entity ref"));
    }
    Ok(GateExpr::Atom(Box::new(Atom::VisibleTo { observer })))
}

fn parse_cmp_op(input: ParseStream) -> Result<CmpOp> {
    if input.peek(Token![<=]) {
        let _: Token![<=] = input.parse()?;
        Ok(CmpOp::Le)
    } else if input.peek(Token![>=]) {
        let _: Token![>=] = input.parse()?;
        Ok(CmpOp::Ge)
    } else if input.peek(Token![==]) {
        let _: Token![==] = input.parse()?;
        Ok(CmpOp::Eq)
    } else if input.peek(Token![!=]) {
        let _: Token![!=] = input.parse()?;
        Ok(CmpOp::Ne)
    } else if input.peek(Token![<]) {
        let _: Token![<] = input.parse()?;
        Ok(CmpOp::Lt)
    } else if input.peek(Token![>]) {
        let _: Token![>] = input.parse()?;
        Ok(CmpOp::Gt)
    } else {
        Err(input.error("expected comparison operator: <, <=, ==, !=, >=, >"))
    }
}

fn parse_eref(input: ParseStream) -> Result<ERef> {
    // If the next token is one of the recognized uppercase keywords,
    // consume it as a symbolic ref. Otherwise parse a full Rust expression
    // and treat as ById(expr).
    if input.peek(Ident) {
        let fork = input.fork();
        if let Ok(ident) = fork.parse::<Ident>() {
            // Only consume from the real input if it's a known symbol AND
            // there isn't more identifier path after it (e.g., `SELF::foo`
            // shouldn't be eaten as Self_).
            let s = ident.to_string();
            let is_keyword = matches!(s.as_str(), "SELF" | "OWNER" | "OBSERVER" | "TARGET" | "IT");
            if is_keyword
                && !fork.peek(Token![::])
                && !fork.peek(Token![.])
                && !fork.peek(token::Paren)
                && !fork.peek(token::Bracket)
            {
                let _: Ident = input.parse()?; // commit
                return Ok(match s.as_str() {
                    "SELF" => ERef::Self_,
                    "OWNER" => ERef::Owner,
                    "OBSERVER" => ERef::Observer,
                    "TARGET" => ERef::Target,
                    "IT" => ERef::It,
                    _ => unreachable!(),
                });
            }
        }
    }
    // Fall back: arbitrary expression yielding EntityId.
    let expr: Expr = input.parse()?;
    Ok(ERef::ById(expr))
}

// --- Lowering --------------------------------------------------------------

pub fn lower(g: &GateExpr) -> TokenStream2 {
    lower_ctx(g, false)
}

/// WI1-FU3: recursive lowering with lexical binding-context tracking.
/// `inside_set_quant` is true inside the body of `any_of`/`all_of`
/// where `EntityRef::It` is bound. Atoms with an implicit target
/// (`visible_to`) read this flag to pick `It` vs `Target`.
fn lower_ctx(g: &GateExpr, inside_set_quant: bool) -> TokenStream2 {
    match g {
        GateExpr::And(_, _) => {
            // Flatten right-spine of nested And nodes into one All(vec![..]).
            let parts = flatten_and(g);
            let lowered: Vec<TokenStream2> = parts.iter().map(|p| lower_ctx(p, inside_set_quant)).collect();
            quote! {
                ::maat::__sdk::predicate::Predicate::All(::std::vec![ #( #lowered ),* ])
            }
        }
        GateExpr::Or(_, _) => {
            let parts = flatten_or(g);
            let lowered: Vec<TokenStream2> = parts.iter().map(|p| lower_ctx(p, inside_set_quant)).collect();
            quote! {
                ::maat::__sdk::predicate::Predicate::Any(::std::vec![ #( #lowered ),* ])
            }
        }
        GateExpr::Not(inner) => {
            let lowered = lower_ctx(inner, inside_set_quant);
            quote! {
                ::maat::__sdk::predicate::Predicate::Not(::std::boxed::Box::new(#lowered))
            }
        }
        GateExpr::Atom(a) => lower_atom(a, inside_set_quant),
    }
}

fn flatten_and(g: &GateExpr) -> Vec<&GateExpr> {
    let mut out = Vec::new();
    fn rec<'a>(g: &'a GateExpr, out: &mut Vec<&'a GateExpr>) {
        match g {
            GateExpr::And(l, r) => {
                rec(l, out);
                rec(r, out);
            }
            other => out.push(other),
        }
    }
    rec(g, &mut out);
    out
}

fn flatten_or(g: &GateExpr) -> Vec<&GateExpr> {
    let mut out = Vec::new();
    fn rec<'a>(g: &'a GateExpr, out: &mut Vec<&'a GateExpr>) {
        match g {
            GateExpr::Or(l, r) => {
                rec(l, out);
                rec(r, out);
            }
            other => out.push(other),
        }
    }
    rec(g, &mut out);
    out
}

fn lower_atom(a: &Atom, inside_set_quant: bool) -> TokenStream2 {
    match a {
        Atom::PoolCmp {
            entity,
            key,
            mode_effective,
            op,
            rhs,
        } => {
            let entity_tok = lower_eref(entity);
            let mode_tok = if *mode_effective {
                quote! { ::maat::__sdk::predicate::PoolMode::Effective }
            } else {
                quote! { ::maat::__sdk::predicate::PoolMode::Base }
            };
            let op_tok = lower_op(*op);
            quote! {
                ::maat::__sdk::predicate::Predicate::Cmp(
                    ::maat::__sdk::predicate::Value::Pool {
                        entity: #entity_tok,
                        key: ::core::clone::Clone::clone(&#key),
                        mode: #mode_tok,
                    },
                    #op_tok,
                    ::maat::__sdk::predicate::Value::Const(#rhs),
                )
            }
        }
        Atom::TypeEq { entity, ty } => {
            let entity_tok = lower_eref(entity);
            quote! {
                ::maat::__sdk::predicate::Predicate::Cmp(
                    ::maat::__sdk::predicate::Value::TypeOf(#entity_tok),
                    ::maat::__sdk::predicate::Op::Eq,
                    ::maat::__sdk::predicate::Value::ConstType(#ty),
                )
            }
        }
        Atom::Exists(e) => {
            let e_tok = lower_eref(e);
            quote! {
                ::maat::__sdk::predicate::Predicate::Exists(#e_tok)
            }
        }
        Atom::DistanceCmp {
            from,
            to,
            kind,
            op,
            rhs,
        } => {
            let from_tok = lower_eref(from);
            let to_tok = lower_eref(to);
            let op_tok = lower_op(*op);
            quote! {
                ::maat::__sdk::predicate::Predicate::Cmp(
                    ::maat::__sdk::predicate::Value::Distance {
                        from: #from_tok,
                        to:   #to_tok,
                        kind: #kind,
                    },
                    #op_tok,
                    ::maat::__sdk::predicate::Value::Const(#rhs),
                )
            }
        }
        Atom::IsSelf => {
            quote! {
                ::maat::__sdk::predicate::Predicate::RefEq(
                    ::maat::__sdk::predicate::EntityRef::Target,
                    ::maat::__sdk::predicate::EntityRef::Self_,
                )
            }
        }
        Atom::InRange { rhs } => {
            quote! {
                ::maat::__sdk::predicate::Predicate::Cmp(
                    ::maat::__sdk::predicate::Value::Distance {
                        from: ::maat::__sdk::predicate::EntityRef::Observer,
                        to:   ::maat::__sdk::predicate::EntityRef::Target,
                        kind: ::maat::__sdk::predicate::MetricKind::MotionLimited,
                    },
                    ::maat::__sdk::predicate::Op::Le,
                    ::maat::__sdk::predicate::Value::Const(#rhs),
                )
            }
        }
        Atom::VisibleTo { observer } => {
            let observer_tok = lower_eref(observer);
            // WI1-FU3 lexical-context dispatch: inside any_of/all_of the
            // iterator binds `It`; outside, the activation target is the
            // implicit subject.
            let target_tok = if inside_set_quant {
                quote! { ::maat::__sdk::predicate::EntityRef::It }
            } else {
                quote! { ::maat::__sdk::predicate::EntityRef::Target }
            };
            quote! {
                ::maat::__sdk::predicate::Predicate::VisibleTo {
                    observer: #observer_tok,
                    target:   #target_tok,
                }
            }
        }
        Atom::Raw { expr } => {
            // O3: pass-through. The expression is expected to evaluate
            // to a `Predicate`; we emit it verbatim so combinators
            // (And, Or, Not) compose with plugin-side helper macros.
            quote! { (#expr) }
        }
        Atom::SetQuant {
            kind,
            set,
            entity,
            body,
        } => {
            let entity_tok = lower_eref(entity);
            let set_tok = match set {
                SetKind::Attached => quote! {
                    ::maat::__sdk::predicate::EntitySet::AttachedTo(#entity_tok)
                },
                SetKind::Contained => quote! {
                    ::maat::__sdk::predicate::EntitySet::ContainedIn(#entity_tok)
                },
            };
            // WI1-FU3: body of any_of/all_of binds EntityRef::It; switch
            // the implicit-target context so visible_to(...) inside picks
            // `It` rather than `Target`.
            let body_tok = lower_ctx(body, true);
            let ctor = match kind {
                SetQuantKind::Any => quote! { ::maat::__sdk::predicate::Predicate::AnyOf },
                SetQuantKind::All => quote! { ::maat::__sdk::predicate::Predicate::AllOf },
            };
            quote! {
                #ctor(#set_tok, ::std::boxed::Box::new(#body_tok))
            }
        }
    }
}

fn lower_op(op: CmpOp) -> TokenStream2 {
    match op {
        CmpOp::Lt => quote! { ::maat::__sdk::predicate::Op::Lt },
        CmpOp::Le => quote! { ::maat::__sdk::predicate::Op::Le },
        CmpOp::Eq => quote! { ::maat::__sdk::predicate::Op::Eq },
        CmpOp::Ne => quote! { ::maat::__sdk::predicate::Op::Ne },
        CmpOp::Ge => quote! { ::maat::__sdk::predicate::Op::Ge },
        CmpOp::Gt => quote! { ::maat::__sdk::predicate::Op::Gt },
    }
}

fn lower_eref(e: &ERef) -> TokenStream2 {
    match e {
        ERef::Self_ => quote! { ::maat::__sdk::predicate::EntityRef::Self_ },
        ERef::Owner => quote! { ::maat::__sdk::predicate::EntityRef::Owner },
        ERef::Observer => quote! { ::maat::__sdk::predicate::EntityRef::Observer },
        ERef::Target => quote! { ::maat::__sdk::predicate::EntityRef::Target },
        ERef::It => quote! { ::maat::__sdk::predicate::EntityRef::It },
        ERef::ById(expr) => quote! {
            ::maat::__sdk::predicate::EntityRef::ById(#expr)
        },
    }
}
