/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::borrow::Cow;
use std::collections::HashMap;
use std::io::Write;

use syn::ext::IdentExt;
use syn::{self, UnOp};

use crate::bindgen::config::{Config, Language};
use crate::bindgen::declarationtyperesolver::DeclarationTypeResolver;
use crate::bindgen::dependencies::Dependencies;
use crate::bindgen::ir::{
    AnnotationSet, Cfg, ConditionWrite, Documentation, GenericParams, Item, ItemContainer, Path,
    Struct, ToCondition, Type,
};
use crate::bindgen::library::Library;
use crate::bindgen::writer::{Source, SourceWriter};
use crate::bindgen::Bindings;

fn member_to_ident(member: &syn::Member) -> String {
    match member {
        syn::Member::Named(ref name) => name.unraw().to_string(),
        syn::Member::Unnamed(ref index) => format!("_{}", index.index),
    }
}

// TODO: Maybe add support to more std associated constants.
fn to_known_assoc_constant(associated_to: &Path, name: &str) -> Option<String> {
    use crate::bindgen::ir::{IntKind, PrimitiveType};

    if name != "MAX" && name != "MIN" {
        return None;
    }

    let prim = PrimitiveType::maybe(associated_to.name())?;
    let prefix = match prim {
        PrimitiveType::Integer {
            kind,
            signed,
            zeroable: _,
        } => match kind {
            IntKind::B8 => {
                if signed {
                    "INT8"
                } else {
                    "UINT8"
                }
            }
            IntKind::B16 => {
                if signed {
                    "INT16"
                } else {
                    "UINT16"
                }
            }
            IntKind::B32 => {
                if signed {
                    "INT32"
                } else {
                    "UINT32"
                }
            }
            IntKind::B64 => {
                if signed {
                    "INT64"
                } else {
                    "UINT64"
                }
            }
            _ => return None,
        },
        _ => return None,
    };
    Some(format!("{}_{}", prefix, name))
}

#[derive(Debug, Clone)]
pub enum Literal {
    Expr(String),
    Path {
        associated_to: Option<(Path, String)>,
        name: String,
    },
    PostfixUnaryOp {
        op: &'static str,
        value: Box<Literal>,
    },
    BinOp {
        left: Box<Literal>,
        op: &'static str,
        right: Box<Literal>,
    },
    FieldAccess {
        base: Box<Literal>,
        field: String,
    },
    Struct {
        path: Path,
        export_name: String,
        fields: HashMap<String, Literal>,
    },
    Cast {
        ty: Type,
        value: Box<Literal>,
    },
}

impl Literal {
    fn replace_self_with(&mut self, self_ty: &Path) {
        match *self {
            Literal::PostfixUnaryOp { ref mut value, .. } => {
                value.replace_self_with(self_ty);
            }
            Literal::BinOp {
                ref mut left,
                ref mut right,
                ..
            } => {
                left.replace_self_with(self_ty);
                right.replace_self_with(self_ty);
            }
            Literal::FieldAccess { ref mut base, .. } => {
                base.replace_self_with(self_ty);
            }
            Literal::Struct {
                ref mut path,
                ref mut export_name,
                ref mut fields,
            } => {
                if path.replace_self_with(self_ty) {
                    *export_name = self_ty.name().to_owned();
                }
                for ref mut expr in fields.values_mut() {
                    expr.replace_self_with(self_ty);
                }
            }
            Literal::Cast {
                ref mut ty,
                ref mut value,
            } => {
                ty.replace_self_with(self_ty);
                value.replace_self_with(self_ty);
            }
            Literal::Path {
                ref mut associated_to,
                ..
            } => {
                if let Some((ref mut path, ref mut export_name)) = *associated_to {
                    if path.replace_self_with(self_ty) {
                        *export_name = self_ty.name().to_owned();
                    }
                }
            }
            Literal::Expr(..) => {}
        }
    }

    fn is_valid(&self, bindings: &Bindings) -> bool {
        match *self {
            Literal::Expr(..) => true,
            Literal::Path {
                ref associated_to,
                ref name,
            } => {
                if let Some((ref path, _export_name)) = associated_to {
                    return bindings.struct_exists(path)
                        || to_known_assoc_constant(path, name).is_some();
                }
                true
            }
            Literal::PostfixUnaryOp { ref value, .. } => value.is_valid(bindings),
            Literal::BinOp {
                ref left,
                ref right,
                ..
            } => left.is_valid(bindings) && right.is_valid(bindings),
            Literal::FieldAccess { ref base, .. } => base.is_valid(bindings),
            Literal::Struct { ref path, .. } => bindings.struct_exists(path),
            Literal::Cast { ref value, .. } => value.is_valid(bindings),
        }
    }

    fn can_be_constexpr(&self) -> bool {
        !self.has_pointer_casts()
    }

    fn visit(&self, visitor: &mut impl FnMut(&Self) -> bool) -> bool {
        if !visitor(self) {
            return false;
        }
        match self {
            Literal::Expr(..) | Literal::Path { .. } => true,
            Literal::PostfixUnaryOp { ref value, .. } => value.visit(visitor),
            Literal::BinOp {
                ref left,
                ref right,
                ..
            } => left.visit(visitor) && right.visit(visitor),
            Literal::FieldAccess { ref base, .. } => base.visit(visitor),
            Literal::Struct { ref fields, .. } => {
                for (_name, field) in fields.iter() {
                    if !field.visit(visitor) {
                        return false;
                    }
                }
                true
            }
            Literal::Cast { ref value, .. } => value.visit(visitor),
        }
    }

    fn has_pointer_casts(&self) -> bool {
        let mut has_pointer_casts = false;
        self.visit(&mut |lit| {
            if let Literal::Cast { ref ty, .. } = *lit {
                has_pointer_casts = has_pointer_casts || ty.is_ptr();
            }
            !has_pointer_casts
        });
        has_pointer_casts
    }

    pub fn uses_only_primitive_types(&self) -> bool {
        let mut uses_only_primitive_types = true;
        self.visit(&mut |lit| {
            // XXX This is a bit sketchy, but alas.
            uses_only_primitive_types = uses_only_primitive_types
                && match *lit {
                    Literal::Struct { .. } => false,
                    Literal::Cast { ref ty, .. } => ty.is_primitive_or_ptr_primitive(),
                    _ => true,
                };
            uses_only_primitive_types
        });
        uses_only_primitive_types
    }
}

impl Literal {
    pub fn rename_for_config(&mut self, config: &Config) {
        match self {
            Literal::Struct {
                ref mut export_name,
                fields,
                ..
            } => {
                config.export.rename(export_name);
                for lit in fields.values_mut() {
                    lit.rename_for_config(config);
                }
            }
            Literal::FieldAccess { ref mut base, .. } => {
                base.rename_for_config(config);
            }
            Literal::Path {
                ref mut associated_to,
                ref mut name,
            } => {
                if let Some((_path, ref mut export_name)) = associated_to {
                    config.export.rename(export_name);
                } else {
                    config.export.rename(name);
                }
            }
            Literal::PostfixUnaryOp { ref mut value, .. } => {
                value.rename_for_config(config);
            }
            Literal::BinOp {
                ref mut left,
                ref mut right,
                ..
            } => {
                left.rename_for_config(config);
                right.rename_for_config(config);
            }
            Literal::Expr(_) => {}
            Literal::Cast {
                ref mut ty,
                ref mut value,
            } => {
                ty.rename_for_config(config, &GenericParams::default());
                value.rename_for_config(config);
            }
        }
    }

    // Translate from full blown `syn::Expr` into a simpler `Literal` type
    pub fn load(expr: &syn::Expr) -> Result<Literal, String> {
        match *expr {
            // Match binary expressions of the form `a * b`
            syn::Expr::Binary(ref bin_expr) => {
                let l = Self::load(&bin_expr.left)?;
                let r = Self::load(&bin_expr.right)?;
                let op = match bin_expr.op {
                    syn::BinOp::Add(..) => "+",
                    syn::BinOp::Sub(..) => "-",
                    syn::BinOp::Mul(..) => "*",
                    syn::BinOp::Div(..) => "/",
                    syn::BinOp::Rem(..) => "%",
                    syn::BinOp::And(..) => "&&",
                    syn::BinOp::Or(..) => "||",
                    syn::BinOp::BitXor(..) => "^",
                    syn::BinOp::BitAnd(..) => "&",
                    syn::BinOp::BitOr(..) => "|",
                    syn::BinOp::Shl(..) => "<<",
                    syn::BinOp::Shr(..) => ">>",
                    syn::BinOp::Eq(..) => "==",
                    syn::BinOp::Lt(..) => "<",
                    syn::BinOp::Le(..) => "<=",
                    syn::BinOp::Ne(..) => "!=",
                    syn::BinOp::Ge(..) => ">=",
                    syn::BinOp::Gt(..) => ">",
                    syn::BinOp::AddEq(..) => "+=",
                    syn::BinOp::SubEq(..) => "-=",
                    syn::BinOp::MulEq(..) => "*=",
                    syn::BinOp::DivEq(..) => "/=",
                    syn::BinOp::RemEq(..) => "%=",
                    syn::BinOp::BitXorEq(..) => "^=",
                    syn::BinOp::BitAndEq(..) => "&=",
                    syn::BinOp::BitOrEq(..) => "|=",
                    syn::BinOp::ShlEq(..) => "<<=",
                    syn::BinOp::ShrEq(..) => ">>=",
                };
                Ok(Literal::BinOp {
                    left: Box::new(l),
                    op,
                    right: Box::new(r),
                })
            }

            // Match literals like true, 'a', 32 etc
            syn::Expr::Lit(syn::ExprLit { ref lit, .. }) => {
                match lit {
                    syn::Lit::Byte(ref value) => Ok(Literal::Expr(format!("{}", value.value()))),
                    syn::Lit::Char(ref value) => Ok(Literal::Expr(match value.value() as u32 {
                        0..=255 => format!("'{}'", value.value().escape_default()),
                        other_code => format!(r"U'\U{:08X}'", other_code),
                    })),
                    syn::Lit::Int(ref value) => {
                        let suffix = match value.suffix() {
                            "u64" => "ull",
                            "i64" => "ll",
                            "u32" => "u",
                            _ if value.base10_parse::<i64>().is_err() => "ull",
                            _ => "",
                        };
                        Ok(Literal::Expr(format!(
                            "{}{}",
                            value.base10_digits(),
                            suffix
                        )))
                    }
                    syn::Lit::Float(ref value) => {
                        Ok(Literal::Expr(value.base10_digits().to_string()))
                    }
                    syn::Lit::Bool(ref value) => Ok(Literal::Expr(format!("{}", value.value))),
                    // TODO: Add support for byte string and Verbatim
                    _ => Err(format!("Unsupported literal expression. {:?}", *lit)),
                }
            }

            syn::Expr::Field(syn::ExprField {
                ref base,
                ref member,
                ..
            }) => Ok(Literal::FieldAccess {
                base: Box::new(Literal::load(base)?),
                field: member_to_ident(member),
            }),

            syn::Expr::Call(syn::ExprCall {
                ref func, ref args, ..
            }) => {
                let struct_name = match Literal::load(func)? {
                    Literal::Path {
                        associated_to: None,
                        name,
                    } => name,
                    _ => return Err(format!("Unsupported call expression. {:?}", *expr)),
                };
                let mut fields = HashMap::<String, Literal>::default();
                for (index, arg) in args.iter().enumerate() {
                    let ident =
                        member_to_ident(&syn::Member::Unnamed(syn::Index::from(index))).to_string();
                    let value = Literal::load(arg)?;
                    fields.insert(ident, value);
                }
                Ok(Literal::Struct {
                    path: Path::new(struct_name.clone()),
                    export_name: struct_name,
                    fields,
                })
            }

            syn::Expr::Struct(syn::ExprStruct {
                ref path,
                ref fields,
                ..
            }) => {
                let struct_name = path.segments[0].ident.unraw().to_string();
                let mut field_map = HashMap::<String, Literal>::default();
                for field in fields {
                    let ident = member_to_ident(&field.member).to_string();
                    let value = Literal::load(&field.expr)?;
                    field_map.insert(ident, value);
                }
                Ok(Literal::Struct {
                    path: Path::new(struct_name.clone()),
                    export_name: struct_name,
                    fields: field_map,
                })
            }

            syn::Expr::Unary(syn::ExprUnary {
                ref op, ref expr, ..
            }) => match *op {
                UnOp::Not(_) => {
                    let val = Self::load(expr)?;
                    Ok(Literal::PostfixUnaryOp {
                        op: "~",
                        value: Box::new(val),
                    })
                }
                UnOp::Neg(_) => {
                    let val = Self::load(expr)?;
                    Ok(Literal::PostfixUnaryOp {
                        op: "-",
                        value: Box::new(val),
                    })
                }
                _ => Err(format!("Unsupported Unary expression. {:?}", *op)),
            },

            // Match identifiers, like `5 << SHIFT`
            syn::Expr::Path(syn::ExprPath { ref path, .. }) => {
                // Handle only the simplest identifiers and Associated::IDENT
                // kind of syntax.
                Ok(match path.segments.len() {
                    1 => Literal::Path {
                        associated_to: None,
                        name: path.segments[0].ident.to_string(),
                    },
                    2 => {
                        let struct_name = path.segments[0].ident.to_string();
                        Literal::Path {
                            associated_to: Some((Path::new(&struct_name), struct_name)),
                            name: path.segments[1].ident.to_string(),
                        }
                    }
                    _ => return Err(format!("Unsupported path expression. {:?}", path)),
                })
            }

            syn::Expr::Paren(syn::ExprParen { ref expr, .. }) => Self::load(expr),

            syn::Expr::Cast(syn::ExprCast {
                ref expr, ref ty, ..
            }) => {
                let val = Self::load(expr)?;
                match Type::load(ty)? {
                    Some(ty) => Ok(Literal::Cast {
                        ty,
                        value: Box::new(val),
                    }),
                    None => Err("Cannot cast to zero sized type.".to_owned()),
                }
            }

            _ => Err(format!("Unsupported expression. {:?}", *expr)),
        }
    }

    pub(crate) fn write<F: Write>(&self, config: &Config, out: &mut SourceWriter<F>) {
        match self {
            Literal::Expr(v) => match (&**v, config.language) {
                ("true", Language::Cython) => write!(out, "True"),
                ("false", Language::Cython) => write!(out, "False"),
                (v, _) => write!(out, "{}", v),
            },
            Literal::Path {
                ref associated_to,
                ref name,
            } => {
                if let Some((ref path, ref export_name)) = associated_to {
                    if let Some(known) = to_known_assoc_constant(path, name) {
                        return write!(out, "{}", known);
                    }
                    let path_separator = match config.language {
                        Language::Cython | Language::C => "_",
                        Language::Cxx => {
                            if config.structure.associated_constants_in_body {
                                "::"
                            } else {
                                "_"
                            }
                        }
                    };
                    write!(out, "{}{}", export_name, path_separator)
                }
                write!(out, "{}", name)
            }
            Literal::FieldAccess {
                ref base,
                ref field,
            } => {
                write!(out, "(");
                base.write(config, out);
                write!(out, ").{}", field);
            }
            Literal::PostfixUnaryOp { op, ref value } => {
                write!(out, "{}", op);
                value.write(config, out);
            }
            Literal::BinOp {
                ref left,
                op,
                ref right,
            } => {
                write!(out, "(");
                left.write(config, out);
                write!(out, " {} ", op);
                right.write(config, out);
                write!(out, ")");
            }
            Literal::Cast { ref ty, ref value } => {
                out.write(if config.language == Language::Cython {
                    "<"
                } else {
                    "("
                });
                ty.write(config, out);
                out.write(if config.language == Language::Cython {
                    ">"
                } else {
                    ")"
                });
                value.write(config, out);
            }
            Literal::Struct {
                export_name,
                fields,
                path,
            } => {
                match config.language {
                    Language::C => write!(out, "({})", export_name),
                    Language::Cxx => write!(out, "{}", export_name),
                    Language::Cython => write!(out, "<{}>", export_name),
                }

                write!(out, "{{ ");
                let mut is_first_field = true;
                // In C++, same order as defined is required.
                let ordered_fields = out.bindings().struct_field_names(path);
                for ordered_key in ordered_fields.iter() {
                    if let Some(lit) = fields.get(ordered_key) {
                        if !is_first_field {
                            write!(out, ", ");
                        } else {
                            is_first_field = false;
                        }
                        match config.language {
                            Language::Cxx => write!(out, "/* .{} = */ ", ordered_key),
                            Language::C => write!(out, ".{} = ", ordered_key),
                            Language::Cython => {}
                        }
                        lit.write(config, out);
                    }
                }
                write!(out, " }}");
            }
        }
    }
}

#[derive(Debug, Clone)]
pub struct Constant {
    pub path: Path,
    pub export_name: String,
    pub ty: Type,
    pub value: Literal,
    pub cfg: Option<Cfg>,
    pub annotations: AnnotationSet,
    pub documentation: Documentation,
    pub associated_to: Option<Path>,
}

impl Constant {
    pub fn load(
        path: Path,
        mod_cfg: Option<&Cfg>,
        ty: &syn::Type,
        expr: &syn::Expr,
        attrs: &[syn::Attribute],
        associated_to: Option<Path>,
    ) -> Result<Constant, String> {
        let ty = Type::load(ty)?;
        let mut ty = match ty {
            Some(ty) => ty,
            None => {
                return Err("Cannot have a zero sized const definition.".to_owned());
            }
        };

        let mut lit = Literal::load(expr)?;

        if let Some(ref associated_to) = associated_to {
            ty.replace_self_with(associated_to);
            lit.replace_self_with(associated_to);
        }

        Ok(Constant::new(
            path,
            ty,
            lit,
            Cfg::append(mod_cfg, Cfg::load(attrs)),
            AnnotationSet::load(attrs)?,
            Documentation::load(attrs),
            associated_to,
        ))
    }

    pub fn new(
        path: Path,
        ty: Type,
        value: Literal,
        cfg: Option<Cfg>,
        annotations: AnnotationSet,
        documentation: Documentation,
        associated_to: Option<Path>,
    ) -> Self {
        let export_name = path.name().to_owned();
        Self {
            path,
            export_name,
            ty,
            value,
            cfg,
            annotations,
            documentation,
            associated_to,
        }
    }

    pub fn uses_only_primitive_types(&self) -> bool {
        self.value.uses_only_primitive_types() && self.ty.is_primitive_or_ptr_primitive()
    }
}

impl Item for Constant {
    fn path(&self) -> &Path {
        &self.path
    }

    fn add_dependencies(&self, library: &Library, out: &mut Dependencies) {
        self.ty.add_dependencies(library, out);
    }

    fn export_name(&self) -> &str {
        &self.export_name
    }

    fn cfg(&self) -> Option<&Cfg> {
        self.cfg.as_ref()
    }

    fn annotations(&self) -> &AnnotationSet {
        &self.annotations
    }

    fn annotations_mut(&mut self) -> &mut AnnotationSet {
        &mut self.annotations
    }

    fn container(&self) -> ItemContainer {
        ItemContainer::Constant(self.clone())
    }

    fn rename_for_config(&mut self, config: &Config) {
        if self.associated_to.is_none() {
            config.export.rename(&mut self.export_name);
        }
        self.value.rename_for_config(config);
        self.ty.rename_for_config(config, &GenericParams::default()); // FIXME: should probably propagate something here
    }

    fn resolve_declaration_types(&mut self, resolver: &DeclarationTypeResolver) {
        self.ty.resolve_declaration_types(resolver);
    }
}

impl Constant {
    pub fn write_declaration<F: Write>(
        &self,
        config: &Config,
        out: &mut SourceWriter<F>,
        associated_to_struct: &Struct,
    ) {
        debug_assert!(self.associated_to.is_some());
        debug_assert!(config.language == Language::Cxx);
        debug_assert!(!associated_to_struct.is_transparent);
        debug_assert!(config.structure.associated_constants_in_body);
        debug_assert!(config.constant.allow_static_const);

        if let Type::Ptr { is_const: true, .. } = self.ty {
            out.write("static ");
        } else {
            out.write("static const ");
        }
        self.ty.write(config, out);
        write!(out, " {};", self.export_name())
    }

    pub fn write<F: Write>(
        &self,
        config: &Config,
        out: &mut SourceWriter<F>,
        associated_to_struct: Option<&Struct>,
    ) {
        if let Some(assoc) = associated_to_struct {
            if assoc.is_generic() {
                return; // Not tested / implemented yet, so bail out.
            }
        }

        if !self.value.is_valid(out.bindings()) {
            return;
        }

        let associated_to_transparent = associated_to_struct.map_or(false, |s| s.is_transparent);

        let in_body = associated_to_struct.is_some()
            && config.language == Language::Cxx
            && config.structure.associated_constants_in_body
            && config.constant.allow_static_const
            && !associated_to_transparent;

        let condition = self.cfg.to_condition(config);
        condition.write_before(config, out);

        let name = if in_body {
            Cow::Owned(format!(
                "{}::{}",
                associated_to_struct.unwrap().export_name(),
                self.export_name(),
            ))
        } else if self.associated_to.is_none() {
            Cow::Borrowed(self.export_name())
        } else {
            let associated_name = match associated_to_struct {
                Some(s) => Cow::Borrowed(s.export_name()),
                None => {
                    let mut name = self.associated_to.as_ref().unwrap().name().to_owned();
                    config.export.rename(&mut name);
                    Cow::Owned(name)
                }
            };

            Cow::Owned(format!("{}_{}", associated_name, self.export_name()))
        };

        let value = match self.value {
            Literal::Struct {
                ref fields,
                ref path,
                ..
            } if out.bindings().struct_is_transparent(path) => fields.iter().next().unwrap().1,
            _ => &self.value,
        };

        self.documentation.write(config, out);

        let allow_constexpr = config.constant.allow_constexpr && self.value.can_be_constexpr();
        match config.language {
            Language::Cxx if config.constant.allow_static_const || allow_constexpr => {
                if allow_constexpr {
                    out.write("constexpr ")
                }

                if config.constant.allow_static_const {
                    out.write(if in_body { "inline " } else { "static " });
                }

                if let Type::Ptr { is_const: true, .. } = self.ty {
                    // Nothing.
                } else {
                    out.write("const ");
                }

                self.ty.write(config, out);
                write!(out, " {} = ", name);
                value.write(config, out);
                write!(out, ";");
            }
            Language::Cxx | Language::C => {
                write!(out, "#define {} ", name);
                value.write(config, out);
            }
            Language::Cython => {
                out.write("const ");
                self.ty.write(config, out);
                // For extern Cython declarations the initializer is ignored,
                // but still useful as documentation, so we write it as a comment.
                write!(out, " {} # = ", name);
                value.write(config, out);
            }
        }

        condition.write_after(config, out);
    }
}
