use crate::{
    utils::{FromMeta, SetOnce},
    Attribute, AttributeContext, CodamaAttribute, CodamaDirective, Resolvable,
};
use codama_errors::{CodamaError, CodamaResult};
use codama_nodes::{
    CamelCaseString, Docs, InstructionAccountNode, InstructionInputValueNode, IsSigner,
};
use codama_syn_helpers::{extensions::*, Meta};

#[derive(Debug, PartialEq)]
pub struct AccountDirective {
    pub name: CamelCaseString,
    pub is_writable: bool,
    pub is_signer: IsSigner,
    pub is_optional: bool,
    pub docs: Docs,
    pub default_value: Option<Resolvable<InstructionInputValueNode>>,
}

impl AccountDirective {
    pub fn parse(meta: &Meta, ctx: &AttributeContext) -> syn::Result<Self> {
        meta.assert_directive("account")?;
        let mut name = SetOnce::<CamelCaseString>::new("name");
        if let AttributeContext::Field(syn::Field {
            ident: Some(ident), ..
        }) = ctx
        {
            name = name.initial_value(ident.to_string().into())
        }
        let mut is_writable = SetOnce::<bool>::new("writable").initial_value(false);
        let mut is_signer = SetOnce::<IsSigner>::new("signer").initial_value(false.into());
        let mut is_optional = SetOnce::<bool>::new("optional").initial_value(false);
        let mut default_value =
            SetOnce::<Resolvable<InstructionInputValueNode>>::new("default_value");
        let mut docs = SetOnce::<Docs>::new("docs");
        match meta.is_path_or_empty_list() {
            true => (),
            false => meta
                .as_path_list()?
                .each(|ref meta| match meta.path_str().as_str() {
                    "name" => name.set(meta.as_value()?.as_expr()?.as_string()?.into(), meta),
                    "writable" => is_writable.set(bool::from_meta(meta)?, meta),
                    "signer" => is_signer.set(IsSigner::from_meta(meta)?, meta),
                    "optional" => is_optional.set(bool::from_meta(meta)?, meta),
                    "default_value" => default_value.set(
                        Resolvable::<InstructionInputValueNode>::from_meta(meta.as_value()?)?,
                        meta,
                    ),
                    "docs" => docs.set(Docs::from_meta(meta)?, meta),
                    _ => Err(meta.error("unrecognized attribute")),
                })?,
        }
        Ok(AccountDirective {
            name: name.take(meta)?,
            is_writable: is_writable.take(meta)?,
            is_signer: is_signer.take(meta)?,
            is_optional: is_optional.take(meta)?,
            docs: docs.option().unwrap_or_default(),
            default_value: default_value.option(),
        })
    }

    /// Construct an `InstructionAccountNode` from this directive.
    /// Returns an error if any unresolved directives remain.
    pub fn to_instruction_account_node(&self) -> CodamaResult<InstructionAccountNode> {
        Ok(InstructionAccountNode {
            name: self.name.clone(),
            is_writable: self.is_writable,
            is_signer: self.is_signer,
            is_optional: if self.is_optional { Some(true) } else { None },
            docs: self.docs.clone(),
            default_value: Box::new(
                self.default_value
                    .as_ref()
                    .map(|r| r.try_resolved().cloned())
                    .transpose()?,
            ),
        })
    }
}

impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a AccountDirective {
    type Error = CodamaError;

    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
        match attribute.directive.as_ref() {
            CodamaDirective::Account(ref a) => Ok(a),
            _ => Err(CodamaError::InvalidCodamaDirective {
                expected: "account".to_string(),
                actual: attribute.directive.name().to_string(),
            }),
        }
    }
}

impl<'a> TryFrom<&'a Attribute<'a>> for &'a AccountDirective {
    type Error = CodamaError;

    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
        <&CodamaAttribute>::try_from(attribute)?.try_into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use codama_nodes::PayerValueNode;

    #[test]
    fn fully_set() {
        let meta: Meta = syn::parse_quote! { account(name = "payer", writable, signer, optional, default_value = payer) };
        let item = syn::parse_quote! { struct Foo; };
        let ctx = AttributeContext::Item(&item);
        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
        assert_eq!(
            directive,
            AccountDirective {
                name: "payer".into(),
                is_writable: true,
                is_signer: IsSigner::True,
                is_optional: true,
                default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())),
                docs: Docs::default(),
            }
        );
    }

    #[test]
    fn fully_set_with_explicit_values() {
        let meta: Meta = syn::parse_quote! { account(
            name = "payer",
            writable = true,
            signer = "either",
            optional = false,
            default_value = payer
        ) };
        let item = syn::parse_quote! { struct Foo; };
        let ctx = AttributeContext::Item(&item);
        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
        assert_eq!(
            directive,
            AccountDirective {
                name: "payer".into(),
                is_writable: true,
                is_signer: IsSigner::Either,
                is_optional: false,
                default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())),
                docs: Docs::default(),
            }
        );
    }

    #[test]
    fn empty_on_named_field() {
        let meta: Meta = syn::parse_quote! { account };
        let field = syn::parse_quote! { authority: AccountMeta };
        let ctx = AttributeContext::Field(&field);
        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
        assert_eq!(
            directive,
            AccountDirective {
                name: "authority".into(),
                is_writable: false,
                is_signer: IsSigner::False,
                is_optional: false,
                default_value: None,
                docs: Docs::default(),
            }
        );
    }

    #[test]
    fn empty_on_struct() {
        let meta: Meta = syn::parse_quote! { account };
        let item = syn::parse_quote! { struct Foo; };
        let ctx = AttributeContext::Item(&item);
        let error = AccountDirective::parse(&meta, &ctx).unwrap_err();
        assert_eq!(error.to_string(), "name is missing");
    }

    #[test]
    fn with_docs() {
        let meta: Meta = syn::parse_quote! { account(name = "stake", writable, docs = "what this account is for") };
        let item = syn::parse_quote! { struct Foo; };
        let ctx = AttributeContext::Item(&item);
        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
        assert_eq!(
            directive,
            AccountDirective {
                name: "stake".into(),
                is_writable: true,
                is_signer: IsSigner::False,
                is_optional: false,
                default_value: None,
                docs: vec!["what this account is for".to_string()].into(),
            }
        );
    }

    #[test]
    fn with_docs_array() {
        let meta: Meta = syn::parse_quote! { account(name = "authority", signer, docs = ["Line 1", "Line 2", "Line 3"]) };
        let item = syn::parse_quote! { struct Foo; };
        let ctx = AttributeContext::Item(&item);
        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
        assert_eq!(
            directive,
            AccountDirective {
                name: "authority".into(),
                is_writable: false,
                is_signer: IsSigner::True,
                is_optional: false,
                default_value: None,
                docs: vec![
                    "Line 1".to_string(),
                    "Line 2".to_string(),
                    "Line 3".to_string()
                ]
                .into(),
            }
        );
    }
}
