/*
 * 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::Cow;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;

use allocative::Allocative;
use buck2_core::fs::paths::RelativePath;
use buck2_core::fs::paths::RelativePathBuf;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use buck2_execute::artifact::fs::ExecutorFs;
use buck2_interpreter::types::cell_root::CellRoot;
use buck2_util::commas::commas;
use buck2_util::thin_box::ThinBoxSlice;
use derive_more::Display;
use dupe::Dupe;
use gazebo::prelude::*;
use regex::Regex;
use serde::Serialize;
use serde::Serializer;
use starlark::coerce::coerce;
use starlark::typing::Ty;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::Freeze;
use starlark::values::Freezer;
use starlark::values::FrozenStringValue;
use starlark::values::FrozenValue;
use starlark::values::StringValue;
use starlark::values::StringValueLike;
use starlark::values::Trace;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueLike;
use static_assertions::assert_eq_size;

use crate::interpreter::rule_defs::artifact::StarlarkArtifactLike;
use crate::interpreter::rule_defs::artifact::ValueAsArtifactLike;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineLocation;

/// Supported ways of quoting arguments.
#[derive(Debug, Clone, Dupe, Trace, Freeze, Serialize, Allocative)]
pub enum QuoteStyle {
    /// Quote arguments for Unix shell:
    /// <https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html>
    Shell,
}

impl Display for QuoteStyle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Shell => write!(f, "shell"),
        }
    }
}

#[derive(Debug, thiserror::Error)]
enum CommandLineArgError {
    #[error("Unknown quoting style `{0}`")]
    UnknownQuotingStyle(String),
    #[error("too many .parent() calls")]
    TooManyParentCalls,
}

impl QuoteStyle {
    pub fn parse(s: &str) -> anyhow::Result<QuoteStyle> {
        match s {
            "shell" => Ok(QuoteStyle::Shell),
            _ => Err(anyhow::anyhow!(CommandLineArgError::UnknownQuotingStyle(
                s.to_owned()
            ))),
        }
    }
}

pub(crate) trait CommandLineOptionsTrait<'v>: Display {
    fn ignore_artifacts(&self) -> bool;
    fn delimiter(&self) -> Option<StringValue<'v>>;

    fn to_command_line_options<'a>(&'a self) -> CommandLineOptionsRef<'v, 'a>;
}

#[derive(Debug, Default_, Clone, Trace, Allocative)]
#[repr(C)]
pub(crate) struct CommandLineOptions<'v> {
    // These impact how artifacts are rendered
    /// The value of V must be convertible to a `RelativeOrigin`
    pub(crate) relative_to: Option<(Value<'v>, usize)>,
    pub(crate) absolute_prefix: Option<StringValue<'v>>,
    pub(crate) absolute_suffix: Option<StringValue<'v>>,
    pub(crate) parent: u32,
    pub(crate) ignore_artifacts: bool,

    // These impact the formatting of each string
    pub(crate) delimiter: Option<StringValue<'v>>,
    pub(crate) format: Option<StringValue<'v>>,
    pub(crate) prepend: Option<StringValue<'v>>,
    pub(crate) quote: Option<QuoteStyle>,
    #[allow(clippy::box_collection)]
    pub(crate) replacements: Option<Box<Vec<(StringValue<'v>, StringValue<'v>)>>>,
}

#[derive(Default, Serialize)]
pub(crate) struct CommandLineOptionsRef<'v, 'a> {
    #[serde(serialize_with = "serialize_opt_display")]
    pub(crate) relative_to: Option<(Value<'v>, usize)>,
    pub(crate) absolute_prefix: Option<StringValue<'v>>,
    pub(crate) absolute_suffix: Option<StringValue<'v>>,
    pub(crate) parent: u32,
    pub(crate) ignore_artifacts: bool,

    pub(crate) delimiter: Option<StringValue<'v>>,
    pub(crate) format: Option<StringValue<'v>>,
    pub(crate) prepend: Option<StringValue<'v>>,
    pub(crate) quote: Option<QuoteStyle>,
    pub(crate) replacements: &'a [(StringValue<'v>, StringValue<'v>)],
}

impl<'v, 'a> CommandLineOptionsRef<'v, 'a> {
    pub(crate) fn to_owned(&self) -> CommandLineOptions<'v> {
        CommandLineOptions {
            relative_to: self.relative_to,
            absolute_prefix: self.absolute_prefix,
            absolute_suffix: self.absolute_suffix,
            parent: self.parent,
            ignore_artifacts: self.ignore_artifacts,
            delimiter: self.delimiter,
            format: self.format,
            prepend: self.prepend,
            quote: self.quote.dupe(),
            replacements: if self.replacements.is_empty() {
                None
            } else {
                Some(Box::new(self.replacements.to_vec()))
            },
        }
    }
}

impl<'v> CommandLineOptionsTrait<'v> for CommandLineOptions<'v> {
    fn ignore_artifacts(&self) -> bool {
        self.ignore_artifacts
    }

    fn delimiter(&self) -> Option<StringValue<'v>> {
        self.delimiter.dupe()
    }

    fn to_command_line_options<'a>(&'a self) -> CommandLineOptionsRef<'v, 'a> {
        CommandLineOptionsRef {
            relative_to: self.relative_to,
            absolute_prefix: self.absolute_prefix,
            absolute_suffix: self.absolute_suffix,
            parent: self.parent,
            ignore_artifacts: self.ignore_artifacts,
            delimiter: self.delimiter,
            format: self.format,
            prepend: self.prepend,
            quote: self.quote.dupe(),
            replacements: match &self.replacements {
                None => &[],
                Some(v) => v.as_slice(),
            },
        }
    }
}

#[derive(Debug, Allocative)]
enum FrozenCommandLineOption {
    RelativeTo(FrozenValue, u32),
    AbsolutePrefix(FrozenStringValue),
    AbsoluteSuffix(FrozenStringValue),
    Parent(u32),
    IgnoreArtifacts,
    Delimiter(FrozenStringValue),
    Format(FrozenStringValue),
    Prepend(FrozenStringValue),
    Quote(QuoteStyle),
    #[allow(clippy::box_collection)]
    Replacements(ThinBoxSlice<(FrozenStringValue, FrozenStringValue)>),
}

assert_eq_size!(FrozenCommandLineOption, [usize; 2]);

#[derive(Debug, Default, Allocative)]
pub(crate) struct FrozenCommandLineOptions {
    options: ThinBoxSlice<FrozenCommandLineOption>,
}

impl FrozenCommandLineOptions {
    pub(crate) fn is_empty(&self) -> bool {
        self.options.is_empty()
    }
}

impl<'v> CommandLineOptionsTrait<'v> for FrozenCommandLineOptions {
    fn ignore_artifacts(&self) -> bool {
        for option in self.options.iter() {
            if let FrozenCommandLineOption::IgnoreArtifacts = option {
                return true;
            }
        }
        false
    }

    fn delimiter(&self) -> Option<StringValue<'v>> {
        for option in self.options.iter() {
            if let FrozenCommandLineOption::Delimiter(value) = option {
                return Some(value.to_string_value());
            }
        }
        None
    }

    fn to_command_line_options<'a>(&'a self) -> CommandLineOptionsRef<'v, 'a> {
        let mut options = CommandLineOptionsRef::default();
        for option in &*self.options {
            match option {
                FrozenCommandLineOption::RelativeTo(value, parent) => {
                    options.relative_to = Some((value.to_value(), *parent as usize));
                }
                FrozenCommandLineOption::AbsolutePrefix(value) => {
                    options.absolute_prefix = Some(value.to_string_value());
                }
                FrozenCommandLineOption::AbsoluteSuffix(value) => {
                    options.absolute_suffix = Some(value.to_string_value());
                }
                FrozenCommandLineOption::Parent(parent) => {
                    options.parent = *parent;
                }
                FrozenCommandLineOption::IgnoreArtifacts => {
                    options.ignore_artifacts = true;
                }
                FrozenCommandLineOption::Delimiter(value) => {
                    options.delimiter = Some(value.to_string_value());
                }
                FrozenCommandLineOption::Format(value) => {
                    options.format = Some(value.to_string_value());
                }
                FrozenCommandLineOption::Prepend(value) => {
                    options.prepend = Some(value.to_string_value());
                }
                FrozenCommandLineOption::Quote(value) => {
                    options.quote = Some(value.dupe());
                }
                FrozenCommandLineOption::Replacements(value) => {
                    options.replacements = coerce::<&[_], &[_]>(value);
                }
            }
        }
        options
    }
}

impl<'v> Serialize for CommandLineOptions<'v> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.to_command_line_options().serialize(serializer)
    }
}

impl Serialize for FrozenCommandLineOptions {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.to_command_line_options().serialize(serializer)
    }
}

impl<'v> Display for CommandLineOptions<'v> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Display::fmt(&self.to_command_line_options(), f)
    }
}

impl Display for FrozenCommandLineOptions {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Display::fmt(&self.to_command_line_options(), f)
    }
}

impl<'v> Freeze for CommandLineOptions<'v> {
    type Frozen = FrozenCommandLineOptions;

    fn freeze(self, freezer: &Freezer) -> anyhow::Result<FrozenCommandLineOptions> {
        let CommandLineOptions {
            relative_to,
            absolute_prefix,
            absolute_suffix,
            parent,
            ignore_artifacts,
            delimiter,
            format,
            prepend,
            quote,
            replacements,
        } = self;

        let mut options = Vec::new();
        if let Some(relative_to) = relative_to {
            let (relative, parent) = relative_to.freeze(freezer)?;
            let parent: u32 = parent.try_into()?;
            options.push(FrozenCommandLineOption::RelativeTo(relative, parent));
        }
        if let Some(absolute_prefix) = absolute_prefix {
            let absolute_prefix = absolute_prefix.freeze(freezer)?;
            options.push(FrozenCommandLineOption::AbsolutePrefix(absolute_prefix));
        }
        if let Some(absolute_suffix) = absolute_suffix {
            let absolute_suffix = absolute_suffix.freeze(freezer)?;
            options.push(FrozenCommandLineOption::AbsoluteSuffix(absolute_suffix));
        }
        if parent != 0 {
            options.push(FrozenCommandLineOption::Parent(parent));
        }
        if ignore_artifacts {
            options.push(FrozenCommandLineOption::IgnoreArtifacts);
        }
        if let Some(delimiter) = delimiter {
            let delimiter = delimiter.freeze(freezer)?;
            options.push(FrozenCommandLineOption::Delimiter(delimiter));
        }
        if let Some(format) = format {
            let format = format.freeze(freezer)?;
            options.push(FrozenCommandLineOption::Format(format));
        }
        if let Some(prepend) = prepend {
            let prepend = prepend.freeze(freezer)?;
            options.push(FrozenCommandLineOption::Prepend(prepend));
        }
        if let Some(quote) = quote {
            options.push(FrozenCommandLineOption::Quote(quote));
        }
        if let Some(replacements) = replacements {
            if !replacements.is_empty() {
                let replacements = ThinBoxSlice::from_iter((*replacements).freeze(freezer)?);
                options.push(FrozenCommandLineOption::Replacements(replacements));
            }
        }

        Ok(FrozenCommandLineOptions {
            options: ThinBoxSlice::from_iter(options),
        })
    }
}

fn serialize_opt_display<V: Display, S>(v: &Option<(V, usize)>, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match v {
        Some((v, u)) => s.serialize_some(&(format!("{}", v), u)),
        None => s.serialize_none(),
    }
}

impl<'v, 'a> Display for CommandLineOptionsRef<'v, 'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut comma = commas();
        if let Some((v, i)) = &self.relative_to {
            comma(f)?;
            write!(f, "relative_to = {}", v)?;
            if *i != 0 {
                comma(f)?;
                write!(f, "relative_to_parent = {}", i)?;
            }
        }
        if let Some(v) = &self.absolute_prefix {
            comma(f)?;
            write!(f, "absolute_prefix = {}", v)?;
        }
        if let Some(v) = &self.absolute_suffix {
            comma(f)?;
            write!(f, "absolute_suffix = {}", v)?;
        }
        if self.parent != 0 {
            comma(f)?;
            write!(f, "parent = {}", self.parent)?;
        }
        if self.ignore_artifacts {
            comma(f)?;
            write!(f, "ignore_artifacts = True")?;
        }
        if let Some(v) = &self.delimiter {
            comma(f)?;
            write!(f, "delimiter = {:?}", v)?;
        }
        if let Some(v) = &self.format {
            comma(f)?;
            write!(f, "format = {:?}", v)?;
        }
        if let Some(v) = &self.prepend {
            comma(f)?;
            write!(f, "prepend = {:?}", v)?;
        }
        if let Some(v) = &self.quote {
            comma(f)?;
            write!(f, "quote = \"{}\"", v)?;
        }
        if !self.replacements.is_empty() {
            comma(f)?;
            write!(f, "replacements = [")?;
            let mut vec_comma = commas();
            for p in self.replacements {
                vec_comma(f)?;
                write!(f, "({:?}, {:?})", p.0, p.1)?;
            }
            write!(f, "]")?;
        }
        Ok(())
    }
}

// NOTE: This is an enum as opposed to a trait because of the `C` parameter on (which is required
// because upcasting is not stable).
#[derive(Display)]
pub(crate) enum RelativeOrigin<'v> {
    Artifact(&'v dyn StarlarkArtifactLike),
    CellRoot(&'v CellRoot),
}

impl<'v> StarlarkTypeRepr for RelativeOrigin<'v> {
    fn starlark_type_repr() -> Ty {
        Ty::union2(
            ValueAsArtifactLike::starlark_type_repr(),
            CellRoot::starlark_type_repr(),
        )
    }
}

impl<'v> UnpackValue<'v> for RelativeOrigin<'v> {
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        if let Some(v) = ValueAsArtifactLike::unpack_value(value) {
            return Some(RelativeOrigin::Artifact(v.0));
        }

        if let Some(v) = value.downcast_ref::<CellRoot>() {
            return Some(RelativeOrigin::CellRoot(v));
        }

        None
    }
}

impl<'v> RelativeOrigin<'v> {
    pub(crate) fn resolve<C>(&self, ctx: &C) -> anyhow::Result<RelativePathBuf>
    where
        C: CommandLineContext + ?Sized,
    {
        let loc = match self {
            Self::Artifact(artifact) => {
                // Shame we require the artifact to be bound here, we really just needs its
                // path even if it is unbound.
                let artifact = artifact.get_bound_artifact()?;
                ctx.resolve_artifact(&artifact)?
            }
            Self::CellRoot(cell_root) => ctx.resolve_cell_path(cell_root.cell_path())?,
        };

        Ok(loc.into_relative())
    }
}

impl<'v, 'x> CommandLineOptionsRef<'v, 'x> {
    fn changes_builder(&self) -> bool {
        match self {
            Self {
                relative_to: None,
                absolute_prefix: None,
                absolute_suffix: None,
                parent: 0,
                delimiter: None,
                format: None,
                prepend: None,
                quote: None,
                replacements: &[],
                ignore_artifacts: _, // Doesn't impact the builder
            } => false,
            _ => true,
        }
    }

    pub(crate) fn wrap_builder<'a, R>(
        &self,
        builder: &'a mut dyn CommandLineBuilder,
        ctx: &'a mut dyn CommandLineContext,
        f: impl for<'b> FnOnce(
            &'b mut dyn CommandLineBuilder,
            &'b mut dyn CommandLineContext,
        ) -> anyhow::Result<R>,
    ) -> anyhow::Result<R> {
        struct ExtrasBuilder<'a, 'v> {
            builder: &'a mut dyn CommandLineBuilder,
            opts: &'a CommandLineOptionsRef<'v, 'a>,
            // Auxiliary field to store concatenation result (when arguments are concatenated) and
            // a flag stating that the result is not yet started to be computated (i.e. the first
            // argument to be concatenated is not yet processed).
            concatenation_context: Option<(String, bool)>,
        }

        struct ExtrasContext<'a, 'v> {
            ctx: &'a mut dyn CommandLineContext,
            opts: &'a CommandLineOptionsRef<'v, 'a>,
            relative_to: Option<RelativePathBuf>,
        }

        impl<'a, 'v> CommandLineContext for ExtrasContext<'a, 'v> {
            fn resolve_project_path(
                &self,
                path: ProjectRelativePathBuf,
            ) -> anyhow::Result<CommandLineLocation> {
                let Self {
                    ctx,
                    relative_to,
                    opts,
                } = self;

                let resolved = ctx.resolve_project_path(path)?;

                if opts.parent == 0
                    && opts.absolute_prefix.is_none()
                    && opts.absolute_suffix.is_none()
                    && relative_to.is_none()
                {
                    return Ok(resolved);
                }

                let mut x = resolved.into_relative();
                if let Some(relative_to) = relative_to {
                    x = relative_to.relative(x);
                }
                let mut parent_ref = x.as_relative_path();
                for _ in 0..opts.parent {
                    parent_ref = parent_ref
                        .parent()
                        .ok_or(CommandLineArgError::TooManyParentCalls)?;
                }
                x = parent_ref.to_owned();
                if opts.absolute_prefix.is_some() || opts.absolute_suffix.is_some() {
                    x = RelativePath::new(&format!(
                        "{}{}{}",
                        opts.absolute_prefix.unwrap_or_default().as_str(),
                        x,
                        opts.absolute_suffix.unwrap_or_default().as_str(),
                    ))
                    .to_owned();
                }
                Ok(CommandLineLocation::from_relative_path(
                    x,
                    self.fs().path_separator(),
                ))
            }

            fn fs(&self) -> &ExecutorFs {
                self.ctx.fs()
            }

            fn next_macro_file_path(&mut self) -> anyhow::Result<RelativePathBuf> {
                let macro_path = self.ctx.next_macro_file_path()?;
                if let Some(relative_to_path) = &self.relative_to {
                    Ok(relative_to_path.relative(macro_path))
                } else {
                    Ok(macro_path)
                }
            }
        }

        impl<'a, 'v> ExtrasBuilder<'a, 'v> {
            /// If any items need to be concatted/formatted and added to the original CLI,
            /// do it here
            fn finalize_args(mut self) -> Self {
                if let Some((concatted_items, _)) = self.concatenation_context.take() {
                    self.builder.push_arg(concatted_items);
                }
                self
            }

            fn add_delimiter(&mut self) {
                if let Some((concatted_items, initital_state)) = self.concatenation_context.as_mut()
                {
                    if *initital_state {
                        *initital_state = false;
                    } else {
                        concatted_items.push_str(self.opts.delimiter.unwrap_or_default().as_str());
                    }
                }
            }

            fn add_arg(&mut self, arg: String) {
                if let Some((concatted_items, _)) = self.concatenation_context.as_mut() {
                    concatted_items.push_str(&arg)
                } else {
                    self.builder.push_arg(arg)
                }
            }

            fn format(&self, mut arg: String) -> String {
                for (pattern, replacement) in self.opts.replacements {
                    // We checked that regex is valid in replace_regex(), so unwrap is safe.
                    let re = Regex::new(pattern.as_str()).unwrap();
                    match re.replace_all(&arg, replacement.as_str()) {
                        Cow::Borrowed(_) => {}
                        Cow::Owned(new) => arg = new,
                    }
                }
                if let Some(format) = &self.opts.format {
                    arg = format.as_str().replace("{}", &arg);
                }
                match &self.opts.quote {
                    Some(QuoteStyle::Shell) => {
                        arg = shlex::quote(&arg).into_owned();
                    }
                    _ => {}
                }
                arg
            }
        }

        impl<'a, 'v> CommandLineBuilder for ExtrasBuilder<'a, 'v> {
            fn push_arg(&mut self, s: String) {
                // We apply options impacting formatting in the order:
                //   format, quote, (prepend + delimiter)
                self.add_delimiter();
                if let Some(i) = self.opts.prepend {
                    self.add_arg(i.as_str().to_owned());
                }
                self.add_arg(self.format(s))
            }
        }

        if !self.changes_builder() {
            f(builder, ctx)
        } else {
            let relative_to = self.relative_to_path(ctx)?;

            let mut extras_builder = ExtrasBuilder {
                builder,
                opts: self,
                concatenation_context: if self.delimiter.is_some() {
                    Some((String::new(), true))
                } else {
                    None
                },
            };

            let mut extras_ctx = ExtrasContext {
                ctx,
                opts: self,
                relative_to,
            };

            let res = f(&mut extras_builder, &mut extras_ctx)?;
            extras_builder.finalize_args();
            Ok(res)
        }
    }

    pub(crate) fn relative_to_path<C>(&self, ctx: &C) -> anyhow::Result<Option<RelativePathBuf>>
    where
        C: CommandLineContext + ?Sized,
    {
        let (value, parent) = match self.relative_to {
            Some(vp) => vp,
            None => return Ok(None),
        };

        let origin = RelativeOrigin::unpack_value(value)
            .expect("Must be a valid RelativeOrigin as this was checked in the setter");
        let mut relative_path = origin.resolve(ctx)?;
        for _ in 0..parent {
            if !relative_path.pop() {
                return Err(
                    anyhow::anyhow!(CommandLineArgError::TooManyParentCalls).context(format!(
                        "Error accessing {}-th parent of {}",
                        parent, origin
                    )),
                );
            }
        }

        Ok(Some(relative_path))
    }
}


/*
 * 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 crate::interpreter::rule_defs::cmd_args::arg_builder::ArgBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;

pub struct SpaceSeparatedCommandLineBuilder<'v> {
    builder: &'v mut dyn ArgBuilder,
    first: bool,
}

impl<'v> SpaceSeparatedCommandLineBuilder<'v> {
    // This can be used to construct a CommandLineBuilder that will append the command line
    // as a space-separated string to the arg.
    pub fn wrap(builder: &'v mut dyn ArgBuilder) -> Self {
        Self {
            builder,
            first: true,
        }
    }
}

impl CommandLineBuilder for SpaceSeparatedCommandLineBuilder<'_> {
    fn push_arg(&mut self, s: String) {
        if self.first {
            self.first = false;
        } else {
            self.builder.push_str(" ");
        }
        self.builder.push_str(&s);
    }
}

#[cfg(test)]
mod tests {
    use crate::interpreter::rule_defs::cmd_args::arg_builder::ArgBuilder;
    use crate::interpreter::rule_defs::cmd_args::space_separated::SpaceSeparatedCommandLineBuilder;
    use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;

    #[test]
    fn cmdline_builder() -> anyhow::Result<()> {
        struct Base {
            val: String,
        }
        impl ArgBuilder for Base {
            fn push_str(&mut self, v: &str) {
                self.val.push_str(v);
            }
        }

        let mut base = Base { val: String::new() };
        SpaceSeparatedCommandLineBuilder::wrap(&mut base);
        assert_eq!("", &base.val);

        {
            let mut builder = SpaceSeparatedCommandLineBuilder::wrap(&mut base);
            builder.push_arg("hello".to_owned());
        }
        assert_eq!("hello", &base.val);

        base.val = String::new();
        {
            let mut builder = SpaceSeparatedCommandLineBuilder::wrap(&mut base);
            builder.push_arg("hello".to_owned());
            builder.push_arg("world!".to_owned());
        }
        assert_eq!("hello world!", &base.val);

        {
            let mut builder = SpaceSeparatedCommandLineBuilder::wrap(&mut base);
            builder.push_arg("hello".to_owned());
            builder.push_arg("again!".to_owned());
            builder.push_arg("and".to_owned());
            builder.push_arg("again!".to_owned());
        }
        assert_eq!("hello world!hello again! and again!", &base.val);

        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 std::borrow::Cow;
use std::fmt::Debug;

use anyhow::Context as _;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_artifact::artifact::artifact_type::OutputArtifact;
use buck2_common::executor_config::PathSeparatorKind;
use buck2_core::cells::cell_path::CellPathRef;
use buck2_core::fs::paths::RelativePathBuf;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
use buck2_execute::artifact::fs::ExecutorFs;
use buck2_interpreter::types::target_label::StarlarkTargetLabel;
use indexmap::IndexSet;
use starlark::any::ProvidesStaticType;
use starlark::values::string::StarlarkStr;

use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact_tagging::ArtifactTag;
use crate::interpreter::rule_defs::resolved_macro::ResolvedMacro;

pub trait CommandLineArtifactVisitor {
    fn visit_input(&mut self, input: ArtifactGroup, tag: Option<&ArtifactTag>);

    fn visit_output(&mut self, artifact: OutputArtifact, tag: Option<&ArtifactTag>);

    /// Those two functions can be used to keep track of recursion when visiting artifacts.
    fn push_frame(&mut self) -> anyhow::Result<()> {
        Ok(())
    }

    fn pop_frame(&mut self) {}
}

/// A CommandLineArtifactVisitor that gathers inputs and outputs.
pub struct SimpleCommandLineArtifactVisitor {
    pub inputs: IndexSet<ArtifactGroup>,
    pub outputs: IndexSet<OutputArtifact>,
}

impl SimpleCommandLineArtifactVisitor {
    pub fn new() -> Self {
        Self {
            inputs: IndexSet::new(),
            outputs: IndexSet::new(),
        }
    }
}

impl CommandLineArtifactVisitor for SimpleCommandLineArtifactVisitor {
    fn visit_input(&mut self, input: ArtifactGroup, _tag: Option<&ArtifactTag>) {
        self.inputs.insert(input);
    }

    fn visit_output(&mut self, artifact: OutputArtifact, _tag: Option<&ArtifactTag>) {
        self.outputs.insert(artifact);
    }
}

pub trait WriteToFileMacroVisitor {
    fn visit_write_to_file_macro(&mut self, m: &ResolvedMacro) -> anyhow::Result<()>;

    /// Generator produces a 'RelativePathBuf' relative to the directory which owning command will run in.
    fn set_current_relative_to_path(
        &mut self,
        gen: &dyn Fn(&dyn CommandLineContext) -> anyhow::Result<Option<RelativePathBuf>>,
    ) -> anyhow::Result<()>;
}

/// Implemented by anything that can show up in a command line. This method adds any args and env vars that are needed
///
/// Certain operations on `CommandLineBuilder` can fail, so propagate those upward
pub trait CommandLineArgLike {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()>;

    fn visit_artifacts(&self, _visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        Ok(())
    }

    /// Allows to query if object contains a value resolved from `attrs.arg()`
    fn contains_arg_attr(&self) -> bool;

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()>;
}

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

impl CommandLineArgLike for &str {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        _context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg((*self).to_owned());
        Ok(())
    }

    fn contains_arg_attr(&self) -> bool {
        false
    }

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        Ok(())
    }
}

impl CommandLineArgLike for StarlarkStr {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        _context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg(self.as_str().to_owned());
        Ok(())
    }

    fn contains_arg_attr(&self) -> bool {
        false
    }

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        Ok(())
    }
}

impl CommandLineArgLike for String {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        _context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg(self.clone());
        Ok(())
    }

    fn contains_arg_attr(&self) -> bool {
        false
    }

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        Ok(())
    }
}

impl CommandLineArgLike for StarlarkTargetLabel {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        _context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg(self.to_string());
        Ok(())
    }

    fn contains_arg_attr(&self) -> bool {
        false
    }

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        Ok(())
    }
}

/// CommandLineLocation represents the path to a resolved artifact. If the root is present, the
/// path is udnerstood to be relative to the root. If no root is present, the path is relative to
/// some contextual location that depends on the CommandLineContext that produced the
/// CommandLineLocation.
#[derive(Debug, Clone)]
pub struct CommandLineLocation<'a> {
    root: Option<&'a ProjectRoot>,
    path: RelativePathBuf,
    path_separator: PathSeparatorKind,
}

impl CommandLineLocation<'_> {
    pub fn into_relative(self) -> RelativePathBuf {
        self.path
    }

    pub fn into_string(self) -> String {
        let Self {
            root,
            path,
            path_separator,
        } = self;

        let mut root_buf;
        let res = match root {
            Some(root) => {
                root_buf = root.root().to_path_buf();
                root_buf.extend(path.iter());
                root_buf.to_string_lossy()
            }
            None => {
                let path = path.as_str();

                if path.contains('/') {
                    Cow::Borrowed(path)
                } else if path.is_empty() {
                    // In command lines, the empty path is the current directory, so use that instead
                    // so we don't have to deal with empty strings being implicit current directory.
                    Cow::Borrowed(".")
                } else {
                    // If the path isn't empty, but doesn't have any path separators, add `./` This
                    // ensures that if we have an executable relative to the repo root, we can
                    // execute it (since `execvp`'s behavior is dependent on the presence of a `/`
                    // in the path).
                    let mut res = String::with_capacity(path.len() + 2);
                    res.push_str("./");
                    res.push_str(path);
                    Cow::Owned(res)
                }
            }
        };

        if path_separator == PathSeparatorKind::Windows {
            res.replace('/', "\\")
        } else {
            res.into_owned()
        }
    }
}

impl<'a> CommandLineLocation<'a> {
    pub fn from_root(
        root: &'a ProjectRoot,
        path: RelativePathBuf,
        path_separator: PathSeparatorKind,
    ) -> Self {
        Self {
            root: Some(root),
            path,
            path_separator,
        }
    }

    pub fn from_relative_path(path: RelativePathBuf, path_separator: PathSeparatorKind) -> Self {
        Self {
            root: None,
            path,
            path_separator,
        }
    }
}

pub trait CommandLineContext {
    fn resolve_project_path(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<CommandLineLocation>;

    fn fs(&self) -> &ExecutorFs;

    /// Resolves the 'Artifact's to a 'CommandLineLocation' relative to the directory this command will run in.
    fn resolve_artifact(&self, artifact: &Artifact) -> anyhow::Result<CommandLineLocation> {
        self.resolve_project_path(artifact.resolve_path(self.fs().fs())?)
            .with_context(|| format!("Error resolving artifact: {}", artifact))
    }

    fn resolve_cell_path(&self, path: CellPathRef) -> anyhow::Result<CommandLineLocation> {
        self.resolve_project_path(self.fs().fs().resolve_cell_path(path)?)
            .with_context(|| format!("Error resolving cell path: {}", path))
    }

    /// Result is 'RelativePathBuf' relative to the directory this command will run in. The path points to the file containing expanded macro.
    fn next_macro_file_path(&mut self) -> anyhow::Result<RelativePathBuf>;
}

/// CommandLineBuilder accumulates elements into some form of list (which might be an actual Vec, a
/// space-separated list, etc.). An API is provided to add individual elements. Lower-level APIs
/// are exposed to allow access to a buffer and control end-of-element.
pub trait CommandLineBuilder {
    /// Add a standalone element to this command line builder. This element
    fn push_arg(&mut self, s: String);
}

impl CommandLineBuilder for Vec<String> {
    fn push_arg(&mut self, s: String) {
        self.push(s)
    }
}


/*
 * 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::Ref;
use std::cell::RefCell;
use std::cell::RefMut;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::marker::PhantomData;

use allocative::Allocative;
use buck2_core::fs::paths::RelativePathBuf;
use buck2_util::commas::commas;
use buck2_util::thin_box::ThinBoxSlice;
use display_container::display_pair;
use display_container::fmt_container;
use display_container::iter_display_chain;
use dupe::Dupe;
use either::Either;
use gazebo::prelude::*;
use indexmap::IndexSet;
use regex::Regex;
use serde::Serialize;
use serde::Serializer;
use starlark::any::ProvidesStaticType;
use starlark::coerce::coerce;
use starlark::docs::StarlarkDocs;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::typing::Ty;
use starlark::values::list::ListRef;
use starlark::values::starlark_value;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::AllocValue;
use starlark::values::Demand;
use starlark::values::Freeze;
use starlark::values::Freezer;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::StringValue;
use starlark::values::Trace;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueError;
use starlark::values::ValueLike;
use starlark::values::ValueOf;
use static_assertions::assert_eq_size;

use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact::associated::AssociatedArtifacts;
use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkOutputArtifact;
use crate::interpreter::rule_defs::cmd_args::options::CommandLineOptions;
use crate::interpreter::rule_defs::cmd_args::options::CommandLineOptionsRef;
use crate::interpreter::rule_defs::cmd_args::options::CommandLineOptionsTrait;
use crate::interpreter::rule_defs::cmd_args::options::FrozenCommandLineOptions;
use crate::interpreter::rule_defs::cmd_args::options::QuoteStyle;
use crate::interpreter::rule_defs::cmd_args::options::RelativeOrigin;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::traits::SimpleCommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::traits::WriteToFileMacroVisitor;
use crate::interpreter::rule_defs::cmd_args::value::CommandLineArg;
use crate::interpreter::rule_defs::cmd_args::value::FrozenCommandLineArg;

/// Fields of `cmd_args`. Abstract mutable and frozen versions.
trait Fields<'v> {
    fn items(&self) -> &[CommandLineArg<'v>];
    fn hidden(&self) -> &[CommandLineArg<'v>];
    fn options(&self) -> Option<&dyn CommandLineOptionsTrait<'v>>;
}

/// Wrapper because we cannot implement traits for traits.
struct FieldsRef<'v, F: Fields<'v>>(F, PhantomData<Value<'v>>);

impl<'v, F: Fields<'v>> Serialize for FieldsRef<'v, F> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        /// Make sure mutable mutable and frozen `cmd_args` are serialized identically
        /// by routing through this struct.
        #[derive(Serialize)]
        struct Mirror<'v, 'a> {
            items: &'a [CommandLineArg<'v>],
            hidden: &'a [CommandLineArg<'v>],
            options: Option<CommandLineOptionsRef<'v, 'a>>,
        }

        Mirror {
            items: self.0.items(),
            hidden: self.0.hidden(),
            options: self.0.options().map(|x| x.to_command_line_options()),
        }
        .serialize(serializer)
    }
}

impl<'v, F: Fields<'v>> Display for FieldsRef<'v, F> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        fmt_container(
            f,
            "cmd_args(",
            ")",
            iter_display_chain(
                self.0.items(),
                iter_display_chain(
                    Some(self.0.hidden())
                        .filter(|x| !x.is_empty())
                        .map(|hidden| {
                            struct Wrapper<'a, 'v>(&'a [CommandLineArg<'v>]);
                            impl<'a, 'v> Display for Wrapper<'a, 'v> {
                                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                                    fmt_container(f, "[", "]", self.0.iter())
                                }
                            }
                            display_pair("hidden", "=", Wrapper(hidden))
                        }),
                    self.0
                        .options()
                        .iter()
                        .map(|options| display_pair("options", "=", options)),
                ),
            ),
        )
    }
}

impl<'v, F: Fields<'v>> FieldsRef<'v, F> {
    fn copy(&self) -> StarlarkCommandLine<'v> {
        StarlarkCommandLine(RefCell::new(StarlarkCommandLineData {
            items: self.0.items().to_vec(),
            hidden: self.0.hidden().to_vec(),
            options: self
                .0
                .options()
                .map(|x| Box::new(x.to_command_line_options().to_owned())),
        }))
    }

    fn ignore_artifacts(&self) -> bool {
        self.0
            .options()
            .map(|o| o.ignore_artifacts())
            .unwrap_or_default()
    }

    fn is_concat(&self) -> bool {
        if let Some(x) = &self.0.options() {
            x.delimiter().is_some()
        } else {
            false
        }
    }

    fn relative_to_path<C>(&self, ctx: &C) -> anyhow::Result<Option<RelativePathBuf>>
    where
        C: CommandLineContext + ?Sized,
    {
        match &self.0.options() {
            None => Ok(None),
            Some(options) => options.to_command_line_options().relative_to_path(ctx),
        }
    }
}

impl<'v, F: Fields<'v>> CommandLineArgLike for FieldsRef<'v, F> {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        match self.0.options() {
            None => {
                for item in self.0.items() {
                    item.as_command_line_arg()
                        .add_to_command_line(cli, context)?;
                }
                Ok(())
            }
            Some(options) => {
                options
                    .to_command_line_options()
                    .wrap_builder(cli, context, |cli, context| {
                        for item in self.0.items() {
                            item.as_command_line_arg()
                                .add_to_command_line(cli, context)?;
                        }
                        Ok(())
                    })
            }
        }
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        if !self.ignore_artifacts() {
            for item in self.0.items().iter().chain(self.0.hidden().iter()) {
                visitor.push_frame()?;
                item.as_command_line_arg().visit_artifacts(visitor)?;
                visitor.pop_frame();
            }
        }
        Ok(())
    }

    fn contains_arg_attr(&self) -> bool {
        self.0
            .items()
            .iter()
            .any(|x| x.as_command_line_arg().contains_arg_attr())
            || self
                .0
                .hidden()
                .iter()
                .any(|x| x.as_command_line_arg().contains_arg_attr())
    }

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        visitor.set_current_relative_to_path(&|ctx| self.relative_to_path(ctx))?;

        for item in self.0.items() {
            item.as_command_line_arg()
                .visit_write_to_file_macros(visitor)?;
        }
        for item in self.0.hidden() {
            item.as_command_line_arg()
                .visit_write_to_file_macros(visitor)?;
        }
        Ok(())
    }
}

/// Starlark object returned by `cmd_args()`
/// A container for all of the args and nested command lines that a users adds to `ctx.args()`
///
/// This allows more efficient iterative argument building, including O(1) insertion of
/// `CommandLine` / `FrozenCommandLine` args.
///
/// When frozen, a `FrozenCommandLine` is created.
///
/// `items` contains strings, artifacts, command line args (frozen and not), but does not
///         contain any builders.
#[derive(Debug, Default, Clone, Trace, ProvidesStaticType, Allocative)]
#[repr(C)]
pub struct StarlarkCommandLineData<'v> {
    items: Vec<CommandLineArg<'v>>,
    hidden: Vec<CommandLineArg<'v>>,
    options: Option<Box<CommandLineOptions<'v>>>,
}

#[derive(
    Debug,
    Default,
    Clone,
    Trace,
    ProvidesStaticType,
    StarlarkDocs,
    Allocative
)]
pub struct StarlarkCommandLine<'v>(RefCell<StarlarkCommandLineData<'v>>);

impl<'v> Serialize for StarlarkCommandLine<'v> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        FieldsRef(self.0.borrow(), PhantomData).serialize(serializer)
    }
}

#[derive(Debug, ProvidesStaticType, Allocative)]
pub struct FrozenStarlarkCommandLine {
    items: ThinBoxSlice<FrozenCommandLineArg>,
    hidden: ThinBoxSlice<FrozenCommandLineArg>,
    options: FrozenCommandLineOptions,
}

impl Serialize for FrozenStarlarkCommandLine {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        FieldsRef(self, PhantomData).serialize(serializer)
    }
}

impl<'a, 'v> Fields<'v> for Ref<'a, StarlarkCommandLineData<'v>> {
    fn items(&self) -> &[CommandLineArg<'v>] {
        &self.items
    }

    fn hidden(&self) -> &[CommandLineArg<'v>] {
        &self.hidden
    }

    fn options(&self) -> Option<&dyn CommandLineOptionsTrait<'v>> {
        match &self.options {
            None => None,
            Some(x) => Some(&**x),
        }
    }
}

impl<'v> Fields<'v> for FrozenStarlarkCommandLine {
    fn items(&self) -> &[CommandLineArg<'v>] {
        coerce(&*self.items)
    }

    fn hidden(&self) -> &[CommandLineArg<'v>] {
        coerce(&*self.hidden)
    }

    fn options(&self) -> Option<&dyn CommandLineOptionsTrait<'v>> {
        if self.options.is_empty() {
            None
        } else {
            Some(&self.options)
        }
    }
}

impl<'a, 'v, F: Fields<'v>> Fields<'v> for &'a F {
    fn items(&self) -> &[CommandLineArg<'v>] {
        (*self).items()
    }

    fn hidden(&self) -> &[CommandLineArg<'v>] {
        (*self).hidden()
    }

    fn options(&self) -> Option<&dyn CommandLineOptionsTrait<'v>> {
        (*self).options()
    }
}

impl<'v, A: Fields<'v>, B: Fields<'v>> Fields<'v> for Either<A, B> {
    fn items(&self) -> &[CommandLineArg<'v>] {
        match self {
            Either::Left(x) => x.items(),
            Either::Right(x) => x.items(),
        }
    }

    fn hidden(&self) -> &[CommandLineArg<'v>] {
        match self {
            Either::Left(x) => x.hidden(),
            Either::Right(x) => x.hidden(),
        }
    }

    fn options(&self) -> Option<&dyn CommandLineOptionsTrait<'v>> {
        match self {
            Either::Left(x) => x.options(),
            Either::Right(x) => x.options(),
        }
    }
}

// These types show up a lot in the frozen heaps, so make sure they don't regress
assert_eq_size!(StarlarkCommandLine<'static>, [usize; 8]);
assert_eq_size!(FrozenStarlarkCommandLine, [usize; 3]);
assert_eq_size!(CommandLineOptions<'static>, [usize; 10]);

impl<'v> Display for StarlarkCommandLine<'v> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self.0.try_borrow() {
            Ok(x) => Display::fmt(&FieldsRef(x, PhantomData), f),
            Err(_) => write!(f, "<cmd_args borrowed>"),
        }
    }
}

impl Display for FrozenStarlarkCommandLine {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Display::fmt(&FieldsRef(self, PhantomData), f)
    }
}

impl<'v> StarlarkCommandLineData<'v> {
    fn options_mut(&mut self) -> &mut CommandLineOptions<'v> {
        if self.options.is_none() {
            self.options = Some(Box::default());
        }
        self.options.as_mut().unwrap()
    }
}

impl<'v> StarlarkCommandLine<'v> {
    pub(crate) fn is_concat(&self) -> bool {
        FieldsRef(self.0.borrow(), PhantomData).is_concat()
    }
}

impl FrozenStarlarkCommandLine {
    pub(crate) fn is_concat(&self) -> bool {
        FieldsRef(self, PhantomData).is_concat()
    }
}

impl<'v> StarlarkCommandLine<'v> {
    pub fn is_empty(&self) -> bool {
        self.0.borrow().items.is_empty()
    }
}

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

    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn CommandLineArgLike>(self);
    }
}

#[starlark_value(type = "cmd_args")]
impl<'v> StarlarkValue<'v> for FrozenStarlarkCommandLine {
    fn get_methods() -> Option<&'static Methods> {
        // We return the same methods for frozen command lines, even though some of them fail,
        // so the methods remain consistent during freezing
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(command_line_builder_methods)
    }

    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn CommandLineArgLike>(self);
    }
}

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

impl<'v> CommandLineArgLike for StarlarkCommandLine<'v> {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        FieldsRef(self.0.borrow(), PhantomData).add_to_command_line(cli, context)
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        FieldsRef(self.0.borrow(), PhantomData).visit_artifacts(visitor)
    }

    fn contains_arg_attr(&self) -> bool {
        FieldsRef(self.0.borrow(), PhantomData).contains_arg_attr()
    }

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        FieldsRef(self.0.borrow(), PhantomData).visit_write_to_file_macros(visitor)
    }
}

impl CommandLineArgLike for FrozenStarlarkCommandLine {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        FieldsRef(self, PhantomData).add_to_command_line(cli, context)
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        FieldsRef(self, PhantomData).visit_artifacts(visitor)
    }

    fn contains_arg_attr(&self) -> bool {
        FieldsRef(self, PhantomData).contains_arg_attr()
    }

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        FieldsRef(self, PhantomData).visit_write_to_file_macros(visitor)
    }
}

impl<'v> Freeze for StarlarkCommandLine<'v> {
    type Frozen = FrozenStarlarkCommandLine;
    fn freeze(self, freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
        let StarlarkCommandLineData {
            items,
            hidden,
            options,
        } = self.0.into_inner();

        let items = ThinBoxSlice::from_iter(items.freeze(freezer)?);
        let hidden = ThinBoxSlice::from_iter(hidden.freeze(freezer)?);
        let options = options
            .try_map(|options| (*options).freeze(freezer))?
            .unwrap_or_default();

        Ok(FrozenStarlarkCommandLine {
            items,
            hidden,
            options,
        })
    }
}

impl<'v> StarlarkCommandLine<'v> {
    pub(crate) fn new() -> Self {
        Self::default()
    }

    pub fn try_from_value(value: Value<'v>) -> anyhow::Result<Self> {
        let mut builder = Self::new();
        builder.0.get_mut().add_value(value)?;
        Ok(builder)
    }

    pub(crate) fn try_from_values_with_options(
        value: &[Value<'v>],
        delimiter: Option<StringValue<'v>>,
        format: Option<StringValue<'v>>,
        prepend: Option<StringValue<'v>>,
        quote: Option<QuoteStyle>,
    ) -> anyhow::Result<Self> {
        let mut builder = StarlarkCommandLineData::default();
        if delimiter.is_some() || format.is_some() || prepend.is_some() || quote.is_some() {
            let opts = builder.options_mut();
            opts.delimiter = delimiter;
            opts.format = format;
            opts.prepend = prepend;
            opts.quote = quote;
        }
        for v in value {
            builder.add_value(*v)?;
        }
        Ok(Self(RefCell::new(builder)))
    }
}

impl<'v> StarlarkCommandLineData<'v> {
    fn add_value(&mut self, value: Value<'v>) -> anyhow::Result<()> {
        if let Some(values) = ListRef::from_value(value) {
            self.add_values(values.content())?;
        } else {
            self.items.push(CommandLineArg::try_from_value(value)?);
        }
        Ok(())
    }

    /// Check the types of a list of values, and modify `data` accordingly
    ///
    /// The values must be one of: CommandLineArgLike or a list thereof.
    fn add_values(&mut self, values: &[Value<'v>]) -> anyhow::Result<()> {
        self.items.reserve(values.len());
        for value in values {
            self.add_value(*value)?
        }
        Ok(())
    }

    /// Add values to the artifact that don't show up on the command line, but do for dependency
    fn add_hidden(&mut self, values: &[Value<'v>]) -> anyhow::Result<()> {
        for value in values {
            if let Some(values) = ListRef::from_value(*value) {
                self.add_hidden(values.content())?;
            } else {
                self.hidden.push(CommandLineArg::try_from_value(*value)?);
            }
        }
        Ok(())
    }
}

struct StarlarkCommandLineMut<'v> {
    value: Value<'v>,
    borrow: RefMut<'v, StarlarkCommandLineData<'v>>,
}

impl<'v> StarlarkTypeRepr for StarlarkCommandLineMut<'v> {
    fn starlark_type_repr() -> Ty {
        StarlarkCommandLine::starlark_type_repr()
    }
}

impl<'v> UnpackValue<'v> for StarlarkCommandLineMut<'v> {
    fn expected() -> String {
        "command line builder; frozen command line cannot be mutated".to_owned()
    }

    fn unpack_value(value: Value<'v>) -> Option<Self> {
        value.downcast_ref::<StarlarkCommandLine>().map(|v| Self {
            value,
            borrow: v.0.borrow_mut(),
        })
    }
}

impl<'v> AllocValue<'v> for StarlarkCommandLineMut<'v> {
    fn alloc_value(self, _heap: &'v Heap) -> Value<'v> {
        self.value
    }
}

fn cmd_args<'v>(x: Value<'v>) -> FieldsRef<'v, impl Fields<'v>> {
    if let Some(x) = x.downcast_ref::<StarlarkCommandLine>() {
        FieldsRef(Either::Left(x.0.borrow()), PhantomData)
    } else if let Some(x) = x.downcast_ref::<FrozenStarlarkCommandLine>() {
        FieldsRef(Either::Right(x), PhantomData)
    } else {
        unreachable!("This parameter must always be a type of command args")
    }
}

/// The `cmd_args` type is created by `cmd_args()` and is consumed by `ctx.actions.run`.
/// The type is a mutable collection of strings and `artifact` values.
/// In general, command lines, artifacts, strings, `RunInfo` and lists thereof can be added to or used to construct a `cmd_args` value.
/// All these methods operate mutably on `cmd` and return that value too.
#[starlark_module]
fn command_line_builder_methods(builder: &mut MethodsBuilder) {
    /// A list of arguments to be added to the command line, which may including `cmd_args`, artifacts, strings, `RunInfo` or lists thereof.
    /// Note that this operation mutates the input `cmd_args`.
    fn add<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        #[starlark(args)] args: Vec<Value<'v>>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.add_values(&args)?;
        Ok(this)
    }

    /// Things to add to the command line which do not show up but are added as dependencies.
    /// The values can be anything normally permissible to pass to `add`.
    ///
    /// Typically used if the command you are running implicitly depends on files that are not
    /// passed on the command line, e.g. headers in the case of a C compilation.
    fn hidden<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        #[starlark(args)] args: Vec<Value<'v>>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.add_hidden(&args)?;
        Ok(this)
    }

    /// Causes this `cmd_args` to have no declared dependencies. Allows you to reference the path of an artifact _without_
    /// introducing dependencies on it.
    ///
    /// As an example where this can be useful, consider passing a dependency that is only accessed at runtime, but whose path
    /// must be baked into the binary. As an example:
    ///
    /// ```python
    /// resources = cmd_args(resource_file, format = "-DFOO={}").ignore_artifacts()
    /// ctx.actions.run(cmd_args("gcc", "-c", source_file, resources))
    /// ```
    ///
    /// Note that `ignore_artifacts` sets all artifacts referenced by this `cmd_args` to be ignored, including those added afterwards,
    /// so generally create a special `cmd_args` and scope it quite tightly.
    ///
    /// If you actually do use the inputs referenced by this command, you will either error out due to missing dependencies (if running actions remotely)
    /// or have untracked dependencies that will fail to rebuild when it should.
    fn ignore_artifacts<'v>(
        mut this: StarlarkCommandLineMut<'v>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.options_mut().ignore_artifacts = true;
        Ok(this)
    }

    /// Make all artifact paths relative to a given location. Typically used when the command
    /// you are running changes directory.
    ///
    /// ```python
    /// dir = symlinked_dir(...)
    /// script = [
    ///     cmd_args(cmd_args(dir, format = "cd {}"),
    ///     original_script.relative_to(dir)
    /// ]
    /// ```
    fn relative_to<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        #[starlark(require = pos)] directory: ValueOf<'v, RelativeOrigin<'v>>,
        #[starlark(require = named, default = 0i32)] parent: i32,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        if parent < 0 {
            return Err(ValueError::IncorrectParameterTypeNamed("parent".to_owned()).into());
        }
        this.borrow.options_mut().relative_to = Some((directory.value, parent as usize));
        Ok(this)
    }

    /// Adds a prefix to the end of start artifact. Often used if you have a `$ROOT` variable
    /// in a shell script and want to use it to make files absolute.
    ///
    /// ```python
    /// cmd_args(script).absolute_prefix("$ROOT/")
    /// ```
    fn absolute_prefix<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        prefix: StringValue<'v>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.options_mut().absolute_prefix = Some(prefix);
        Ok(this)
    }

    /// Adds a suffix to the end of every artifact. Useful in conjunction with `absolute_prefix` to wrap
    /// artifacts in function calls.
    ///
    /// ```python
    /// cmd_args(script).absolute_prefix("call(").absolute_suffix(")")
    /// ```
    fn absolute_suffix<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        suffix: StringValue<'v>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.options_mut().absolute_suffix = Some(suffix);
        Ok(this)
    }

    /// For all the artifacts listed in this `cmd_args`, use their parent directory.
    ///
    /// Typically used when the file name is passed one way, and the directory another,
    /// e.g. `cmd_args(artifact, format="-L{}").parent()`.
    fn parent<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        #[starlark(require = pos, default = 1u32)] count: u32,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        this.borrow.options_mut().parent += count;
        Ok(this)
    }

    /// Replaces all parts matching pattern regular expression in each argument with replacement string.
    /// Several replacements can be added by multiple replace_regex calls.
    fn replace_regex<'v>(
        mut this: StarlarkCommandLineMut<'v>,
        pattern: StringValue<'v>,
        replacement: StringValue<'v>,
    ) -> anyhow::Result<StarlarkCommandLineMut<'v>> {
        let options = this.borrow.options_mut();
        // Validate that regex is valid
        Regex::new(pattern.as_str())?;
        if let Some(replacements) = &mut options.replacements {
            replacements.push((pattern, replacement));
        } else {
            options.replacements = Some(Box::new(vec![(pattern, replacement)]));
        }
        Ok(this)
    }

    /// Returns a copy of the `cmd_args` such that any modifications to the original or the returned value will not impact each other.
    /// Note that this is a shallow copy, so any inner `cmd_args` can still be modified.
    fn copy<'v>(this: Value<'v>) -> anyhow::Result<StarlarkCommandLine<'v>> {
        Ok(cmd_args(this).copy())
    }

    /// Collect all the inputs (including hidden) referenced by this command line.
    /// The output can be compared for equality and have its `len` requested to see whether
    /// there are any inputs, but is otherwise mostly opaque.
    #[starlark(attribute)]
    fn inputs<'v>(this: Value<'v>) -> anyhow::Result<StarlarkCommandLineInputs> {
        let mut visitor = SimpleCommandLineArtifactVisitor::new();
        cmd_args(this).visit_artifacts(&mut visitor)?;
        Ok(StarlarkCommandLineInputs {
            inputs: visitor.inputs,
        })
    }

    /// Collect all the outputs (including hidden) referenced by this command line.
    #[starlark(attribute)]
    fn outputs<'v>(
        this: Value<'v>,
        heap: &Heap,
    ) -> anyhow::Result<Vec<StarlarkOutputArtifact<'v>>> {
        let mut visitor = SimpleCommandLineArtifactVisitor::new();
        cmd_args(this).visit_artifacts(&mut visitor)?;
        let mut outputs = Vec::with_capacity(visitor.outputs.len());
        for out in visitor.outputs {
            let declared = heap.alloc_typed(StarlarkDeclaredArtifact::new(
                None,
                (*out).dupe(),
                AssociatedArtifacts::new(),
            ));
            outputs.push(StarlarkOutputArtifact::new(declared));
        }
        Ok(outputs)
    }
}

/// A wrapper for a [StarlarkCommandLine]'s inputs. This is an opaque type that only allows
/// debug-printing and querying the length to tell if any inputs exist.
#[derive(Debug, PartialEq, ProvidesStaticType, NoSerialize, Allocative)]
pub struct StarlarkCommandLineInputs {
    pub inputs: IndexSet<ArtifactGroup>,
}

starlark_simple_value!(StarlarkCommandLineInputs);

impl Display for StarlarkCommandLineInputs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut comma = commas();
        write!(f, "command_line_inputs(")?;
        for v in self.inputs.iter() {
            comma(f)?;
            Display::fmt(v, f)?;
        }
        write!(f, ")")
    }
}

#[starlark_value(type = "command_line_inputs")]
impl<'v> StarlarkValue<'v> for StarlarkCommandLineInputs {
    fn length(&self) -> anyhow::Result<i32> {
        Ok(self.inputs.len().try_into()?)
    }

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


/*
 * 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::fmt::Display;

use allocative::Allocative;
use dupe::Dupe;
use serde::Serializer;
use starlark::__derive_refs::serde::Serialize;
use starlark::coerce::Coerce;
use starlark::typing::Ty;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::Freeze;
use starlark::values::Freezer;
use starlark::values::FrozenValue;
use starlark::values::Trace;
use starlark::values::Value;
use starlark::values::ValueLike;

use crate::interpreter::rule_defs::cmd_args::value_as::ValueAsCommandLineLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;

fn serialize_as_display<V: Display, S>(v: &V, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.collect_str(v)
}

/// A tiny wrapper around `Value`/`FrozenValue` that proxies `CommandLineArgLike` calls.
///
/// This should be unnecessary, however I'm not smart enough to figure out how to get
/// things to live long enough, in `ValueAsCommandLineArgLike`, so I'm moving on with my life
/// for now. All values contained in here are guaranteed to implement `CommandLineArgLike`.
#[derive(
    Debug,
    Clone,
    Copy,
    Dupe,
    Trace,
    derive_more::Display,
    Serialize,
    Allocative,
    Coerce
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct CommandLineArg<'v>(#[serde(serialize_with = "serialize_as_display")] Value<'v>);

impl<'v> Freeze for CommandLineArg<'v> {
    type Frozen = FrozenCommandLineArg;

    fn freeze(self, freezer: &Freezer) -> anyhow::Result<FrozenCommandLineArg> {
        Ok(FrozenCommandLineArg(self.0.freeze(freezer)?))
    }
}

impl<'v> CommandLineArg<'v> {
    pub(crate) fn try_from_value(value: Value<'v>) -> anyhow::Result<Self> {
        value.to_value().as_command_line_err()?;
        Ok(Self(value))
    }

    pub(crate) fn as_command_line_arg(self) -> &'v dyn CommandLineArgLike {
        self.0
            .as_command_line_err()
            .expect("checked type in constructor")
    }
}

impl<'v> StarlarkTypeRepr for CommandLineArg<'v> {
    fn starlark_type_repr() -> Ty {
        Ty::name("cmd_args")
    }
}

#[derive(
    Debug,
    Allocative,
    Eq,
    PartialEq,
    derive_more::Display,
    Clone,
    Copy,
    Dupe
)]
#[repr(transparent)]
pub struct FrozenCommandLineArg(FrozenValue);

unsafe impl<'v> Coerce<CommandLineArg<'v>> for FrozenCommandLineArg {}

impl FrozenCommandLineArg {
    pub fn new(value: FrozenValue) -> anyhow::Result<FrozenCommandLineArg> {
        value.to_value().as_command_line_err()?;
        Ok(FrozenCommandLineArg(value))
    }

    pub fn as_command_line_arg<'v>(self) -> &'v dyn CommandLineArgLike {
        CommandLineArg(self.0.to_value()).as_command_line_arg()
    }
}


/*
 * 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_interpreter::types::label_relative_path::LabelRelativePath;
use buck2_interpreter::types::target_label::StarlarkTargetLabel;
use starlark::values::Value;
use starlark::values::ValueLike;

use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::provider::builtin::run_info::FrozenRunInfo;
use crate::interpreter::rule_defs::provider::builtin::run_info::RunInfo;

#[derive(Debug, thiserror::Error)]
enum CommandLineArgError {
    #[error(
        "expected command line item to be a string, artifact, or label, or list thereof, not `{repr}`"
    )]
    InvalidItemType { repr: String },
}

pub trait ValueAsCommandLineLike<'v> {
    fn as_command_line(&self) -> Option<&'v dyn CommandLineArgLike>;
    fn as_command_line_err(&self) -> anyhow::Result<&'v dyn CommandLineArgLike>;
}

impl<'v> ValueAsCommandLineLike<'v> for Value<'v> {
    fn as_command_line(&self) -> Option<&'v dyn CommandLineArgLike> {
        if let Some(x) = self.to_value().unpack_starlark_str() {
            return Some(x as &dyn CommandLineArgLike);
        }

        macro_rules! check {
            ($t:ty) => {
                if let Some(v) = self.to_value().downcast_ref::<$t>() {
                    return Some(v as &dyn CommandLineArgLike);
                }
            };
        }

        // Typically downcasting is provided by implementing `StarlarkValue::provide`.
        // These are exceptions:
        // * either providers, where `StarlarkValue` is generated,
        //   and plugging in `provide` is tricky
        // * or live outside of `buck2_build_api` crate,
        //   so `impl StarlarkValue` cannot provide `CommandLineArgLike`
        check!(RunInfo);
        check!(FrozenRunInfo);
        check!(LabelRelativePath);
        check!(StarlarkTargetLabel);

        self.request_value()
    }

    fn as_command_line_err(&self) -> anyhow::Result<&'v dyn CommandLineArgLike> {
        self.as_command_line().ok_or_else(|| {
            CommandLineArgError::InvalidItemType {
                repr: self.to_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::cell::RefCell;
use std::fmt;
use std::fmt::Display;
use std::sync::Arc;

use allocative::Allocative;
use buck2_core::cells::cell_path::CellPath;
use buck2_core::provider::id::ProviderId;
use buck2_interpreter::types::provider::callable::ProviderCallableLike;
use dupe::Dupe;
use once_cell::unsync;
use starlark::any::ProvidesStaticType;
use starlark::docs::DocItem;
use starlark::docs::DocString;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::eval::Arguments;
use starlark::eval::Evaluator;
use starlark::eval::ParametersSpec;
use starlark::values::starlark_value;
use starlark::values::AllocValue;
use starlark::values::Demand;
use starlark::values::Freeze;
use starlark::values::Freezer;
use starlark::values::FrozenRef;
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 starlark::values::ValueLike;
use starlark_map::small_set::SmallSet;

use crate::interpreter::rule_defs::provider::user::user_provider_creator;

#[derive(Debug, thiserror::Error)]
enum ProviderCallableError {
    #[error(
        "The result of `provider()` must be assigned to a top-level variable before it can be called"
    )]
    NotBound,
    #[error(
        "Provider type must be assigned to a variable, e.g. `ProviderInfo = provider(fields = {0:?})`"
    )]
    ProviderNotAssigned(SmallSet<String>),
}

fn create_callable_function_signature(
    function_name: &str,
    fields: &SmallSet<String>,
) -> ParametersSpec<FrozenValue> {
    let mut signature = ParametersSpec::with_capacity(function_name.to_owned(), fields.len());
    // TODO(nmj): Should double check we don't actually need positional args in-repo
    signature.no_more_positional_args();
    for field in fields {
        signature.defaulted(field, FrozenValue::new_none());
    }

    signature.finish()
}

#[derive(Debug, Allocative)]
pub(crate) struct UserProviderCallableData {
    pub(crate) provider_id: Arc<ProviderId>,
    pub(crate) fields: SmallSet<String>,
}

#[derive(Debug, Trace, Allocative)]
enum UserProviderCallableImpl {
    Unbound,
    Bound(
        ParametersSpec<FrozenValue>,
        FrozenRef<'static, UserProviderCallableData>,
    ),
}

impl UserProviderCallableImpl {
    fn invoke<'v>(
        &self,
        args: &Arguments<'v, '_>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        match self {
            UserProviderCallableImpl::Unbound => Err(ProviderCallableError::NotBound.into()),
            UserProviderCallableImpl::Bound(signature, data) => {
                signature.parser(args, eval, |parser, eval| {
                    user_provider_creator(*data, eval, parser)
                })
            }
        }
    }
}

/// The result of calling `provider()`. This is a callable that accepts the fields
/// provided in the `provider()` call, and generates a Starlark `UserProvider` object.
///
/// This object must be assigned to a variable at the top level of the module before it may be invoked
///
/// Field values default to `None`
#[derive(Debug, ProvidesStaticType, Trace, NoSerialize, Allocative)]
pub struct UserProviderCallable {
    /// The name of this provider, filled in by `export_as()`. This must be set before this
    /// object can be called and Providers created.
    id: unsync::OnceCell<Arc<ProviderId>>,
    /// The path where this `ProviderCallable` is created and assigned
    path: CellPath,
    /// The docstring for this provider
    docs: Option<DocString>,
    /// The docstrings for each field. The length of must be identical to `fields`
    field_docs: Vec<Option<DocString>>,
    /// The names of the fields used in `callable`
    fields: SmallSet<String>,
    /// The actual callable that creates instances of `UserProvider`
    callable: RefCell<UserProviderCallableImpl>,
}

impl Display for UserProviderCallable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.id() {
            None => write!(f, "unnamed provider"),
            Some(id) => {
                write!(f, "{}(", id.name)?;
                for (i, x) in self.fields.iter().enumerate() {
                    if i != 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{}", x)?;
                }
                write!(f, ")")
            }
        }
    }
}

impl UserProviderCallable {
    pub fn new(
        path: CellPath,
        docs: Option<DocString>,
        field_docs: Vec<Option<DocString>>,
        fields: SmallSet<String>,
    ) -> Self {
        assert_eq!(
            field_docs.len(),
            fields.len(),
            "Expected {} fields, but got docs for {} fields",
            fields.len(),
            field_docs.len()
        );
        Self {
            id: unsync::OnceCell::new(),
            path,
            docs,
            field_docs,
            fields,
            callable: RefCell::new(UserProviderCallableImpl::Unbound),
        }
    }
}

impl ProviderCallableLike for UserProviderCallable {
    fn id(&self) -> Option<&Arc<ProviderId>> {
        self.id.get()
    }
}

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

impl Freeze for UserProviderCallable {
    type Frozen = FrozenUserProviderCallable;
    fn freeze(self, _freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
        let callable = self.callable.into_inner();
        let id = match self.id.into_inner() {
            Some(x) => x,
            None => {
                // Unfortunately we have no name or location for the provider at this point,
                // so reproduce the fields so that the provider can be identified.
                return Err(ProviderCallableError::ProviderNotAssigned(self.fields).into());
            }
        };

        Ok(FrozenUserProviderCallable::new(
            id,
            self.docs,
            self.field_docs,
            self.fields,
            callable,
        ))
    }
}

#[starlark_value(type = "provider_callable")]
impl<'v> StarlarkValue<'v> for UserProviderCallable {
    fn export_as(&self, variable_name: &str, eval: &mut Evaluator<'v, '_>) {
        // First export wins
        self.id.get_or_init(|| {
            let new_id = Arc::new(ProviderId {
                path: Some(self.path.clone()),
                name: variable_name.to_owned(),
            });
            *self.callable.borrow_mut() = UserProviderCallableImpl::Bound(
                create_callable_function_signature(&new_id.name, &self.fields),
                eval.frozen_heap()
                    .alloc_any_display_from_debug(UserProviderCallableData {
                        provider_id: new_id.dupe(),
                        fields: self.fields.clone(),
                    }),
            );
            new_id
        });
    }

    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(provider_callable_methods)
    }

    fn invoke(
        &self,
        _me: Value<'v>,
        args: &Arguments<'v, '_>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        self.callable.borrow().invoke(args, eval)
    }

    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn ProviderCallableLike>(self);
    }

    fn documentation(&self) -> Option<DocItem> {
        let return_types = vec![None; self.fields.len()];
        self.provider_callable_documentation(
            None,
            &self.docs,
            &self.fields.iter().map(|x| x.as_str()).collect::<Vec<_>>(),
            &self.field_docs,
            &return_types,
        )
    }
}

#[derive(Debug, ProvidesStaticType, NoSerialize, Allocative)]
pub struct FrozenUserProviderCallable {
    /// The name of this provider, filled in by `export_as()`. This must be set before this
    /// object can be called and Providers created.
    id: Arc<ProviderId>,
    /// The docstring for this provider
    docs: Option<DocString>,
    /// The docstrings for each field. The length of must be identical to `fields`
    field_docs: Vec<Option<DocString>>,
    /// The names of the fields used in `callable`
    fields: SmallSet<String>,
    /// The actual callable that creates instances of `UserProvider`
    callable: UserProviderCallableImpl,
}
starlark_simple_value!(FrozenUserProviderCallable);

impl Display for FrozenUserProviderCallable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}(", self.id.name)?;
        for (i, x) in self.fields.iter().enumerate() {
            if i != 0 {
                write!(f, ", ")?;
            }
            write!(f, "{}", x)?;
        }
        write!(f, ")")
    }
}

impl FrozenUserProviderCallable {
    fn new(
        id: Arc<ProviderId>,
        docs: Option<DocString>,
        field_docs: Vec<Option<DocString>>,
        fields: SmallSet<String>,
        callable: UserProviderCallableImpl,
    ) -> Self {
        assert_eq!(
            field_docs.len(),
            fields.len(),
            "Expected {} fields, but got docs for {} fields",
            fields.len(),
            field_docs.len()
        );
        Self {
            id,
            docs,
            field_docs,
            fields,
            callable,
        }
    }
}

impl ProviderCallableLike for FrozenUserProviderCallable {
    fn id(&self) -> Option<&Arc<ProviderId>> {
        Some(&self.id)
    }
}

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

    fn invoke(
        &self,
        _me: Value<'v>,
        args: &Arguments<'v, '_>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        self.callable.invoke(args, eval)
    }

    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn ProviderCallableLike>(self);
    }

    fn documentation(&self) -> Option<DocItem> {
        let return_types = vec![None; self.fields.len()];
        self.provider_callable_documentation(
            None,
            &self.docs,
            &self.fields.iter().map(|x| x.as_str()).collect::<Vec<_>>(),
            &self.field_docs,
            &return_types,
        )
    }
}

#[starlark_module]
fn provider_callable_methods(builder: &mut MethodsBuilder) {
    #[starlark(attribute)]
    fn r#type<'v>(this: Value<'v>, heap: &Heap) -> anyhow::Result<Value<'v>> {
        if let Some(x) = this.downcast_ref::<UserProviderCallable>() {
            match x.id.get() {
                None => Err(ProviderCallableError::ProviderNotAssigned(x.fields.clone()).into()),
                Some(id) => Ok(heap.alloc(id.name.as_str())),
            }
        } else if let Some(x) = this.downcast_ref::<FrozenUserProviderCallable>() {
            Ok(heap.alloc(x.id.name.as_str()))
        } else {
            unreachable!(
                "This parameter must be one of the types, but got `{}`",
                this.get_type()
            )
        }
    }
}