// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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
//
//   http://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::layout::{self, AlignReq};
use proc_macro2::{Ident, Punct, Span};
use quote::quote;
use std::alloc::Layout;
use syn::parse::{Parse, ParseStream, Result};
use syn::{
    parse_quote, punctuated::Punctuated, DeriveInput, Error, Expr, Fields, Lit, LitInt, Meta,
    Token, Type,
};

#[allow(dead_code)]
struct KeyValue {
    pub ident: Ident,
    pub punct: Punct,
    pub value: Expr,
}

impl Parse for KeyValue {
    fn parse(input: ParseStream) -> Result<Self> {
        let ident = input.parse::<Ident>()?;
        let punct = input.parse::<Punct>()?;
        let value = input.parse::<Expr>()?;
        Ok(KeyValue {
            ident,
            punct,
            value,
        })
    }
}

pub struct AlignArgs {
    vars: Vec<KeyValue>,
}

impl Parse for AlignArgs {
    fn parse(input: ParseStream) -> Result<Self> {
        let keyvalue = Punctuated::<KeyValue, Token![,]>::parse_terminated(input)?;
        Ok(AlignArgs {
            vars: keyvalue.into_iter().collect(),
        })
    }
}

impl AlignArgs {
    pub fn get_layout(&self) -> Result<Layout> {
        let align_iter = self.vars.iter().find(|v| v.ident == "align");
        let align: usize = if let Some(align_value) = align_iter {
            self.parse_align(&align_value.value)?
        } else {
            return Err(Error::new(
                Span::call_site(),
                "Invalid type of align attribute parsing",
            ));
        };

        let size_iter = self.vars.iter().find(|v| v.ident == "size");
        let size: usize = if let Some(size_value) = size_iter {
            self.parse_size(&size_value.value)?
        } else {
            return Err(Error::new(
                Span::call_site(),
                "Invalid type of size attribute parsing",
            ));
        };
        Layout::from_size_align(size, align)
            .map_err(|_e| Error::new(Span::call_site(), "Layout illegal"))
    }

    fn parse_align(&self, align_expr: &Expr) -> Result<usize> {
        if let Expr::Lit(expr) = align_expr {
            if let Lit::Int(ref lit) = expr.lit {
                let align = lit.base10_parse::<u32>()?;
                Ok(align as usize)
            } else {
                Err(Error::new(
                    Span::call_site(),
                    "Invalid align attribute parsing",
                ))
            }
        } else {
            Err(Error::new(
                Span::call_site(),
                "Invalid align attribute parsing",
            ))
        }
    }

    fn parse_size(&self, size_expr: &Expr) -> Result<usize> {
        if let Expr::Lit(expr) = size_expr {
            if let Lit::Int(ref lit) = expr.lit {
                let size = lit.base10_parse::<u32>()?;
                Ok(size as usize)
            } else {
                Err(Error::new(
                    Span::call_site(),
                    "Invalid size attribute parsing",
                ))
            }
        } else {
            Err(Error::new(
                Span::call_site(),
                "Invalid size attribute parsing",
            ))
        }
    }
}

pub struct AlignStruct {
    input: DeriveInput,
    layout: Layout,
    align_layout: Layout,
}

struct FiledExt<'a> {
    ty: &'a Type,
    name: Option<&'a Ident>,
}

impl<'a> FiledExt<'a> {
    pub fn new(ty: &'a Type, name: Option<&'a Ident>) -> Self {
        FiledExt { ty, name }
    }

    pub fn as_origin_named_field(&self) -> proc_macro2::TokenStream {
        let ty = &self.ty;
        let name = self.name.as_ref().unwrap();
        quote! {
            #name: #ty,
        }
    }
}

impl AlignStruct {
    pub fn new(layout: Layout, input: DeriveInput) -> Self {
        AlignStruct {
            input,
            layout,
            align_layout: unsafe { Layout::from_size_align_unchecked(0, 0) },
        }
    }

    pub fn build(&mut self) -> proc_macro2::TokenStream {
        if !(self.is_contains_specified_attr("C")
            && !self.is_contains_specified_attr("align")
            && !self.is_contains_specified_attr("packed"))
        {
            panic!("Structure attribute must require repr C ");
        }

        let pad_item = self.get_align_pad_field();
        let attrs = self.get_attrs();
        let vis = &self.input.vis;
        let name = &self.input.ident;
        let generics = &self.input.generics;
        let fields = &self.get_origin_fields();
        let align_attr = &self.generate_align_attr();
        quote! {
            #attrs
            #align_attr
            #vis struct #name #generics {
            #pad_item,
            #fields
            }
        }
    }

    #[allow(clippy::filter_next, clippy::collapsible_match)]
    fn is_contains_specified_attr(&self, atrr: &str) -> bool {
        let mut erxcept_attr = false;
        for attr in &self.input.attrs {
            let ident = attr.path.get_ident();
            let meta = attr.parse_meta();
            if ident.map_or(false, |v| *v == "repr") && meta.is_ok() {
                if let Ok(Meta::List(ref m)) = meta {
                    if !m.nested.is_empty() {
                        erxcept_attr = m
                            .nested
                            .iter()
                            .filter(|x| {
                                let mut find = false;
                                if let syn::NestedMeta::Meta(ref s) = x {
                                    if let syn::Meta::Path(p) = s {
                                        find = p.get_ident().map_or(false, |v| *v == atrr);
                                    }
                                }
                                find
                            })
                            .next()
                            .is_some();
                    }
                }
            }
        }
        erxcept_attr
    }

    fn get_attrs(&self) -> proc_macro2::TokenStream {
        let attrs = &self.input.attrs;
        quote! {
            #(#attrs)*
        }
    }

    fn generate_align_attr(&self) -> proc_macro2::TokenStream {
        let align = self.align_layout.align();
        let litint = LitInt::new(&format!("{}", align), Span::call_site());
        quote! {
           #[repr(align(#litint))]
        }
    }

    fn get_origin_fields(&self) -> proc_macro2::TokenStream {
        if let syn::Data::Struct(ref data) = self.input.data {
            if let Fields::Named(ref fields) = data.fields {
                let fields: Vec<_> = fields
                    .named
                    .iter()
                    .map(|f| FiledExt::new(&f.ty, f.ident.as_ref()))
                    .collect();
                let item = fields.iter().map(|x| x.as_origin_named_field());
                quote! {
                    #(#item)*
                }
            } else {
                panic!("Structure fields must have names");
            }
        } else {
            panic!("Only supports struct type");
        }
    }

    fn get_align_pad_field(&mut self) -> proc_macro2::TokenStream {
        let align_req: &[AlignReq] = &[AlignReq {
            offset: 0,
            len: self.layout.size(),
        }];
        let align_layout =
            layout::pad_align_to(self.layout, align_req).expect("Align layout illegal");
        self.align_layout = align_layout;
        let pad = align_layout.size() - align_layout.align() - self.layout.size();
        let ty: syn::Type = parse_quote!([u8; #pad]);
        let name = Ident::new("no_secret_allowed_in_here", Span::call_site());
        quote! {
            #name: #ty
        }
    }
}
