/*
 * Copyright 2018 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use crate as starlark;
use crate::coerce::coerce;
use crate::coerce::Coerce;
use crate::typing::Ty;
use crate::values::type_repr::StarlarkTypeRepr;
use crate::values::types::tuple::value::FrozenTuple;
use crate::values::types::tuple::value::Tuple;
use crate::values::FrozenValue;
use crate::values::UnpackValue;
use crate::values::Value;
use crate::values::ValueLike;

/// Reference to tuple data in Starlark heap.
#[derive(Coerce, Debug)]
#[repr(transparent)]
pub struct TupleRef<'v> {
    contents: [Value<'v>],
}

/// Reference to tuple data in frozen Starlark heap.
#[repr(transparent)]
#[derive(Coerce, Debug)]
pub struct FrozenTupleRef {
    contents: [FrozenValue],
}

impl<'v> TupleRef<'v> {
    /// `type(())`, which is `"tuple"`.
    pub const TYPE: &'static str = FrozenTupleRef::TYPE;

    /// Downcast a value to a tuple.
    pub fn from_value(value: Value<'v>) -> Option<&'v TupleRef<'v>> {
        Some(coerce(Tuple::from_value(value)?.content()))
    }

    /// Downcast a value to a tuple.
    pub fn from_frozen_value(value: FrozenValue) -> Option<&'v TupleRef<'v>> {
        Self::from_value(value.to_value())
    }

    /// Number of elements.
    pub fn len(&self) -> usize {
        self.contents.len()
    }

    /// Tuple elements.
    pub fn content(&self) -> &[Value<'v>] {
        &self.contents
    }

    /// Iterate over the contents.
    pub fn iter(&self) -> impl ExactSizeIterator<Item = Value<'v>> + '_ {
        self.content().iter().copied()
    }
}

impl FrozenTupleRef {
    /// `type(())`, which is `"tuple"`.
    pub const TYPE: &'static str = FrozenTuple::TYPE;

    /// Downcast a value to a tuple.
    pub fn from_frozen_value(value: FrozenValue) -> Option<&'static FrozenTupleRef> {
        Some(coerce(value.downcast_ref::<FrozenTuple>()?.content()))
    }

    /// Number of elements.
    pub fn len(&self) -> usize {
        self.contents.len()
    }

    /// Tuple elements.
    pub fn content(&self) -> &[FrozenValue] {
        &self.contents
    }

    /// Iterate over contents.
    pub fn iter(&self) -> impl ExactSizeIterator<Item = FrozenValue> + '_ {
        self.content().iter().copied()
    }
}

impl<'v> StarlarkTypeRepr for &'v TupleRef<'v> {
    fn starlark_type_repr() -> Ty {
        Ty::name(TupleRef::TYPE)
    }
}

impl<'a> StarlarkTypeRepr for &'a FrozenTupleRef {
    fn starlark_type_repr() -> Ty {
        Ty::name(FrozenTupleRef::TYPE)
    }
}

impl<'v> UnpackValue<'v> for &'v TupleRef<'v> {
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        TupleRef::from_value(value)
    }
}

impl<'v> UnpackValue<'v> for &'v FrozenTupleRef {
    fn unpack_value(value: Value<'v>) -> Option<Self> {
        FrozenTupleRef::from_frozen_value(value.unpack_frozen()?)
    }
}


/*
 * Copyright 2018 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Bindings to/from Rust tuple types.

use crate::typing::Ty;
use crate::values::type_repr::StarlarkTypeRepr;
use crate::values::types::tuple::value::Tuple;
use crate::values::AllocValue;
use crate::values::Heap;
use crate::values::UnpackValue;
use crate::values::Value;

impl<'v, T1: AllocValue<'v>> AllocValue<'v> for (T1,) {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_tuple(&[self.0.alloc_value(heap)])
    }
}

impl<'v, T1: AllocValue<'v>, T2: AllocValue<'v>> AllocValue<'v> for (T1, T2) {
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_tuple(&[self.0.alloc_value(heap), self.1.alloc_value(heap)])
    }
}

impl<'v, T1: AllocValue<'v>, T2: AllocValue<'v>, T3: AllocValue<'v>> AllocValue<'v>
    for (T1, T2, T3)
{
    fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
        heap.alloc_tuple(&[
            self.0.alloc_value(heap),
            self.1.alloc_value(heap),
            self.2.alloc_value(heap),
        ])
    }
}

impl<T1: StarlarkTypeRepr, T2: StarlarkTypeRepr> StarlarkTypeRepr for (T1, T2) {
    fn starlark_type_repr() -> Ty {
        Ty::tuple2(T1::starlark_type_repr(), T2::starlark_type_repr())
    }
}

impl<T1: StarlarkTypeRepr> StarlarkTypeRepr for (T1,) {
    fn starlark_type_repr() -> Ty {
        Ty::Tuple(vec![T1::starlark_type_repr()])
    }
}

impl<T1: StarlarkTypeRepr, T2: StarlarkTypeRepr, T3: StarlarkTypeRepr> StarlarkTypeRepr
    for (T1, T2, T3)
{
    fn starlark_type_repr() -> Ty {
        Ty::Tuple(vec![
            T1::starlark_type_repr(),
            T2::starlark_type_repr(),
            T3::starlark_type_repr(),
        ])
    }
}

impl<'v, T1: UnpackValue<'v>, T2: UnpackValue<'v>> UnpackValue<'v> for (T1, T2) {
    fn expected() -> String {
        format!("tuple ({}, {})", T1::expected(), T2::expected())
    }

    fn unpack_value(value: Value<'v>) -> Option<Self> {
        let t = Tuple::from_value(value)?;
        if t.len() != 2 {
            return None;
        }
        Some((
            T1::unpack_value(t.content()[0])?,
            T2::unpack_value(t.content()[1])?,
        ))
    }
}


/*
 * Copyright 2018 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::cmp::Ordering;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::slice;

use allocative::Allocative;
use display_container::fmt_container;
use serde::ser::SerializeTuple;
use serde::Serialize;
use starlark_derive::starlark_value;
use starlark_derive::StarlarkDocs;

use crate as starlark;
use crate::any::ProvidesStaticType;
use crate::coerce::coerce;
use crate::coerce::Coerce;
use crate::collections::StarlarkHasher;
use crate::private::Private;
use crate::values::comparison::compare_slice;
use crate::values::comparison::equals_slice;
use crate::values::index::apply_slice;
use crate::values::index::convert_index;
use crate::values::FrozenValue;
use crate::values::Heap;
use crate::values::StarlarkValue;
use crate::values::UnpackValue;
use crate::values::Value;
use crate::values::ValueError;
use crate::values::ValueLike;

/// Define the tuple type. See [`Tuple`] and [`FrozenTuple`] as the two aliases.
#[repr(C)]
#[derive(ProvidesStaticType, StarlarkDocs, Allocative)]
#[starlark_docs(builtin = "standard")]
pub(crate) struct TupleGen<V> {
    len: usize,
    /// The data stored by the tuple.
    content: [V; 0],
}

impl<'v, V: ValueLike<'v>> Display for TupleGen<V> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // For single-item tuples we need to add a trailing ',' and easier to just handle that ourself than configure display_container correctly
        if self.len() == 1 {
            if f.alternate() {
                write!(f, "( {:#}, )", self.content()[0])
            } else {
                write!(f, "({},)", self.content()[0])
            }
        } else {
            fmt_container(f, "(", ")", self.content().iter())
        }
    }
}

impl<'v, V: ValueLike<'v>> Debug for TupleGen<V> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("TupleGen")
            .field("content", &self.content())
            .finish()
    }
}

impl<V> TupleGen<V> {
    /// `type(())`.
    pub(crate) const TYPE: &'static str = "tuple";

    pub(crate) const unsafe fn new(len: usize) -> TupleGen<V> {
        TupleGen { len, content: [] }
    }

    pub(crate) fn offset_of_content() -> usize {
        memoffset::offset_of!(Self, content)
    }
}

/// Runtime type of unfrozen tuple.
pub(crate) type Tuple<'v> = TupleGen<Value<'v>>;
/// Runtime type of frozen tuple.
pub(crate) type FrozenTuple = TupleGen<FrozenValue>;

unsafe impl<'v> Coerce<Tuple<'v>> for FrozenTuple {}

impl<'v> Tuple<'v> {
    /// Downcast a value to a tuple.
    pub(crate) fn from_value(value: Value<'v>) -> Option<&'v Self> {
        if value.unpack_frozen().is_some() {
            value.downcast_ref::<FrozenTuple>().map(coerce)
        } else {
            value.downcast_ref::<Tuple<'v>>()
        }
    }
}

impl<'v, V: ValueLike<'v>> TupleGen<V> {
    /// Get the length of the tuple.
    pub(crate) fn len(&self) -> usize {
        self.content().len()
    }

    /// Tuple elements.
    pub(crate) fn content(&self) -> &[V] {
        unsafe { slice::from_raw_parts(self.content.as_ptr(), self.len) }
    }

    pub(crate) fn content_mut(&mut self) -> &mut [V] {
        unsafe { slice::from_raw_parts_mut(self.content.as_mut_ptr(), self.len) }
    }

    /// Iterate over the elements of the tuple.
    pub(crate) fn iter<'a>(&'a self) -> impl Iterator<Item = Value<'v>> + 'a
    where
        'v: 'a,
    {
        self.content().iter().map(|e| e.to_value())
    }
}

#[starlark_value(type = Tuple::TYPE)]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for TupleGen<V>
where
    Self: ProvidesStaticType<'v>,
{
    fn is_special(_: Private) -> bool
    where
        Self: Sized,
    {
        true
    }

    fn to_bool(&self) -> bool {
        self.len() != 0
    }

    fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
        for v in self.content() {
            v.write_hash(hasher)?;
        }
        Ok(())
    }

    fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
        match Tuple::from_value(other) {
            None => Ok(false),
            Some(other) => equals_slice(self.content(), other.content(), |x, y| x.equals(*y)),
        }
    }

    fn compare(&self, other: Value<'v>) -> anyhow::Result<Ordering> {
        match Tuple::from_value(other) {
            None => ValueError::unsupported_with(self, "cmp()", other),
            Some(other) => compare_slice(self.content(), other.content(), |x, y| x.compare(*y)),
        }
    }

    fn at(&self, index: Value, _heap: &'v Heap) -> anyhow::Result<Value<'v>> {
        let i = convert_index(index, self.len() as i32)? as usize;
        Ok(self.content()[i].to_value())
    }

    fn length(&self) -> anyhow::Result<i32> {
        Ok(self.len() as i32)
    }

    fn is_in(&self, other: Value<'v>) -> anyhow::Result<bool> {
        for x in self.content() {
            if x.equals(other)? {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn slice(
        &self,
        start: Option<Value>,
        stop: Option<Value>,
        stride: Option<Value>,
        heap: &'v Heap,
    ) -> anyhow::Result<Value<'v>> {
        Ok(heap.alloc_tuple(&apply_slice(coerce(self.content()), start, stop, stride)?))
    }

    unsafe fn iterate(&self, me: Value<'v>, _heap: &'v Heap) -> anyhow::Result<Value<'v>> {
        Ok(me)
    }

    unsafe fn iter_size_hint(&self, index: usize) -> (usize, Option<usize>) {
        debug_assert!(index <= self.len());
        let rem = self.len() - index;
        (rem, Some(rem))
    }

    unsafe fn iter_next(&self, index: usize, _heap: &'v Heap) -> Option<Value<'v>> {
        self.content().get(index).map(|v| v.to_value())
    }

    unsafe fn iter_stop(&self) {}

    fn add(&self, other: Value<'v>, heap: &'v Heap) -> Option<anyhow::Result<Value<'v>>> {
        if let Some(other) = Tuple::from_value(other) {
            let mut result = Vec::with_capacity(self.len() + other.len());
            for x in self.iter() {
                result.push(x);
            }
            for x in other.iter() {
                result.push(x);
            }
            Some(Ok(heap.alloc_tuple(&result)))
        } else {
            None
        }
    }

    fn mul(&self, other: Value, heap: &'v Heap) -> anyhow::Result<Value<'v>> {
        let l = i32::unpack_param(other)?;
        let mut result = Vec::new();
        for _i in 0..l {
            result.extend(self.content().iter().map(|e| e.to_value()));
        }
        Ok(heap.alloc_tuple(&result))
    }

    fn collect_repr_cycle(&self, collector: &mut String) {
        collector.push_str("(...)");
    }
}

impl<'v, V: ValueLike<'v>> Serialize for TupleGen<V> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut tuple_serializer = serializer.serialize_tuple(self.len)?;

        for e in self.content().iter() {
            tuple_serializer.serialize_element(e)?;
        }

        tuple_serializer.end()
    }
}

#[cfg(test)]
mod tests {
    use crate::assert;
    use crate::assert::Assert;

    #[test]
    fn test_to_str() {
        assert::all_true(
            r#"
str((1, 2, 3)) == "(1, 2, 3)"
str((1, (2, 3))) == "(1, (2, 3))"
str((1,)) == "(1,)"
"#,
        );
    }

    #[test]
    fn test_repr_cycle() {
        let mut a = Assert::new();
        // TODO(nga): fix and enable.
        a.disable_static_typechecking();
        a.eq("l = []; t = (l,); l.append(t); repr(t)", "'([(...)],)'");
        a.eq("l = []; t = (l,); l.append(t); str(t)", "'([(...)],)'");
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro2::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::DeriveInput;

fn punctuated_try_map<A, B, P: Clone>(
    punctuated: &Punctuated<A, P>,
    f: impl Fn(&A) -> syn::Result<B>,
) -> syn::Result<Punctuated<B, P>> {
    let mut new_punctuated = Punctuated::new();
    for pair in punctuated.pairs() {
        new_punctuated.push_value(f(pair.value())?);
        if let Some(p) = pair.punct() {
            new_punctuated.push_punct((*p).clone());
        }
    }
    Ok(new_punctuated)
}

fn generic_argument_replace_lifetimes_with_static(
    generic_argument: &syn::GenericArgument,
) -> syn::Result<syn::GenericArgument> {
    match generic_argument {
        syn::GenericArgument::Lifetime(lifetime) => Ok(syn::GenericArgument::Lifetime(
            syn::Lifetime::new("'static", lifetime.span()),
        )),
        a @ syn::GenericArgument::Const(..) => Ok(a.clone()),
        c => Err(syn::Error::new_spanned(c, "unsupported generic argument")),
    }
}

fn path_arguments_replace_lifetimes_with_static(
    path_arguments: &syn::PathArguments,
) -> syn::Result<syn::PathArguments> {
    Ok(match path_arguments {
        syn::PathArguments::None => syn::PathArguments::None,
        syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
            colon2_token,
            lt_token,
            args,
            gt_token,
        }) => syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
            colon2_token: *colon2_token,
            lt_token: *lt_token,
            args: punctuated_try_map(args, generic_argument_replace_lifetimes_with_static)?,
            gt_token: *gt_token,
        }),
        syn::PathArguments::Parenthesized(_) => {
            return Err(syn::Error::new_spanned(
                path_arguments,
                "unsupported path arguments",
            ));
        }
    })
}

fn path_segment_replace_lifetimes_with_static(
    path_segment: &syn::PathSegment,
) -> syn::Result<syn::PathSegment> {
    Ok(syn::PathSegment {
        ident: path_segment.ident.clone(),
        arguments: path_arguments_replace_lifetimes_with_static(&path_segment.arguments)?,
    })
}

fn path_replace_lifetimes_with_static(path: &syn::Path) -> syn::Result<syn::Path> {
    Ok(syn::Path {
        leading_colon: path.leading_colon,
        segments: punctuated_try_map(&path.segments, path_segment_replace_lifetimes_with_static)?,
    })
}

/// Replace `X<'a>` with `X<'static>`.
fn type_param_bound_replace_lifetimes_with_static(
    bound: &syn::TypeParamBound,
) -> syn::Result<syn::TypeParamBound> {
    match bound {
        syn::TypeParamBound::Lifetime(lifetime) => Ok(syn::TypeParamBound::Lifetime(
            syn::Lifetime::new("'static", lifetime.span()),
        )),
        syn::TypeParamBound::Trait(trait_bound) => {
            if trait_bound.lifetimes.is_some() {
                return Err(syn::Error::new_spanned(
                    trait_bound,
                    "trait bounds with generic lifetimes are not supported",
                ));
            }
            Ok(syn::TypeParamBound::Trait(syn::TraitBound {
                paren_token: trait_bound.paren_token,
                modifier: trait_bound.modifier,
                lifetimes: trait_bound.lifetimes.clone(),
                path: path_replace_lifetimes_with_static(&trait_bound.path)?,
            }))
        }
        _ => Ok(bound.clone()),
    }
}

pub(crate) fn derive_provides_static_type(
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    match derive_provides_static_type_impl(input) {
        Ok(gen) => gen,
        Err(e) => e.to_compile_error().into(),
    }
}

/// Single lifetime parameter for `ProvidesStaticType`
fn pst_lifetime<'a>(
    params: impl Iterator<Item = &'a syn::GenericParam>,
) -> syn::Result<TokenStream> {
    let mut lifetime = None;
    for param in params {
        if let syn::GenericParam::Lifetime(param) = param {
            if lifetime.is_some() {
                return Err(syn::Error::new_spanned(
                    param,
                    "only one lifetime parameter is supported",
                ));
            }
            lifetime = Some(param);
        }
    }
    Ok(match lifetime {
        Some(lifetime) => quote! { #lifetime },
        None => quote! { 'pst },
    })
}

fn derive_provides_static_type_impl(
    input: proc_macro::TokenStream,
) -> syn::Result<proc_macro::TokenStream> {
    let input: DeriveInput = syn::parse(input)?;

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let lifetime = pst_lifetime(input.generics.params.iter())?;

    let mut lifetimes = Vec::new();
    let mut static_lifetimes = Vec::new();
    let mut type_param_names = Vec::new();
    let mut type_param_bounds = Vec::new();
    let mut type_param_static_type_bounds = Vec::new();
    let mut static_type_params = Vec::new();
    let mut const_params = Vec::new();
    let mut const_param_names = Vec::new();
    for param in &input.generics.params {
        match param {
            syn::GenericParam::Lifetime(param) => {
                lifetimes.push(param.lifetime.clone());
                static_lifetimes.push(quote! {'static});
            }
            syn::GenericParam::Type(param) => {
                let has_static_lifetime_bound = param.bounds.iter().any(|bound| {
                    if let syn::TypeParamBound::Lifetime(lifetime) = bound {
                        lifetime.ident == "static"
                    } else {
                        false
                    }
                });

                let param_name = &param.ident;
                let param_bounds = param.bounds.iter();
                if has_static_lifetime_bound {
                    type_param_bounds.push(quote! {
                        #param_name : #(#param_bounds+)* Sized
                    });
                    let param_bounds = param
                        .bounds
                        .iter()
                        .map(type_param_bound_replace_lifetimes_with_static)
                        .collect::<syn::Result<Vec<_>>>()?;
                    type_param_static_type_bounds.push(quote! {
                        #param_name : #(#param_bounds+)* Sized
                    });
                    static_type_params.push(quote! { #param_name});
                } else {
                    type_param_bounds.push(quote! {
                        #param_name : #(#param_bounds+)* starlark::any::ProvidesStaticType<#lifetime> + Sized
                    });
                    let param_bounds = param
                        .bounds
                        .iter()
                        .map(type_param_bound_replace_lifetimes_with_static)
                        .collect::<syn::Result<Vec<_>>>()?;
                    type_param_static_type_bounds.push(quote! {
                        #param_name :: StaticType : #(#param_bounds+)* Sized
                    });
                    static_type_params.push(quote! { #param_name :: StaticType });
                }
                type_param_names.push(param.ident.clone());
            }
            syn::GenericParam::Const(params) => {
                const_params.push(params.clone());
                const_param_names.push(params.ident.clone());
            }
        }
    }

    let gen = if input.generics.lt_token.is_none() {
        quote! {
            unsafe impl<#lifetime> #impl_generics starlark::any::ProvidesStaticType<#lifetime> for #name #ty_generics #where_clause {
                type StaticType = #name #ty_generics;
            }
        }
    } else {
        quote! {
            unsafe impl <
                #lifetime,
                #(#type_param_bounds,)*
                #(#const_params,)*
                    > starlark::any::ProvidesStaticType<#lifetime>
            for #name <
                #(#lifetimes,)*
                #(#type_param_names,)*
                #(#const_param_names,)*
                    > #where_clause
            where
                #(#type_param_static_type_bounds,)*
            {
                type StaticType = #name <
                    #(#static_lifetimes,)*
                    #(#static_type_params,)*
                    #(#const_param_names,)*
                        >;
            }
        }
    };

    Ok(gen.into())
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::Attribute;
use syn::Data;
use syn::DeriveInput;
use syn::Error;
use syn::Result;
use syn::Token;
use syn::Type;

pub fn derive_attrs(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let data = input.data;
    let name = input.ident;
    expand_attrs_derive(data, name)
        .unwrap_or_else(|e| e.to_compile_error())
        .into()
}

struct Field {
    ident: Ident,
    starlark_args: Vec<Ident>,
    ty: Type,
}

impl Field {
    fn name(&self) -> String {
        self.ident.to_string()
    }

    /// Owned String fields should not be cloned before alloc-ing on the
    /// Starlark heap. All other field types are just cloned for convenience of
    /// implementation.
    fn is_owned_string(&self) -> bool {
        match &self.ty {
            Type::Path(p) => {
                p.path == syn::parse_str("std::string::String").unwrap()
                    || p.path.is_ident("String")
            }
            _ => false,
        }
    }

    fn should_clone(&self) -> bool {
        !self.is_owned_string()
    }

    fn skip(&self) -> bool {
        self.starlark_args.iter().any(|i| i == "skip")
    }

    fn has_attr_match_item(&self) -> proc_macro2::TokenStream {
        let name = self.name();
        quote! {
            #name => true
        }
    }

    fn get_attr_match_item(&self) -> proc_macro2::TokenStream {
        let name = self.name();
        let ident = &self.ident;
        match self.should_clone() {
            false => quote! {
                #name => Some(heap.alloc(&self.#ident))
            },
            true => quote! {
                #name => Some(heap.alloc(self.#ident.clone()))
            },
        }
    }
}

fn expand_attrs_derive(data: Data, name: Ident) -> Result<proc_macro2::TokenStream> {
    let fields: Vec<_> = match data {
        Data::Struct(s) => Ok(s.fields.iter().cloned().collect()),
        Data::Enum(e) => Err(Error::new(
            e.enum_token.span(),
            "#[derive(StarlarkAttrs)] does not support enums",
        )),
        Data::Union(u) => Err(Error::new(
            u.union_token.span(),
            "#[derive(StarlarkAttrs)] does not support unions",
        )),
    }?;

    let expose_fields: Vec<Field> = fields
        .into_iter()
        .map(|field| {
            match field_attr(&field, "starlark") {
                // by default, include all fields
                None => Ok(Field {
                    ident: field.ident.unwrap(),
                    starlark_args: vec![],
                    ty: field.ty,
                }),
                Some(attr) => {
                    let starlark_args = attr
                        .parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated)?
                        .into_iter()
                        .collect();
                    Ok(Field {
                        ident: field.ident.unwrap(),
                        starlark_args,
                        ty: field.ty,
                    })
                }
            }
        })
        .filter(|f| f.as_ref().map(|f| !f.skip()).unwrap_or(true))
        .collect::<Result<_>>()?;

    let has_attr_items = expose_fields.iter().map(|f| f.has_attr_match_item());
    let has_attr = quote! {
        pub(crate) fn attrs_has_attr(&self, attr: &str) -> bool {
            match attr {
                #(#has_attr_items),*,
                _ => false,
            }
        }
    };

    let get_attr_items = expose_fields.iter().map(|f| f.get_attr_match_item());
    let get_attr = quote! {
        pub(crate) fn attrs_get_attr<'v>(&self, attr: &str, heap: &'v starlark::values::Heap) -> Option<starlark::values::Value<'v>> {
            match attr {
                #(#get_attr_items),*,
                _ => None,
            }
        }
    };

    let dir_names = expose_fields.iter().map(|f| f.ident.to_string());
    let dir_attr = quote! {
        pub(crate) fn attrs_dir_attr(&self) -> Vec<String> {
            vec![
                #(#dir_names.to_owned()),*
            ]
        }
    };

    let expanded = quote! {
        // Unfortunately, we can't actually implement the direct methods for
        // `StarlarkValue`, because then we would have conflicting
        // implementations. However, we can implement wrappers in another
        // proc-macro that can then be called.
        impl #name {
            #has_attr
            #get_attr
            #dir_attr
        }
    };

    Ok(expanded)
}

fn field_attr<'a, I: ?Sized>(field: &'a syn::Field, path: &I) -> Option<&'a Attribute>
where
    Ident: PartialEq<I>,
{
    field.attrs.iter().find(|a| a.path().is_ident(path))
}

pub fn starlark_attrs() -> TokenStream {
    // Clearly, this could be a regular macro, but since it is required by the
    // derive macro, export it as a proc-macro anyway so that it can come from
    // the same crate.
    let expanded = quote! {
        // proxy all attr methods to the implementations generated by
        // derive(Attrs)
        fn has_attr(&self, attr: &str, _heap: &'v starlark::values::Heap) -> bool {
            self.attrs_has_attr(attr)
        }
        fn get_attr(&self, attr: &str, heap: &'v starlark::values::Heap) -> Option<starlark::values::Value<'v>> {
            self.attrs_get_attr(attr, heap)
        }
        fn dir_attr(&self) -> Vec<String> {
            self.attrs_dir_attr()
        }
    };
    expanded.into()
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Generate stubs for bytecode interpreter.

use proc_macro2::Ident;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote_spanned;
use syn::parse_macro_input;
use syn::spanned::Spanned;
use syn::Fields;
use syn::ItemEnum;
use syn::Variant;

struct BcOpcodeEnum {
    span: Span,
    variants: Vec<Ident>,
}

impl BcOpcodeEnum {
    fn parse_variant(input: &Variant) -> Ident {
        assert!(
            input.discriminant.is_none(),
            "BcOpcode relies on sequential numbering of opcodes"
        );
        assert!(
            matches!(input.fields, Fields::Unit),
            "BcOpcode variants must be unit"
        );
        input.ident.clone()
    }

    fn parse(input: ItemEnum) -> BcOpcodeEnum {
        BcOpcodeEnum {
            span: input.span(),
            variants: input
                .variants
                .iter()
                .map(BcOpcodeEnum::parse_variant)
                .collect(),
        }
    }

    fn render_dispatch_variant(&self, variant: &Ident) -> TokenStream {
        let instr = format_ident!("Instr{}", variant);
        quote_spanned! {
            variant.span()=>
            BcOpcode::#variant => handler.handle::<#instr>(),
        }
    }

    fn render_dispatch(&self) -> TokenStream {
        let variants = self
            .variants
            .iter()
            .map(|v| self.render_dispatch_variant(v));
        quote_spanned! {
            self.span=>
            impl BcOpcode {
                #[inline(always)]
                fn do_dispatch<R>(self, handler: impl BcOpcodeHandler<R>) -> R {
                    match self {
                        #(#variants)*
                    }
                }
            }
        }
    }

    fn render_dispatch_all_variant(&self, variant: &Ident) -> TokenStream {
        let instr = format_ident!("Instr{}", variant);
        quote_spanned! {
            variant.span()=>
            handler.handle::<#instr>(BcOpcode::#variant);
        }
    }

    fn render_dispatch_all(&self) -> TokenStream {
        let variants = self
            .variants
            .iter()
            .map(|v| self.render_dispatch_all_variant(v));
        quote_spanned! {
            self.span=>
            impl BcOpcode {
                #[inline(always)]
                fn do_dispatch_all(handler: &mut impl BcOpcodeAllHandler) {
                    #(#variants)*
                }
            }
        }
    }
}

pub(crate) fn starlark_internal_bc(
    _attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let input_clone = input.clone();
    let bc_opcode_enum = parse_macro_input!(input_clone as ItemEnum);
    let bc_opcode_enum = BcOpcodeEnum::parse(bc_opcode_enum);
    let input = TokenStream::from(input);
    let dispatch = bc_opcode_enum.render_dispatch();
    let dispatch_all = bc_opcode_enum.render_dispatch_all();
    proc_macro::TokenStream::from(quote_spanned! {
        input.span()=>
        #input
        #dispatch
        #dispatch_all
    })
}


/*
 * Copyright 2018 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashSet;

use proc_macro2::Span;
use proc_macro2::TokenTree;
use quote::format_ident;
use quote::quote;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::token;
use syn::Data;
use syn::DeriveInput;
use syn::Expr;
use syn::Field;
use syn::GenericArgument;
use syn::Ident;
use syn::Path;
use syn::PathArguments;
use syn::ReturnType;
use syn::Token;
use syn::Type;

// This macro does two related derivations depending on whether there are any generic parameters.
//
// struct A(B) ==> coerce both ways between A and B
// struct A<T>(...) => coerce A<T1> to A<T2> if coerce T1 to T2 and all the fields support it
pub fn derive_coerce(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    match derive_coerce_impl(input) {
        Ok(x) => x.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn derive_coerce_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    check_repr(&input)?;

    if input.generics.type_params().count() == 0 {
        derive_coerce_inner(input)
    } else {
        derive_coerce_params(input)
    }
}

fn derive_coerce_inner(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let lifetimes = input.generics.lifetimes().collect::<Vec<_>>();

    let field = match &input.data {
        Data::Struct(x) if x.fields.len() == 1 => x.fields.iter().next().unwrap(),
        _ => {
            return Err(syn::Error::new_spanned(
                input,
                "Type-parameter free types must be a single field struct",
            ));
        }
    };

    let type1 = input.ident;
    let type2 = &field.ty;
    Ok(quote! {
        unsafe impl < #(#lifetimes),* > starlark::coerce::Coerce<#type1< #(#lifetimes),* >> for #type2 {}
        unsafe impl < #(#lifetimes),* > starlark::coerce::Coerce<#type2> for #type1< #(#lifetimes),* > {}
    })
}

#[derive(Copy, Clone)]
enum ParamNameMapping {
    From,
    To,
}

impl ParamNameMapping {
    fn format_ident(self, ident: &Ident) -> Ident {
        match self {
            ParamNameMapping::From => format_ident!("From{}", ident),
            ParamNameMapping::To => format_ident!("To{}", ident),
        }
    }
}

fn derive_coerce_params(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let lifetimes = input.generics.lifetimes().collect::<Vec<_>>();

    // unsafe impl <To__T, From__T> Coerce<X<To__T>> X<From__T> where ...
    let name = &input.ident;
    let mut ty_args = HashSet::new();
    let mut ty_args_to = Vec::new();
    let mut ty_args_from = Vec::new();
    let mut ty_args_to1 = Vec::new();
    let mut ty_args_from1 = Vec::new();
    for t in input.generics.type_params() {
        ty_args.insert(t.ident.clone());
        let to_ident = ParamNameMapping::To.format_ident(&t.ident);
        let from_ident = ParamNameMapping::From.format_ident(&t.ident);
        ty_args_to.push(to_ident.clone());
        ty_args_from.push(from_ident.clone());
        if t.bounds.is_empty() {
            ty_args_to1.push(quote! { #to_ident });
            ty_args_from1.push(quote! { #from_ident });
        } else {
            let bounds = &t.bounds;
            ty_args_to1.push(quote! { #to_ident: #bounds });
            ty_args_from1.push(quote! { #from_ident: #bounds });
        }
    }

    let mut constraints = Vec::new();
    let fields = collect_fields(&input)?;
    for x in fields {
        let mut to_ty = x.ty.clone();
        let mut from_ty = x.ty.clone();
        replace_type(&mut to_ty, &ty_args, ParamNameMapping::To)?;
        replace_type(&mut from_ty, &ty_args, ParamNameMapping::From)?;
        if to_ty != from_ty {
            constraints.push(quote! { #from_ty : starlark::coerce::Coerce< #to_ty >});
        }
    }

    Ok(quote! {
        unsafe impl < #(#lifetimes,)* #(#ty_args_from1,)* #(#ty_args_to1,)* >
            starlark::coerce::Coerce<#name < #(#lifetimes,)* #(#ty_args_to,)* >>
            for #name < #(#lifetimes,)* #(#ty_args_from,)* >
            where #(#constraints,)* {}
    })
}

fn collect_fields(input: &DeriveInput) -> syn::Result<Vec<&Field>> {
    match &input.data {
        Data::Struct(x) => Ok(x.fields.iter().collect()),
        Data::Enum(x) => Ok(x.variants.iter().flat_map(|x| &x.fields).collect()),
        Data::Union(..) => Err(syn::Error::new_spanned(
            input,
            "Type-parameter cannot be a union",
        )),
    }
}

fn replace_type(
    ty: &mut Type,
    idents: &HashSet<Ident>,
    mapping: ParamNameMapping,
) -> syn::Result<()> {
    match ty {
        Type::Path(x)
            if x.qself.is_none()
                && x.path.segments.len() == 1
                && x.path.segments[0].arguments.is_empty() =>
        {
            let i = &mut x.path.segments[0].ident;
            if idents.contains(i) {
                *i = mapping.format_ident(i);
            }
            Ok(())
        }
        _ => descend_type(ty, |ty| replace_type(ty, idents, mapping)),
    }
}

/// Descend into all the nested type values within a type,
/// or return an error if you don't know how.
fn descend_type(ty: &mut Type, op: impl Fn(&mut Type) -> syn::Result<()>) -> syn::Result<()> {
    match ty {
        Type::Array(x) => op(&mut x.elem),
        Type::Group(x) => op(&mut x.elem),
        Type::Never(_) => Ok(()),
        Type::Paren(x) => op(&mut x.elem),
        Type::Path(x) => {
            if let Some(qself) = &mut x.qself {
                op(&mut qself.ty)?;
            }
            for p in x.path.segments.iter_mut() {
                match &mut p.arguments {
                    PathArguments::None => {}
                    PathArguments::AngleBracketed(x) => {
                        x.args.iter_mut().try_for_each(|x| match x {
                            GenericArgument::Type(x) => op(x),
                            _ => Ok(()),
                        })?
                    }
                    PathArguments::Parenthesized(x) => {
                        x.inputs.iter_mut().try_for_each(&op)?;
                        if let ReturnType::Type(_, ty) = &mut x.output {
                            op(ty)?;
                        }
                    }
                }
            }
            Ok(())
        }
        Type::Ptr(x) => op(&mut x.elem),
        Type::Reference(x) => op(&mut x.elem),
        Type::Slice(x) => op(&mut x.elem),
        Type::Tuple(xs) => xs.elems.iter_mut().try_for_each(op),
        _ => Err(syn::Error::new_spanned(ty, "Unsupported type")),
    }
}

// Taken from ref_cast: https://github.com/dtolnay/ref-cast/blob/eadd839cc0db116e4fc9bfee5dd29dde82089eed/derive/src/lib.rs#L91
fn check_repr(input: &DeriveInput) -> syn::Result<()> {
    let mut has_repr = false;
    let mut errors = None;
    let mut push_error = |error| match &mut errors {
        Some(errors) => syn::Error::combine(errors, error),
        None => errors = Some(error),
    };

    for attr in &input.attrs {
        if attr.path().is_ident("repr") {
            if let Err(error) = attr.parse_args_with(|input: ParseStream| {
                while !input.is_empty() {
                    let path = input.call(Path::parse_mod_style)?;
                    if path.is_ident("C") || path.is_ident("transparent") {
                        has_repr = true;
                    } else if path.is_ident("packed") {
                        // ignore
                    } else {
                        let meta_item_span = if input.peek(token::Paren) {
                            let group: TokenTree = input.parse()?;
                            quote!(#path #group)
                        } else if input.peek(Token![=]) {
                            let eq_token: Token![=] = input.parse()?;
                            let value: Expr = input.parse()?;
                            quote!(#path #eq_token #value)
                        } else {
                            quote!(#path)
                        };
                        let msg = if path.is_ident("align") {
                            "aligned repr on struct that implements Coerce is not supported"
                        } else {
                            "unrecognized repr on struct that implements Coerce"
                        };
                        push_error(syn::Error::new_spanned(meta_item_span, msg));
                    }
                    if !input.is_empty() {
                        input.parse::<Token![,]>()?;
                    }
                }
                Ok(())
            }) {
                push_error(error);
            }
        }
    }

    if !has_repr {
        let mut requires_repr = syn::Error::new(
            Span::call_site(),
            "Coerce trait requires #[repr(C)] or #[repr(transparent)]",
        );
        if let Some(errors) = errors {
            requires_repr.combine(errors);
        }
        errors = Some(requires_repr);
    }

    match errors {
        None => Ok(()),
        Some(errors) => Err(errors),
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashMap;

use proc_macro2::Ident;
use quote::format_ident;
use quote::quote;
use quote::quote_spanned;
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::Attribute;
use syn::DeriveInput;
use syn::Expr;
use syn::ExprLit;
use syn::MetaNameValue;
use syn::Token;

const STARLARK_DOCS_ATTRS: &str = "starlark_docs";

pub(crate) fn derive_docs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    expand_docs_derive(input)
        .unwrap_or_else(|e| e.to_compile_error())
        .into()
}

fn expand_docs_derive(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let span = input.span();
    let DeriveInput {
        ident: name,
        generics,
        attrs,
        ..
    } = input;

    let parsed_attrs = parse_custom_attributes(attrs)?;

    let use_inventory = if parsed_attrs.contains_key("builtin") {
        quote! {}
    } else {
        quote! {
            use starlark::__derive_refs::inventory as inventory;
        }
    };

    let name_str = name.to_string();
    let custom_attrs: Vec<_> = parsed_attrs
        .into_iter()
        .map(|(k, v)| {
            quote! { (#k, #v)}
        })
        .collect();

    // Complex values take a generic `V`, so we cannot call their impl's __generated_documentation
    // function directly. We need to have a different getter, and to avoid requiring a lifetime,
    // we just use FrozenValue.
    //
    // For now, we just assume if the struct ends in "Gen", it is a starlark_complex_value.
    // It would be simple enough to make this configurable in the future if required.
    let is_complex_value = name.to_string().ends_with("Gen");
    let frozen_name = match is_complex_value {
        true => {
            let frozen = Ident::new(
                &format!(
                    "Frozen{}",
                    name_str.strip_suffix("Gen").unwrap_or(&name_str)
                ),
                name.span(),
            );
            quote_spanned! {span=> #frozen }
        }
        false => {
            if generics.type_params().count() != 0 {
                return Err(syn::Error::new(
                    span,
                    "If a an item name does not end in `Gen`, it must have no type parameters",
                ));
            }

            quote_spanned! {span=> #name }
        }
    };

    let namespace_fn_name = format_ident!("_{}_register_starlark_docs", name_str.to_lowercase());

    Ok(quote_spanned! {span=>
        fn #namespace_fn_name() {
            // `ctor` fails at compile time on wasm32.
            #[cfg(not(target_arch = "wasm32"))]
            {
                #use_inventory
                starlark::__derive_refs::inventory::submit! {
                    starlark::docs::RegisteredDoc {
                        getter: || starlark::docs::RegisteredDoc::for_type::<#frozen_name>(&[#(#custom_attrs),*]),
                    }
                };
            }
        }
    })
}

fn get_attrs(attr: Attribute) -> syn::Result<HashMap<String, String>> {
    let mut found = HashMap::new();
    let args: Punctuated<MetaNameValue, Token![,]> =
        attr.parse_args_with(Punctuated::parse_terminated)?;
    for arg in args {
        match &arg {
            MetaNameValue {
                path,
                value:
                    Expr::Lit(ExprLit {
                        lit: syn::Lit::Str(s),
                        ..
                    }),
                ..
            } => {
                let ident = path.get_ident().unwrap();
                let attr_name = ident.to_string();
                if found.insert(attr_name, s.value()).is_some() {
                    return Err(syn::Error::new(
                        arg.span(),
                        format!("Argument {} was specified twice", ident),
                    ));
                }
            }
            MetaNameValue { path, .. } => {
                return Err(syn::Error::new(
                    arg.span(),
                    format!(
                        "Argument {} must have a string literal value",
                        path.get_ident().unwrap(),
                    ),
                ));
            }
        }
    }
    Ok(found)
}

fn parse_custom_attributes(attrs: Vec<Attribute>) -> syn::Result<HashMap<String, String>> {
    for attr in attrs {
        if attr.path().is_ident(STARLARK_DOCS_ATTRS) {
            return get_attrs(attr);
        }
    }

    Ok(HashMap::new())
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use quote::quote_spanned;
use syn::Data;
use syn::DataEnum;
use syn::DataStruct;
use syn::Field;
use syn::Fields;
use syn::Variant;

fn for_each_field_in_struct(
    s: &DataStruct,
    field_handler: impl Fn(&Ident, &Field) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
    match &s.fields {
        Fields::Unit => Ok(quote! {}),
        Fields::Unnamed(fields) => {
            let field_names: Vec<Ident> = (0..fields.unnamed.len())
                .map(|i| format_ident!("f{}", i))
                .collect();
            let field_handlers = field_names
                .iter()
                .zip(fields.unnamed.iter())
                .map(|(name, field)| field_handler(name, field))
                .collect::<syn::Result<Vec<TokenStream>>>()?;
            Ok(quote_spanned! {
                s.struct_token.span => {
                    let Self(#(#field_names),*) = self;
                    #(#field_handlers)*
                }
            })
        }
        Fields::Named(fields) => {
            let field_names = fields.named.iter().map(|f| &f.ident);
            let field_handlers = fields
                .named
                .iter()
                .map(|f| field_handler(f.ident.as_ref().unwrap(), f))
                .collect::<syn::Result<Vec<TokenStream>>>()?;
            Ok(quote_spanned! {
                s.struct_token.span => {
                    let Self { #(#field_names),* } = self;
                    #(#field_handlers)*
                }
            })
        }
    }
}

fn for_each_field_in_enum_variant(
    v: &Variant,
    field_handler: &impl Fn(&Ident, &Field) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
    let variant_name = &v.ident;
    match &v.fields {
        Fields::Unit => Ok(quote_spanned! {
            v.ident.span() => Self::#variant_name => {}
        }),
        Fields::Unnamed(fields) => {
            let field_names: Vec<Ident> = (0..fields.unnamed.len())
                .map(|i| format_ident!("f{}", i))
                .collect();
            let field_handlers = field_names
                .iter()
                .zip(fields.unnamed.iter())
                .map(|(name, field)| field_handler(name, field))
                .collect::<syn::Result<Vec<TokenStream>>>()?;
            Ok(quote_spanned! {
                v.ident.span() =>
                Self::#variant_name(#(#field_names),*) => {
                    #(#field_handlers)*
                }
            })
        }
        Fields::Named(fields) => {
            let field_names = fields.named.iter().map(|f| &f.ident);
            let field_handlers = fields
                .named
                .iter()
                .map(|f| field_handler(f.ident.as_ref().unwrap(), f))
                .collect::<syn::Result<Vec<TokenStream>>>()?;
            Ok(quote_spanned! {
                v.ident.span() =>
                Self::#variant_name { #(#field_names),* } => {
                    #(#field_handlers)*
                }
            })
        }
    }
}

fn for_each_field_in_enum(
    e: &DataEnum,
    field_handler: impl Fn(&Ident, &Field) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
    let variants = e
        .variants
        .iter()
        .map(|v| for_each_field_in_enum_variant(v, &field_handler))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(quote_spanned! {
        e.enum_token.span =>
        match self {
            #(#variants)*
        }
    })
}

/// Generate code for each struct of enum field of the given type.
pub(crate) fn for_each_field(
    data: &Data,
    field_handler: impl Fn(&Ident, &Field) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
    match data {
        Data::Struct(s) => for_each_field_in_struct(s, field_handler),
        Data::Enum(e) => for_each_field_in_enum(e, field_handler),
        Data::Union(u) => Err(syn::Error::new_spanned(
            u.union_token,
            "Unions are not supported",
        )),
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote_spanned;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::spanned::Spanned;
use syn::Attribute;
use syn::Data;
use syn::DataEnum;
use syn::DataStruct;
use syn::DeriveInput;
use syn::Error;
use syn::Fields;
use syn::GenericParam;
use syn::Index;
use syn::LitStr;
use syn::Token;
use syn::Variant;
use syn::WherePredicate;

struct Input<'a> {
    input: &'a DeriveInput,
}

impl<'a> Input<'a> {
    fn angle_brankets(&self, tokens: &[TokenStream]) -> TokenStream {
        let span = self.input.span();
        if tokens.is_empty() {
            quote_spanned! { span=> }
        } else {
            quote_spanned! { span=> < #(#tokens,)* > }
        }
    }

    fn format_impl_generics(
        &self,
        bounds: bool,
    ) -> syn::Result<(TokenStream, TokenStream, TokenStream)> {
        let span = self.input.span();
        let mut impl_params = Vec::new();
        let mut input_params = Vec::new();
        let mut output_params = Vec::new();
        if bounds {
            impl_params.push(quote_spanned! { span=> 'freeze });
        }
        for param in &self.input.generics.params {
            match param {
                GenericParam::Type(t) => {
                    let name = &t.ident;
                    let bounds = t.bounds.iter();
                    impl_params.push(quote_spanned! {
                        span=>
                        #name: #(#bounds +)* starlark::values::Freeze
                    });
                    input_params.push(quote_spanned! {
                        span=>
                        #name
                    });
                    output_params.push(quote_spanned! {
                        span=>
                        <#name as starlark::values::Freeze>::Frozen
                    });
                }
                GenericParam::Lifetime(lt) => {
                    impl_params.push(quote_spanned! { span=> #lt });
                    input_params.push(quote_spanned! { span=> #lt });
                    output_params.push(quote_spanned! { span=> 'static });
                }
                GenericParam::Const(_) => {
                    return Err(Error::new_spanned(param, "const generics not supported"));
                }
            }
        }
        Ok((
            self.angle_brankets(&impl_params),
            self.angle_brankets(&input_params),
            self.angle_brankets(&output_params),
        ))
    }
}

fn derive_freeze_impl(input: DeriveInput) -> syn::Result<TokenStream> {
    let span = input.span();
    let input = Input { input: &input };

    let name = &input.input.ident;

    let opts = extract_options(&input.input.attrs)?;
    let (impl_params, input_params, output_params) =
        input.format_impl_generics(opts.bounds.is_some())?;

    let validate_body = match opts.validator {
        Some(validator) => quote_spanned! {
            span=>
            #validator(&frozen)?;
        },
        None => quote_spanned! { span=> },
    };

    let bounds_body = match opts.bounds {
        Some(bounds) => quote_spanned! { span=> where #bounds },
        None => quote_spanned! { span=> },
    };

    let body = freeze_impl(name, &input.input.data)?;

    let gen = quote_spanned! {
        span=>
        impl #impl_params starlark::values::Freeze for #name #input_params #bounds_body {
            type Frozen = #name #output_params;
            #[allow(unused_variables)]
            fn freeze(self, freezer: &starlark::values::Freezer) -> anyhow::Result<Self::Frozen> {
                let frozen = #body;
                #validate_body
                std::result::Result::Ok(frozen)
            }
        }
    };

    Ok(gen)
}

#[derive(Default)]
struct FreezeDeriveOptions {
    validator: Option<Ident>,
    bounds: Option<WherePredicate>,
}

/// Parse a #[freeze(validator = function)] annotation.
#[cfg_attr(feature = "gazebo_lint", allow(gazebo_lint_impl_dupe))] // The custom_keyword macro
fn extract_options(attrs: &[Attribute]) -> syn::Result<FreezeDeriveOptions> {
    syn::custom_keyword!(validator);
    syn::custom_keyword!(bounds);

    let mut opts = FreezeDeriveOptions::default();

    for attr in attrs.iter() {
        if !attr.path().is_ident("freeze") {
            continue;
        }

        attr.parse_args_with(|input: ParseStream| {
            loop {
                if input.parse::<validator>().is_ok() {
                    if opts.validator.is_some() {
                        return Err(input.error("`validator` was set twice"));
                    }
                    input.parse::<Token![=]>()?;
                    opts.validator = Some(input.parse()?);
                } else if input.parse::<bounds>().is_ok() {
                    if opts.bounds.is_some() {
                        return Err(input.error("`bounds` was set twice"));
                    }
                    input.parse::<Token![=]>()?;
                    let bounds_input = input.parse::<LitStr>()?;
                    opts.bounds = Some(bounds_input.parse()?);
                } else {
                    return Err(input.lookahead1().error());
                }

                if input.parse::<Option<Token![,]>>()?.is_none() {
                    break;
                }
            }

            Ok(())
        })?;
    }

    Ok(opts)
}

/// Parse attribute `#[freeze(identity)]`.
///
/// Currently it fails on any attribute argument other than `id`.
#[cfg_attr(feature = "gazebo_lint", allow(gazebo_lint_impl_dupe))] // The custom_keyword macro
fn is_identity(attrs: &[Attribute]) -> syn::Result<bool> {
    syn::custom_keyword!(identity);

    for attr in attrs.iter() {
        if !attr.path().is_ident("freeze") {
            continue;
        }

        let ignore = attr.parse_args_with(|input: ParseStream| {
            let ignore = input.parse::<Option<identity>>()?.is_some();
            Ok(ignore)
        })?;

        if !ignore {
            continue;
        }

        return Ok(true);
    }

    Ok(false)
}

fn freeze_struct(name: &Ident, data: &DataStruct) -> syn::Result<TokenStream> {
    let span = name.span();
    let res = match data.fields {
        Fields::Named(ref fields) => {
            let xs = fields
                .named
                .iter()
                .map(|f| {
                    let name = &f.ident;
                    let res = if is_identity(&f.attrs)? {
                        quote_spanned! { span=>
                            #name: self.#name,
                        }
                    } else {
                        quote_spanned! { span=>
                            #name: starlark::values::Freeze::freeze(self.#name, freezer)?,
                        }
                    };

                    syn::Result::Ok(res)
                })
                .collect::<Result<Vec<_>, _>>()?;
            quote_spanned! {
                span=>
                #name {
                    #(#xs)*
                }
            }
        }
        Fields::Unnamed(ref fields) => {
            let xs = fields
                .unnamed
                .iter()
                .enumerate()
                .map(|(i, f)| {
                    let i = Index::from(i);

                    let res = if is_identity(&f.attrs)? {
                        quote_spanned! { span=>
                            self.#i,
                        }
                    } else {
                        quote_spanned! {
                            span=>
                            starlark::values::Freeze::freeze(self.#i, freezer)?,
                        }
                    };

                    syn::Result::Ok(res)
                })
                .collect::<Result<Vec<_>, _>>()?;
            quote_spanned! {
                span=>
                #name (
                    #(#xs)*
                )
            }
        }
        Fields::Unit => {
            quote_spanned! { span=> }
        }
    };

    Ok(res)
}

fn freeze_enum_variant(name: &Ident, variant: &Variant) -> syn::Result<TokenStream> {
    let span = variant.span();
    let variant_name = &variant.ident;
    match &variant.fields {
        Fields::Unit => Ok(quote_spanned! {
            span=>
            #name::#variant_name => #name::#variant_name,
        }),
        Fields::Unnamed(fields) => {
            let field_names: Vec<_> = (0..fields.unnamed.len())
                .map(|i| format_ident!("f_{}", i))
                .collect();
            Ok(quote_spanned! {
                span=>
                #name::#variant_name(#(#field_names),*) => {
                    #name::#variant_name(
                        #(starlark::values::Freeze::freeze(#field_names, freezer)?),*
                    )
                }
            })
        }
        Fields::Named(field) => {
            let field_names: Vec<_> = field.named.iter().map(|f| &f.ident).collect();
            Ok(quote_spanned! {
                span=>
                #name::#variant_name { #(#field_names),* } => {
                    #name::#variant_name {
                        #(#field_names: starlark::values::Freeze::freeze(#field_names, freezer)?,)*
                    }
                }
            })
        }
    }
}

fn freeze_enum(name: &Ident, data: &DataEnum) -> syn::Result<TokenStream> {
    let span = name.span();
    let variants = data
        .variants
        .iter()
        .map(|v| freeze_enum_variant(name, v))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(quote_spanned! {
        span=>
        match self {
            #(#variants)*
        }
    })
}

fn freeze_impl(name: &Ident, data: &Data) -> syn::Result<TokenStream> {
    match data {
        Data::Struct(data) => freeze_struct(name, data),
        Data::Enum(data) => freeze_enum(name, data),
        Data::Union(_) => Err(Error::new_spanned(name, "Can't derive freeze for unions")),
    }
}

pub fn derive_freeze(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match derive_freeze_impl(input) {
        Ok(s) => s.into(),
        Err(e) => e.to_compile_error().into(),
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! A proc-macro for writing functions in Rust that can be called from Starlark.

#![cfg_attr(feature = "gazebo_lint", feature(plugin))]
#![cfg_attr(feature = "gazebo_lint", allow(deprecated))] // :(
#![cfg_attr(feature = "gazebo_lint", plugin(gazebo_lint))]

#[allow(unused_extern_crates)] // proc_macro is very special
extern crate proc_macro;

use proc_macro::TokenStream;

mod any_lifetime;
mod attrs;
mod bc;
mod coerce;
mod docs;
mod for_each_field;
mod freeze;
mod module;
mod serde;
mod starlark_value;
mod trace;
mod visit_span;
mod vtable;

/// Write Starlark modules concisely in Rust syntax.
///
/// For example:
///
/// ```ignore
/// #[starlark_module]
/// fn global(builder: &mut GlobalsBuilder) {
///     fn cc_binary(name: &str, srcs: Vec<&str>) -> String {
///         Ok(format!("{:?} {:?}", name, srcs))
///     }
/// }
/// ```
///
/// Parameters operate as named parameters of a given type. Each parameter will be unpacked with `UnpackValue`, unless:
///
/// * It is type `Option`, in which case it will be considered optional.
/// * It is a single argument of type `Arguments`, in which case all arguments will be passed together with minimal interpretation.
///
/// There are a number of attributes that can be add to each parameter by writing attributes before the
/// parameter name:
///
/// * `#[starlark(default = "a default")]` - provide a deafult for the parameter if it is omitted.
/// * `#[starlark(require = pos)]` - require the parameter to be passed by position, not named.
/// * `#[starlark(require = named)]` - require the parameter to be passed by name, not by position.
/// * `#[starlark(args)]` - treat the argument as `*args` in Starlark, receiving all additional positional arguments as a tuple.
/// * `#[starlark(kwargs)]` - treat the argument as `**kwargs` in Starlark, receiving all additional named arguments as a dictionary.
///
/// There are a number of attributes that can be applied to the entire function by writing attributes
/// before the `fn` of the function:
///
/// * `#[starlark(attribute_type = "foo")]` - if the function has `.type` applied, return this string. Usually used on
///   constructor functions so that `ctor.type` can be used in Starlark code.
/// * `#[starlark(return_type = "foo")]` - the return type of the function used for documention.
/// * `#[starlark(speculative_exec_safe)]` - the function
///   is considered safe to execute speculatively: the function should have
///   no global side effects, should not panic, and should finish in reasonable time.
///   The evaluator may invoke such functions early to generate more efficient code.
/// * `#[starlark(attribute)]` to turn the name into
///   an attribute on the value. Such a function must take exactly one argument, namely a value
///   of the type you have attached it to.
///
/// Multiple attributes can be specified either separately `#[starlark(require = named)] #[starlark(default = "")]` or
/// separated with a comman `#[starlark(require = named, default = "")]`.
///
/// There are two special arguments, distinguished by their type, which provides access to interpreter state:
///
/// * `heap: &'v Heap` gives access to the Starlark heap, for allocating things.
/// * `eval: &mut Evaluator<'v, '_>` gives access to the Starlark evaluator, which can be used to look at interpreter state.
///
/// A module can be used to define globals (with `GlobalsBuilder`) or methods on an object (with `MethodsBuilder`).
/// In the case of methods, the first argument to each function will be the object itself, typically named `this`.
///
/// All these functions interoperate properly with `dir()`, `getattr()` and `hasattr()`.
///
/// If a desired function name is also a Rust keyword, use the `r#` prefix, e.g. `r#type`.
///
/// As a more complex example:
///
/// ```ignore
/// #[starlark_module]
/// fn methods(builder: &mut MethodsBuilder) {
///     fn r#enum<'v>(
///         this: Value<'v>,
///         #[starlark(require = named, default = 3)] index: i32,
///         heap: &'v Heap,
///     ) -> anyhow::Result<StringValue<'v>> {
///         Ok(heap.alloc_str(&format!("{this} {index}")))
///     }
/// }
/// ```
///
/// This defines a method such that when attached to an object `object.enum(index = 12)` will
/// return the string of the object and the index.
#[proc_macro_attribute]
pub fn starlark_module(attr: TokenStream, input: TokenStream) -> TokenStream {
    module::starlark_module(attr, input)
}

/// Stubs for Starlark bytecode interpreter.
#[proc_macro_attribute]
pub fn starlark_internal_bc(
    attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    bc::starlark_internal_bc(attr, input)
}

#[proc_macro_attribute]
pub fn starlark_internal_vtable(
    attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    vtable::starlark_internal_vtable(attr, input)
}

#[proc_macro_derive(VisitSpanMut)]
pub fn derive_visit_span_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    visit_span::derive_visit_span_mut(input)
}

/// Derive the `Trace` trait.
#[proc_macro_derive(Trace, attributes(trace))]
pub fn derive_trace(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    trace::derive_trace(input)
}

/// Derive the `Freeze` trait.
#[proc_macro_derive(Freeze, attributes(freeze))]
pub fn derive_freeze(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    freeze::derive_freeze(input)
}

/// Derive the `NoSerialize` trait for serde.
#[proc_macro_derive(NoSerialize)]
pub fn derive_no_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    serde::derive_no_serialize(input)
}

/// Derive accessor methods that are designed to be used from {has,get,dir}_attr
/// in an `impl StarlarkValue` block. All fields in the struct that are not
/// marked with #[starlark(skip)] are exported to Starlark code as attributes.
/// NOTE: Any usage must also call `starlark_attrs!()` in the impl block for
/// `StarlarkValue`, otherwise the generated attr methods will not be used.
#[proc_macro_derive(StarlarkAttrs, attributes(starlark))]
pub fn derive_starlark_attrs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    attrs::derive_attrs(input)
}

/// Generate an accessor function on the provided type that returns its documentation
/// based on `StarlarkValue::get_methods()`. This macro requires that the type implements
/// `starlark::StarlarkValue`.
///
/// Types that derive `StarlarkDocs` are also registered automatically with the `inventory` crate.
/// To get all types annotated with `StarlarkDocs`, see `starlark::docs::get_registered_starlark_docs()`
///
/// Note that for statically linked binaries, documentation from all compiled crates in the binary
/// will be included.
///
/// For dynamically linked binaries, documentation will only be able to retrieved after the crate's
/// library is `dlopen()`ed.
///
/// `#[starlark_docs(key="value", second_key="second_value",...)]` can be used to insert
/// arbitrary keys and string values into the generated `Docs::custom_attrs` for use
/// by documentation tooling.
///
/// Types provided by the `starlark` library itself will have the `builtin` key set to either
/// `standard` or `extension` depending on whether the type is part of the Starlark standard.
#[proc_macro_derive(StarlarkDocs, attributes(starlark_docs))]
pub fn derive_starlark_docs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    docs::derive_docs(input)
}

/// Generate `{has,get,dir}_attr` in the `StarlarkValue` impl block that proxy
/// to the ones generated by `derive(StarlarkAttrs)`
#[proc_macro]
pub fn starlark_attrs(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
    attrs::starlark_attrs()
}

/// Generate missing elements of `StarlarkValue` trait when this attribute
/// is applied to an impl block of `StarlarkValue`.
#[proc_macro_attribute]
pub fn starlark_value(
    attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    starlark_value::derive_starlark_value(attr, input)
}

/// Derive the `ProvidesStaticType` trait. Requires the type has no type arguments, no constant arguments,
/// and at most one lifetime argument.
#[proc_macro_derive(ProvidesStaticType)]
pub fn derive_provides_static_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    any_lifetime::derive_provides_static_type(input)
}

// Derive the `Coerce` trait.
#[proc_macro_derive(Coerce)]
pub fn derive_coerce(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    coerce::derive_coerce(input)
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro2::Span;
use quote::quote;
use syn::parse_macro_input;
use syn::DeriveInput;
use syn::GenericParam;
use syn::Lifetime;
use syn::LifetimeParam;

pub fn derive_no_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let mut input = parse_macro_input!(input as DeriveInput);
    let tick_v = GenericParam::Lifetime(LifetimeParam::new(Lifetime::new("'v", Span::call_site())));

    let mut has_tick_v = false;
    for param in &mut input.generics.params {
        if let GenericParam::Lifetime(t) = param {
            if t.lifetime.ident == "v" {
                has_tick_v = true;
            }
        }
    }
    let mut generics2 = input.generics.clone();

    let (_, ty_generics, _) = input.generics.split_for_impl();
    if !has_tick_v {
        generics2.params.insert(0, tick_v);
    }
    let (impl_generics, _, _) = generics2.split_for_impl();

    let name = &input.ident;
    let gen = quote! {
        impl #impl_generics starlark::__derive_refs::serde::Serialize for #name #ty_generics where Self : starlark::values::StarlarkValue<'v> {
            fn serialize<__NoSerializeS>(&self, serializer: __NoSerializeS) -> std::result::Result<__NoSerializeS::Ok, __NoSerializeS::Error>
            where
                __NoSerializeS: starlark::__derive_refs::serde::Serializer,
            {
                Err(starlark::__derive_refs::serde::Error::custom(format!("Operation `serde::serialize` not supported on type `{}`", <Self as starlark::values::StarlarkValue>::TYPE)))
            }
        }
    };

    gen.into()
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use quote::quote_spanned;
use syn::spanned::Spanned;

pub(crate) fn derive_starlark_value(
    attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let attr = syn::parse_macro_input!(attr as StarlarkValueAttrs);
    let input = syn::parse_macro_input!(input as syn::ItemImpl);
    match derive_starlark_value_impl(attr, input) {
        Ok(gen) => gen.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

struct StarlarkValueAttrs {
    typ: syn::Expr,
    /// Implement `UnpackValue` for `&T`.
    /// Note we are implementing `UnpackValue` for `&T` instead of `T`,
    /// therefore we use proc macro on impl trait instead of `#[derive]` on struct.
    unpack_value: bool,
    /// Implement `StarlarkTypeRepr` for `&T`.
    starlark_type_repr: bool,
}

impl syn::parse::Parse for StarlarkValueAttrs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        input.parse::<syn::Token![type]>()?;
        input.parse::<syn::Token![=]>()?;
        let typ = input.parse::<syn::Expr>()?;
        let mut attrs = StarlarkValueAttrs {
            typ,
            unpack_value: false,
            starlark_type_repr: false,
        };

        loop {
            if input.is_empty() {
                break;
            }
            input.parse::<syn::Token![,]>()?;
            if input.is_empty() {
                // Allow trailing comma.
                break;
            }
            let name = input.parse::<syn::Ident>()?;
            if name == "UnpackValue" {
                attrs.unpack_value = true;
            } else if name == "StarlarkTypeRepr" {
                attrs.starlark_type_repr = true;
            } else {
                return Err(syn::Error::new_spanned(
                    name,
                    "unknown attribute, allowed attribute is `UnpackValue`, `StarlarkTypeRepr`",
                ));
            }
        }

        Ok(attrs)
    }
}

struct ImplStarlarkValue<'a> {
    input: &'a syn::ItemImpl,
    lifetime_param: &'a syn::Lifetime,
    attrs: StarlarkValueAttrs,
}

fn is_impl_starlark_value(
    input: &syn::ItemImpl,
    attrs: StarlarkValueAttrs,
) -> syn::Result<ImplStarlarkValue> {
    let err = "expected `impl StarlarkValue for ...`";
    let Some((_, path, _)) = &input.trait_ else {
        return Err(syn::Error::new_spanned(
            input,
            err,
        ));
    };
    let Some(last) = path.segments.last() else {
        return Err(syn::Error::new_spanned(
            path,
            err,
        ));
    };
    if last.ident != "StarlarkValue" {
        return Err(syn::Error::new_spanned(&last.ident, err));
    }
    let mut lifetime_param = None;
    for lt in input.generics.lifetimes() {
        if lifetime_param.is_some() {
            return Err(syn::Error::new_spanned(
                lt,
                "multiple lifetime parameters are not supported",
            ));
        }
        lifetime_param = Some(lt);
    }
    let lifetime_param = match lifetime_param {
        Some(lt) => &lt.lifetime,
        None => {
            return Err(syn::Error::new_spanned(
                input,
                "expected a lifetime parameter",
            ));
        }
    };
    Ok(ImplStarlarkValue {
        input,
        lifetime_param,
        attrs,
    })
}

impl<'a> ImplStarlarkValue<'a> {
    fn span(&self) -> proc_macro2::Span {
        self.input.span()
    }

    /// Impl `UnpackValue for &T`.
    fn impl_unpack_value(&self) -> syn::Result<proc_macro2::TokenStream> {
        if !self.attrs.unpack_value && !self.attrs.starlark_type_repr {
            return Ok(proc_macro2::TokenStream::new());
        }

        if !self.attrs.unpack_value || !self.attrs.starlark_type_repr {
            return Err(syn::Error::new_spanned(
                self.input,
                "`UnpackValue` and `StarlarkTypeRepr` can only be specified together",
            ));
        }

        for param in &self.input.generics.params {
            match param {
                syn::GenericParam::Lifetime(_) => {}
                _ => {
                    return Err(syn::Error::new_spanned(
                        param,
                        "only lifetime parameters are supported to implement `UnpackValue` or `StarlarkTypeRepr`",
                    ));
                }
            }
        }

        let lt = self.lifetime_param;
        let params = &self.input.generics.params;
        let where_clause = &self.input.generics.where_clause;
        let self_ty = &self.input.self_ty;
        Ok(quote_spanned! {
            self.span() =>

            impl<#params> starlark::values::type_repr::StarlarkTypeRepr for &#lt #self_ty
            #where_clause
            {
                fn starlark_type_repr() -> starlark::typing::Ty {
                    <#self_ty as starlark::values::type_repr::StarlarkTypeRepr>::starlark_type_repr()
                }
            }

            impl<#params> starlark::values::UnpackValue<#lt> for &#lt #self_ty
            #where_clause
            {
                fn unpack_value(value: starlark::values::Value<#lt>) -> Option<&#lt #self_ty> {
                    starlark::values::ValueLike::downcast_ref(value)
                }
            }
        })
    }

    fn please_use_starlark_type_macro(&self) -> syn::Result<syn::ImplItem> {
        syn::parse2(quote_spanned! { self.span() =>
            fn please_use_starlark_type_macro() {}
        })
    }

    fn const_type(&self) -> syn::Result<syn::ImplItem> {
        let typ = &self.attrs.typ;
        syn::parse2(quote_spanned! { self.span() =>
            const TYPE: &'static str = #typ;
        })
    }

    fn get_type_value_static(&self) -> syn::Result<syn::ImplItem> {
        let typ = &self.attrs.typ;
        syn::parse2(quote_spanned! { self.span() =>
            #[inline]
            fn get_type_value_static() -> starlark::values::FrozenStringValue {
                starlark::const_frozen_string!(#typ)
            }
        })
    }
}

fn derive_starlark_value_impl(
    attr: StarlarkValueAttrs,
    mut input: syn::ItemImpl,
) -> syn::Result<proc_macro2::TokenStream> {
    let impl_starlark_value = is_impl_starlark_value(&input, attr)?;

    let impl_unpack_value = impl_starlark_value.impl_unpack_value()?;

    let please_use_starlark_type_macro = impl_starlark_value.please_use_starlark_type_macro()?;
    let const_type = impl_starlark_value.const_type()?;
    let get_type_value_static = impl_starlark_value.get_type_value_static()?;

    input.items.splice(
        0..0,
        [
            const_type,
            get_type_value_static,
            please_use_starlark_type_macro,
        ],
    );

    Ok(quote_spanned! {
        input.span() =>

        #impl_unpack_value

        #input
    })
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::HashSet;

use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::quote;
use quote::quote_spanned;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::parse_quote;
use syn::spanned::Spanned;
use syn::Attribute;
use syn::Data;
use syn::DeriveInput;
use syn::GenericArgument;
use syn::GenericParam;
use syn::Generics;
use syn::Lifetime;
use syn::LifetimeParam;
use syn::PathArguments;
use syn::ReturnType;
use syn::Type;
use syn::TypeParamBound;

use crate::for_each_field::for_each_field;

pub fn derive_trace(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let mut input = parse_macro_input!(input as DeriveInput);
    let tick_v = GenericParam::Lifetime(LifetimeParam::new(Lifetime::new("'v", Span::call_site())));

    let bound: TypeParamBound = parse_quote!(starlark::values::Trace<'v>);
    let mut has_tick_v = false;
    for param in &mut input.generics.params {
        if let GenericParam::Type(type_param) = param {
            type_param.bounds.push(bound.clone());
        }
        if let GenericParam::Lifetime(t) = param {
            if t.lifetime.ident == "v" {
                has_tick_v = true;
            }
        }
    }
    let mut generics2 = input.generics.clone();

    let (_, ty_generics, where_clause) = input.generics.split_for_impl();
    if !has_tick_v {
        generics2.params.insert(0, tick_v);
    }
    let (impl_generics, _, _) = generics2.split_for_impl();

    let name = &input.ident;
    let body = match trace_impl(&input.data, &input.generics) {
        Ok(body) => body,
        Err(e) => {
            return e.to_compile_error().into();
        }
    };
    let gen = quote! {
        unsafe impl #impl_generics starlark::values::Trace<'v> for #name #ty_generics #where_clause {
            #[allow(unused_variables)]
            fn trace(&mut self, tracer: &starlark::values::Tracer<'v>) {
                #body
            }
        }
    };
    gen.into()
}

/// Parse attribute `#[trace(unsafe_ignore)]`.
///
/// Currently it fails on any attribute argument other than `unsafe_ignore`.
#[cfg_attr(feature = "gazebo_lint", allow(gazebo_lint_impl_dupe))] // The custom_keyword macro
fn is_ignore(attrs: &[Attribute]) -> bool {
    syn::custom_keyword!(unsafe_ignore);

    attrs.iter().any(|a| {
        a.path().is_ident("trace")
            && a.parse_args_with(|input: ParseStream| {
                let ignore = input.parse::<Option<unsafe_ignore>>()?.is_some();
                Ok(ignore)
            })
            .unwrap()
    })
}

fn trace_impl(data: &Data, generics: &Generics) -> syn::Result<TokenStream> {
    let generic_types = generics
        .params
        .iter()
        .filter_map(|x| match x {
            GenericParam::Type(x) => Some(x.ident.to_string()),
            _ => None,
        })
        .collect();

    for_each_field(data, |name, field| {
        if is_ignore(&field.attrs) {
            Ok(quote! {})
        } else if is_static(&field.ty, &generic_types) {
            Ok(quote_spanned! {
                field.span()=>
                starlark::values::Tracer::trace_static(tracer, #name);
            })
        } else {
            Ok(quote_spanned! {
                field.span()=>
                starlark::values::Trace::trace(#name, tracer);
            })
        }
    })
}

/// Return `true` if this type definitely satisfies 'static,
/// which means it has no lifetimes that aren't 'static and no generic parameters
/// (as they might have lifetime bounds on them)
fn is_static(ty: &Type, generics: &HashSet<String>) -> bool {
    let f = |x| is_static(x, generics);

    match ty {
        Type::Array(x) => f(&x.elem),
        Type::BareFn(_) => true,
        Type::Never(_) => true,
        Type::Paren(x) => f(&x.elem),
        Type::Path(x) => {
            x.qself.is_none()
                && x.path.segments.iter().all(|x| {
                    !generics.contains(&x.ident.to_string())
                        && is_static_path_arguments(&x.arguments, generics)
                })
        }
        Type::Ptr(_) => true,
        Type::Reference(x) => f(&x.elem) && is_static_lifetime(x.lifetime.as_ref()),
        Type::Slice(x) => f(&x.elem),
        Type::Tuple(x) => x.elems.iter().all(f),
        _ => false,
    }
}

fn is_static_path_arguments(x: &PathArguments, generics: &HashSet<String>) -> bool {
    let f = |x| is_static(x, generics);

    match x {
        PathArguments::None => true,
        PathArguments::AngleBracketed(x) => x.args.iter().all(|x| match x {
            GenericArgument::Type(x) => f(x),
            GenericArgument::Lifetime(x) => is_static_lifetime(Some(x)),
            _ => false,
        }),
        PathArguments::Parenthesized(x) => match &x.output {
            ReturnType::Default => false, // An inferred type we can't see
            ReturnType::Type(_, ty) => f(ty) && x.inputs.iter().all(f),
        },
    }
}

fn is_static_lifetime(x: Option<&Lifetime>) -> bool {
    match x {
        None => false,
        Some(x) => x.ident == "static",
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use proc_macro2::TokenStream;
use quote::quote;
use quote::quote_spanned;
use syn::parse_macro_input;
use syn::spanned::Spanned;
use syn::DeriveInput;

use crate::for_each_field::for_each_field;

fn derive_body(input: &DeriveInput) -> syn::Result<TokenStream> {
    for_each_field(&input.data, |field_name, field| {
        Ok(quote_spanned! {
            field.span() =>
            crate::eval::runtime::visit_span::VisitSpanMut::visit_spans(#field_name, visitor);
        })
    })
}

fn derive(input: DeriveInput) -> syn::Result<TokenStream> {
    let (_impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
    let name = &input.ident;
    let body = derive_body(&input)?;

    let impl_generics = if input.generics.params.is_empty() {
        quote! {}
    } else {
        let params = input
            .generics
            .params
            .iter()
            .map(|p| match p {
                syn::GenericParam::Type(t) => {
                    let t = &t.ident;
                    Ok(quote! {
                        #t: crate::eval::runtime::visit_span::VisitSpanMut
                    })
                }
                _ => Err(syn::Error::new_spanned(
                    p,
                    "VisitSpanMut cannot be derived for generics with non-type params",
                )),
            })
            .collect::<syn::Result<Vec<_>>>()?;
        quote! {
            < #(#params,)* >
        }
    };

    Ok(quote! {
        impl #impl_generics crate::eval::runtime::visit_span::VisitSpanMut for #name #type_generics #where_clause {
            fn visit_spans(&mut self, visitor: &mut impl FnMut(&mut crate::eval::runtime::frame_span::FrameSpan)) {
                #body
            }
        }
    })
}

pub(crate) fn derive_visit_span_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    match derive(input) {
        Ok(output) => output.into(),
        Err(e) => e.to_compile_error().into(),
    }
}