/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::hash::Hash;

use allocative::Allocative;
use buck2_core::target::label::ConfiguredTargetLabel;
use buck2_core::target::label::TargetLabel;
use derive_more::Display;
use derive_more::From;
use dupe::Dupe;
use serde::Serialize;
use starlark::any::ProvidesStaticType;
use starlark::collections::StarlarkHasher;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::values::starlark_value;
use starlark::values::Heap;
use starlark::values::StarlarkValue;
use starlark::values::Value;
use starlark::values::ValueError;
use starlark::values::ValueLike;

use crate::starlark::values::AllocValue;
use crate::types::configuration::StarlarkConfiguration;
use crate::types::label_relative_path::LabelRelativePath;

#[derive(
    Clone,
    Dupe,
    Debug,
    Hash,
    Display,
    PartialEq,
    Eq,
    From,
    ProvidesStaticType,
    Serialize,
    Allocative
)]
#[serde(transparent)]
pub struct StarlarkTargetLabel {
    label: TargetLabel,
}

starlark_simple_value!(StarlarkTargetLabel);

impl StarlarkTargetLabel {
    pub fn label(&self) -> &TargetLabel {
        &self.label
    }

    pub fn new(label: TargetLabel) -> Self {
        StarlarkTargetLabel { label }
    }
}

#[starlark_value(type = "target_label")]
impl<'v> StarlarkValue<'v> for StarlarkTargetLabel {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(label_methods)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        self.hash(hasher);
        Ok(())
    }

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        if let Some(other) = other.downcast_ref::<Self>() {
            Ok(self.label == other.label)
        } else {
            Ok(false)
        }
    }

    fn compare(&self, other: Value<'v>) -> anyhow::Result<std::cmp::Ordering> {
        if let Some(other) = other.downcast_ref::<Self>() {
            Ok(self.label.cmp(&other.label))
        } else {
            ValueError::unsupported_with(self, "compare", other)
        }
    }
}

#[starlark_module]
fn label_methods(builder: &mut MethodsBuilder) {
    #[starlark(attribute)]
    fn package<'v>(this: &StarlarkTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.pkg().cell_relative_path().as_str())
    }

    #[starlark(attribute)]
    fn name<'v>(this: &StarlarkTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.name().as_str())
    }

    #[starlark(attribute)]
    fn cell<'v>(this: &'v StarlarkTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.pkg().cell_name().as_str())
    }
}

#[derive(
    Clone,
    Dupe,
    Debug,
    Hash,
    Display,
    PartialEq,
    Eq,
    From,
    ProvidesStaticType,
    Serialize,
    Allocative
)]
#[serde(transparent)]
pub struct StarlarkConfiguredTargetLabel {
    label: ConfiguredTargetLabel,
}

starlark_simple_value!(StarlarkConfiguredTargetLabel);

impl StarlarkConfiguredTargetLabel {
    pub fn label(&self) -> &ConfiguredTargetLabel {
        &self.label
    }

    pub fn new(label: ConfiguredTargetLabel) -> Self {
        StarlarkConfiguredTargetLabel { label }
    }
}

#[starlark_value(type = "configured_target_label")]
impl<'v> StarlarkValue<'v> for StarlarkConfiguredTargetLabel {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(configured_label_methods)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        self.hash(hasher);
        Ok(())
    }

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        if let Some(other) = other.downcast_ref::<Self>() {
            Ok(self.label == other.label)
        } else {
            Ok(false)
        }
    }

    fn compare(&self, other: Value<'v>) -> anyhow::Result<std::cmp::Ordering> {
        if let Some(other) = other.downcast_ref::<Self>() {
            Ok(self.label.cmp(&other.label))
        } else {
            ValueError::unsupported_with(self, "compare", other)
        }
    }
}

#[starlark_module]
fn configured_label_methods(builder: &mut MethodsBuilder) {
    #[starlark(attribute)]
    fn package<'v>(this: &StarlarkConfiguredTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.pkg().cell_relative_path().as_str())
    }

    #[starlark(attribute)]
    fn name<'v>(this: &StarlarkConfiguredTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.name().as_str())
    }

    #[starlark(attribute)]
    fn cell<'v>(this: &'v StarlarkConfiguredTargetLabel) -> anyhow::Result<&'v str> {
        Ok(this.label.pkg().cell_name().as_str())
    }

    #[starlark(attribute)]
    fn path<'v>(this: &StarlarkConfiguredTargetLabel, heap: &Heap) -> anyhow::Result<Value<'v>> {
        let path = LabelRelativePath(this.label.pkg().to_cell_path());
        Ok(path.alloc_value(heap))
    }

    fn config<'v>(this: &StarlarkConfiguredTargetLabel) -> anyhow::Result<StarlarkConfiguration> {
        Ok(StarlarkConfiguration((this.label.cfg()).dupe()))
    }

    /// Returns the unconfigured underlying target label.
    fn raw_target(this: &StarlarkConfiguredTargetLabel) -> anyhow::Result<StarlarkTargetLabel> {
        Ok(StarlarkTargetLabel::new(
            (*this.label.unconfigured()).dupe(),
        ))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use buck2_core::configuration::transition::id::TransitionId;
use starlark::any::ProvidesStaticType;
use starlark::values::Value;

#[derive(Debug, thiserror::Error)]
enum TransitionError {
    #[error("cfg parameter is not a transition object: {}", _0)]
    WrongType(String),
}

/// Implemented by starlark transition objects.
pub trait TransitionValue {
    fn transition_id(&self) -> anyhow::Result<Arc<TransitionId>>;
}

unsafe impl<'v> ProvidesStaticType<'v> for &'v dyn TransitionValue {
    type StaticType = &'static dyn TransitionValue;
}

pub fn transition_id_from_value(value: Value) -> anyhow::Result<Arc<TransitionId>> {
    match value.request_value::<&dyn TransitionValue>() {
        Some(has) => has.transition_id(),
        None => Err(TransitionError::WrongType(value.to_repr()).into()),
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use buck2_core::provider::id::ProviderId;
use dupe::Dupe;
use itertools::Itertools;
use starlark::any::ProvidesStaticType;
use starlark::docs::DocFunction;
use starlark::docs::DocItem;
use starlark::docs::DocMember;
use starlark::docs::DocObject;
use starlark::docs::DocProperty;
use starlark::docs::DocReturn;
use starlark::docs::DocString;
use starlark::docs::DocType;
use starlark::environment::GlobalsBuilder;
use starlark::typing::Ty;
use starlark::values::ValueLike;

#[derive(Debug, thiserror::Error)]
enum ProviderCallableError {
    #[error("provider callable did not have a bound id; this is an internal error")]
    ProviderCallableMissingID,
}

pub trait ProviderCallableLike {
    fn id(&self) -> Option<&Arc<ProviderId>>;

    /// Frozen callables should always have this set. It's an error if somehow it doesn't.
    fn require_id(&self) -> anyhow::Result<Arc<ProviderId>> {
        match self.id() {
            Some(id) => Ok(id.dupe()),
            None => Err(ProviderCallableError::ProviderCallableMissingID.into()),
        }
    }

    fn provider_callable_documentation(
        &self,
        creator: Option<for<'a> fn(&'a mut GlobalsBuilder)>,
        overall: &Option<DocString>,
        fields: &[&str],
        field_docs: &[Option<DocString>],
        field_types: &[Option<DocType>],
    ) -> Option<DocItem> {
        let members = itertools::izip!(fields.iter(), field_docs.iter(), field_types.iter())
            .map(|(name, docs, return_type)| {
                let prop = DocProperty {
                    docs: docs.clone(),
                    typ: return_type.clone(),
                };
                (*name, prop)
            })
            .collect::<Vec<_>>();

        let ctor = match creator {
            Some(creator) => {
                let docs = GlobalsBuilder::new().with(creator).build().documentation();
                if docs.members.len() == 1 {
                    match docs.members.into_iter().next() {
                        Some((name, DocMember::Function(x))) => Some((name, x)),
                        _ => None,
                    }
                } else {
                    None
                }
            }
            _ => None,
        };

        match ctor {
            None => Some(DocItem::Object(DocObject {
                docs: overall.clone(),
                members: members
                    .into_iter()
                    .map(|(a, b)| (a.to_owned(), DocMember::Property(b)))
                    .collect(),
            })),
            Some((
                name,
                DocFunction {
                    docs,
                    params,
                    ret,
                    dot_type,
                },
            )) => {
                let summary = if let Some(x) = &docs {
                    x.summary.clone()
                } else if let Some(x) = &overall {
                    x.summary.clone()
                } else {
                    "A provider that can be constructed and have its fields accessed. Returned by rules.".to_owned()
                };
                let mut details = vec![
                    docs.as_ref().and_then(|x| x.details.clone()),
                    if docs.is_some() {
                        overall.as_ref().map(|x| x.summary.clone())
                    } else {
                        None
                    },
                    overall.as_ref().and_then(|x| x.details.clone()),
                    Some("Provides a number of fields that can be accessed:".to_owned()),
                ];
                for (name, member) in members {
                    let typ = member
                        .typ
                        .as_ref()
                        .map_or(&Ty::Any, |x| &x.raw_type)
                        .to_string();
                    let description = member.docs.map_or_else(
                        || "field".to_owned(),
                        |x| x.summary + &x.details.unwrap_or_default(),
                    );
                    details.push(Some(format!("* `{name}: {typ}` - {description}")));
                }
                let docs = Some(DocString {
                    summary,
                    details: Some(details.iter().flatten().join("\n\n")),
                });
                let ret = DocReturn {
                    docs: ret.docs,
                    typ: Some(DocType {
                        raw_type: Ty::name(&name),
                    }),
                };
                Some(DocItem::Function(DocFunction {
                    docs,
                    params,
                    ret,
                    dot_type,
                }))
            }
        }
    }
}

unsafe impl<'v> ProvidesStaticType<'v> for &'v dyn ProviderCallableLike {
    type StaticType = &'static dyn ProviderCallableLike;
}

pub trait ValueAsProviderCallableLike<'v> {
    fn as_provider_callable(&self) -> Option<&'v dyn ProviderCallableLike>;
}

impl<'v, V: ValueLike<'v>> ValueAsProviderCallableLike<'v> for V {
    fn as_provider_callable(&self) -> Option<&'v dyn ProviderCallableLike> {
        self.to_value().request_value::<&dyn ProviderCallableLike>()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod callable;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod testing {
    use buck2_core::configuration::data::ConfigurationData;
    use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
    use buck2_core::pattern::pattern_type::TargetPatternExtra;
    use buck2_core::pattern::ParsedPattern;
    use buck2_core::target::label::TargetLabel;
    use buck2_interpreter::types::label::Label;
    use buck2_interpreter::types::target_label::StarlarkTargetLabel;
    use starlark::environment::GlobalsBuilder;
    use starlark::eval::Evaluator;
    use starlark::starlark_module;

    use crate::interpreter::build_context::BuildContext;

    #[derive(Debug, thiserror::Error)]
    enum LabelCreatorError {
        #[error("Expected provider, found something else: `{0}`")]
        ExpectedProvider(String),
        #[error("Expected target, found something else: `{0}`")]
        ExpectedTarget(String),
    }

    #[starlark_module]
    pub fn label_creator(builder: &mut GlobalsBuilder) {
        fn label<'v>(s: &str, eval: &mut Evaluator<'v, '_>) -> anyhow::Result<Label> {
            let c = BuildContext::from_context(eval)?;
            let target = match ParsedPattern::<ProvidersPatternExtra>::parse_precise(
                s,
                c.cell_info().name().name(),
                c.cell_info().cell_resolver(),
            )? {
                ParsedPattern::Target(package, target_name, providers) => {
                    providers.into_providers_label(package, target_name.as_ref())
                }
                _ => return Err(LabelCreatorError::ExpectedProvider(s.to_owned()).into()),
            };
            Ok(Label::new(
                target.configure(ConfigurationData::testing_new()),
            ))
        }

        fn target_label<'v>(
            s: &str,
            eval: &mut Evaluator<'v, '_>,
        ) -> anyhow::Result<StarlarkTargetLabel> {
            let c = BuildContext::from_context(eval)?;
            let target = match ParsedPattern::<TargetPatternExtra>::parse_precise(
                s,
                c.cell_info().name().name(),
                c.cell_info().cell_resolver(),
            )? {
                ParsedPattern::Target(package, target_name, TargetPatternExtra) => {
                    TargetLabel::new(package, target_name.as_ref())
                }
                _ => return Err(LabelCreatorError::ExpectedTarget(s.to_owned()).into()),
            };
            Ok(StarlarkTargetLabel::new(target))
        }
    }
}

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

    use super::testing::label_creator;
    use crate::interpreter::testing::expect_error;
    use crate::interpreter::testing::Tester;

    #[test]
    fn labels_are_usable() -> anyhow::Result<()> {
        fn new_tester() -> anyhow::Result<Tester> {
            let mut tester = Tester::new()?;
            tester.additional_globals(label_creator);
            Ok(tester)
        }

        let mut tester = new_tester()?;
        tester.run_starlark_bzl_test(indoc!(
            r#"
            frozen_l_default = label("root//foo/bar:baz")
            frozen_l = label("root//foo/bar:baz[something]")
            def test():
                l_default = label("root//foo/bar:baz")
                l = label("root//foo/bar:baz[something]")

                assert_eq_ignore_hash("root//foo/bar:baz (<testing>#<HASH>)", repr(frozen_l_default))
                assert_eq_ignore_hash("root//foo/bar:baz (<testing>#<HASH>)", str(frozen_l_default))
                assert_eq("foo/bar", frozen_l_default.package)
                assert_eq("baz", frozen_l_default.name)
                assert_eq(None, frozen_l_default.sub_target)
                assert_eq("root", frozen_l_default.cell)

                assert_eq_ignore_hash("root//foo/bar:baz[something] (<testing>#<HASH>)", repr(frozen_l))
                assert_eq_ignore_hash("root//foo/bar:baz[something] (<testing>#<HASH>)", str(frozen_l))
                assert_eq("foo/bar", frozen_l.package)
                assert_eq("baz", frozen_l.name)
                assert_eq(["something"], frozen_l.sub_target)

                assert_eq_ignore_hash("root//foo/bar:baz (<testing>#<HASH>)", repr(l_default))
                assert_eq_ignore_hash("root//foo/bar:baz (<testing>#<HASH>)", str(l_default))
                assert_eq("foo/bar", l_default.package)
                assert_eq("baz", l_default.name)
                assert_eq(None, l_default.sub_target)

                assert_eq_ignore_hash("root//foo/bar:baz[something] (<testing>#<HASH>)", repr(l))
                assert_eq_ignore_hash("root//foo/bar:baz[something] (<testing>#<HASH>)", str(l))
                assert_eq("foo/bar", l.package)
                assert_eq("baz", l.name)
                assert_eq(["something"], l.sub_target)
                assert_eq("root", l.cell)

            "#
        ))?;

        let mut tester = new_tester()?;
        let invalid_fields = indoc!(
            r#"
            l = label("root//foo:bar[baz]")
            def test():
                l.invalid_field
            "#
        );
        expect_error(
            tester.run_starlark_test(invalid_fields),
            invalid_fields,
            "Object of type `label` has no attribute `invalid_field`",
        );
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

#![feature(try_blocks)]

use std::sync::Once;

pub mod attrs;
pub mod interpreter;
pub mod label;
pub mod nodes;
pub mod rule;
pub mod super_package;

pub fn init_late_bindings() {
    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        attrs::attrs_global::init_coerce_target_label();
        interpreter::calculation::init_interpreter_calculation_impl();
        interpreter::calculation::init_target_graph_calculation_impl();
        interpreter::build_context::init_starlark_path_from_build_context();
        rule::init_frozen_rule_get_impl();
    });
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::cell::RefCell;
use std::fmt;
use std::fmt::Display;
use std::sync::Arc;

use allocative::Allocative;
use anyhow::Context;
use buck2_core::bzl::ImportPath;
use buck2_core::configuration::transition::id::TransitionId;
use buck2_interpreter::types::rule::FROZEN_RULE_GET_IMPL;
use buck2_interpreter::types::transition::transition_id_from_value;
use buck2_node::attrs::attr::Attribute;
use buck2_node::attrs::spec::AttributeSpec;
use buck2_node::nodes::unconfigured::RuleKind;
use buck2_node::nodes::unconfigured::TargetNode;
use buck2_node::rule::Rule;
use buck2_node::rule_type::RuleType;
use buck2_node::rule_type::StarlarkRuleType;
use derive_more::Display;
use dupe::Dupe;
use gazebo::prelude::*;
use itertools::Itertools;
use starlark::any::ProvidesStaticType;
use starlark::docs::DocFunction;
use starlark::docs::DocItem;
use starlark::docs::DocStringKind;
use starlark::docs::DocType;
use starlark::environment::GlobalsBuilder;
use starlark::eval::Arguments;
use starlark::eval::Evaluator;
use starlark::eval::ParametersSpec;
use starlark::starlark_module;
use starlark::starlark_simple_value;
use starlark::typing::Ty;
use starlark::values::dict::DictOf;
use starlark::values::starlark_value;
use starlark::values::AllocValue;
use starlark::values::Freeze;
use starlark::values::Freezer;
use starlark::values::FrozenValue;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::Value;

use crate::attrs::attribute_as_starlark_value::AttributeAsStarlarkValue;
use crate::interpreter::build_context::BuildContext;
use crate::interpreter::build_context::PerFileTypeContext;
use crate::interpreter::module_internals::ModuleInternals;
use crate::nodes::attr_spec::AttributeSpecExt;
use crate::nodes::unconfigured::TargetNodeExt;

pub static NAME_ATTRIBUTE_FIELD: &str = "name";

/// The callable that's returned from a `rule()` call. Once frozen, and called, it adds targets'
/// parameters to the context
#[derive(Debug, ProvidesStaticType, Trace, NoSerialize, Allocative)]
struct RuleCallable<'v> {
    /// The import path that contains the rule() call; stored here so we can retrieve extra
    /// information during `export_as()`
    import_path: ImportPath,
    /// Once exported, the `import_path` and `name` of the callable. Used in DICE to retrieve rule
    /// implementations
    id: RefCell<Option<StarlarkRuleType>>,
    /// The implementation function for this rule. Must be callable and take a
    /// ctx
    implementation: Value<'v>,
    // Field Name -> Attribute
    attributes: AttributeSpec,
    /// When specified, this transition will be applied to the target before configuring it.
    cfg: Option<Arc<TransitionId>>,
    /// This kind of the rule, e.g. whether it can be used in configuration context.
    rule_kind: RuleKind,
    /// The raw docstring for this rule
    docs: Option<String>,
    /// When evaluating rule function, take only the `name` argument, ignore the others.
    ignore_attrs_for_profiling: bool,
}

impl<'v> Display for RuleCallable<'v> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self.id.borrow() {
            Some(id) => write!(f, "{}()", id.name),
            None => write!(f, "<unbound rule>"),
        }
    }
}

/// Errors around rule declaration, instantiation, validation, etc
#[derive(Debug, thiserror::Error)]
enum RuleError {
    #[error("The output of rule() may only be called after the module is loaded")]
    RuleCalledBeforeFreezing,
    #[error("`{0}` is not a valid attribute name")]
    InvalidParameterName(String),
    #[error("Rule defined in `{0}` must be assigned to a variable, e.g. `my_rule = rule(...)`")]
    RuleNotAssigned(ImportPath),
    #[error(
        "Rule defined with both `is_configuration_rule` and `is_toolchain_rule`, these options are mutually exclusive"
    )]
    IsConfigurationAndToolchain,
    #[error("`rule` can only be declared in bzl files")]
    RuleNonInBzl,
}

impl<'v> AllocValue<'v> for RuleCallable<'v> {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_complex(self)
    }
}

#[starlark_value(type = "rule")]
impl<'v> StarlarkValue<'v> for RuleCallable<'v> {
    fn export_as(&self, variable_name: &str, _eval: &mut Evaluator<'v, '_>) {
        *self.id.borrow_mut() = Some(StarlarkRuleType {
            import_path: self.import_path.clone(),
            name: variable_name.to_owned(),
        });
    }

    fn invoke(
        &self,
        _me: Value<'v>,
        _args: &Arguments<'v, '_>,
        _eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        Err(RuleError::RuleCalledBeforeFreezing.into())
    }

    fn documentation(&self) -> Option<DocItem> {
        let name = self
            .id
            .borrow()
            .as_ref()
            .map_or_else(|| "unbound_rule".to_owned(), |rt| rt.name.clone());
        // TODO(nmj): These return 'None' for default values right now. It's going to take some
        //            refactoring to get that pulled out of the attributespec
        let parameters_spec = self.attributes.signature(name);

        let parameter_types = self
            .attributes
            .starlark_types()
            .into_iter()
            .enumerate()
            .map(|(i, raw_type)| (i, DocType { raw_type }))
            .collect();
        let parameter_docs = self.attributes.docstrings();
        let function_docs = DocFunction::from_docstring(
            DocStringKind::Starlark,
            parameters_spec.documentation(parameter_types, parameter_docs),
            Some(DocType {
                raw_type: Ty::none(),
            }),
            self.docs.as_deref(),
            None,
        );

        Some(DocItem::Function(function_docs))
    }
}

impl<'v> Freeze for RuleCallable<'v> {
    type Frozen = FrozenRuleCallable;
    fn freeze(self, freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
        let frozen_impl = self.implementation.freeze(freezer)?;
        let rule_docs = self.documentation();
        let id = match self.id.into_inner() {
            Some(x) => x,
            None => return Err(RuleError::RuleNotAssigned(self.import_path).into()),
        };
        let rule_type = Arc::new(id);
        let rule_name = rule_type.name.to_owned();
        let signature = self.attributes.signature(rule_name).freeze(freezer)?;

        Ok(FrozenRuleCallable {
            rule: Arc::new(Rule {
                attributes: self.attributes,
                rule_type: RuleType::Starlark(rule_type.dupe()),
                cfg: self.cfg,
                rule_kind: self.rule_kind,
            }),
            rule_type,
            implementation: frozen_impl,
            signature,
            rule_docs,
            ignore_attrs_for_profiling: self.ignore_attrs_for_profiling,
        })
    }
}

#[derive(Debug, Display, ProvidesStaticType, NoSerialize, Allocative)]
#[display(fmt = "{}()", "rule.rule_type.name()")]
pub struct FrozenRuleCallable {
    rule: Arc<Rule>,
    /// Identical to `rule.rule_type` but more specific type.
    rule_type: Arc<StarlarkRuleType>,
    implementation: FrozenValue,
    signature: ParametersSpec<FrozenValue>,
    rule_docs: Option<DocItem>,
    ignore_attrs_for_profiling: bool,
}
starlark_simple_value!(FrozenRuleCallable);

pub(crate) fn init_frozen_rule_get_impl() {
    FROZEN_RULE_GET_IMPL.init(|rule| {
        let rule = rule
            .downcast_frozen_ref::<FrozenRuleCallable>()
            .context("Expecting FrozenRuleCallable")?;
        Ok(rule.implementation)
    })
}

impl FrozenRuleCallable {
    pub fn implementation(&self) -> FrozenValue {
        self.implementation
    }

    pub fn rule_type(&self) -> &Arc<StarlarkRuleType> {
        &self.rule_type
    }

    pub fn attributes(&self) -> &AttributeSpec {
        &self.rule.attributes
    }
}

#[starlark_value(type = "rule")]
impl<'v> StarlarkValue<'v> for FrozenRuleCallable {
    fn invoke(
        &self,
        _me: Value<'v>,
        args: &Arguments<'v, '_>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        let record_target_call_stack =
            ModuleInternals::from_context(eval, self.rule.rule_type.name())?
                .record_target_call_stacks();
        let call_stack = if record_target_call_stack {
            Some(eval.call_stack())
        } else {
            None
        };
        let arg_count = args.len()?;
        self.signature.parser(args, eval, |param_parser, eval| {
            // The body of the callable returned by `rule()`.
            // Records the target in this package's `TargetMap`.
            let internals = ModuleInternals::from_context(eval, self.rule.rule_type.name())?;
            let target_node = TargetNode::from_params(
                self.rule.dupe(),
                internals.package(),
                internals,
                param_parser,
                arg_count,
                self.ignore_attrs_for_profiling,
                call_stack,
            )?;
            internals.record(target_node)?;
            Ok(Value::new_none())
        })
    }

    fn documentation(&self) -> Option<DocItem> {
        self.rule_docs.clone()
    }
}

#[starlark_module]
pub fn register_rule_function(builder: &mut GlobalsBuilder) {
    /// Define a rule. As a simple example:
    ///
    /// ```python
    /// def _my_rule(ctx: "context") -> ["provider"]:
    ///     output = ctx.actions.write("hello.txt", ctx.attrs.contents, executable = ctx.attrs.exe)
    ///     return [DefaultInfo(outputs = [output])]
    ///
    /// MyRule = rule(impl = _my_rule, attrs = {
    ///     "contents": attrs.string(),
    ///     "exe": attrs.option(attrs.bool(), default = False),
    /// })
    /// ```
    fn rule<'v>(
        #[starlark(require = named)] r#impl: Value<'v>,
        #[starlark(require = named)] attrs: DictOf<'v, &'v str, &'v AttributeAsStarlarkValue>,
        #[starlark(require = named)] cfg: Option<Value>,
        #[starlark(require = named, default = "")] doc: &str,
        #[starlark(require = named, default = false)] is_configuration_rule: bool,
        #[starlark(require = named, default = false)] is_toolchain_rule: bool,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<RuleCallable<'v>> {
        // TODO(nmj): Add default attributes in here like 'name', 'visibility', etc
        // TODO(nmj): Verify that names are valid. This is technically handled by the Params
        //                 objects, but will blow up in a friendlier way here.

        let implementation = r#impl;

        let build_context = BuildContext::from_context(eval)?;
        let bzl_path: ImportPath = match &build_context.additional {
            PerFileTypeContext::Bzl(bzl_path) => (*bzl_path).clone(),
            _ => return Err(RuleError::RuleNonInBzl.into()),
        };
        let sorted_validated_attrs = attrs
            .to_dict()
            .into_iter()
            .sorted_by(|(k1, _), (k2, _)| Ord::cmp(k1, k2))
            .map(|(name, value)| {
                if name == NAME_ATTRIBUTE_FIELD {
                    Err(RuleError::InvalidParameterName(NAME_ATTRIBUTE_FIELD.to_owned()).into())
                } else {
                    Ok((name.to_owned(), value.clone_attribute()))
                }
            })
            .collect::<anyhow::Result<Vec<(String, Attribute)>>>()?;

        let cfg = cfg.try_map(transition_id_from_value)?;

        let rule_kind = match (is_configuration_rule, is_toolchain_rule) {
            (false, false) => RuleKind::Normal,
            (true, false) => RuleKind::Configuration,
            (false, true) => RuleKind::Toolchain,
            (true, true) => return Err(RuleError::IsConfigurationAndToolchain.into()),
        };

        Ok(RuleCallable {
            import_path: bzl_path,
            id: RefCell::new(None),
            implementation,
            attributes: AttributeSpec::from(sorted_validated_attrs)?,
            cfg,
            rule_kind,
            docs: Some(doc.to_owned()),
            ignore_attrs_for_profiling: build_context.ignore_attrs_for_profiling,
        })
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use allocative::Allocative;
use buck2_node::attrs::attr::Attribute;
use buck2_node::attrs::attr_type::AttrType;
use buck2_node::attrs::coerced_attr::CoercedAttr;
use dupe::Dupe;
use starlark::any::ProvidesStaticType;
use starlark::starlark_simple_value;
use starlark::values::starlark_value;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;

#[derive(Debug, thiserror::Error)]
enum AttributeAsStarlarkValueError {
    #[error("`attrs.default_only()` cannot be used in nested attributes")]
    DefaultOnlyInNested,
}

#[derive(
    derive_more::Display,
    Debug,
    ProvidesStaticType,
    NoSerialize,
    Allocative
)]
pub struct AttributeAsStarlarkValue(Attribute);

starlark_simple_value!(AttributeAsStarlarkValue);

#[starlark_value(type = "attribute")]
impl<'v> StarlarkValue<'v> for AttributeAsStarlarkValue {}

impl AttributeAsStarlarkValue {
    pub fn new(attr: Attribute) -> Self {
        Self(attr)
    }

    pub fn clone_attribute(&self) -> Attribute {
        self.0.clone()
    }

    /// Coercer to put into higher lever coercer (e. g. for `attrs.list(xxx)`).
    pub fn coercer_for_inner(&self) -> anyhow::Result<AttrType> {
        if self.0.is_default_only() {
            return Err(AttributeAsStarlarkValueError::DefaultOnlyInNested.into());
        }
        Ok(self.0.coercer().dupe())
    }

    pub fn coercer_for_default_only(&self) -> AttrType {
        self.0.coercer().dupe()
    }

    pub fn default(&self) -> Option<&Arc<CoercedAttr>> {
        self.0.default()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use allocative::Allocative;
use anyhow::Context as _;
use buck2_core::provider::label::ProvidersLabel;
use buck2_interpreter::coerce::COERCE_TARGET_LABEL;
use buck2_interpreter::types::provider::callable::ValueAsProviderCallableLike;
use buck2_interpreter::types::transition::transition_id_from_value;
use buck2_node::attrs::attr::Attribute;
use buck2_node::attrs::attr_type::any::AnyAttrType;
use buck2_node::attrs::attr_type::AttrType;
use buck2_node::attrs::coercion_context::AttrCoercionContext;
use buck2_node::attrs::configurable::AttrIsConfigurable;
use buck2_node::attrs::display::AttrDisplayWithContextExt;
use buck2_node::provider_id_set::ProviderIdSet;
use derive_more::Display;
use dupe::Dupe;
use dupe::OptionDupedExt;
use gazebo::prelude::*;
use starlark::environment::GlobalsBuilder;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::eval::Evaluator;
use starlark::starlark_module;
use starlark::values::starlark_value;
use starlark::values::NoSerialize;
use starlark::values::ProvidesStaticType;
use starlark::values::StarlarkValue;
use starlark::values::Value;
use starlark::values::ValueError;
use starlark::StarlarkDocs;
use thiserror::Error;
use tracing::error;

use crate::attrs::attribute_as_starlark_value::AttributeAsStarlarkValue;
use crate::attrs::coerce::attr_type::AttrTypeExt;
use crate::attrs::coerce::ctx::BuildAttrCoercionContext;
use crate::interpreter::build_context::BuildContext;

const OPTION_NONE_EXPLANATION: &str = "`None` as an attribute value always picks the default. For `attrs.option`, if the default isn't `None`, there is no way to express `None`.";

#[derive(Error, Debug)]
enum AttrError {
    #[error(
        "`attrs.option` `default` parameter must be `None` or absent, got `{0}`.\n{}",
        OPTION_NONE_EXPLANATION
    )]
    OptionDefaultNone(String),
    #[error("`attrs.default_only` argument must have a default")]
    DefaultOnlyMustHaveDefault,
}

pub trait AttributeExt {
    /// Helper to create an attribute from attrs.foo functions
    fn attr<'v>(
        eval: &mut Evaluator<'v, '_>,
        default: Option<Value<'v>>,
        doc: &str,
        coercer: AttrType,
    ) -> anyhow::Result<AttributeAsStarlarkValue>;

    /// An `attr` which is not allowed to have a default as a relative label.
    fn check_not_relative_label<'v>(default: Option<Value<'v>>, attr: &str) -> anyhow::Result<()>;
}

impl AttributeExt for Attribute {
    /// Helper to create an attribute from attrs.foo functions
    fn attr<'v>(
        eval: &mut Evaluator<'v, '_>,
        default: Option<Value<'v>>,
        doc: &str,
        coercer: AttrType,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let default = match default {
            None => None,
            Some(x) => Some(Arc::new(
                coercer
                    .coerce(
                        AttrIsConfigurable::Yes,
                        &get_attr_coercion_context(eval)?,
                        x,
                    )
                    .context("Error coercing attribute default")?,
            )),
        };
        Ok(AttributeAsStarlarkValue::new(Attribute::new(
            default, doc, coercer,
        )))
    }

    /// An `attr` which is not allowed to have a default as a relative label.
    fn check_not_relative_label<'v>(default: Option<Value<'v>>, attr: &str) -> anyhow::Result<()> {
        if let Some(as_str) = default.and_then(|x| x.unpack_str()) {
            if ProvidersLabel::maybe_relative_label(as_str) {
                return Err(DepError::DepRelativeDefault {
                    invalid_label: as_str.to_owned(),
                    attr: attr.to_owned(),
                }
                .into());
            }
        }
        Ok(())
    }
}

/// Grab a new coercion context object based on the main build file that is being evaluated.
/// This is used because we do not have access to a specific shared instance via ctx.extra
/// when evaluating .bzl files
pub(crate) fn get_attr_coercion_context<'v>(
    eval: &Evaluator<'v, '_>,
) -> anyhow::Result<BuildAttrCoercionContext> {
    let build_context = BuildContext::from_context(eval)?;
    Ok(BuildAttrCoercionContext::new_no_package(
        build_context.cell_info().cell_resolver().dupe(),
        build_context.cell_info().name().name(),
    ))
}

pub(crate) fn init_coerce_target_label() {
    COERCE_TARGET_LABEL.init(|eval, value| get_attr_coercion_context(eval)?.coerce_target(value))
}

#[derive(Debug, thiserror::Error)]
enum DepError {
    #[error(
        "relative labels ('{invalid_label}') are not permitted as default values for `{attr}` \
        attributes. Use a fully qualified one like //foo:bar"
    )]
    DepRelativeDefault { invalid_label: String, attr: String },
}

/// Common code to handle `providers` argument of dep-like attrs.
fn dep_like_attr_handle_providers_arg(providers: Vec<Value>) -> anyhow::Result<ProviderIdSet> {
    Ok(ProviderIdSet::from(providers.try_map(
        |v| match v.as_provider_callable() {
            Some(callable) => callable.require_id(),
            None => Err(ValueError::IncorrectParameterTypeNamed("providers".to_owned()).into()),
        },
    )?))
}

/// This type is available as a global `attrs` symbol, to allow the definition of attributes to the `rule` function.
///
/// As an example:
///
/// ```python
/// rule(impl = _impl, attrs = {"foo": attrs.string(), "bar": attrs.int(default = 42)})
/// ```
///
/// Most attributes take at least two optional parameters:
///
/// * A `doc` parameter, which specifies documentation for the attribute.
///
/// * A `default` parameter, which if present specifies the default value for the attribute if omitted.
///   If there is no default, the user of the rule must supply that parameter.
///
/// Each attribute defines what values it accepts from the user, and which values it gives to the rule.
/// For simple types like `attrs.string` these are the same, for more complex types like `attrs.dep` these
/// are different (string from the user, dependency to the rule).
#[starlark_module]
fn attr_module(registry: &mut MethodsBuilder) {
    /// Takes a string from the user, supplies a string to the rule.
    fn string<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named)] validate: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let _unused = validate;
        Attribute::attr(eval, default, doc, AttrType::string())
    }

    /// Takes a list from the user, supplies a list to the rule.
    fn list<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] inner: &AttributeAsStarlarkValue,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let coercer = AttrType::list(inner.coercer_for_inner()?);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes a target from the user, as a string, and supplies a dependency to the rule.
    /// The dependency will transition to the execution platform. Use `exec_dep` if you
    /// plan to execute things from this dependency as part of the compilation.
    fn exec_dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.exec_dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let coercer = AttrType::exec_dep(required_providers);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes a target from the user, as a string, and supplies a dependency to the rule.
    /// The dependency will be a toolchain dependency, meaning that its execution platform
    /// dependencies will be used to select the execution platform for this rule.
    fn toolchain_dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.toolchain_dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let coercer = AttrType::toolchain_dep(required_providers);
        Attribute::attr(eval, default, doc, coercer)
    }

    fn transition_dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] cfg: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.transition_dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let transition_id = transition_id_from_value(cfg)?;
        let coercer = AttrType::transition_dep(required_providers, transition_id);

        let coerced_default = match default {
            None => None,
            Some(default) => {
                match coercer.coerce(
                    AttrIsConfigurable::Yes,
                    &get_attr_coercion_context(eval)?,
                    default,
                ) {
                    Ok(coerced_default) => Some(coerced_default),
                    Err(_) => return Err(ValueError::IncorrectParameterType.into()),
                }
            }
        };

        Ok(AttributeAsStarlarkValue::new(Attribute::new(
            coerced_default.map(Arc::new),
            doc,
            coercer,
        )))
    }

    fn configured_dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.configured_dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let coercer = AttrType::configured_dep(required_providers);
        Attribute::attr(eval, default, doc, coercer)
    }

    fn split_transition_dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] cfg: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.split_transition_dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let transition_id = transition_id_from_value(cfg)?;
        let coercer = AttrType::split_transition_dep(required_providers, transition_id);

        let coerced_default = match default {
            None => None,
            Some(default) => {
                match coercer.coerce(
                    AttrIsConfigurable::Yes,
                    &get_attr_coercion_context(eval)?,
                    default,
                ) {
                    Ok(coerced_default) => Some(coerced_default),
                    Err(_) => return Err(ValueError::IncorrectParameterType.into()),
                }
            }
        };

        Ok(AttributeAsStarlarkValue::new(Attribute::new(
            coerced_default.map(Arc::new),
            doc,
            coercer,
        )))
    }

    /// Takes a target from the user, as a string, and supplies a dependency to the rule.
    /// A target can be specified as an absolute dependency `foo//bar:baz`, omitting the
    /// cell (`//bar:baz`) or omitting the package name (`:baz`).
    ///
    /// If supplied the `providers` argument ensures that specific providers will be present
    /// on the dependency.
    fn dep<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = Vec::new())] providers: Vec<Value<'v>>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.dep")?;
        let required_providers = dep_like_attr_handle_providers_arg(providers)?;
        let coercer = AttrType::dep(required_providers);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes most builtin literals and passes them to the rule as a string.
    /// Discouraged, as it provides little type safety and destroys the structure.
    fn any<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = "")] doc: &str,
        #[starlark(require = named)] default: Option<Value<'v>>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, default, doc, AttrType::any())
    }

    /// Takes a boolean and passes it to the rule as a boolean.
    fn bool<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, default, doc, AttrType::bool())
    }

    /// Takes a value that may be `None` or some inner type, and passes either `None` or the
    /// value corresponding to the inner to the rule. Often used to make a rule optional:
    ///
    /// ```python
    /// attrs.option(attr.string(), default = None)
    /// ```
    fn option<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] inner: &AttributeAsStarlarkValue,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let coercer = AttrType::option(inner.coercer_for_inner()?);
        let attr = Attribute::attr(eval, default, doc, coercer)?;

        match attr.default() {
            Some(default) if !default.may_return_none() => {
                Err(AttrError::OptionDefaultNone(default.as_display_no_ctx().to_string()).into())
            }
            _ => Ok(attr),
        }
    }

    /// Rejects all values and uses the default for the inner argument.
    /// Often used to resolve dependencies, which otherwise can't be resolved inside a rule.
    ///
    /// ```python
    /// attrs.default_only(attrs.dep(default = "foo//my_package:my_target"))
    /// ```
    fn default_only<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] inner: &AttributeAsStarlarkValue,
        #[starlark(require = named, default = "")] doc: &str,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let Some(default) = inner.default().duped() else {
            return Err(AttrError::DefaultOnlyMustHaveDefault.into());
        };
        Ok(AttributeAsStarlarkValue::new(Attribute::new_default_only(
            default,
            doc,
            inner.coercer_for_default_only(),
        )))
    }

    /// Takes a target (as per `deps`) and passes a `label` to the rule.
    /// Validates that the target exists, but does not introduce a dependency on it.
    fn label<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, default, doc, AttrType::label())
    }

    /// Takes a dict from the user, supplies a dict to the rule.
    fn dict<'v>(
        #[starlark(this)] _this: Value<'v>,
        // TODO(nga): require positional only for key and value.
        key: &AttributeAsStarlarkValue,
        value: &AttributeAsStarlarkValue,
        #[starlark(require = named, default = false)] sorted: bool,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let coercer = AttrType::dict(key.coercer_for_inner()?, value.coercer_for_inner()?, sorted);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes a command line argument from the user and supplies a `cmd_args` compatible value to the rule.
    /// The argument may contain special macros such as `$(location :my_target)` or `$(exe :my_target)` which
    /// will be replaced with references to those values in the rule. Takes in an optional `anon_target_compatible`
    /// flag, which indicates whether the args can be passed into anon targets. Note that there is a slight memory
    /// hit when using this flag.
    fn arg<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = false)] json: bool,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        #[starlark(require = named, default = false)] anon_target_compatible: bool,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let _unused = json;
        Attribute::attr(eval, default, doc, AttrType::arg(anon_target_compatible))
    }

    /// Takes a string from one of the variants given, and gives that string to the rule.
    /// Strings are matched case-insensitively, and always passed to the rule lowercase.
    fn r#enum<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] variants: Vec<String>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        // Value seems to usually be a `[String]`, listing the possible values of the
        // enumeration. Unfortunately, for things like `exported_lang_preprocessor_flags`
        // it ends up being `Type` which doesn't match the data we see.
        Attribute::attr(eval, default, doc, AttrType::enumeration(variants)?)
    }

    fn configuration_label<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        // TODO(nga): explain how this is different from `dep`.
        //   This probably meant to be similar to `label`, but not configurable.
        Attribute::attr(eval, None, doc, AttrType::dep(ProviderIdSet::EMPTY))
    }

    /// Currently an alias for `attrs.string`.
    fn regex<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, default, doc, AttrType::string())
    }

    fn set<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] value_type: &AttributeAsStarlarkValue,
        #[starlark(require = named, default = false)] sorted: bool,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let _unused = sorted;
        let coercer = AttrType::list(value_type.coercer_for_inner()?);
        Attribute::attr(eval, default, doc, coercer)
    }

    fn named_set<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = pos)] value_type: &AttributeAsStarlarkValue,
        #[starlark(require = named, default = false)] sorted: bool,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let value_coercer = value_type.coercer_for_inner()?;
        let coercer = AttrType::one_of(vec![
            AttrType::dict(AttrType::string(), value_coercer.dupe(), sorted),
            AttrType::list(value_coercer),
        ]);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Given a list of alternative attributes, selects the first that matches and gives that to the rule.
    fn one_of<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(args)] args: Vec<&AttributeAsStarlarkValue>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let coercer = AttrType::one_of(args.into_try_map(|arg| arg.coercer_for_inner())?);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes a tuple of values and gives a tuple to the rule.
    fn tuple<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(args)] args: Vec<&AttributeAsStarlarkValue>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        let coercer = AttrType::tuple(args.into_try_map(|arg| arg.coercer_for_inner())?);
        Attribute::attr(eval, default, doc, coercer)
    }

    /// Takes an int from the user, supplies an int to the rule.
    fn int<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, default, doc, AttrType::int())
    }

    fn query<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::attr(eval, None, doc, AttrType::query())
    }

    fn versioned<'v>(
        #[starlark(this)] _this: Value<'v>,
        value_type: &AttributeAsStarlarkValue,
        #[starlark(require = named, default = "")] doc: &str,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        // A versioned field looks like:
        // [ ({"key":"value1"}, arg), ({"key":"value2"}, arg) ]
        let element_type = AttrType::tuple(vec![
            AttrType::dict(AttrType::string(), AttrType::string(), false),
            value_type.coercer_for_inner()?,
        ]);
        let coercer = AttrType::list(element_type.dupe());

        Ok(AttributeAsStarlarkValue::new(Attribute::new(
            Some(Arc::new(AnyAttrType::empty_list())),
            doc,
            coercer,
        )))
    }

    /// Takes a source file from the user, supplies an artifact to the rule.
    /// The source file may be specified as a literal string
    /// (representing the path within this package), or a target (which must have a
    /// `DefaultInfo` with a `default_outputs` value).
    fn source<'v>(
        #[starlark(this)] _this: Value<'v>,
        #[starlark(require = named, default = false)] allow_directory: bool,
        #[starlark(require = named)] default: Option<Value<'v>>,
        #[starlark(require = named, default = "")] doc: &str,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<AttributeAsStarlarkValue> {
        Attribute::check_not_relative_label(default, "attrs.source")?;
        Attribute::attr(eval, default, doc, AttrType::source(allow_directory))
    }
}

#[derive(
    Display,
    Debug,
    StarlarkDocs,
    Allocative,
    ProvidesStaticType,
    NoSerialize
)]
#[display(fmt = "<attrs>")]
struct Attrs;

#[starlark_value(type = "attrs")]
impl<'v> StarlarkValue<'v> for Attrs {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(attr_module)
    }
}

pub fn register_attrs(globals: &mut GlobalsBuilder) {
    globals.set("attrs", globals.frozen_heap().alloc_simple(Attrs));
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::interpreter::testing::Tester;

    #[test]
    fn test_attr_display() -> anyhow::Result<()> {
        let mut tester = Tester::new().unwrap();
        tester.additional_globals(register_attrs);
        tester.run_starlark_bzl_test(r#"
def assert_eq(a, b):
    if a != b:
        fail(a + " != " + b)

assert_eq(repr(attrs.bool(default = True)), "attrs.bool(default=True)")
assert_eq(repr(attrs.string()), "attrs.string()")
assert_eq(repr(attrs.list(attrs.string())), "attrs.list(attrs.string())")
assert_eq(repr(attrs.dict(attrs.string(), attrs.string())), "attrs.dict(attrs.string(), attrs.string(), sorted=False)")
assert_eq(repr(attrs.one_of(attrs.string())), "attrs.one_of(attrs.string())")
assert_eq(repr(attrs.tuple(attrs.string())), "attrs.tuple(attrs.string())")
assert_eq(repr(attrs.option(attrs.string())), "attrs.option(attrs.string())")

def test(): pass
"#)?;
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use anyhow::Context;
use buck2_node::attrs::attr::Attribute;
use buck2_node::attrs::attr::CoercedValue;
use buck2_node::attrs::coercion_context::AttrCoercionContext;
use buck2_node::attrs::configurable::AttrIsConfigurable;
use starlark::docs::DocString;
use starlark::docs::DocStringKind;
use starlark::typing::Ty;
use starlark::values::Value;

use crate::attrs::coerce::attr_type::AttrTypeExt;
use crate::attrs::coerce::error::CoercionError;

pub mod attribute_as_starlark_value;
pub mod attrs_global;
pub mod coerce;

#[derive(Debug, thiserror::Error)]
enum AttrCoerceError {
    #[error("Parameter `{0}` had no value provided, but it is mandatory")]
    MissingMandatoryParameter(String),
}

pub trait AttributeCoerceExt {
    fn coerce<'v>(
        &self,
        param_name: &str,
        configurable: AttrIsConfigurable,
        coercer_ctx: &dyn AttrCoercionContext,
        value: Value<'v>,
    ) -> anyhow::Result<CoercedValue>;

    fn docstring(&self) -> Option<DocString>;

    fn starlark_type(&self) -> Ty;
}

impl AttributeCoerceExt for Attribute {
    /// Attempt to coerce a value. If the value provided is `None`, and a default value is available,
    /// that default value is returned.
    fn coerce<'v>(
        &self,
        param_name: &str,
        configurable: AttrIsConfigurable,
        coercer_ctx: &dyn AttrCoercionContext,
        value: Value<'v>,
    ) -> anyhow::Result<CoercedValue> {
        if self.is_default_only() {
            if value.is_none() {
                return Ok(CoercedValue::Default);
            } else {
                return Err(CoercionError::DefaultOnly(value.to_string()).into());
            }
        }

        match self.default() {
            default if !value.is_none() => self
                .coercer()
                .coerce_with_default(configurable, coercer_ctx, value, default.map(|x| &**x))
                .map(CoercedValue::Custom)
                .with_context(|| {
                    format!(
                        "Error coercing attribute `{}` of type `{}`",
                        param_name, self
                    )
                }),
            Some(_) => Ok(CoercedValue::Default),
            None => Err(AttrCoerceError::MissingMandatoryParameter(param_name.to_owned()).into()),
        }
    }

    fn docstring(&self) -> Option<DocString> {
        DocString::from_docstring(DocStringKind::Starlark, self.doc())
    }

    fn starlark_type(&self) -> Ty {
        self.coercer().starlark_type()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::cell::RefCell;

use buck2_util::arc_str::ArcStr;
use dupe::Dupe;
use hashbrown::raw::RawTable;

use crate::attrs::coerce::str_hash::str_hash;

pub(crate) struct ArcStrInterner {
    cache: RefCell<RawTable<(u64, ArcStr)>>,
}

impl ArcStrInterner {
    pub(crate) fn new() -> ArcStrInterner {
        ArcStrInterner {
            cache: RefCell::new(RawTable::new()),
        }
    }

    pub(crate) fn intern(&self, s: &str) -> ArcStr {
        if s.is_empty() {
            return ArcStr::default();
        }

        let hash = str_hash(s);
        let mut cache = self.cache.borrow_mut();

        if let Some((_h, v)) = cache.get(hash, |(_h, v)| s == v.as_str()) {
            return v.dupe();
        }

        let value = ArcStr::from(s);
        cache.insert(hash, (hash, value.dupe()), |(h, _v)| *h);
        value
    }
}

#[cfg(test)]
mod tests {
    use std::ptr;

    use crate::attrs::coerce::arc_str_interner::ArcStrInterner;

    #[test]
    fn test_arc_str_interner() {
        let interner = ArcStrInterner::new();
        let foo0 = interner.intern("foo");
        let foo1 = interner.intern("foo");

        assert!(ptr::eq(foo0.as_ptr(), foo1.as_ptr()));

        let bar0 = interner.intern("bar");
        let bar1 = interner.intern("bar");

        assert!(ptr::eq(bar0.as_ptr(), bar1.as_ptr()));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

//! Contains the internal support within the attribute framework for `select()`.

use anyhow::Context;
use buck2_node::attrs::attr_type::AttrType;
use buck2_node::attrs::coerced_attr::CoercedAttr;
use buck2_node::attrs::coerced_attr::CoercedSelector;
use buck2_node::attrs::coercion_context::AttrCoercionContext;
use buck2_node::attrs::configurable::AttrIsConfigurable;
use starlark::values::dict::DictRef;
use starlark::values::Value;
use thiserror::Error;

use crate::attrs::coerce::attr_type::AttrTypeExt;
use crate::interpreter::selector::StarlarkSelector;
use crate::interpreter::selector::StarlarkSelectorGen;

#[derive(Error, Debug)]
enum SelectError {
    #[error("select() condition was not a string, got `{0}`.")]
    KeyNotString(String),
    #[error("select() value was not a dict, got `{0}`.")]
    ValueNotDict(String),
    #[error("addition not supported for this attribute type `{0}`, got `{1}`.")]
    ConcatNotSupported(String, String),
    #[error("select() cannot be used in non-configuable attribute")]
    SelectCannotBeUsedForNonConfigurableAttr,
    #[error("duplicate `\"DEFAULT\"` key in `select()` (internal error)")]
    DuplicateDefaultKey,
}

pub trait CoercedAttrExr: Sized {
    fn coerce(
        attr: &AttrType,
        configuable: AttrIsConfigurable,
        ctx: &dyn AttrCoercionContext,
        value: Value,
        default_attr: Option<&Self>,
    ) -> anyhow::Result<Self>;
}

impl CoercedAttrExr for CoercedAttr {
    fn coerce(
        attr: &AttrType,
        configuable: AttrIsConfigurable,
        ctx: &dyn AttrCoercionContext,
        value: Value,
        default_attr: Option<&Self>,
    ) -> anyhow::Result<Self> {
        // A Selector in starlark is currently implemented as simply a Value (holding a
        // dict if valid).
        //
        // TODO(cjhopman): the select() function itself should
        // perform the conversion of its case arguments to configuration labels.
        //
        // TODO(cjhopman): Selectable addition (__ladd__ and __radd__) should perform
        // verification that the two sides of the addition have the same type.
        // Even if it did, we still need to verify that the two sides
        // are actually compatible (i.e. selectable can ensure that both sides are
        // lists, we can ensure that  both sides are List<T>)
        if let Some(selector) = StarlarkSelector::from_value(value) {
            if let AttrIsConfigurable::No = configuable {
                return Err(SelectError::SelectCannotBeUsedForNonConfigurableAttr.into());
            }

            match *selector {
                StarlarkSelectorGen::Inner(v) => {
                    if let Some(dict) = DictRef::from_value(v) {
                        let has_default = dict.get_str("DEFAULT").is_some();
                        let mut entries =
                            Vec::with_capacity(dict.len().saturating_sub(has_default as usize));

                        let mut default = None;
                        for (k, v) in dict.iter() {
                            let k = k.unpack_str().ok_or_else(|| {
                                anyhow::anyhow!(SelectError::KeyNotString(k.to_repr()))
                            })?;
                            let v = match default_attr {
                                Some(default_attr) if v.is_none() => default_attr.clone(),
                                _ => CoercedAttr::coerce(attr, configuable, ctx, v, None)?,
                            };
                            if k == "DEFAULT" {
                                if default.is_some() {
                                    return Err(SelectError::DuplicateDefaultKey.into());
                                }
                                default = Some(v);
                            } else {
                                let target = ctx.coerce_target(k)?;
                                entries.push((target, v));
                            }
                        }

                        assert_eq!(entries.capacity(), entries.len());

                        Ok(CoercedAttr::Selector(Box::new(CoercedSelector::new(
                            ctx.intern_select(entries),
                            default,
                        )?)))
                    } else {
                        Err(anyhow::anyhow!(SelectError::ValueNotDict(v.to_repr())))
                    }
                }
                StarlarkSelectorGen::Added(l, r) => {
                    if !attr.supports_concat() {
                        return Err(anyhow::anyhow!(SelectError::ConcatNotSupported(
                            attr.to_string(),
                            format!("{} + {}", l, r)
                        )));
                    }
                    let l = CoercedAttr::coerce(attr, configuable, ctx, l, None)?;
                    let mut l = match l {
                        CoercedAttr::Concat(l) => l.into_vec(),
                        l => vec![l],
                    };
                    let r = CoercedAttr::coerce(attr, configuable, ctx, r, None)?;
                    let r = match r {
                        CoercedAttr::Concat(r) => r.into_vec(),
                        r => vec![r],
                    };

                    l.extend(r);
                    Ok(CoercedAttr::Concat(l.into_boxed_slice()))
                }
            }
        } else {
            Ok(attr
                .coerce_item(configuable, ctx, value)
                .with_context(|| format!("Error coercing {}", value))?)
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::sync::Arc;

    use buck2_core::configuration::config_setting::ConfigSettingData;
    use buck2_core::configuration::constraints::ConstraintKey;
    use buck2_core::configuration::constraints::ConstraintValue;
    use buck2_core::configuration::data::ConfigurationData;
    use buck2_core::configuration::pair::ConfigurationNoExec;
    use buck2_core::configuration::pair::ConfigurationWithExec;
    use buck2_core::configuration::transition::applied::TransitionApplied;
    use buck2_core::configuration::transition::id::TransitionId;
    use buck2_core::target::label::TargetLabel;
    use buck2_node::attrs::attr_type::bool::BoolLiteral;
    use buck2_node::attrs::attr_type::string::StringLiteral;
    use buck2_node::attrs::coerced_attr::CoercedAttr;
    use buck2_node::attrs::coerced_attr::CoercedSelector;
    use buck2_node::attrs::configuration_context::AttrConfigurationContext;
    use buck2_node::attrs::fmt_context::AttrFmtContext;
    use buck2_util::arc_str::ArcSlice;
    use buck2_util::arc_str::ArcStr;
    use buck2_util::collections::ordered_map::OrderedMap;
    use dupe::Dupe;

    #[test]
    fn selector_equals_accounts_for_ordering() {
        let s1 = CoercedAttr::Selector(Box::new(
            CoercedSelector::new(
                ArcSlice::new([
                    (
                        TargetLabel::testing_parse("cell1//pkg1:target1"),
                        CoercedAttr::Bool(BoolLiteral(true)),
                    ),
                    (
                        TargetLabel::testing_parse("cell2//pkg2:target2"),
                        CoercedAttr::Bool(BoolLiteral(false)),
                    ),
                ]),
                None,
            )
            .unwrap(),
        ));
        let s2 = CoercedAttr::Selector(Box::new(
            CoercedSelector::new(
                ArcSlice::new([
                    (
                        TargetLabel::testing_parse("cell1//pkg1:target1"),
                        CoercedAttr::Bool(BoolLiteral(true)),
                    ),
                    (
                        TargetLabel::testing_parse("cell2//pkg2:target2"),
                        CoercedAttr::Bool(BoolLiteral(false)),
                    ),
                ]),
                None,
            )
            .unwrap(),
        ));

        assert_eq!(s1 == s2, true);

        let s2 = CoercedAttr::Selector(Box::new(
            CoercedSelector::new(
                ArcSlice::new([
                    (
                        TargetLabel::testing_parse("cell2//pkg2:target2"),
                        CoercedAttr::Bool(BoolLiteral(false)),
                    ),
                    (
                        TargetLabel::testing_parse("cell1//pkg1:target1"),
                        CoercedAttr::Bool(BoolLiteral(true)),
                    ),
                ]),
                None,
            )
            .unwrap(),
        ));

        assert_eq!(s1 == s2, false);
    }

    #[test]
    fn select_the_most_specific() {
        struct SelectTestConfigurationContext {
            settings: BTreeMap<TargetLabel, ConfigSettingData>,
        }

        impl AttrConfigurationContext for SelectTestConfigurationContext {
            fn matches<'a>(&'a self, label: &TargetLabel) -> Option<&'a ConfigSettingData> {
                self.settings.get(label)
            }

            fn cfg(&self) -> ConfigurationNoExec {
                panic!()
            }

            fn exec_cfg(&self) -> ConfigurationNoExec {
                unimplemented!()
            }

            fn toolchain_cfg(&self) -> ConfigurationWithExec {
                panic!("not used in test")
            }

            fn platform_cfg(&self, _label: &TargetLabel) -> anyhow::Result<ConfigurationData> {
                panic!("not used in test")
            }

            fn resolved_transitions(
                &self,
            ) -> &OrderedMap<Arc<TransitionId>, Arc<TransitionApplied>> {
                panic!("not used in test")
            }
        }

        fn constraint_key(t: &str) -> ConstraintKey {
            ConstraintKey(TargetLabel::testing_parse(t))
        }

        fn constraint_value(t: &str) -> ConstraintValue {
            ConstraintValue(TargetLabel::testing_parse(t))
        }

        let c_os = constraint_key("config//c:os");
        let c_linux = constraint_value("config//c:linux");
        let c_cpu = constraint_key("config//c:cpu");
        let c_arm64 = constraint_value("config//c:arm64");
        let c_x86_64 = constraint_value("config//c:x86_64");

        let linux = TargetLabel::testing_parse("config//:linux");
        let linux_arm64 = TargetLabel::testing_parse("config//:linux-arm64");
        let linux_x86_64 = TargetLabel::testing_parse("config//:linux-x86_64");

        let ctx = SelectTestConfigurationContext {
            settings: BTreeMap::from_iter([
                (
                    linux.dupe(),
                    ConfigSettingData {
                        constraints: BTreeMap::from_iter([(c_os.dupe(), c_linux.dupe())]),
                        buckconfigs: BTreeMap::new(),
                    },
                ),
                (
                    linux_arm64.dupe(),
                    ConfigSettingData {
                        constraints: BTreeMap::from_iter([
                            (c_os.dupe(), c_linux.dupe()),
                            (c_cpu.dupe(), c_arm64.dupe()),
                        ]),
                        buckconfigs: BTreeMap::new(),
                    },
                ),
                (
                    linux_x86_64.dupe(),
                    ConfigSettingData {
                        constraints: BTreeMap::from_iter([
                            (c_os.dupe(), c_linux.dupe()),
                            (c_cpu.dupe(), c_x86_64.dupe()),
                        ]),
                        buckconfigs: BTreeMap::new(),
                    },
                ),
            ]),
        };

        fn literal_true() -> CoercedAttr {
            CoercedAttr::Bool(BoolLiteral(true))
        }
        fn literal_str() -> CoercedAttr {
            CoercedAttr::String(StringLiteral(ArcStr::from("linux")))
        }

        // Test more specific is selected even if it is not first.
        let select_entries = Box::new([
            (linux.dupe(), literal_true()),
            (linux_x86_64.dupe(), literal_str()),
        ]);
        assert_eq!(
            Some(&literal_str()),
            CoercedAttr::select_the_most_specific(&ctx, &*select_entries).unwrap()
        );

        // Test more specific is selected even if it is first.
        let select_entries = Box::new([
            (linux_x86_64.dupe(), literal_str()),
            (linux.dupe(), literal_true()),
        ]);
        assert_eq!(
            Some(&literal_str()),
            CoercedAttr::select_the_most_specific(&ctx, &*select_entries).unwrap()
        );

        // Conflicting keys.
        let select_entries = Box::new([
            (linux_arm64.dupe(), literal_true()),
            (linux_x86_64.dupe(), literal_str()),
        ]);
        assert_eq!(
            "Both select keys `config//:linux-arm64` and `config//:linux-x86_64` \
            match the configuration, but neither is more specific",
            CoercedAttr::select_the_most_specific(&ctx, &*select_entries)
                .unwrap_err()
                .to_string()
        );
    }

    #[test]
    fn test_to_json_concat() {
        assert_eq!(
            r#"{"__type":"concat","items":["a","b","c","d"]}"#,
            CoercedAttr::Concat(Box::new([
                CoercedAttr::String(StringLiteral(ArcStr::from("a"))),
                CoercedAttr::String(StringLiteral(ArcStr::from("b"))),
                CoercedAttr::String(StringLiteral(ArcStr::from("c"))),
                CoercedAttr::String(StringLiteral(ArcStr::from("d"))),
            ]))
            .to_json(&AttrFmtContext::NO_CONTEXT)
            .unwrap()
            .to_string()
        );
    }

    #[test]
    fn test_to_json_selector() {
        assert_eq!(
            r#"{"__type":"selector","entries":{"DEFAULT":"ddd","config//:a":true,"config//:b":10}}"#,
            CoercedAttr::Selector(Box::new(
                CoercedSelector::new(
                    ArcSlice::new([
                        (
                            TargetLabel::testing_parse("config//:a"),
                            CoercedAttr::Bool(BoolLiteral(true))
                        ),
                        (
                            TargetLabel::testing_parse("config//:b"),
                            CoercedAttr::Int(10)
                        ),
                    ]),
                    Some(CoercedAttr::String(StringLiteral(ArcStr::from("ddd")))),
                )
                .unwrap()
            ))
            .to_json(&AttrFmtContext::NO_CONTEXT)
            .unwrap()
            .to_string()
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::cell::RefCell;
use std::fmt;
use std::fmt::Debug;

use buck2_common::package_listing::listing::PackageListing;
use buck2_core::cells::name::CellName;
use buck2_core::cells::CellResolver;
use buck2_core::package::package_relative_path::PackageRelativePath;
use buck2_core::package::package_relative_path::PackageRelativePathBuf;
use buck2_core::package::PackageLabel;
use buck2_core::pattern::pattern_type::PatternType;
use buck2_core::pattern::pattern_type::ProvidersPatternExtra;
use buck2_core::pattern::pattern_type::TargetPatternExtra;
use buck2_core::pattern::ParsedPattern;
use buck2_core::provider::label::ProvidersLabel;
use buck2_core::soft_error;
use buck2_core::target::label::TargetLabel;
use buck2_node::attrs::coerced_attr::CoercedAttr;
use buck2_node::attrs::coerced_path::CoercedDirectory;
use buck2_node::attrs::coerced_path::CoercedPath;
use buck2_node::attrs::coercion_context::AttrCoercionContext;
use buck2_node::query::query_functions::CONFIGURED_GRAPH_QUERY_FUNCTIONS;
use buck2_query::query::syntax::simple::eval::error::QueryError;
use buck2_query::query::syntax::simple::functions::QueryLiteralVisitor;
use buck2_query_parser::spanned::Spanned;
use buck2_query_parser::Expr;
use buck2_util::arc_str::ArcSlice;
use buck2_util::arc_str::ArcStr;
use bumpalo::Bump;
use dupe::Dupe;
use dupe::IterDupedExt;
use hashbrown::raw::RawTable;
use tracing::info;

use super::interner::AttrCoercionInterner;
use crate::attrs::coerce::arc_str_interner::ArcStrInterner;
use crate::attrs::coerce::str_hash::str_hash;

#[derive(Debug, thiserror::Error)]
enum BuildAttrCoercionContextError {
    #[error("Expected a label, got the pattern `{0}`.")]
    RequiredLabel(String),
    #[error("Expected a package: `{0}` can only be specified in a build file.")]
    NotBuildFileContext(String),
    #[error("Expected file, but got a directory for path `{1}` in package `{0}`.")]
    SourceFileIsDirectory(PackageLabel, String),
    #[error("Source file `{1}` does not exist as a member of package `{0}`.")]
    SourceFileMissing(PackageLabel, String),
    #[error(
        "Directory `{1}` of package `{0}` may not cover any subpackages, but includes subpackage `{2}`."
    )]
    SourceDirectoryIncludesSubPackage(PackageLabel, String, PackageRelativePathBuf),
}

/// An incomplete attr coercion context. Will be replaced with a real one later.
pub struct BuildAttrCoercionContext {
    /// Used to coerce targets
    cell_resolver: CellResolver,
    cell_name: CellName,
    /// Used to resolve relative targets. This is present when a build file
    /// is being evaluated, however it is absent if an extension file is being
    /// evaluated. The latter case occurs when default values for attributes
    /// are coerced when a UDR is declared.
    enclosing_package: Option<(PackageLabel, PackageListing)>,
    /// Does this package (if present) have a package boundary exception on it.
    package_boundary_exception: bool,
    /// Allocator for `label_cache`.
    alloc: Bump,
    /// Label coercion cache. We use `RawTable` where because `HashMap` API
    /// requires either computing hash twice (for get, then for insert) or
    /// allocating a key to perform a query using `entry` API.
    /// Strings are owned by `alloc`, using bump allocator makes evaluation 0.5% faster.
    label_cache: RefCell<RawTable<(u64, *const str, ProvidersLabel)>>,
    str_interner: ArcStrInterner,
    list_interner: AttrCoercionInterner<ArcSlice<CoercedAttr>>,
    // TODO(scottcao): Dict and selects need separate interners right now because
    // they have different key types. We can optimize this by interning keys and values
    // separately and use the same interner for dict and select values. This will also
    // reduce key duplication in selects since select keys are more likely to be deduplicated
    // than select values
    dict_interner: AttrCoercionInterner<ArcSlice<(CoercedAttr, CoercedAttr)>>,
    select_interner: AttrCoercionInterner<ArcSlice<(TargetLabel, CoercedAttr)>>,
}

impl Debug for BuildAttrCoercionContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BuildAttrCoercionContext")
            .finish_non_exhaustive()
    }
}

impl BuildAttrCoercionContext {
    fn new(
        cell_resolver: CellResolver,
        cell_name: CellName,
        enclosing_package: Option<(PackageLabel, PackageListing)>,
        package_boundary_exception: bool,
    ) -> Self {
        Self {
            cell_resolver,
            cell_name,
            enclosing_package,
            package_boundary_exception,
            alloc: Bump::new(),
            label_cache: RefCell::new(RawTable::new()),
            str_interner: ArcStrInterner::new(),
            list_interner: AttrCoercionInterner::new(),
            dict_interner: AttrCoercionInterner::new(),
            select_interner: AttrCoercionInterner::new(),
        }
    }

    pub fn new_no_package(cell_resolver: CellResolver, cell_name: CellName) -> Self {
        Self::new(cell_resolver, cell_name, None, false)
    }

    pub fn new_with_package(
        cell_resolver: CellResolver,
        enclosing_package: (PackageLabel, PackageListing),
        package_boundary_exception: bool,
    ) -> Self {
        Self::new(
            cell_resolver,
            enclosing_package.0.cell_name(),
            Some(enclosing_package),
            package_boundary_exception,
        )
    }

    pub fn parse_pattern<P: PatternType>(&self, value: &str) -> anyhow::Result<ParsedPattern<P>> {
        ParsedPattern::parsed_opt_absolute(
            value,
            self.enclosing_package.as_ref().map(|x| x.0.as_cell_path()),
            self.cell_name,
            &self.cell_resolver,
        )
    }

    fn coerce_label_no_cache(&self, value: &str) -> anyhow::Result<ProvidersLabel> {
        // TODO(nmj): Make this take an import path / package
        match self.parse_pattern::<ProvidersPatternExtra>(value)? {
            ParsedPattern::Target(package, target_name, providers) => {
                Ok(providers.into_providers_label(package, target_name.as_ref()))
            }
            _ => Err(BuildAttrCoercionContextError::RequiredLabel(value.to_owned()).into()),
        }
    }

    fn require_enclosing_package(
        &self,
        msg: &str,
    ) -> anyhow::Result<&(PackageLabel, PackageListing)> {
        self.enclosing_package.as_ref().ok_or_else(|| {
            BuildAttrCoercionContextError::NotBuildFileContext(msg.to_owned()).into()
        })
    }
}

impl AttrCoercionContext for BuildAttrCoercionContext {
    fn coerce_label(&self, value: &str) -> anyhow::Result<ProvidersLabel> {
        let hash = str_hash(value);
        let mut label_cache = self.label_cache.borrow_mut();

        if let Some((_h, _v, label)) = label_cache.get(hash, |(_h, v, _)| value == unsafe { &**v })
        {
            return Ok(label.clone());
        }

        let label = self.coerce_label_no_cache(value)?;
        label_cache.insert(
            hash,
            (hash, self.alloc.alloc_str(value), label.clone()),
            |(h, _v, _)| *h,
        );
        Ok(label)
    }

    fn intern_str(&self, value: &str) -> ArcStr {
        self.str_interner.intern(value)
    }

    fn intern_list(&self, value: Vec<CoercedAttr>) -> ArcSlice<CoercedAttr> {
        self.list_interner.intern(value)
    }

    fn intern_dict(
        &self,
        value: Vec<(CoercedAttr, CoercedAttr)>,
    ) -> ArcSlice<(CoercedAttr, CoercedAttr)> {
        self.dict_interner.intern(value)
    }

    fn intern_select(
        &self,
        value: Vec<(TargetLabel, CoercedAttr)>,
    ) -> ArcSlice<(TargetLabel, CoercedAttr)> {
        self.select_interner.intern(value)
    }

    fn coerce_path(&self, value: &str, allow_directory: bool) -> anyhow::Result<CoercedPath> {
        let path = <&PackageRelativePath>::try_from(value)?;
        let (package, listing) = self.require_enclosing_package(value)?;

        if let Some(path) = listing.get_file(path) {
            return Ok(CoercedPath::File(path));
        }

        // TODO: Make the warnings below into errors
        if let Some(path) = listing.get_dir(path) {
            if !allow_directory {
                return Err(BuildAttrCoercionContextError::SourceFileIsDirectory(
                    package.dupe(),
                    value.to_owned(),
                )
                .into());
            } else if let Some(subpackage) = listing.subpackages_within(&path).next() {
                let e = BuildAttrCoercionContextError::SourceDirectoryIncludesSubPackage(
                    package.dupe(),
                    value.to_owned(),
                    subpackage.to_owned(),
                );
                if self.package_boundary_exception {
                    info!("{} (could be due to a package boundary violation)", e);
                } else {
                    soft_error!("source_directory_includes_subpackage", e.into())?;
                }
            }
            let files = listing.files_within(&path).duped().collect();
            Ok(CoercedPath::Directory(Box::new(CoercedDirectory {
                dir: path,
                files,
            })))
        } else {
            let e =
                BuildAttrCoercionContextError::SourceFileMissing(package.dupe(), value.to_owned());
            if self.package_boundary_exception {
                info!("{} (could be due to a package boundary violation)", e);
            } else {
                soft_error!("source_file_missing", e.into())?;
            }

            Ok(CoercedPath::File(path.to_arc()))
        }
    }

    fn coerce_target_pattern(
        &self,
        pattern: &str,
    ) -> anyhow::Result<ParsedPattern<TargetPatternExtra>> {
        self.parse_pattern(pattern)
    }

    fn visit_query_function_literals(
        &self,
        visitor: &mut dyn QueryLiteralVisitor,
        expr: &Spanned<Expr>,
        query: &str,
    ) -> anyhow::Result<()> {
        CONFIGURED_GRAPH_QUERY_FUNCTIONS
            .get()?
            .visit_literals(visitor, expr)
            .map_err(|e| QueryError::convert_error(e, query))?;
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_core::provider::label::ProvidersLabel;
use gazebo::prelude::*;
use starlark::values::Value;

#[derive(thiserror::Error, Debug)]
pub(crate) enum CoercionError {
    #[error("Expected value of type `{0}`, got value with type `{1}` (value was `{2}`)")]
    TypeError(String, String, String),
    #[error("Used one_of with an empty list.")]
    OneOfEmpty,
    #[error("one_of fails, the errors against each alternative in turn were:\n{}", .0.map(|x| format!("{:#}", x)).join("\n"))]
    OneOfMany(Vec<anyhow::Error>),
    #[error("default_only is not allowed to be specified, but got `{0}`")]
    DefaultOnly(String),
    #[error("enum called with `{0}`, only allowed: {}", .1.map(|x| format!("`{}`", x)).join(", "))]
    InvalidEnumVariant(String, Vec<String>),
    #[error("attrs.configuration_dep() attributes shouldn't have any subtargets, but got `{0}`")]
    UnexpectedSubTarget(ProvidersLabel),
}

impl CoercionError {
    pub(crate) fn type_error(expected_type: &str, value: Value) -> CoercionError {
        CoercionError::TypeError(
            expected_type.to_owned(),
            value.get_type().to_owned(),
            value.to_repr(),
        )
    }

    pub fn one_of_many(mut errs: Vec<anyhow::Error>) -> anyhow::Error {
        if errs.is_empty() {
            CoercionError::OneOfEmpty.into()
        } else if errs.len() == 1 {
            errs.pop().unwrap()
        } else {
            CoercionError::OneOfMany(errs).into()
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::borrow::Borrow;
use std::cell::RefCell;
use std::hash::Hash;
use std::hash::Hasher;
use std::marker;
use std::sync::Arc;

use buck2_util::arc_str::ArcSlice;
use buck2_util::arc_str::ArcStr;
use dupe::Dupe;
use hashbrown::raw::RawTable;
use twox_hash::XxHash64;

/// An interner specific to our AttrCoercionContext used for interning different kinds of attributes.
/// Things specific about this interner:
/// - Requires interned values to be Dupe, so that you can intern both Arc<...> and specific Arc types like ArcStr.
/// - Interner is not static, so it's not required to take up memory for the entire duration of the program.
pub(crate) struct AttrCoercionInterner<T: Dupe + Hash + Eq, H = XxHash64> {
    /// We use `RawTable` where because `HashMap` API
    /// requires either computing hash twice (for get, then for insert) or
    /// allocating a key to perform a query using `entry` API.
    cache: RefCell<RawTable<(u64, T)>>,
    _marker: marker::PhantomData<H>,
}

impl<T: Dupe + Hash + Eq, H: Hasher + Default> AttrCoercionInterner<T, H> {
    pub(crate) fn new() -> Self {
        Self {
            cache: RefCell::new(RawTable::new()),
            _marker: marker::PhantomData,
        }
    }

    pub(crate) fn intern<S: Hash + Equiv<T> + Into<T>>(&self, internable: S) -> T {
        fn compute_hash<T: Hash, H: Hasher + Default>(t: T, mut hasher: H) -> u64 {
            t.hash(&mut hasher);
            hasher.finish()
        }

        let hash = compute_hash(&internable, H::default());
        let mut cache = self.cache.borrow_mut();

        if let Some((_h, v)) = cache.get(hash, |(_h, v)| internable.equivalent(v)) {
            return v.dupe();
        }

        let value: T = internable.into();
        cache.insert(hash, (hash, value.dupe()), |(h, _v)| *h);
        value
    }
}

pub(crate) trait Equiv<T> {
    fn equivalent(&self, value: &T) -> bool;
}

impl<T: Eq> Equiv<T> for T {
    fn equivalent(&self, value: &T) -> bool {
        self == value
    }
}

impl Equiv<ArcStr> for &str {
    fn equivalent(&self, value: &ArcStr) -> bool {
        *self == &**value
    }
}

impl<T: Eq> Equiv<Arc<T>> for T {
    fn equivalent(&self, value: &Arc<T>) -> bool {
        self == value.borrow()
    }
}

impl<T: Eq> Equiv<ArcSlice<T>> for Vec<T> {
    fn equivalent(&self, value: &ArcSlice<T>) -> bool {
        self == &**value
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::AttrCoercionInterner;

    #[test]
    fn test_intern() {
        let interner: AttrCoercionInterner<Arc<String>> = AttrCoercionInterner::new();
        let mut interned_strings = Vec::new();
        for i in 0..100000 {
            let interned = interner.intern(i.to_string());
            assert_eq!(i.to_string().as_str(), interned.as_str());
            interned_strings.push(interned);
        }

        for s in interned_strings {
            let interned = interner.intern(s.as_ref().clone());
            assert_eq!(s, interned);
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_node::attrs::coerced_attr::CoercedAttr;
use buck2_node::attrs::coercion_context::AttrCoercionContext;
use buck2_node::attrs::configurable::AttrIsConfigurable;
use starlark::typing::Ty;
use starlark::values::Value;

mod arc_str_interner;
pub mod attr_type;
pub mod coerced_attr;
pub mod ctx;
pub mod error;
mod interner;
pub(crate) mod str_hash;
pub mod testing;

pub trait AttrTypeCoerce {
    fn coerce_item(
        &self,
        configurable: AttrIsConfigurable,
        _ctx: &dyn AttrCoercionContext,
        value: Value,
    ) -> anyhow::Result<CoercedAttr>;

    fn starlark_type(&self) -> Ty;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use twox_hash::xxh3;

/// Unspecified fast hash for short strings.
pub(crate) fn str_hash(s: &str) -> u64 {
    xxh3::hash64(s.as_bytes())
}