// Rust language amplification derive library providing multiple generic trait
// implementations, type wrappers, derive macros and other language enhancements
//
// Written in 2019-2021 by
//     Dr. Maxim Orlovsky <orlovsky@pandoracore.com>
//
// To the extent possible under law, the author(s) have dedicated all
// copyright and related and neighboring rights to this software to
// the public domain worldwide. This software is distributed without
// any warranty.
//
// You should have received a copy of the MIT License
// along with this software.
// If not, see <https://opensource.org/licenses/MIT>.

use syn::{Lit, Type};

use crate::{ArgValue, Error};

/// Constrains for attribute value type
#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub enum ValueClass {
    /// The value must be a literal matching given literal constraints (see
    /// [`LiteralClass`])
    Literal(LiteralClass),

    /// The value must be of a native rust type matching given type constraints
    /// (see [`TypeClass`])
    Type(TypeClass),

    /// The value must be of a native rust expression.
    Expr,
}

impl From<Lit> for ValueClass {
    fn from(lit: Lit) -> Self { ValueClass::Literal(LiteralClass::from(lit)) }
}

impl From<&Lit> for ValueClass {
    fn from(lit: &Lit) -> Self { ValueClass::Literal(LiteralClass::from(lit)) }
}

impl From<Type> for ValueClass {
    fn from(ty: Type) -> Self { ValueClass::Type(TypeClass::from(ty)) }
}

impl From<&Type> for ValueClass {
    fn from(ty: &Type) -> Self { ValueClass::Type(TypeClass::from(ty)) }
}

impl From<LiteralClass> for ValueClass {
    fn from(cls: LiteralClass) -> Self { ValueClass::Literal(cls) }
}

impl From<TypeClass> for ValueClass {
    fn from(cls: TypeClass) -> Self { ValueClass::Type(cls) }
}

impl ValueClass {
    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Str)`
    pub fn str() -> ValueClass { ValueClass::Literal(LiteralClass::Str) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::ByteStr)`
    pub fn byte_str() -> ValueClass { ValueClass::Literal(LiteralClass::ByteStr) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Byte)`
    pub fn byte() -> ValueClass { ValueClass::Literal(LiteralClass::Byte) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Int)`
    pub fn int() -> ValueClass { ValueClass::Literal(LiteralClass::Int) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Float)`
    pub fn float() -> ValueClass { ValueClass::Literal(LiteralClass::Float) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Char)`
    pub fn char() -> ValueClass { ValueClass::Literal(LiteralClass::Char) }

    /// Convenience constructor creating
    /// `ValueClass::Literal(LiteralClass::Bool)`
    pub fn bool() -> ValueClass { ValueClass::Literal(LiteralClass::Bool) }
}

impl ValueClass {
    /// Checks the value against value class requirements, generating [`Error`]
    /// if the requirements are not met.
    pub fn check(
        self,
        value: &ArgValue,
        attr: impl ToString,
        arg: impl ToString,
    ) -> Result<(), Error> {
        match (self, value) {
            (ValueClass::Literal(lit), ArgValue::Literal(ref value)) => lit.check(value, attr, arg),
            (ValueClass::Type(ty), ArgValue::Type(ref value)) => ty.check(value, attr, arg),
            (ValueClass::Expr, ArgValue::Type(_) | ArgValue::Expr(_) | ArgValue::Literal(_)) => {
                Ok(())
            }
            _ => Err(Error::ArgValueTypeMismatch {
                attr: attr.to_string(),
                arg: arg.to_string(),
            }),
        }
    }
}

/// Constrains for literal value type
#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub enum LiteralClass {
    /// Literal must be a string
    Str,

    /// Literal must be a byte string
    ByteStr,

    /// Literal must be a byte (in form of `b'f'`)
    Byte,

    /// Literal must be a character
    Char,

    /// Literal must be an integer
    Int,

    /// Literal must be a float
    Float,

    /// Literal must be a boolean
    Bool,

    /// Literal must be a verbatim form
    Any,
}

impl From<Lit> for LiteralClass {
    #[inline]
    fn from(lit: Lit) -> Self { LiteralClass::from(&lit) }
}

impl From<&Lit> for LiteralClass {
    fn from(lit: &Lit) -> Self {
        match lit {
            Lit::Str(_) => LiteralClass::Str,
            Lit::ByteStr(_) => LiteralClass::ByteStr,
            Lit::Byte(_) => LiteralClass::Byte,
            Lit::Char(_) => LiteralClass::Char,
            Lit::Int(_) => LiteralClass::Int,
            Lit::Float(_) => LiteralClass::Float,
            Lit::Bool(_) => LiteralClass::Bool,
            Lit::Verbatim(_) => LiteralClass::Any,
        }
    }
}

impl LiteralClass {
    /// Checks the literal against current requirements, generating [`Error`] if
    /// the requirements are not met.
    pub fn check(self, lit: &Lit, attr: impl ToString, arg: impl ToString) -> Result<(), Error> {
        if self == LiteralClass::Any {
            Ok(())
        } else if self != LiteralClass::from(lit) {
            Err(Error::ArgValueTypeMismatch {
                attr: attr.to_string(),
                arg: arg.to_string(),
            })
        } else {
            Ok(())
        }
    }
}

/// Constrains for the possible types that a Rust value could have.
#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub enum TypeClass {
    /// A fixed size array type: `[T; n]`.
    Array,

    /// A bare function type: `fn(usize) -> bool`.
    BareFn,

    /// A type contained within invisible delimiters.
    Group,

    /// An `impl Bound1 + Bound2 + Bound3` type where `Bound` is a trait or
    /// a lifetime.
    ImplTrait,

    /// Indication that a type should be inferred by the compiler: `_`.
    Infer,

    /// A macro in the type position.
    Macro,

    /// The never type: `!`.
    Never,

    /// A parenthesized type equivalent to the inner type.
    Paren,

    /// A path like `std::slice::Iter`, optionally qualified with a
    /// self-type as in `<Vec<T> as SomeTrait>::Associated`.
    Path,

    /// A raw pointer type: `*const T` or `*mut T`.
    Ptr,

    /// A reference type: `&'a T` or `&'a mut T`.
    Reference,

    /// A dynamically sized slice type: `[T]`.
    Slice,

    /// A trait object type `Bound1 + Bound2 + Bound3` where `Bound` is a
    /// trait or a lifetime.
    TraitObject,

    /// A tuple type: `(A, B, C, String)`.
    Tuple,

    /// Tokens in type position not interpreted by Syn.
    Any,
}

impl From<Type> for TypeClass {
    #[inline]
    fn from(ty: Type) -> Self { TypeClass::from(&ty) }
}

impl From<&Type> for TypeClass {
    fn from(ty: &Type) -> Self {
        match ty {
            Type::Array(_) => TypeClass::Array,
            Type::BareFn(_) => TypeClass::BareFn,
            Type::Group(_) => TypeClass::Group,
            Type::ImplTrait(_) => TypeClass::ImplTrait,
            Type::Infer(_) => TypeClass::Infer,
            Type::Macro(_) => TypeClass::Macro,
            Type::Never(_) => TypeClass::Never,
            Type::Paren(_) => TypeClass::Paren,
            Type::Path(_) => TypeClass::Path,
            Type::Ptr(_) => TypeClass::Ptr,
            Type::Reference(_) => TypeClass::Reference,
            Type::Slice(_) => TypeClass::Slice,
            Type::TraitObject(_) => TypeClass::TraitObject,
            Type::Tuple(_) => TypeClass::Tuple,
            Type::Verbatim(_) => TypeClass::Any,
            _ => unreachable!(),
        }
    }
}

impl TypeClass {
    /// Checks the [`Type`] against current requirements, generating [`Error`]
    /// if the requirements are not met.
    pub fn check(self, ty: &Type, attr: impl ToString, arg: impl ToString) -> Result<(), Error> {
        if self == TypeClass::Any {
            Ok(())
        } else if self != TypeClass::from(ty) {
            Err(Error::ArgValueTypeMismatch {
                attr: attr.to_string(),
                arg: arg.to_string(),
            })
        } else {
            Ok(())
        }
    }
}
