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

impl CommandLineArgLike for LabelRelativePath {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        let path = ctx.resolve_cell_path(self.0.as_ref())?.into_string();
        cli.push_arg(path);
        Ok(())
    }

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

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        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 starlark::environment::GlobalsBuilder;

use crate::interpreter::rule_defs::provider::registration::register_builtin_providers;

pub mod artifact;
pub mod artifact_tagging;
pub mod cmd_args;
pub mod command_executor_config;
pub mod context;
pub mod label_relative_path;
pub mod provider;
pub mod resolve_query_macro;
pub mod resolved_macro;
pub mod transitive_set;

pub fn register_rule_defs(globals: &mut GlobalsBuilder) {
    cmd_args::register_cmd_args(globals);
    register_builtin_providers(globals);
}


/*
 * 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.
 */

//! Provides the starlark values representing resolved attrs.arg() attributes.

use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;

use allocative::Allocative;
use buck2_node::attrs::attr_type::arg::ConfiguredStringWithMacros;
use buck2_util::arc_str::ArcStr;
use starlark::any::ProvidesStaticType;
use starlark::values::starlark_value;
use starlark::values::Demand;
use starlark::values::FrozenRef;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Value;
use static_assertions::assert_eq_size;

use crate::interpreter::rule_defs::artifact::StarlarkArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkArtifactLike;
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::value::FrozenCommandLineArg;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;
use crate::interpreter::rule_defs::provider::builtin::default_info::FrozenDefaultInfo;
use crate::interpreter::rule_defs::resolve_query_macro::ResolvedQueryMacro;

// TODO(cjhopman): Consider making DefaultOutputs implement CommandLineArgLike
// itself, and then a resolved macro is just a CommandLineArgLike.

// TODO(cjhopman): Consider making ResolvedMacro, ResolvedStringWithMacros etc
// parameterized on a Value type so that we can have non-frozen things. At that
// point we could get rid of the Query variant for ResolvedMacro.

#[derive(Debug, PartialEq, Allocative)]
pub enum ResolvedMacro {
    Location(FrozenRef<'static, FrozenDefaultInfo>),
    /// Holds an arg-like value
    ArgLike(FrozenCommandLineArg),
    /// Holds a resolved query placeholder
    Query(ResolvedQueryMacro),
}

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

impl Display for ResolvedMacro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ResolvedMacro::Location(_) => {
                // Unfortunately we don't keep the location here, which makes it harder to show
                write!(f, "$(location ...)")
            }
            ResolvedMacro::ArgLike(x) => Display::fmt(x, f),
            ResolvedMacro::Query(x) => Display::fmt(x, f),
        }
    }
}

pub fn add_output_to_arg(
    builder: &mut dyn ArgBuilder,
    ctx: &mut dyn CommandLineContext,
    artifact: &StarlarkArtifact,
) -> anyhow::Result<()> {
    let path = ctx
        .resolve_artifact(&artifact.get_bound_artifact()?)?
        .into_string();
    builder.push_str(&path);
    Ok(())
}

fn add_outputs_to_arg(
    builder: &mut dyn ArgBuilder,
    ctx: &mut dyn CommandLineContext,
    outputs_list: &[StarlarkArtifact],
) -> anyhow::Result<()> {
    for (i, value) in outputs_list.iter().enumerate() {
        if i != 0 {
            builder.push_str(" ");
        }
        add_output_to_arg(builder, ctx, value)?;
    }
    Ok(())
}

impl ResolvedMacro {
    pub fn add_to_arg(
        &self,
        builder: &mut dyn ArgBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        match self {
            Self::Location(info) => {
                let outputs = &info.default_outputs();

                add_outputs_to_arg(builder, ctx, outputs)?;
            }
            Self::ArgLike(command_line_like) => {
                let mut cli_builder = SpaceSeparatedCommandLineBuilder::wrap(builder);
                command_line_like
                    .as_command_line_arg()
                    .add_to_command_line(&mut cli_builder, ctx)?;
            }
            Self::Query(value) => value.add_to_arg(builder, ctx)?,
        };

        Ok(())
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        match self {
            Self::Location(info) => {
                info.for_each_output(&mut |i| {
                    visitor.visit_input(i, None);
                    Ok(())
                })?;
            }
            Self::ArgLike(command_line_like) => {
                command_line_like
                    .as_command_line_arg()
                    .visit_artifacts(visitor)?;
            }
            Self::Query(value) => value.visit_artifacts(visitor)?,
        }
        Ok(())
    }
}

#[derive(Debug, PartialEq, Allocative)]
pub enum ResolvedStringWithMacrosPart {
    String(ArcStr),
    Macro(/* write_to_file */ bool, ResolvedMacro),
}

impl Display for ResolvedStringWithMacrosPart {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::String(x) => f.write_str(x),
            Self::Macro(b, x) => {
                if *b {
                    write!(f, "@")?;
                }
                Display::fmt(x, f)
            }
        }
    }
}

#[derive(Debug, PartialEq, ProvidesStaticType, NoSerialize, Allocative)]
pub struct ResolvedStringWithMacros {
    parts: Vec<ResolvedStringWithMacrosPart>,
    configured_macros: Option<ConfiguredStringWithMacros>,
}

starlark_simple_value!(ResolvedStringWithMacros);

impl Display for ResolvedStringWithMacros {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "\"")?;
        for x in &self.parts {
            Display::fmt(x, f)?;
        }
        write!(f, "\"")
    }
}

impl ResolvedStringWithMacros {
    pub fn new(
        parts: Vec<ResolvedStringWithMacrosPart>,
        configured_macros: Option<&ConfiguredStringWithMacros>,
    ) -> Self {
        Self {
            parts,
            configured_macros: configured_macros.cloned(),
        }
    }

    /// Access the `&str` in this ResolvedStringWithMacros, *if* this ResolvedStringWithMacros is
    /// secretely just one String.
    pub fn downcast_str(&self) -> Option<&str> {
        let mut iter = self.parts.iter();
        match (iter.next(), iter.next()) {
            (Some(ResolvedStringWithMacrosPart::String(s)), None) => Some(s),
            _ => None,
        }
    }

    pub fn configured_macros(&self) -> &Option<ConfiguredStringWithMacros> {
        &self.configured_macros
    }
}

impl CommandLineArgLike for ResolvedStringWithMacros {
    fn add_to_command_line(
        &self,
        cmdline_builder: &mut dyn CommandLineBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        struct Builder {
            arg: String,
        }

        impl Builder {
            fn push_path(&mut self, ctx: &mut dyn CommandLineContext) -> anyhow::Result<()> {
                let next_path = ctx.next_macro_file_path()?;
                self.push_str(next_path.as_str());
                Ok(())
            }
        }

        impl ArgBuilder for Builder {
            /// Add the string representation to the list of command line arguments.
            fn push_str(&mut self, s: &str) {
                self.arg.push_str(s)
            }
        }

        let mut builder = Builder { arg: String::new() };

        for part in &*self.parts {
            match part {
                ResolvedStringWithMacrosPart::String(s) => {
                    builder.arg.push_str(s);
                }
                ResolvedStringWithMacrosPart::Macro(write_to_file, val) => {
                    if *write_to_file {
                        builder.push_str("@");
                        builder.push_path(ctx)?;
                    } else {
                        val.add_to_arg(&mut builder, ctx)?;
                    }
                }
            }
        }

        let Builder { arg } = builder;
        cmdline_builder.push_arg(arg);
        Ok(())
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        for part in &*self.parts {
            if let ResolvedStringWithMacrosPart::Macro(_, val) = part {
                val.visit_artifacts(visitor)?;
            }
        }

        Ok(())
    }

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

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        for part in &*self.parts {
            match part {
                ResolvedStringWithMacrosPart::String(_) => {
                    // nop
                }
                ResolvedStringWithMacrosPart::Macro(write_to_file, val) => {
                    if *write_to_file {
                        visitor.visit_write_to_file_macro(val)?;
                    } else {
                        // nop
                    }
                }
            }
        }
        Ok(())
    }
}

#[starlark_value(type = "resolved_macro")]
impl<'v> StarlarkValue<'v> for ResolvedStringWithMacros {
    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        match ResolvedStringWithMacros::from_value(other) {
            None => Ok(false),
            Some(other) => Ok(*self == *other),
        }
    }

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


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

use allocative::Allocative;
use buck2_core::target::label::ConfiguredTargetLabel;
use buck2_util::thin_box::ThinBoxSlice;
use static_assertions::assert_eq_size;

use crate::interpreter::rule_defs::artifact::starlark_artifact_like::StarlarkArtifactLike;
use crate::interpreter::rule_defs::artifact::StarlarkArtifact;
use crate::interpreter::rule_defs::cmd_args::arg_builder::ArgBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::resolved_macro::add_output_to_arg;

#[derive(Debug, PartialEq, Allocative)]
pub struct ResolvedQueryMacroTargetAndOutputs {
    pub sep: Box<str>,
    pub list: Box<[(ConfiguredTargetLabel, Box<[StarlarkArtifact]>)]>,
}

#[derive(Debug, PartialEq, Allocative)]
pub enum ResolvedQueryMacro {
    Outputs(ThinBoxSlice<Box<[StarlarkArtifact]>>),
    Targets(ThinBoxSlice<ConfiguredTargetLabel>),
    TargetsAndOutputs(Box<ResolvedQueryMacroTargetAndOutputs>),
}

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

impl Display for ResolvedQueryMacro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // TODO(ndmitchell): Include the information in the format output
        match self {
            ResolvedQueryMacro::Outputs(_) => {
                write!(f, "$(query_outputs ...)")
            }
            ResolvedQueryMacro::Targets(_) => write!(f, "$(query_targets ...)"),

            ResolvedQueryMacro::TargetsAndOutputs(_) => {
                write!(f, "$(query_targets_and_outputs ...)")
            }
        }
    }
}

impl ResolvedQueryMacro {
    pub fn add_to_arg(
        &self,
        builder: &mut dyn ArgBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        match self {
            Self::Outputs(list) => {
                let mut first = true;
                for target_outputs in list.iter() {
                    for output in target_outputs.iter() {
                        if !first {
                            builder.push_str(" ");
                        }
                        first = false;
                        add_output_to_arg(builder, ctx, output)?;
                    }
                }
            }
            Self::TargetsAndOutputs(target_and_outputs) => {
                let ResolvedQueryMacroTargetAndOutputs { sep, list } = &**target_and_outputs;
                let mut first = true;
                for (target, target_outputs) in list.iter() {
                    for output in target_outputs.iter() {
                        if !first {
                            builder.push_str(sep);
                        }
                        first = false;
                        builder.push_str(&target.unconfigured().to_string());
                        builder.push_str(sep);
                        add_output_to_arg(builder, ctx, output)?;
                    }
                }
            }
            Self::Targets(list) => {
                // This is defined to add the plain (unconfigured) labels.
                for (i, target) in list.iter().enumerate() {
                    if i != 0 {
                        builder.push_str(" ");
                    }
                    builder.push_str(&target.unconfigured().to_string());
                }
            }
        }
        Ok(())
    }

    pub fn visit_artifacts(
        &self,
        visitor: &mut dyn CommandLineArtifactVisitor,
    ) -> anyhow::Result<()> {
        match self {
            Self::Outputs(list) => {
                for target_outputs in list.iter() {
                    for artifact in target_outputs.iter() {
                        artifact.as_command_line_like().visit_artifacts(visitor)?;
                    }
                }
            }
            Self::TargetsAndOutputs(targets_and_outputs) => {
                for (_, target_outputs) in &*targets_and_outputs.list {
                    for artifact in target_outputs.iter() {
                        artifact.as_command_line_like().visit_artifacts(visitor)?;
                    }
                }
            }
            Self::Targets(..) => {
                // no inputs
            }
        }
        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::sync::Arc;

use allocative::Allocative;
use buck2_util::collections::ordered_set::OrderedSet;
use dupe::*;
use starlark::values::Trace;

use crate::artifact_groups::ArtifactGroup;

#[derive(Debug, Clone, Dupe_, Allocative, Trace, PartialEq)]
pub struct AssociatedArtifacts(Option<Arc<OrderedSet<ArtifactGroup>>>);

impl AssociatedArtifacts {
    pub fn new() -> Self {
        Self(None)
    }

    pub fn union(&self, other: AssociatedArtifacts) -> AssociatedArtifacts {
        match (&self.0, &other.0) {
            (_, None) => self.dupe(),
            (None, _) => other,
            (Some(left), Some(right)) => Self::from(left.iter().chain(right.iter()).duped()),
        }
    }

    pub fn from<T: IntoIterator<Item = ArtifactGroup>>(from: T) -> Self {
        let values: OrderedSet<_> = from.into_iter().collect();
        if values.is_empty() {
            Self(None)
        } else {
            Self(Some(Arc::new(values)))
        }
    }

    pub fn len(&self) -> usize {
        match &self.0 {
            Some(v) => v.len(),
            None => 0,
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &ArtifactGroup> {
        self.0.iter().map(|v| v.iter()).flatten()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}


/*
 * 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 associated;
pub mod output_artifact_like;
mod starlark_artifact;
pub mod starlark_artifact_like;
mod starlark_artifact_value;
mod starlark_declared_artifact;
mod starlark_output_artifact;
mod starlark_promise_artifact;

use std::fmt::Debug;

use buck2_core::base_deferred_key::BaseDeferredKey;

pub use self::starlark_artifact::StarlarkArtifact;
pub(crate) use self::starlark_artifact_like::StarlarkArtifactLike;
pub(crate) use self::starlark_artifact_like::ValueAsArtifactLike;
pub use self::starlark_artifact_value::StarlarkArtifactValue;
pub use self::starlark_declared_artifact::StarlarkDeclaredArtifact;
pub use self::starlark_output_artifact::FrozenStarlarkOutputArtifact;
pub use self::starlark_output_artifact::StarlarkOutputArtifact;
pub use self::starlark_output_artifact::StarlarkOutputOrDeclaredArtifact;
pub use self::starlark_promise_artifact::StarlarkPromiseArtifact;

#[derive(Debug, thiserror::Error)]
pub(crate) enum ArtifactError {
    #[error("expected artifact {repr} to be used as the output of an action, but it was not")]
    DeclaredArtifactWasNotBound { repr: String },
    #[error(
        "attempted to use source artifact {repr} as the output of an action. Source \
        artifacts may not be outputs."
    )]
    SourceArtifactAsOutput { repr: String },
    #[error(
        "attempted to use artifact {artifact_repr} as the output of an action, but \
        it was already used by another action in {existing_owner}"
    )]
    BoundArtifactAsOutput {
        artifact_repr: String,
        existing_owner: BaseDeferredKey,
    },
    #[error(
        "attempted to use promise artifact {artifact_repr} as the output of an action, but \
        only declared artifacts can be used as an output"
    )]
    PromiseArtifactAsOutput { artifact_repr: 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 starlark::typing::Ty;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueTyped;

use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkOutputArtifact;
use crate::interpreter::rule_defs::artifact::ValueAsArtifactLike;

pub enum OutputArtifactArg<'v> {
    Str(&'v str),
    OutputArtifact(ValueTyped<'v, StarlarkOutputArtifact<'v>>),
    DeclaredArtifact(ValueTyped<'v, StarlarkDeclaredArtifact>),
    /// This for error reporting.
    WrongArtifact(ValueAsArtifactLike<'v>),
}

impl<'v> StarlarkTypeRepr for OutputArtifactArg<'v> {
    fn starlark_type_repr() -> Ty {
        Ty::unions(vec![
            <&str>::starlark_type_repr(),
            StarlarkOutputArtifact::starlark_type_repr(),
            ValueAsArtifactLike::starlark_type_repr(),
        ])
    }
}

impl<'v> UnpackValue<'v> for OutputArtifactArg<'v> {
    #[allow(clippy::manual_map, clippy::same_functions_in_if_condition)]
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        if let Some(s) = value.unpack_str() {
            Some(OutputArtifactArg::Str(s))
        } else if let Some(artifact) = ValueTyped::new(value) {
            Some(OutputArtifactArg::OutputArtifact(artifact))
        } else if let Some(artifact) = ValueTyped::new(value) {
            Some(OutputArtifactArg::DeclaredArtifact(artifact))
        } else if let Some(artifact_like) = ValueAsArtifactLike::unpack_value(value) {
            Some(OutputArtifactArg::WrongArtifact(artifact_like))
        } else {
            None
        }
    }
}


/*
 * 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 buck2_artifact::artifact::artifact_type::Artifact;
use buck2_artifact::artifact::artifact_type::BaseArtifactKind;
use buck2_core::base_deferred_key::BaseDeferredKey;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersName;
use buck2_execute::path::artifact_path::ArtifactPath;
use buck2_interpreter::types::label::Label;
use dupe::Dupe;
use serde::Serialize;
use serde::Serializer;
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::Demand;
use starlark::values::Heap;
use starlark::values::StarlarkValue;
use starlark::values::StringValue;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueLike;
use thiserror::Error;

use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact::associated::AssociatedArtifacts;
use crate::interpreter::rule_defs::artifact::starlark_artifact_like::ArtifactFingerprint;
use crate::interpreter::rule_defs::artifact::ArtifactError;
use crate::interpreter::rule_defs::artifact::StarlarkArtifactLike;
use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkOutputArtifact;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;

/// A wrapper for an `Artifact` that is guaranteed to be bound, such as outputs
/// from dependencies, or source files.
#[derive(
    Debug,
    Dupe,
    Clone,
    PartialEq,
    ProvidesStaticType,
    Allocative,
    StarlarkDocs
)]
pub struct StarlarkArtifact {
    pub(crate) artifact: Artifact,
    // A set of ArtifactGroups that should be materialized along with the main artifact
    pub(crate) associated_artifacts: AssociatedArtifacts,
}

starlark_simple_value!(StarlarkArtifact);

impl<'v> UnpackValue<'v> for StarlarkArtifact {
    fn expected() -> String {
        format!(
            "either {} or {}",
            StarlarkArtifact::get_type_value_static().as_str(),
            StarlarkDeclaredArtifact::get_type_value_static().as_str()
        )
    }

    fn unpack_value(value: Value<'v>) -> Option<Self> {
        if let Some(x) = value.downcast_ref::<StarlarkArtifact>() {
            Some(x.dupe())
        } else if let Some(x) = value.downcast_ref::<StarlarkDeclaredArtifact>() {
            x.get_bound_artifact().ok().map(|a| StarlarkArtifact {
                artifact: a,
                associated_artifacts: x.associated_artifacts.dupe(),
            })
        } else {
            None
        }
    }
}

impl StarlarkArtifact {
    pub fn new(artifact: Artifact) -> Self {
        StarlarkArtifact {
            artifact,
            associated_artifacts: AssociatedArtifacts::new(),
        }
    }

    pub fn artifact(&self) -> Artifact {
        self.artifact.dupe()
    }

    pub fn get_artifact_path(&self) -> ArtifactPath<'_> {
        self.artifact.get_path()
    }
}

impl Display for StarlarkArtifact {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // FIXME(ndmitchell): This display is not the same as the underlying Artifact, but they should probably be the same
        write!(
            f,
            "<{} ",
            if self.artifact.is_source() {
                "source"
            } else {
                "build artifact"
            }
        )?;

        // Historically this has used the full path whereas everything uses the short path.
        self.artifact
            .get_path()
            .with_full_path(|p| write!(f, "{}", p))?;

        if let Some(owner) = self.artifact.owner() {
            write!(f, " bound to {}", owner)?;
        }

        write!(f, ">")?;

        Ok(())
    }
}

impl Serialize for StarlarkArtifact {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        s.collect_str(self)
    }
}

impl StarlarkArtifactLike for StarlarkArtifact {
    fn as_output_error(&self) -> anyhow::Error {
        match self.artifact.as_parts().0 {
            BaseArtifactKind::Source(_) => ArtifactError::SourceArtifactAsOutput {
                repr: self.to_string(),
            }
            .into(),
            BaseArtifactKind::Build(b) => ArtifactError::BoundArtifactAsOutput {
                artifact_repr: self.to_string(),
                existing_owner: b.get_path().owner().dupe(),
            }
            .into(),
        }
    }

    fn get_bound_artifact(&self) -> anyhow::Result<Artifact> {
        Ok(self.artifact.dupe())
    }

    fn get_associated_artifacts(&self) -> Option<&AssociatedArtifacts> {
        Some(&self.associated_artifacts)
    }

    fn as_command_line_like(&self) -> &dyn CommandLineArgLike {
        self
    }

    fn fingerprint(&self) -> ArtifactFingerprint<'_> {
        {
            let path = self.artifact.get_path();
            let associated_artifacts = self.get_associated_artifacts();
            ArtifactFingerprint::Normal {
                path,
                associated_artifacts,
            }
        }
    }

    fn get_artifact_group(&self) -> anyhow::Result<ArtifactGroup> {
        Ok(ArtifactGroup::Artifact(self.get_bound_artifact()?))
    }
}

impl CommandLineArgLike for StarlarkArtifact {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg(ctx.resolve_artifact(&self.artifact)?.into_string());
        Ok(())
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        visitor.visit_input(ArtifactGroup::Artifact(self.artifact.dupe()), None);
        self.associated_artifacts
            .iter()
            .for_each(|ag| visitor.visit_input(ag.dupe(), None));
        Ok(())
    }

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

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

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

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        StarlarkArtifactLike::equals(self, other)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        StarlarkArtifactLike::write_hash(self, hasher)
    }

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

#[derive(Error, Debug)]
enum CannotProject {
    #[error("Source artifacts cannot be projected")]
    SourceArtifact,
    #[error("This artifact was declared by another rule: `{0}`")]
    DeclaredElsewhere(BaseDeferredKey),
}

pub(crate) struct StarlarkArtifactHelpers;
impl StarlarkArtifactHelpers {
    /// The base name of this artifact. e.g. for an artifact at `foo/bar`, this is `bar`
    pub(crate) fn basename<'v>(
        artifact: &Artifact,
        heap: &'v Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        artifact
            .get_path()
            .with_filename(|filename| Ok(heap.alloc_str(filename?.as_str())))
    }

    /// The file extension of this artifact. e.g. for an artifact at foo/bar.sh,
    /// this is `.sh`. If no extension is present, `""` is returned.
    pub(crate) fn extension<'v>(
        artifact: &Artifact,
        heap: &'v Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        artifact.get_path().with_filename(|filename| {
            Ok(match filename?.extension() {
                None => heap.alloc_str(""),
                Some(x) => heap.alloc_str_concat(".", x),
            })
        })
    }

    /// The `Label` of the rule that originally created this artifact. May also be None in
    /// the case of source files, or if the artifact has not be used in an action, or if the
    /// action was not created by a rule.
    pub(crate) fn owner(artifact: &Artifact) -> anyhow::Result<Option<Label>> {
        match artifact.owner() {
            None => Ok(None),
            Some(BaseDeferredKey::TargetLabel(target)) => Ok(Some(Label::new(
                ConfiguredProvidersLabel::new(target.dupe(), ProvidersName::Default),
            ))),
            Some(BaseDeferredKey::AnonTarget(_) | BaseDeferredKey::BxlLabel(_)) => Ok(None),
        }
    }

    /// The interesting part of the path, relative to somewhere in the output directory.
    /// For an artifact declared as `foo/bar`, this is `foo/bar`.
    pub(crate) fn short_path<'v>(
        artifact: &Artifact,
        heap: &'v Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        artifact
            .get_path()
            .with_short_path(|short_path| Ok(heap.alloc_str(short_path.as_str())))
    }
}

/// A single input or output file for an action.
///
/// There is no `.parent` method on `artifact`, but in most cases
/// `cmd_args(my_artifact).parent()` can be used to similar effect.
#[starlark_module]
fn artifact_methods(builder: &mut MethodsBuilder) {
    /// The base name of this artifact. e.g. for an artifact at `foo/bar`, this is `bar`
    #[starlark(attribute)]
    fn basename<'v>(this: &'v StarlarkArtifact, heap: &Heap) -> anyhow::Result<StringValue<'v>> {
        StarlarkArtifactHelpers::basename(&this.artifact, heap)
    }

    /// The file extension of this artifact. e.g. for an artifact at foo/bar.sh,
    /// this is `.sh`. If no extension is present, `""` is returned.
    #[starlark(attribute)]
    fn extension<'v>(this: &StarlarkArtifact, heap: &Heap) -> anyhow::Result<StringValue<'v>> {
        StarlarkArtifactHelpers::extension(&this.artifact, heap)
    }

    /// Whether the artifact represents a source file
    #[starlark(attribute)]
    fn is_source(this: &StarlarkArtifact) -> anyhow::Result<bool> {
        Ok(this.artifact.is_source())
    }

    /// The `Label` of the rule that originally created this artifact. May also be None in
    /// the case of source files, or if the artifact has not be used in an action, or if the
    /// action was not created by a rule.
    #[starlark(attribute)]
    fn owner<'v>(this: &StarlarkArtifact) -> anyhow::Result<Option<Label>> {
        StarlarkArtifactHelpers::owner(&this.artifact)
    }

    /// The interesting part of the path, relative to somewhere in the output directory.
    /// For an artifact declared as `foo/bar`, this is `foo/bar`.
    #[starlark(attribute)]
    fn short_path<'v>(this: &'v StarlarkArtifact, heap: &Heap) -> anyhow::Result<StringValue<'v>> {
        StarlarkArtifactHelpers::short_path(&this.artifact, heap)
    }

    /// Returns a `StarlarkOutputArtifact` instance, or fails if the artifact is
    /// either an `Artifact`, or is a bound `Artifact` (You cannot bind twice)
    fn as_output<'v>(this: &'v StarlarkArtifact) -> anyhow::Result<StarlarkOutputArtifact<'v>> {
        match this.artifact.as_parts().0 {
            BaseArtifactKind::Source(_) => Err(ArtifactError::SourceArtifactAsOutput {
                repr: this.to_string(),
            }
            .into()),
            BaseArtifactKind::Build(b) => Err(ArtifactError::BoundArtifactAsOutput {
                artifact_repr: this.to_string(),
                existing_owner: b.get_path().owner().dupe(),
            }
            .into()),
        }
    }

    /// Create an artifact that lives at path relative from this artifact.
    /// For example, if artifact foo is a directory containing a file bar, then foo.project("bar") yields the file bar.
    /// It is possible for projected artifacts to hide the prefix in order to have the short name of the resulting artifact only contain the projected path, by passing hide_prefix = True to project().
    fn project<'v>(
        this: &'v StarlarkArtifact,
        #[starlark(require = pos)] path: &str,
        #[starlark(require = named, default = false)] hide_prefix: bool,
    ) -> anyhow::Result<StarlarkArtifact> {
        let _ignored = hide_prefix;

        let err = anyhow::Error::from(match this.artifact.owner() {
            Some(owner) => CannotProject::DeclaredElsewhere(owner.dupe()),
            None => CannotProject::SourceArtifact,
        });

        Err(err.context(format!(
            "Cannot project path `{}` in artifact `{}`",
            path, this
        )))
    }

    // Returns a `StarlarkArtifact` instance which is identical to the original artifact, except with no associated artifacts
    fn without_associated_artifacts(this: &StarlarkArtifact) -> anyhow::Result<StarlarkArtifact> {
        Ok(StarlarkArtifact {
            artifact: this.artifact.dupe(),
            associated_artifacts: AssociatedArtifacts::new(),
        })
    }
}


/*
 * 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 std::hash::Hash;
use std::hash::Hasher;

use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_execute::path::artifact_path::ArtifactPath;
use starlark::collections::StarlarkHasher;
use starlark::typing::Ty;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueLike;

use crate::artifact_groups::promise::PromiseArtifactId;
use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact::associated::AssociatedArtifacts;
use crate::interpreter::rule_defs::artifact::StarlarkArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkPromiseArtifact;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;

/// The Starlark representation of an `Artifact`
///
/// The following fields are available in Starlark:
/// `.basename`: The base name of this artifact. e.g. for an artifact
///              at `foo/bar`, this is `bar`
/// `.extension`: The file extension of this artifact. e.g. for an artifact at foo/bar.sh,
///               this is `sh`. If no extension is present, an empty string is returned
/// `.is_source`: Whether the artifact represents a source file
/// `.owner`: The `Label` of the rule that originally created this artifact. May also be None in
///           the case of source files, or if the artifact has not be used in an action.
/// `as_output()`: Returns a `StarlarkOutputArtifact` instance, or fails if the artifact is
///                either an `Artifact`, or is a bound `DeclaredArtifact` (You cannot bind twice)
/// `.short_path`: The interesting part of the path, relative to somewhere in the output directory.
///                For an artifact declared as `foo/bar`, this is `foo/bar`.
/// This trait also has some common functionality for `StarlarkValue` that we want shared between
/// `StarlarkArtifact` and `StarlarkDeclaredArtifact`
pub trait StarlarkArtifactLike: Display {
    /// Returns an apppropriate error for when this is used in a location that expects an output declaration.
    fn as_output_error(&self) -> anyhow::Error;

    /// Gets the bound main artifact, or errors if the artifact is not bound
    fn get_bound_artifact(&self) -> anyhow::Result<Artifact>;

    /// Gets any associated artifacts that should be materialized along with the bound artifact
    fn get_associated_artifacts(&self) -> Option<&AssociatedArtifacts>;

    /// Return an interface for frozen and bound artifacts (`StarlarkArtifact`) to add to a CLI
    ///
    /// Returns None if this artifact isn't the correct type to be added to a CLI object
    fn as_command_line_like(&self) -> &dyn CommandLineArgLike;

    /// It's very important that the Hash/Eq of the StarlarkArtifactLike things doesn't change
    /// during freezing, otherwise Starlark invariants are broken. Use the fingerprint
    /// as the inputs to Hash/Eq to ensure they are consistent
    fn fingerprint(&self) -> ArtifactFingerprint<'_>;

    fn equals<'v>(&self, other: Value<'v>) -> anyhow::Result<bool> {
        Ok(ValueAsArtifactLike::unpack_value(other)
            .map_or(false, |other| self.fingerprint() == other.0.fingerprint()))
    }

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

    /// Gets a copy of the StarlarkArtifact, ensuring that the artifact is bound.
    fn get_bound_starlark_artifact(&self) -> anyhow::Result<StarlarkArtifact> {
        let artifact = self.get_bound_artifact()?;
        let associated_artifacts = self.get_associated_artifacts();
        Ok(StarlarkArtifact {
            artifact,
            associated_artifacts: associated_artifacts
                .map_or(AssociatedArtifacts::new(), |a| a.clone()),
        })
    }

    /// Gets the artifact group.
    fn get_artifact_group(&self) -> anyhow::Result<ArtifactGroup>;
}

pub struct ValueAsArtifactLike<'v>(pub &'v dyn StarlarkArtifactLike);

impl<'v> StarlarkTypeRepr for ValueAsArtifactLike<'v> {
    fn starlark_type_repr() -> Ty {
        Ty::unions(vec![
            StarlarkArtifact::starlark_type_repr(),
            StarlarkDeclaredArtifact::starlark_type_repr(),
            StarlarkPromiseArtifact::starlark_type_repr(),
        ])
    }
}

impl<'v> UnpackValue<'v> for ValueAsArtifactLike<'v> {
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        value
            .downcast_ref::<StarlarkArtifact>()
            .map(|o| o as &dyn StarlarkArtifactLike)
            .or_else(|| {
                value
                    .downcast_ref::<StarlarkDeclaredArtifact>()
                    .map(|o| o as &dyn StarlarkArtifactLike)
            })
            .or_else(|| {
                value
                    .downcast_ref::<StarlarkPromiseArtifact>()
                    .map(|o| o as &dyn StarlarkArtifactLike)
            })
            .map(ValueAsArtifactLike)
    }
}

#[derive(PartialEq)]
pub enum ArtifactFingerprint<'a> {
    Normal {
        path: ArtifactPath<'a>,
        associated_artifacts: Option<&'a AssociatedArtifacts>,
    },
    Promise {
        id: PromiseArtifactId,
    },
}

impl Hash for ArtifactFingerprint<'_> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match &self {
            ArtifactFingerprint::Normal {
                path,
                associated_artifacts,
            } => {
                path.hash(state);
                if let Some(associated) = associated_artifacts {
                    associated.len().hash(state);
                    associated.iter().for_each(|ag| ag.hash(state));
                }
            }
            ArtifactFingerprint::Promise { id } => id.hash(state),
        }
    }
}


/*
 * 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;
use std::fmt::Display;
use std::fs::File;
use std::io::BufReader;

use allocative::Allocative;
use anyhow::Context;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_core::fs::fs_util;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use gazebo::prelude::*;
use starlark::any::ProvidesStaticType;
use starlark::collections::SmallMap;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::values::dict::Dict;
use starlark::values::starlark_value;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Value;
use thiserror::Error;

/// The Starlark representation of an `Artifact` on disk which can be accessed.
#[derive(Debug, ProvidesStaticType, NoSerialize, Allocative)]
pub struct StarlarkArtifactValue {
    // We only keep the artifact for Display, since we don't want to leak the underlying path by default
    artifact: Artifact,
    path: ProjectRelativePathBuf,
    fs: ProjectRoot,
}

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

starlark_simple_value!(StarlarkArtifactValue);

impl StarlarkArtifactValue {
    /// Create a new artifact value. Must be materialised to disk before calling this function.
    pub fn new(artifact: Artifact, path: ProjectRelativePathBuf, fs: ProjectRoot) -> Self {
        Self { artifact, path, fs }
    }
}

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

#[derive(Debug, Error)]
enum JsonError {
    #[error("JSON number is outside the bounds that Starlark supports, `{0}`")]
    NumberOutOfBounds(String),
}

fn json_convert<'v>(v: serde_json::Value, heap: &'v Heap) -> anyhow::Result<Value<'v>> {
    match v {
        serde_json::Value::Null => Ok(Value::new_none()),
        serde_json::Value::Bool(x) => Ok(Value::new_bool(x)),
        serde_json::Value::Number(x) => {
            if let Some(x) = x.as_i64().and_then(|x| i32::try_from(x).ok()) {
                Ok(heap.alloc(x))
            } else if let Some(x) = x.as_f64() {
                Ok(heap.alloc(x))
            } else {
                Err(JsonError::NumberOutOfBounds(x.to_string()).into())
            }
        }
        serde_json::Value::String(x) => Ok(heap.alloc(x)),
        serde_json::Value::Array(xs) => Ok(heap.alloc(xs.into_try_map(|v| json_convert(v, heap))?)),
        serde_json::Value::Object(mp) => {
            let mut res = SmallMap::with_capacity(mp.len());
            for (k, v) in mp.into_iter() {
                res.insert_hashed(heap.alloc(k).get_hashed()?, json_convert(v, heap)?);
            }
            Ok(heap.alloc(Dict::new(res)))
        }
    }
}

#[starlark_module]
fn artifact_value_methods(builder: &mut MethodsBuilder) {
    fn read_string(this: &StarlarkArtifactValue) -> anyhow::Result<String> {
        let path = this.fs.resolve(&this.path);
        fs_util::read_to_string(path)
    }

    fn read_json<'v>(this: &StarlarkArtifactValue, heap: &'v Heap) -> anyhow::Result<Value<'v>> {
        let path = this.fs.resolve(&this.path);
        let file = File::open(&path).with_context(|| format!("Error opening file `{}`", path))?;
        let reader = BufReader::new(file);
        let value: serde_json::Value = serde_json::from_reader(reader)
            .with_context(|| format!("Error parsing JSON file `{}`", path))?;
        json_convert(value, heap)
    }
}

#[cfg(test)]
mod tests {
    use starlark::values::Heap;

    use super::*;

    #[test]
    fn test_json_convert() {
        let heap = Heap::new();
        let testcase = "{\"test\": [1, true, \"pi\", 7.5, {}]}";
        let value: serde_json::Value = serde_json::from_str(testcase).unwrap();
        let res = json_convert(value, &heap).unwrap().to_repr();
        assert_eq!(res, testcase.replace("true", "True"))
    }
}


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

use allocative::Allocative;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_artifact::artifact::artifact_type::DeclaredArtifact;
use buck2_artifact::artifact::artifact_type::OutputArtifact;
use buck2_core::base_deferred_key::BaseDeferredKey;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
use buck2_core::provider::label::ConfiguredProvidersLabel;
use buck2_core::provider::label::ProvidersName;
use buck2_execute::path::artifact_path::ArtifactPath;
use buck2_interpreter::types::label::Label;
use dupe::Dupe;
use starlark::any::ProvidesStaticType;
use starlark::codemap::FileSpan;
use starlark::collections::StarlarkHasher;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::values::starlark_value;
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::Value;
use starlark::values::ValueTyped;

use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact::associated::AssociatedArtifacts;
use crate::interpreter::rule_defs::artifact::starlark_artifact_like::ArtifactFingerprint;
use crate::interpreter::rule_defs::artifact::ArtifactError;
use crate::interpreter::rule_defs::artifact::StarlarkArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkArtifactLike;
use crate::interpreter::rule_defs::artifact::StarlarkOutputArtifact;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;

/// A wrapper for an artifact that has been declared in the user's implementation function.
///
/// The `DeclaredArtifact` may or may not have been bound yet, but must be by the end of the
/// user's implementation function. This turns into a `StarlarkArtifact` when frozen, and
/// /must/ have had `ensure_bound()` called on it successfully before freezing.
#[derive(Clone, Debug, Dupe, NoSerialize, ProvidesStaticType, Trace, Allocative)]
pub struct StarlarkDeclaredArtifact {
    // FileSpan is not Hash, and the location is not really relevant for equality.
    // We also expect each artifact to be declared at one unique location anyway.
    pub(super) declaration_location: Option<FileSpan>,
    pub(super) artifact: DeclaredArtifact,
    // A set of ArtifactGroups that should be materialized along with the main artifact
    pub(super) associated_artifacts: AssociatedArtifacts,
}

impl Display for StarlarkDeclaredArtifact {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "<build artifact {}", self.artifact)?;
        if let Some(location) = &self.declaration_location {
            write!(f, " declared at {}", location)?;
        }
        if let Some(label) = self.artifact.owner() {
            write!(f, " bound to {}", label)?;
        }
        write!(f, ">")
    }
}

impl StarlarkDeclaredArtifact {
    pub fn new(
        declaration_location: Option<FileSpan>,
        artifact: DeclaredArtifact,
        associated_artifacts: AssociatedArtifacts,
    ) -> Self {
        StarlarkDeclaredArtifact {
            declaration_location,
            artifact,
            associated_artifacts,
        }
    }

    pub fn output_artifact(&self) -> OutputArtifact {
        self.artifact.as_output()
    }

    pub fn get_artifact_path(&self) -> ArtifactPath<'_> {
        self.artifact.get_path()
    }

    pub fn with_extended_associated_artifacts(
        &self,
        extra_associated_artifacts: AssociatedArtifacts,
    ) -> Self {
        let merged = self.associated_artifacts.union(extra_associated_artifacts);
        Self {
            declaration_location: self.declaration_location.clone(),
            artifact: self.artifact.dupe(),
            associated_artifacts: merged,
        }
    }
}

impl StarlarkArtifactLike for StarlarkDeclaredArtifact {
    fn as_output_error(&self) -> anyhow::Error {
        // This shouldn't ever be called for StarlarkDeclaredArtifact
        anyhow::anyhow!(
            "error trying to use declared artifact as an output, this indicates an internal buck error"
        )
    }

    fn get_bound_artifact(&self) -> anyhow::Result<Artifact> {
        Ok(self.artifact.dupe().ensure_bound()?.into_artifact())
    }

    fn get_associated_artifacts(&self) -> Option<&AssociatedArtifacts> {
        Some(&self.associated_artifacts)
    }

    fn as_command_line_like(&self) -> &dyn CommandLineArgLike {
        self
    }

    fn fingerprint(&self) -> ArtifactFingerprint<'_> {
        {
            let path = self.artifact.get_path();
            let associated_artifacts = self.get_associated_artifacts();
            ArtifactFingerprint::Normal {
                path,
                associated_artifacts,
            }
        }
    }

    fn get_artifact_group(&self) -> anyhow::Result<ArtifactGroup> {
        Ok(ArtifactGroup::Artifact(self.get_bound_artifact()?))
    }
}

impl CommandLineArgLike for StarlarkDeclaredArtifact {
    fn add_to_command_line(
        &self,
        _cli: &mut dyn CommandLineBuilder,
        _ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        // TODO: proper error message
        Err(anyhow::anyhow!(
            "proper error here; we should not be adding mutable starlark objects to clis"
        ))
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        visitor.visit_input(
            ArtifactGroup::Artifact(self.artifact.dupe().ensure_bound()?.into_artifact()),
            None,
        );
        self.associated_artifacts
            .iter()
            .for_each(|ag| visitor.visit_input(ag.dupe(), None));
        Ok(())
    }

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

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

impl Freeze for StarlarkDeclaredArtifact {
    type Frozen = StarlarkArtifact;
    fn freeze(self, _freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
        // ensure_bound() moves out of self and so we can't construct the error
        // after calling that, so we need to check first.
        if !self.artifact.is_bound() {
            return Err(ArtifactError::DeclaredArtifactWasNotBound {
                repr: self.to_string(),
            }
            .into());
        }
        let artifact = self.artifact.ensure_bound()?.into_artifact();
        Ok(StarlarkArtifact {
            artifact,
            associated_artifacts: self.associated_artifacts,
        })
    }
}

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

#[starlark_value(type = "artifact", StarlarkTypeRepr, UnpackValue)]
impl<'v> StarlarkValue<'v> for StarlarkDeclaredArtifact {
    fn get_methods() -> Option<&'static Methods> {
        static RES: MethodsStatic = MethodsStatic::new();
        RES.methods(artifact_methods)
    }

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        StarlarkArtifactLike::equals(self, other)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        StarlarkArtifactLike::write_hash(self, hasher)
    }

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

/// A single input or output for an action
#[starlark_module]
fn artifact_methods(builder: &mut MethodsBuilder) {
    /// The base name of this artifact. e.g. for an artifact at `foo/bar`, this is `bar`
    #[starlark(attribute)]
    fn basename<'v>(
        this: &StarlarkDeclaredArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        this.artifact
            .get_path()
            .with_filename(|filename| Ok(heap.alloc_str(filename?.as_str())))
    }

    /// The file extension of this artifact. e.g. for an artifact at foo/bar.sh,
    /// this is `.sh`. If no extension is present, `""` is returned.
    #[starlark(attribute)]
    fn extension<'v>(
        this: &StarlarkDeclaredArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        this.artifact.get_path().with_filename(|filename| {
            Ok(match filename?.extension() {
                None => heap.alloc_str(""),
                Some(x) => heap.alloc_str_concat(".", x),
            })
        })
    }

    /// Whether the artifact represents a source file
    #[starlark(attribute)]
    fn is_source(this: &StarlarkDeclaredArtifact) -> anyhow::Result<bool> {
        Ok(false)
    }

    /// The `Label` of the rule that originally created this artifact. May also be None in
    /// the case of source files, or if the artifact has not be used in an action, or if the
    /// action was not created by a rule.
    #[starlark(attribute)]
    fn owner<'v>(this: &StarlarkDeclaredArtifact) -> anyhow::Result<Option<Label>> {
        match this.artifact.owner() {
            None => Ok(None),
            Some(x) => Ok(match x {
                BaseDeferredKey::TargetLabel(t) => Some(Label::new(ConfiguredProvidersLabel::new(
                    t,
                    ProvidersName::Default,
                ))),
                BaseDeferredKey::AnonTarget(_) | BaseDeferredKey::BxlLabel(_) => None,
            }),
        }
    }

    /// Returns a `StarlarkOutputArtifact` instance, or fails if the artifact is
    /// either an `Artifact`, or is a bound `DeclaredArtifact` (You cannot bind twice)
    fn as_output<'v>(
        this: ValueTyped<'v, StarlarkDeclaredArtifact>,
    ) -> anyhow::Result<StarlarkOutputArtifact<'v>> {
        Ok(StarlarkOutputArtifact::new(this))
    }

    /// The interesting part of the path, relative to somewhere in the output directory.
    /// For an artifact declared as `foo/bar`, this is `foo/bar`.
    #[starlark(attribute)]
    fn short_path<'v>(
        this: &StarlarkDeclaredArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        this.artifact
            .get_path()
            .with_short_path(|short_path| Ok(heap.alloc_str(short_path.as_str())))
    }

    /// Create an artifact that lives at path relative from this artifact
    /// For example, if artifact foo is a directory containing a file bar, then foo.project("bar") yields the file bar.
    /// It is possible for projected artifacts to hide the prefix in order to have the short name of the resulting artifact only contain the projected path, by passing hide_prefix = True to project().
    fn project<'v>(
        this: &'v StarlarkDeclaredArtifact,
        path: &str,
        #[starlark(require = named, default = false)] hide_prefix: bool,
    ) -> anyhow::Result<StarlarkDeclaredArtifact> {
        let path = ForwardRelativePath::new(path)?;
        // Not sure if this.declaration_location is or the project() call is more appropriate here.
        Ok(StarlarkDeclaredArtifact {
            declaration_location: this.declaration_location.dupe(),
            artifact: this.artifact.project(path, hide_prefix),
            associated_artifacts: this.associated_artifacts.dupe(),
        })
    }

    // Returns a `StarlarkDeclaredArtifact` instance which is identical to the original artifact, except with no associated artifacts
    fn without_associated_artifacts(
        this: &StarlarkDeclaredArtifact,
    ) -> anyhow::Result<StarlarkDeclaredArtifact> {
        Ok(StarlarkDeclaredArtifact {
            declaration_location: this.declaration_location.dupe(),
            artifact: this.artifact.dupe(),
            associated_artifacts: AssociatedArtifacts::new(),
        })
    }
}


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

use allocative::Allocative;
use buck2_artifact::artifact::artifact_type::OutputArtifact;
use dupe::Dupe;
use either::Either;
use starlark::any::ProvidesStaticType;
use starlark::typing::Ty;
use starlark::values::starlark_value;
use starlark::values::type_repr::StarlarkTypeRepr;
use starlark::values::Coerce;
use starlark::values::Demand;
use starlark::values::Freeze;
use starlark::values::FrozenValueTyped;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::UnpackValue;
use starlark::values::Value;
use starlark::values::ValueLike;
use starlark::values::ValueTyped;

use crate::interpreter::rule_defs::artifact::StarlarkArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;

/// Thin wrapper around `OutputArtifact`.
///
/// Allows actions to distinguish between inputs and outputs, and can validate whether the
/// underlying artifact is bound or not yet.
#[derive(
    Debug,
    Clone,
    Dupe,
    ProvidesStaticType,
    Trace,
    NoSerialize,
    Allocative,
    Freeze,
    Coerce
)]
#[repr(C)]
pub struct StarlarkOutputArtifactGen<V> {
    pub(super) declared_artifact: V,
}

starlark_complex_value!(pub StarlarkOutputArtifact);

impl<'v> Display for StarlarkOutputArtifact<'v> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "<output artifact for {}>",
            self.inner().get_artifact_path()
        )
    }
}

impl Display for FrozenStarlarkOutputArtifact {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "<output artifact for {}>",
            self.inner().get_artifact_path()
        )
    }
}

/// A wrapper for `UnpackValue` that accepts either an output artifact,
/// or an artifact declared in the same rule.
pub struct StarlarkOutputOrDeclaredArtifact<'v>(pub StarlarkOutputArtifact<'v>);

impl<'v> StarlarkTypeRepr for StarlarkOutputOrDeclaredArtifact<'v> {
    fn starlark_type_repr() -> Ty {
        Either::<StarlarkOutputArtifact, StarlarkDeclaredArtifact>::starlark_type_repr()
    }
}

impl<'v> UnpackValue<'v> for StarlarkOutputOrDeclaredArtifact<'v> {
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        #[allow(clippy::manual_map)]
        if let Some(x) = value.downcast_ref::<StarlarkOutputArtifact>() {
            Some(Self(x.dupe()))
        } else if let Some(x) = ValueTyped::<StarlarkDeclaredArtifact>::new(value) {
            Some(Self(StarlarkOutputArtifact::new(x)))
        } else {
            None
        }
    }
}

impl<'v> StarlarkOutputArtifact<'v> {
    pub fn new(v: ValueTyped<'v, StarlarkDeclaredArtifact>) -> Self {
        Self {
            declared_artifact: v.to_value(),
        }
    }

    pub(crate) fn inner(&self) -> ValueTyped<'v, StarlarkDeclaredArtifact> {
        ValueTyped::new(self.declared_artifact).unwrap()
    }

    pub fn artifact(&self) -> OutputArtifact {
        self.inner().output_artifact()
    }
}

impl FrozenStarlarkOutputArtifact {
    pub(crate) fn inner(&self) -> FrozenValueTyped<StarlarkArtifact> {
        FrozenValueTyped::new(self.declared_artifact).unwrap()
    }

    pub fn artifact(&self) -> OutputArtifact {
        self.inner().artifact().as_output_artifact().unwrap()
    }
}

impl<'v> CommandLineArgLike for StarlarkOutputArtifact<'v> {
    fn add_to_command_line(
        &self,
        _cli: &mut dyn CommandLineBuilder,
        _ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        // TODO: proper error message
        Err(anyhow::anyhow!(
            "proper error here; we should not be adding mutable starlark objects to clis"
        ))
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        visitor.visit_output(self.artifact(), None);
        Ok(())
    }

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

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

#[starlark_value(type = "output_artifact")]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for StarlarkOutputArtifactGen<V>
where
    Self: ProvidesStaticType<'v> + Display + CommandLineArgLike,
{
    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn CommandLineArgLike>(self);
    }
}

impl CommandLineArgLike for FrozenStarlarkOutputArtifact {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        cli.push_arg(
            ctx.resolve_artifact(&self.inner().artifact())?
                .into_string(),
        );
        Ok(())
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        visitor.visit_output(self.artifact(), None);
        Ok(())
    }

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

    fn visit_write_to_file_macros(
        &self,
        _visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        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::fmt;
use std::fmt::Debug;
use std::fmt::Display;

use allocative::Allocative;
use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePathBuf;
use buck2_interpreter::types::label::Label;
use dupe::Dupe;
use starlark::any::ProvidesStaticType;
use starlark::codemap::FileSpan;
use starlark::collections::StarlarkHasher;
use starlark::environment::Methods;
use starlark::environment::MethodsBuilder;
use starlark::environment::MethodsStatic;
use starlark::values::starlark_value;
use starlark::values::Demand;
use starlark::values::Heap;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::StringValue;
use starlark::values::Trace;
use starlark::values::Value;
use thiserror::Error;

use crate::artifact_groups::promise::PromiseArtifact;
use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::artifact::associated::AssociatedArtifacts;
use crate::interpreter::rule_defs::artifact::starlark_artifact::StarlarkArtifactHelpers;
use crate::interpreter::rule_defs::artifact::starlark_artifact_like::ArtifactFingerprint;
use crate::interpreter::rule_defs::artifact::ArtifactError;
use crate::interpreter::rule_defs::artifact::StarlarkArtifactLike;
use crate::interpreter::rule_defs::artifact::StarlarkDeclaredArtifact;
use crate::interpreter::rule_defs::artifact::StarlarkOutputArtifact;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;

#[derive(Debug, Error)]
enum PromiseArtifactError {
    #[error("cannot access {1} on unresolved promise artifact ({0})")]
    MethodUnsupported(StarlarkPromiseArtifact, &'static str),
    #[error(
        "tried to expand unresolved promise artifact `{0}` to command line args, this should only happen after starlark objects are frozen and promises are resolved"
    )]
    UnresolvedAddedToCommandLine(StarlarkPromiseArtifact),
    #[error("promise artifact ({0}) cannot be projected")]
    CannotProject(StarlarkPromiseArtifact),
    #[error(
        "`short_path` was called on promise artifact ({0}), but no short path was promised. To access the short path on an unresolved promise artifact, `short_path` arg needs to be passed into `artifact_promise()`"
    )]
    NoShortPathPromised(StarlarkPromiseArtifact),
}

/// An artifact wrapper for a StarlarkPromise that will resolve to an artifact
///
/// The `StarlarkPromiseArtifact` is created with `ctx.actions.promise_artifact()` and will
/// not have been bound until promises are resolved at the end of analysis.
#[derive(
    Debug,
    NoSerialize,
    ProvidesStaticType,
    Trace,
    Allocative,
    Clone,
    Hash,
    Eq,
    PartialEq
)]
pub struct StarlarkPromiseArtifact {
    declaration_location: Option<FileSpan>,
    artifact: PromiseArtifact,
    short_path: Option<ForwardRelativePathBuf>,
}

starlark_simple_value!(StarlarkPromiseArtifact);

impl Display for StarlarkPromiseArtifact {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "<promise artifact")?;
        if let Some(location) = &self.declaration_location {
            write!(f, " declared at {}", location)?;
        }

        if let Some(v) = self.artifact.get() {
            write!(f, " resolved to {}", v)?;
        }
        write!(f, ">")?;
        Ok(())
    }
}

impl StarlarkPromiseArtifact {
    pub fn new(
        declaration_location: Option<FileSpan>,
        artifact: PromiseArtifact,
        short_path: Option<ForwardRelativePathBuf>,
    ) -> Self {
        Self {
            declaration_location,
            artifact,
            short_path,
        }
    }

    pub fn as_artifact(&self) -> ArtifactGroup {
        match self.artifact.get() {
            Some(artifact) => ArtifactGroup::Artifact(artifact.dupe()),
            None => ArtifactGroup::Promise(self.artifact.dupe()),
        }
    }
}

impl StarlarkArtifactLike for StarlarkPromiseArtifact {
    fn get_bound_artifact(&self) -> anyhow::Result<Artifact> {
        match self.artifact.get() {
            Some(v) => Ok(v.dupe()),
            None => Err(PromiseArtifactError::MethodUnsupported(
                self.clone(),
                "get_bound_artifact",
            )
            .into()),
        }
    }

    /// Gets any associated artifacts that should be materialized along with the bound artifact
    fn get_associated_artifacts(&self) -> Option<&AssociatedArtifacts> {
        None
    }

    fn as_command_line_like(&self) -> &dyn CommandLineArgLike {
        self
    }

    fn fingerprint(&self) -> ArtifactFingerprint<'_> {
        {
            let id = self.artifact.id().dupe();
            ArtifactFingerprint::Promise { id }
        }
    }

    fn as_output_error(&self) -> anyhow::Error {
        ArtifactError::PromiseArtifactAsOutput {
            artifact_repr: self.to_string(),
        }
        .into()
    }

    fn get_artifact_group(&self) -> anyhow::Result<ArtifactGroup> {
        Ok(self.as_artifact())
    }
}

impl CommandLineArgLike for StarlarkPromiseArtifact {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        ctx: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        match self.artifact.get() {
            Some(v) => {
                cli.push_arg(ctx.resolve_artifact(&v)?.into_string());
                Ok(())
            }
            None => Err(PromiseArtifactError::UnresolvedAddedToCommandLine(self.clone()).into()),
        }
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        visitor.visit_input(self.as_artifact(), None);
        Ok(())
    }

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

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

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

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        StarlarkArtifactLike::equals(self, other)
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        StarlarkArtifactLike::write_hash(self, hasher)
    }

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

    fn matches_type(&self, ty: &str) -> bool {
        Self::TYPE == ty || ty == "artifact"
    }
}

/// A single input or output for an action
#[starlark_module]
fn artifact_methods(builder: &mut MethodsBuilder) {
    /// The base name of this artifact. e.g. for an artifact at `foo/bar`, this is `bar`
    #[starlark(attribute)]
    fn basename<'v>(
        this: &StarlarkPromiseArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        match this.artifact.get() {
            Some(v) => StarlarkArtifactHelpers::basename(v, heap),
            None => Err(PromiseArtifactError::MethodUnsupported(this.clone(), "basename").into()),
        }
    }

    /// The file extension of this artifact. e.g. for an artifact at foo/bar.sh,
    /// this is `.sh`. If no extension is present, `""` is returned.
    #[starlark(attribute)]
    fn extension<'v>(
        this: &StarlarkPromiseArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        match this.artifact.get() {
            Some(v) => StarlarkArtifactHelpers::extension(v, heap),
            None => Err(PromiseArtifactError::MethodUnsupported(this.clone(), "extension").into()),
        }
    }

    /// Whether the artifact represents a source file
    #[starlark(attribute)]
    fn is_source(this: &StarlarkPromiseArtifact) -> anyhow::Result<bool> {
        Ok(false)
    }

    /// The `Label` of the rule that originally created this artifact. May also be None in
    /// the case of source files, or if the artifact has not be used in an action, or if the
    /// action was not created by a rule.
    #[starlark(attribute)]
    fn owner<'v>(this: &StarlarkPromiseArtifact) -> anyhow::Result<Option<Label>> {
        match this.artifact.get() {
            Some(v) => StarlarkArtifactHelpers::owner(v),
            None => Err(PromiseArtifactError::MethodUnsupported(this.clone(), "owner").into()),
        }
    }

    /// Returns a `StarlarkOutputArtifact` instance, or fails if the artifact is
    /// either an `Artifact`, or is a bound `DeclaredArtifact` (You cannot bind twice)
    fn as_output<'v>(
        this: &'v StarlarkPromiseArtifact,
    ) -> anyhow::Result<StarlarkOutputArtifact<'v>> {
        Err(this.as_output_error())
    }

    /// The interesting part of the path, relative to somewhere in the output directory.
    /// For an artifact declared as `foo/bar`, this is `foo/bar`.
    #[starlark(attribute)]
    fn short_path<'v>(
        this: &StarlarkPromiseArtifact,
        heap: &Heap,
    ) -> anyhow::Result<StringValue<'v>> {
        match this.artifact.get() {
            Some(v) => StarlarkArtifactHelpers::short_path(v, heap),
            None => {
                if let Some(short_path) = &this.short_path {
                    Ok(heap.alloc_str(short_path.as_str()))
                } else {
                    Err(PromiseArtifactError::NoShortPathPromised(this.clone()).into())
                }
            }
        }
    }

    /// Create an artifact that lives at path relative from this artifact
    /// For example, if artifact foo is a directory containing a file bar, then foo.project("bar") yields the file bar.
    /// It is possible for projected artifacts to hide the prefix in order to have the short name of the resulting artifact only contain the projected path, by passing hide_prefix = True to project().
    fn project<'v>(
        this: &'v StarlarkPromiseArtifact,
        path: &str,
        #[starlark(require = named, default = false)] hide_prefix: bool,
    ) -> anyhow::Result<StarlarkDeclaredArtifact> {
        let _ = (path, hide_prefix);
        Err(PromiseArtifactError::CannotProject(this.clone()).into())
    }

    // Returns a `StarlarkPromiseArtifact` instance which is identical to the original artifact, except with no associated artifacts
    fn without_associated_artifacts(
        this: &StarlarkPromiseArtifact,
    ) -> anyhow::Result<StarlarkPromiseArtifact> {
        Ok(this.clone())
    }
}


/*
 * 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;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;

use allocative::Allocative;
use dupe::Dupe;
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::Freeze;
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 crate::interpreter::rule_defs::artifact_tagging::TaggedCommandLine;
use crate::interpreter::rule_defs::artifact_tagging::TaggedValue;
use crate::interpreter::rule_defs::cmd_args::value_as::ValueAsCommandLineLike;

/// ArtifactTag allows wrapping input and output artifacts in a command line with tags. Those tags
/// will be made visible to artifact visitors. The tags themselves don't have meaning on their own,
/// but they can be compared to each other, which allows grouping inputs and outputs in meaningful
/// categories. This is notably used for dep files to associate inputs tracked by a dep file with
/// the dep file itself.
#[derive(
    Debug,
    Clone,
    Dupe,
    Freeze,
    Trace,
    ProvidesStaticType,
    NoSerialize,
    Allocative
)]
pub struct ArtifactTag {
    #[cfg_attr(feature = "gazebo_lint", allow(gazebo_lint_arc_on_dupe))]
    #[freeze(identity)]
    identity: Arc<()>,
}

impl ArtifactTag {
    pub fn new() -> Self {
        Self {
            identity: Arc::new(()),
        }
    }
}

impl fmt::Display for ArtifactTag {
    fn fmt(&self, w: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(w, "ArtifactTag({:x})", Arc::as_ptr(&self.identity) as usize)
    }
}

impl PartialEq for ArtifactTag {
    fn eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.identity, &other.identity)
    }
}

impl Eq for ArtifactTag {}

impl Hash for ArtifactTag {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        hasher.write_usize(Arc::as_ptr(&self.identity) as usize);
    }
}

starlark_simple_value!(ArtifactTag);

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

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

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

#[starlark_module]
fn input_tag_methods(_: &mut MethodsBuilder) {
    fn tag_artifacts<'v>(
        this: &ArtifactTag,
        inner: Value<'v>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        let value = TaggedValue::new(inner, this.dupe());

        Ok(if inner.as_command_line().is_some() {
            heap.alloc(TaggedCommandLine::new(value))
        } else {
            heap.alloc(value)
        })
    }

    fn tag_inputs<'v>(
        this: &ArtifactTag,
        inner: Value<'v>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        let value = TaggedValue::inputs_only(inner, this.dupe());

        Ok(if inner.as_command_line().is_some() {
            heap.alloc(TaggedCommandLine::new(value))
        } else {
            heap.alloc(value)
        })
    }
}


/*
 * 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.
 */

mod artifact_tag;
mod tagged_command_line;
mod tagged_value;
mod tagged_visitor;

pub use artifact_tag::ArtifactTag;
pub use tagged_command_line::FrozenTaggedCommandLine;
pub use tagged_command_line::TaggedCommandLine;
pub use tagged_value::TaggedValue;
use tagged_value::TaggedValueGen;
pub use tagged_visitor::TaggedVisitor;


/*
 * 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 allocative::Allocative;
use derive_more::Display;
use starlark::any::ProvidesStaticType;
use starlark::coerce::Coerce;
use starlark::values::starlark_value;
use starlark::values::Demand;
use starlark::values::Freeze;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::ValueLike;

use super::TaggedValueGen;
use crate::interpreter::rule_defs::cmd_args::value_as::ValueAsCommandLineLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArgLike;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineBuilder;
use crate::interpreter::rule_defs::cmd_args::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::WriteToFileMacroVisitor;

/// TaggedCommandLine wraps a CommandLineArgLike to apply a given ArtifactTag to all its inputs and
/// outputs.
#[derive(
    Debug,
    Clone,
    Trace,
    Coerce,
    Freeze,
    Display,
    ProvidesStaticType,
    Allocative
)]
#[derive(NoSerialize)] // TODO make artifacts serializable
#[repr(C)]
#[display(fmt = "TaggedCommandLine({})", inner)]
pub struct TaggedCommandLineGen<V> {
    inner: TaggedValueGen<V>,
}

impl<V> TaggedCommandLineGen<V> {
    pub fn new(inner: TaggedValueGen<V>) -> Self {
        Self { inner }
    }
}

starlark_complex_value!(pub TaggedCommandLine);

#[starlark_value(type = "tagged_command_line")]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for TaggedCommandLineGen<V>
where
    Self: ProvidesStaticType<'v>,
{
    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
        demand.provide_value::<&dyn CommandLineArgLike>(self);
    }
}

impl<'v, V: ValueLike<'v>> CommandLineArgLike for TaggedCommandLineGen<V> {
    fn add_to_command_line(
        &self,
        cli: &mut dyn CommandLineBuilder,
        context: &mut dyn CommandLineContext,
    ) -> anyhow::Result<()> {
        self.inner
            .value()
            .to_value()
            .as_command_line_err()?
            .add_to_command_line(cli, context)
    }

    fn visit_artifacts(&self, visitor: &mut dyn CommandLineArtifactVisitor) -> anyhow::Result<()> {
        let mut visitor = self.inner.wrap_visitor(visitor);

        self.inner
            .value()
            .to_value()
            .as_command_line_err()?
            .visit_artifacts(&mut visitor)
    }

    fn contains_arg_attr(&self) -> bool {
        self.inner
            .value()
            .to_value()
            .as_command_line()
            .map_or(false, |inner| inner.contains_arg_attr())
    }

    fn visit_write_to_file_macros(
        &self,
        visitor: &mut dyn WriteToFileMacroVisitor,
    ) -> anyhow::Result<()> {
        self.inner
            .value()
            .to_value()
            .as_command_line_err()?
            .visit_write_to_file_macros(visitor)
    }
}


/*
 * 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 allocative::Allocative;
use derive_more::Display;
use starlark::any::ProvidesStaticType;
use starlark::coerce::Coerce;
use starlark::values::starlark_value;
use starlark::values::Freeze;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::Trace;
use starlark::values::Value;
use starlark::values::ValueLike;

use super::ArtifactTag;
use super::TaggedVisitor;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;

/// TaggedValue wraps a value to apply a given ArtifactTag to all its inputs and outputs. When
/// tagging a command line, we use TaggedCommandLine instead, but this one is consulted by
/// write_json.
#[derive(
    Debug,
    Clone,
    Trace,
    Coerce,
    Freeze,
    Display,
    ProvidesStaticType,
    Allocative
)]
#[derive(NoSerialize)] // TODO make artifacts serializable
#[repr(C)]
#[display(fmt = "TaggedValue({}, tagged {})", inner, tag)]
pub struct TaggedValueGen<V> {
    inner: V,
    tag: ArtifactTag,
    inputs_only: bool,
}

impl<'v> TaggedValue<'v> {
    pub fn new(inner: Value<'v>, tag: ArtifactTag) -> Self {
        Self {
            inner,
            tag,
            inputs_only: false,
        }
    }

    pub fn inputs_only(inner: Value<'v>, tag: ArtifactTag) -> Self {
        Self {
            inner,
            tag,
            inputs_only: true,
        }
    }
}

starlark_complex_value!(pub TaggedValue);

#[starlark_value(type = "tagged_value")]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for TaggedValueGen<V> where
    Self: ProvidesStaticType<'v>
{
}

impl<V> TaggedValueGen<V> {
    pub fn value(&self) -> &V {
        &self.inner
    }

    pub fn wrap_visitor<'a, 'b>(
        &'a self,
        visitor: &'b mut dyn CommandLineArtifactVisitor,
    ) -> TaggedVisitor<'a, 'b> {
        TaggedVisitor::wrap(&self.tag, self.inputs_only, visitor)
    }
}


/*
 * 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_artifact::artifact::artifact_type::OutputArtifact;

use super::ArtifactTag;
use crate::artifact_groups::ArtifactGroup;
use crate::interpreter::rule_defs::cmd_args::CommandLineArtifactVisitor;

/// Wrap an existing CommandLineArtifactVisitor into one that adds an ArtifactTag.
pub struct TaggedVisitor<'a, 'b> {
    inner: &'b mut dyn CommandLineArtifactVisitor,
    tag: &'a ArtifactTag,
    inputs_only: bool,
}

impl<'a, 'b> TaggedVisitor<'a, 'b> {
    pub fn wrap(
        tag: &'a ArtifactTag,
        inputs_only: bool,
        inner: &'b mut dyn CommandLineArtifactVisitor,
    ) -> Self {
        Self {
            inner,
            tag,
            inputs_only,
        }
    }
}

impl<'a, 'b> CommandLineArtifactVisitor for TaggedVisitor<'a, 'b> {
    /// Ignore the inner tag, set our own. Nesting input groups generally isn't a great idea, but
    /// we can't statically prevent it.
    fn visit_input(&mut self, input: ArtifactGroup, _tag: Option<&ArtifactTag>) {
        self.inner.visit_input(input, Some(self.tag))
    }

    /// Same as above, no nesting here.
    fn visit_output(&mut self, artifact: OutputArtifact, _tag: Option<&ArtifactTag>) {
        let tag = if self.inputs_only {
            None
        } else {
            Some(self.tag)
        };
        self.inner.visit_output(artifact, tag)
    }
}


/*
 * 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.
 */

/// An ArgBuilder is almost exactly a CommandLineBuilder. The difference is that while a commandline
/// builder is building a list of strings, argbuilder is appending the values to a single string.
pub trait ArgBuilder {
    /// Add the string representation to the list of command line arguments.
    fn push_str(&mut self, s: &str);
}


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

use buck2_artifact::artifact::artifact_type::Artifact;
use buck2_core::fs::paths::RelativePathBuf;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use buck2_execute::artifact::artifact_dyn::ArtifactDyn;
use buck2_execute::artifact::fs::ExecutorFs;
use indexmap::IndexSet;
use thiserror::Error;

use crate::interpreter::rule_defs::cmd_args::traits::CommandLineContext;
use crate::interpreter::rule_defs::cmd_args::traits::CommandLineLocation;

#[derive(Error, Debug)]
pub enum CommandLineBuilderErrors {
    #[error(
        "write-to-file macro is only supported as a part of command line argument which is written to a file"
    )]
    WriteToFileMacroNotSupported,
    #[error(
        "Number of write-to-file macro artifacts during analysis time should be consistent with when the command line is formed."
    )]
    InconsistentNumberOfMacroArtifacts,
}

/// Builds up arguments needed to construct a command line
pub struct DefaultCommandLineContext<'v> {
    fs: &'v ExecutorFs<'v>,
    // First element is list of artifacts, each corresponding to a file with macro contents. Ordering is very important.
    // Second element is a current position in that list.
    maybe_macros_state: Option<(&'v IndexSet<Artifact>, usize)>,
}

impl<'v> DefaultCommandLineContext<'v> {
    /// Create a new builder
    ///
    /// `fs`: The `ExecutorFs` that things like `Artifact` can use to generate strings
    pub fn new(fs: &'v ExecutorFs) -> Self {
        Self {
            fs,
            maybe_macros_state: None,
        }
    }

    pub fn new_with_write_to_file_macros_support(
        fs: &'v ExecutorFs,
        macro_files: &'v IndexSet<Artifact>,
    ) -> Self {
        Self {
            fs,
            maybe_macros_state: Some((macro_files, 0)),
        }
    }

    /// The `ArtifactFilesystem` to resolve `Artifact`s
    pub fn fs(&self) -> &ExecutorFs {
        self.fs
    }
}

impl CommandLineContext for DefaultCommandLineContext<'_> {
    fn resolve_project_path(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<CommandLineLocation> {
        Ok(CommandLineLocation::from_relative_path(
            path.into(),
            self.fs.path_separator(),
        ))
    }

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

    fn next_macro_file_path(&mut self) -> anyhow::Result<RelativePathBuf> {
        if let Some((files, pos)) = self.maybe_macros_state {
            if pos >= files.len() {
                return Err(anyhow::anyhow!(
                    CommandLineBuilderErrors::InconsistentNumberOfMacroArtifacts
                ));
            }
            self.maybe_macros_state = Some((files, pos + 1));
            Ok(self
                .resolve_project_path(files[pos].resolve_path(self.fs.fs())?)?
                .into_relative())
        } else {
            Err(anyhow::anyhow!(
                CommandLineBuilderErrors::WriteToFileMacroNotSupported
            ))
        }
    }
}

pub struct AbsCommandLineContext<'v>(DefaultCommandLineContext<'v>);

impl<'v> AbsCommandLineContext<'v> {
    pub fn new(executor_fs: &'v ExecutorFs) -> Self {
        Self(DefaultCommandLineContext::<'v>::new(executor_fs))
    }
}

impl CommandLineContext for AbsCommandLineContext<'_> {
    fn resolve_project_path(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<CommandLineLocation> {
        Ok(CommandLineLocation::from_root(
            self.0.fs().fs().fs(),
            path.into(),
            self.fs().path_separator(),
        ))
    }

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

    fn next_macro_file_path(&mut self) -> anyhow::Result<RelativePathBuf> {
        let executor_fs = self.0.fs();
        let mut path = executor_fs.fs().fs().root().to_path_buf();
        path.extend(self.0.next_macro_file_path()?.iter());
        RelativePathBuf::from_path(path).map_err(|e| anyhow::anyhow!(e))
    }
}

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

    use buck2_common::executor_config::PathSeparatorKind;
    use buck2_core::cells::cell_root_path::CellRootPathBuf;
    use buck2_core::cells::name::CellName;
    use buck2_core::cells::CellResolver;
    use buck2_core::fs::artifact_path_resolver::ArtifactFs;
    use buck2_core::fs::buck_out_path::BuckOutPathResolver;
    use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
    use buck2_core::fs::project::ProjectRoot;
    use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;

    use super::*;
    use crate::interpreter::rule_defs::cmd_args::builder::DefaultCommandLineContext;
    use crate::interpreter::rule_defs::cmd_args::traits::CommandLineArgLike;

    #[test]
    fn adds_args_and_builds() -> anyhow::Result<()> {
        let project_fs =
            ProjectRoot::new(AbsNormPathBuf::try_from(std::env::current_dir().unwrap()).unwrap())
                .unwrap();
        let fs = ArtifactFs::new(
            CellResolver::testing_with_name_and_path(
                CellName::testing_new("cell"),
                CellRootPathBuf::new(ProjectRelativePathBuf::unchecked_new("cell_path".into())),
            ),
            BuckOutPathResolver::new(ProjectRelativePathBuf::unchecked_new("buck_out".into())),
            project_fs,
        );
        let executor_fs = ExecutorFs::new(&fs, PathSeparatorKind::Unix);

        let mut cli = Vec::<String>::new();
        let mut ctx = DefaultCommandLineContext::new(&executor_fs);

        "foo".add_to_command_line(&mut cli, &mut ctx)?;
        "bar".to_owned().add_to_command_line(&mut cli, &mut ctx)?;

        assert_eq!(&["foo".to_owned(), "bar".to_owned()], cli.as_slice());
        Ok(())
    }

    #[test]
    fn test_command_line_location() {
        let unix_path_sep = PathSeparatorKind::Unix;
        let windows_path_sep = PathSeparatorKind::Windows;
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::new(), unix_path_sep)
                .into_string(),
            "."
        );
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::new(), windows_path_sep)
                .into_string(),
            "."
        );
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::from("a"), unix_path_sep)
                .into_string(),
            "./a"
        );
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::from("a"), windows_path_sep)
                .into_string(),
            ".\\a"
        );
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::from("a/b"), unix_path_sep)
                .into_string(),
            "a/b"
        );
        assert_eq!(
            CommandLineLocation::from_relative_path(RelativePathBuf::from("a/b"), windows_path_sep)
                .into_string(),
            "a\\b"
        );
    }

    #[cfg(not(unix))]
    #[test]
    fn test_abs_command_line_location_windows() {
        let root = ProjectRoot::new_unchecked(AbsNormPathBuf::unchecked_new(PathBuf::from(
            "C:\\foo\\bar",
        )));

        assert_eq!(
            CommandLineLocation::from_root(
                &root,
                RelativePathBuf::new(),
                PathSeparatorKind::Windows
            )
            .into_string(),
            "C:\\foo\\bar",
        );

        assert_eq!(
            CommandLineLocation::from_root(
                &root,
                RelativePathBuf::from("baz"),
                PathSeparatorKind::Windows
            )
            .into_string(),
            "C:\\foo\\bar\\baz",
        );

        assert_eq!(
            CommandLineLocation::from_root(
                &root,
                RelativePathBuf::from("baz/qux"),
                PathSeparatorKind::Windows
            )
            .into_string(),
            "C:\\foo\\bar\\baz\\qux",
        );
    }

    #[cfg(unix)]
    #[test]
    fn test_abs_command_line_location_unix() {
        let root =
            ProjectRoot::new_unchecked(AbsNormPathBuf::unchecked_new(PathBuf::from("/foo/bar")));

        assert_eq!(
            CommandLineLocation::from_root(&root, RelativePathBuf::new(), PathSeparatorKind::Unix)
                .into_string(),
            "/foo/bar",
        );

        assert_eq!(
            CommandLineLocation::from_root(
                &root,
                RelativePathBuf::from("baz"),
                PathSeparatorKind::Unix
            )
            .into_string(),
            "/foo/bar/baz",
        );

        assert_eq!(
            CommandLineLocation::from_root(
                &root,
                RelativePathBuf::from("baz/qux"),
                PathSeparatorKind::Unix
            )
            .into_string(),
            "/foo/bar/baz/qux",
        );
    }
}


/*
 * 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 gazebo::prelude::*;
use starlark::environment::GlobalsBuilder;
use starlark::values::StringValue;
use starlark::values::Value;

use crate::interpreter::rule_defs::cmd_args::options::QuoteStyle;

pub mod arg_builder;
mod builder;
mod options;
pub(crate) mod space_separated;
mod traits;
mod typ;
pub mod value;
pub mod value_as;

pub use builder::*;
pub use traits::*;
pub use typ::*;

#[starlark_module]
pub fn register_cmd_args(builder: &mut GlobalsBuilder) {
    #[starlark(as_type = FrozenStarlarkCommandLine)]
    /// The `cmd_args` type is created by this function 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.
    ///
    /// The arguments are:
    ///
    /// * `*args` - a list of things to add to the command line, each of which must be coercible to a command line. Further items can be added with `cmd.add`.
    /// * `format` - a string that provides a format to apply to the argument. for example, `cmd_args(x, format="--args={}")` would prepend `--args=` before `x`, or if `x` was a list, before each element in `x`.
    /// * `delimiter` - added between arguments to join them together. For example, `cmd_args(["--args=",x], delimiter="")` would produce a single argument to the underlying tool.
    /// * `prepend` - added as a separate argument before each argument.
    /// * `quote` - indicates whether quoting is to be applied to each argument. The only current valid value is `"shell"`.
    fn cmd_args<'v>(
        #[starlark(args)] args: Vec<Value<'v>>,
        delimiter: Option<StringValue<'v>>,
        format: Option<StringValue<'v>>,
        prepend: Option<StringValue<'v>>,
        quote: Option<&str>,
    ) -> anyhow::Result<StarlarkCommandLine<'v>> {
        StarlarkCommandLine::try_from_values_with_options(
            &args,
            delimiter,
            format,
            prepend,
            quote.try_map(QuoteStyle::parse)?,
        )
    }
}