//! The basic structure for the field's data, containing all the available
//! attributes in `#[conf(...)]` and some additional contents.

use crate::error::{Error, ErrorKind, Result};
use crate::opt::{Opt, OptArgData, OptBaseData, OptFileData, OptKind};

use darling::FromField;
use std::rc::Rc;
use syn::{spanned::Spanned, Field, Ident, Path, Type, TypePath};

#[derive(FromField)]
#[darling(attributes(conf))]
pub struct Attrs {
    pub ident: Option<Ident>,
    pub ty: Type,
    #[darling(skip)]
    pub is_option: bool,
    #[darling(skip)]
    pub takes_value: bool,
    #[darling(default)]
    pub default: Option<String>,
    #[darling(default)]
    pub no_long: bool,
    #[darling(default)]
    pub long: Option<String>,
    #[darling(default)]
    pub no_short: bool,
    #[darling(default)]
    pub short: Option<String>,
    #[darling(default)]
    pub help: Option<String>,
    #[darling(default)]
    pub negated_arg: bool,
    #[darling(default)]
    pub no_file: bool,
    #[darling(default)]
    pub file: Option<String>,
    #[darling(default)]
    pub section: Option<String>,
}

impl Attrs {
    /// Method to initialize a `Attrs` completely, from the parsing
    /// done by `darling`, and some extra checks for conflicts and for the
    /// type. The `Attrs::from_field` method generated by `darling`
    /// shouldn't be used by itself.
    pub fn init(field: Field) -> Result<Attrs> {
        let mut attrs = Attrs::from_field(&field)?;
        attrs.apply_rules();
        attrs.check_conflicts()?;

        Ok(attrs)
    }

    /// Applies some rules to itself depending on its attributes.
    fn apply_rules(&mut self) {
        // Painfully obtaining the type `T` inside `Option<T>` to assign
        // `is_optional`.
        if let Type::Path(TypePath {
            path: Path { segments, .. },
            ..
        }) = &self.ty
        {
            if segments.len() == 1 && segments.first().unwrap().ident == "Option" {
                let args = &segments.first().unwrap().arguments;
                use syn::{
                    AngleBracketedGenericArguments as Brackets, GenericArgument::Type as InnerType,
                    PathArguments::AngleBracketed as PathAngles,
                };

                // Obtaining the type inside the `Option<T>`.
                if let PathAngles(Brackets { args, .. }) = args {
                    if let InnerType(ty) = args.first().unwrap() {
                        self.ty = ty.clone();
                        self.is_option = true;
                    }
                }
            }
        }

        // Only boolean flags won't take value.
        self.takes_value = true;
        if let Type::Path(TypePath {
            path: Path { segments, .. },
            ..
        }) = &self.ty
        {
            if segments.len() == 1 && segments.first().unwrap().ident == "bool" {
                self.takes_value = false;
            }
        }
    }

    fn check_conflicts(&self) -> Result<()> {
        // Given an original expression and a list of other expressions it
        // conflicts with, it returns an error in case both of them are true.
        // The macro makes this a bit less repetitive.
        macro_rules! check_conflicts {
            ($orig:expr, $others:expr) => {
                let (orig, orig_name) = $orig;
                if orig {
                    for (confl, confl_name) in $others.iter() {
                        if *confl {
                            return Err(Error {
                                span: self.ident.span(),
                                kind: ErrorKind::ConflictAttrs(
                                    orig_name.to_string(),
                                    confl_name.to_string(),
                                ),
                            });
                        }
                    }
                }
            };
        }

        // Empty fields may not use any other attribute, except for `default`.
        check_conflicts!(
            (
                self.no_short && self.no_long && self.no_file,
                "no_short, no_long and no_file"
            ),
            [
                (self.long.is_some(), "long"),
                (self.short.is_some(), "short"),
                (self.help.is_some(), "help"),
                (self.negated_arg, "negated_arg"),
                (self.file.is_some(), "file"),
                (self.section.is_some(), "section"),
            ]
        );

        check_conflicts!(
            (self.no_short && self.no_long, "no_short and no_long"),
            [
                (self.negated_arg, "negated_arg"),
                (self.help.is_some(), "help"),
            ]
        );

        check_conflicts!(
            (self.no_short, "no_short"),
            [(self.short.is_some(), "short"),]
        );

        check_conflicts!((self.no_long, "no_long"), [(self.long.is_some(), "long"),]);

        check_conflicts!(
            (self.negated_arg, "negated_arg"),
            [
                (self.takes_value, "field's type"),
                (self.default.is_some(), "default"),
            ]
        );

        check_conflicts!(
            (self.no_file, "no_file"),
            [
                (self.file.is_some(), "file"),
                (self.section.is_some(), "section"),
            ]
        );

        Ok(())
    }

    pub fn get_file_data(&self) -> OptFileData {
        OptFileData {
            name: self
                .file
                .clone()
                .unwrap_or_else(|| self.ident.as_ref().unwrap().to_string()),
            section: self
                .section
                .clone()
                .unwrap_or_else(|| "Defaults".to_string()),
        }
    }

    pub fn get_arg_data(&self) -> Result<OptArgData> {
        // The long or short values may be empty, meaning that the
        // value should be converted from the field name.
        let ident = self.ident.clone().unwrap().to_string();

        let long = if self.no_long {
            None
        } else {
            let long = self.long.clone().unwrap_or_else(|| ident.clone());
            Some(long.replace("_", "-"))
        };

        let short = if self.no_short {
            None
        } else {
            match &self.short {
                Some(s) => {
                    // If the user provides the short name, this makes
                    // sure it's a single character.
                    let mut chars = s.chars();
                    let first = chars.next();
                    let second = chars.next();

                    match (first, second) {
                        (Some(ch), None) => Some(ch.to_string()),
                        _ => {
                            return Err(Error {
                                span: self.ident.span(),
                                kind: ErrorKind::Parse(
                                    "short argument can't be longer than \
                                    one character"
                                        .to_string(),
                                ),
                            })
                        }
                    }
                }
                None => {
                    // Otherwise, the short name is obtained from the
                    // identifier, which must be at least a character
                    // long, so `unwrap()` is used.
                    Some(ident.chars().next().unwrap().to_string())
                }
            }
        };

        Ok(OptArgData {
            long,
            short,
            help: self.help.clone(),
            negated: self.negated_arg,
        })
    }

    /// Parses the attributes into an option, which may be empty, an argument,
    /// a file, or both. Thus, two options are returned in case it's both.
    pub fn parse_opt(self) -> Result<(Opt, Option<Opt>)> {
        let base = Rc::new(OptBaseData {
            is_option: self.is_option,
            default: self.default.clone(),
            id: self.ident.clone().unwrap(),
            ty: self.ty.clone(),
        });

        let arg_kind = if self.takes_value {
            OptKind::Arg
        } else {
            OptKind::Flag
        };

        let ret = if self.no_long && self.no_short && self.no_file {
            (
                Opt {
                    base,
                    kind: OptKind::Empty,
                },
                None,
            )
        } else if self.no_file {
            (
                Opt {
                    base,
                    kind: arg_kind(self.get_arg_data()?),
                },
                None,
            )
        } else if self.no_long && self.no_short {
            (
                Opt {
                    base,
                    kind: OptKind::File(self.get_file_data()),
                },
                None,
            )
        } else {
            (
                Opt {
                    base: Rc::clone(&base),
                    kind: arg_kind(self.get_arg_data()?),
                },
                Some(Opt {
                    base,
                    kind: OptKind::File(self.get_file_data()),
                }),
            )
        };

        Ok(ret)
    }
}
