// Copyright (c) 2021 The Vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.

use super::{write_file, IndexMap, VkRegistryData};
use ahash::HashMap;
use heck::ToSnakeCase;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use regex::Regex;
use std::{collections::hash_map::Entry, fmt::Write as _};
use vk_parse::{Extension, Type, TypeMember, TypeMemberMarkup, TypeSpec};

pub fn write(vk_data: &VkRegistryData) {
    let properties_output = properties_output(&properties_members(&vk_data.types));
    let properties_ffi_output =
        properties_ffi_output(&properties_ffi_members(&vk_data.types, &vk_data.extensions));
    write_file(
        "properties.rs",
        format!(
            "vk.xml header version {}.{}.{}",
            vk_data.header_version.0, vk_data.header_version.1, vk_data.header_version.2
        ),
        quote! {
            #properties_output
            #properties_ffi_output
        },
    );
}

#[derive(Clone, Debug)]
struct PropertiesMember {
    name: Ident,
    ty: TokenStream,
    doc: String,
    raw: String,
    ffi_name: Ident,
    ffi_members: Vec<(Ident, TokenStream)>,
    optional: bool,
}

fn properties_output(members: &[PropertiesMember]) -> TokenStream {
    let struct_items = members.iter().map(
        |PropertiesMember {
             name,
             ty,
             doc,
             optional,
             ..
         }| {
            if *optional {
                quote! {
                    #[doc = #doc]
                    pub #name: Option<#ty>,
                }
            } else {
                quote! {
                    #[doc = #doc]
                    pub #name: #ty,
                }
            }
        },
    );

    let default_items = members.iter().map(|PropertiesMember { name, .. }| {
        quote! {
            #name: Default::default(),
        }
    });

    let from_items = members.iter().map(
        |PropertiesMember {
             name,
             ty,
             ffi_name,
             ffi_members,
             optional,
             ..
         }| {
            if *optional {
                let ffi_members = ffi_members.iter().map(|(ffi_member, ffi_member_field)| {
                    quote! { properties_ffi.#ffi_member.map(|s| s #ffi_member_field .#ffi_name) }
                });

                quote! {
                    #name: [
                        #(#ffi_members),*
                    ].into_iter().flatten().next().and_then(<#ty>::from_vulkan),
                }
            } else {
                let ffi_members = ffi_members.iter().map(|(ffi_member, ffi_member_field)| {
                    quote! { properties_ffi.#ffi_member #ffi_member_field .#ffi_name }
                });

                quote! {
                    #name: [
                        #(#ffi_members),*
                    ].into_iter().next().and_then(<#ty>::from_vulkan).unwrap(),
                }
            }
        },
    );

    quote! {
        /// Represents all the properties of a physical device.
        ///
        /// Depending on the highest version of Vulkan supported by the physical device, and the
        /// available extensions, not every property may be available. For that reason, some
        /// properties are wrapped in an `Option`.
        #[derive(Clone, Debug)]
        pub struct Properties {
            #(#struct_items)*
            pub _ne: crate::NonExhaustive,
        }

        impl Default for Properties {
            fn default() -> Self {
                Properties {
                    #(#default_items)*
                    _ne: crate::NonExhaustive(()),
                }
            }
        }

        impl From<&PropertiesFfi> for Properties {
            fn from(properties_ffi: &PropertiesFfi) -> Self {
                Properties {
                    #(#from_items)*
                    _ne: crate::NonExhaustive(()),
                }
            }
        }
    }
}

fn properties_members(types: &HashMap<&str, (&Type, Vec<&str>)>) -> Vec<PropertiesMember> {
    let mut properties = HashMap::default();

    [
        &types["VkPhysicalDeviceProperties"],
        &types["VkPhysicalDeviceLimits"],
        &types["VkPhysicalDeviceSparseProperties"],
    ]
    .into_iter()
    .chain(sorted_structs(types).into_iter())
    .filter(|(ty, _)| {
        let name = ty.name.as_deref();
        name == Some("VkPhysicalDeviceProperties")
            || name == Some("VkPhysicalDeviceLimits")
            || name == Some("VkPhysicalDeviceSparseProperties")
            || ty.structextends.as_deref() == Some("VkPhysicalDeviceProperties2")
    })
    .for_each(|(ty, _)| {
        let vulkan_ty_name = ty.name.as_ref().unwrap();

        let (ty_name, optional) = if vulkan_ty_name == "VkPhysicalDeviceProperties" {
            (
                (format_ident!("properties_vulkan10"), quote! { .properties }),
                false,
            )
        } else if vulkan_ty_name == "VkPhysicalDeviceLimits" {
            (
                (
                    format_ident!("properties_vulkan10"),
                    quote! { .properties.limits },
                ),
                false,
            )
        } else if vulkan_ty_name == "VkPhysicalDeviceSparseProperties" {
            (
                (
                    format_ident!("properties_vulkan10"),
                    quote! { .properties.sparse_properties },
                ),
                false,
            )
        } else {
            (
                (format_ident!("{}", ffi_member(vulkan_ty_name)), quote! {}),
                true,
            )
        };

        members(ty)
            .into_iter()
            .for_each(|Member { name, ty, len }| {
                if ty == "VkPhysicalDeviceLimits" || ty == "VkPhysicalDeviceSparseProperties" {
                    return;
                }

                let vulkano_member = name.to_snake_case();
                let vulkano_ty = match name {
                    "apiVersion" => quote! { Version },
                    "bufferImageGranularity"
                    | "minStorageBufferOffsetAlignment"
                    | "minTexelBufferOffsetAlignment"
                    | "minUniformBufferOffsetAlignment"
                    | "nonCoherentAtomSize"
                    | "optimalBufferCopyOffsetAlignment"
                    | "optimalBufferCopyRowPitchAlignment"
                    | "robustStorageBufferAccessSizeAlignment"
                    | "robustUniformBufferAccessSizeAlignment"
                    | "storageTexelBufferOffsetAlignmentBytes"
                    | "uniformTexelBufferOffsetAlignmentBytes" => {
                        quote! { DeviceAlignment }
                    }
                    _ => vulkano_type(ty, len),
                };
                match properties.entry(vulkano_member.clone()) {
                    Entry::Vacant(entry) => {
                        let mut member = PropertiesMember {
                            name: format_ident!("{}", vulkano_member),
                            ty: vulkano_ty,
                            doc: String::new(),
                            raw: name.to_owned(),
                            ffi_name: format_ident!("{}", vulkano_member),
                            ffi_members: vec![ty_name.clone()],
                            optional,
                        };
                        make_doc(&mut member, vulkan_ty_name);
                        entry.insert(member);
                    }
                    Entry::Occupied(entry) => {
                        entry.into_mut().ffi_members.push(ty_name.clone());
                    }
                };
            });
    });

    let mut names: Vec<_> = properties
        .values()
        .map(|prop| prop.name.to_string())
        .collect();
    names.sort_unstable();
    names
        .into_iter()
        .map(|name| properties.remove(&name).unwrap())
        .collect()
}

fn make_doc(prop: &mut PropertiesMember, vulkan_ty_name: &str) {
    let writer = &mut prop.doc;
    write!(
        writer,
        "- [Vulkan documentation](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/{}.html#limits-{})",
        vulkan_ty_name,
        prop.raw
    )
    .unwrap();
}

#[derive(Clone, Debug)]
struct PropertiesFfiMember {
    name: Ident,
    ty: Ident,
    provided_by: Vec<TokenStream>,
    conflicts: Vec<Ident>,
}

fn properties_ffi_output(members: &[PropertiesFfiMember]) -> TokenStream {
    let struct_items = members.iter().map(|PropertiesFfiMember { name, ty, .. }| {
        quote! { #name: Option<ash::vk::#ty>, }
    });

    let make_chain_items = members.iter().map(
        |PropertiesFfiMember {
             name,
             provided_by,
             conflicts,
             ..
         }| {
            quote! {
                if [#(#provided_by),*].into_iter().any(|x| x) &&
                    [#(self.#conflicts.is_none()),*].into_iter().all(|x| x) {
                    self.#name = Some(Default::default());
                    let member = self.#name.as_mut().unwrap();
                    member.p_next = head.p_next;
                    head.p_next = member as *mut _ as _;
                }
            }
        },
    );

    quote! {
        #[derive(Default)]
        pub(crate) struct PropertiesFfi {
            properties_vulkan10: ash::vk::PhysicalDeviceProperties2KHR,
            #(#struct_items)*
        }

        impl PropertiesFfi {
            pub(crate) fn make_chain(
                &mut self,
                api_version: Version,
                device_extensions: &DeviceExtensions,
                instance_extensions: &InstanceExtensions,
            ) {
                self.properties_vulkan10 = Default::default();
                let head = &mut self.properties_vulkan10;
                #(#make_chain_items)*
            }

            pub(crate) fn head_as_mut(&mut self) -> &mut ash::vk::PhysicalDeviceProperties2KHR {
                &mut self.properties_vulkan10
            }
        }
    }
}

fn properties_ffi_members<'a>(
    types: &'a HashMap<&str, (&Type, Vec<&str>)>,
    extensions: &IndexMap<&'a str, &Extension>,
) -> Vec<PropertiesFfiMember> {
    let mut property_included_in: HashMap<&str, Vec<&str>> = HashMap::default();
    sorted_structs(types)
        .into_iter()
        .map(|(ty, provided_by)| {
            let ty_name = ty.name.as_ref().unwrap();
            let provided_by = provided_by
                .iter()
                .map(|provided_by| {
                    if let Some(version) = provided_by.strip_prefix("VK_VERSION_") {
                        let version = format_ident!("V{}", version);
                        quote! { api_version >= Version::#version }
                    } else {
                        let member = format_ident!(
                            "{}_extensions",
                            extensions[provided_by].ext_type.as_ref().unwrap().as_str()
                        );
                        let name = format_ident!(
                            "{}",
                            provided_by
                                .strip_prefix("VK_")
                                .unwrap()
                                .to_ascii_lowercase(),
                        );

                        quote! { #member.#name }
                    }
                })
                .collect();
            let mut conflicts = vec![];
            members(ty).into_iter().for_each(|Member { name, .. }| {
                match property_included_in.entry(name) {
                    Entry::Vacant(entry) => {
                        entry.insert(vec![ty_name]);
                    }
                    Entry::Occupied(entry) => {
                        let conflicters = entry.into_mut();
                        conflicters.iter().for_each(|conflicter| {
                            let conflicter = ffi_member(conflicter);
                            if !conflicts.contains(&conflicter) {
                                conflicts.push(conflicter);
                            }
                        });
                        conflicters.push(ty_name);
                    }
                }
            });

            PropertiesFfiMember {
                name: format_ident!("{}", ffi_member(ty_name)),
                ty: format_ident!("{}", ty_name.strip_prefix("Vk").unwrap()),
                provided_by,
                conflicts: conflicts
                    .into_iter()
                    .map(|s| format_ident!("{}", s))
                    .collect(),
            }
        })
        .collect()
}

fn sorted_structs<'a>(
    types: &'a HashMap<&str, (&'a Type, Vec<&'a str>)>,
) -> Vec<&'a (&'a Type, Vec<&'a str>)> {
    let mut structs: Vec<_> = types
        .values()
        .filter(|(ty, _)| ty.structextends.as_deref() == Some("VkPhysicalDeviceProperties2"))
        .collect();
    let regex = Regex::new(r"^VkPhysicalDeviceVulkan\d+Properties$").unwrap();
    structs.sort_unstable_by_key(|&(ty, provided_by)| {
        let name = ty.name.as_ref().unwrap();
        (
            !regex.is_match(name),
            if let Some(version) = provided_by
                .iter()
                .find_map(|s| s.strip_prefix("VK_VERSION_"))
            {
                let (major, minor) = version.split_once('_').unwrap();
                major.parse::<i32>().unwrap() << 22 | minor.parse::<i32>().unwrap() << 12
            } else if provided_by.iter().any(|s| s.starts_with("VK_KHR_")) {
                i32::MAX - 2
            } else if provided_by.iter().any(|s| s.starts_with("VK_EXT_")) {
                i32::MAX - 1
            } else {
                i32::MAX
            },
            name,
        )
    });

    structs
}

fn ffi_member(ty_name: &str) -> String {
    let ty_name = ty_name
        .strip_prefix("VkPhysicalDevice")
        .unwrap()
        .to_snake_case();
    let (base, suffix) = ty_name.rsplit_once("_properties").unwrap();
    format!("properties_{}{}", base, suffix)
}

struct Member<'a> {
    name: &'a str,
    ty: &'a str,
    len: Option<&'a str>,
}

fn members(ty: &Type) -> Vec<Member> {
    let regex = Regex::new(r"\[([A-Za-z0-9_]+)\]\s*$").unwrap();
    if let TypeSpec::Members(members) = &ty.spec {
        members
            .iter()
            .filter_map(|member| {
                if let TypeMember::Definition(def) = member {
                    let name = def.markup.iter().find_map(|markup| match markup {
                        TypeMemberMarkup::Name(name) => Some(name.as_str()),
                        _ => None,
                    });
                    let ty = def.markup.iter().find_map(|markup| match markup {
                        TypeMemberMarkup::Type(ty) => Some(ty.as_str()),
                        _ => None,
                    });
                    let len = def
                        .markup
                        .iter()
                        .find_map(|markup| match markup {
                            TypeMemberMarkup::Enum(len) => Some(len.as_str()),
                            _ => None,
                        })
                        .or_else(|| {
                            regex
                                .captures(&def.code)
                                .and_then(|cap| cap.get(1))
                                .map(|m| m.as_str())
                        });
                    if name != Some("sType") && name != Some("pNext") {
                        return name.map(|name| Member {
                            name,
                            ty: ty.unwrap(),
                            len,
                        });
                    }
                }
                None
            })
            .collect()
    } else {
        vec![]
    }
}

fn vulkano_type(ty: &str, len: Option<&str>) -> TokenStream {
    if let Some(len) = len {
        match ty {
            "char" => quote! { String },
            "uint8_t" if len == "VK_LUID_SIZE" => quote! { [u8; 8] },
            "uint8_t" if len == "VK_UUID_SIZE" => quote! { [u8; 16] },
            "uint32_t" if len == "2" => quote! { [u32; 2] },
            "uint32_t" if len == "3" => quote! { [u32; 3] },
            "float" if len == "2" => quote! { [f32; 2] },
            _ => unimplemented!("{}[{}]", ty, len),
        }
    } else {
        match ty {
            "float" => quote! { f32 },
            "int32_t" => quote! { i32 },
            "int64_t" => quote! { i64 },
            "size_t" => quote! { usize },
            "uint8_t" => quote! { u8 },
            "uint32_t" => quote! { u32 },
            "uint64_t" => quote! { u64 },
            "VkBool32" => quote! { bool },
            "VkConformanceVersion" => quote! { ConformanceVersion },
            "VkDeviceSize" => quote! { DeviceSize },
            "VkDriverId" => quote! { DriverId },
            "VkExtent2D" => quote! { [u32; 2] },
            "VkMemoryDecompressionMethodFlagsNV" => quote! { MemoryDecompressionMethods },
            "VkOpticalFlowGridSizeFlagsNV" => quote! { OpticalFlowGridSizes },
            "VkPhysicalDeviceType" => quote! { PhysicalDeviceType },
            "VkPipelineRobustnessBufferBehaviorEXT" => quote! { PipelineRobustnessBufferBehavior },
            "VkPipelineRobustnessImageBehaviorEXT" => quote! { PipelineRobustnessImageBehavior },
            "VkPointClippingBehavior" => quote! { PointClippingBehavior },
            "VkQueueFlags" => quote! { QueueFlags },
            "VkRayTracingInvocationReorderModeNV" => quote! { RayTracingInvocationReorderMode },
            "VkResolveModeFlags" => quote! { ResolveModes },
            "VkSampleCountFlags" => quote! { SampleCounts },
            "VkSampleCountFlagBits" => quote! { SampleCount },
            "VkShaderCorePropertiesFlagsAMD" => quote! { ShaderCoreProperties },
            "VkShaderFloatControlsIndependence" => quote! { ShaderFloatControlsIndependence },
            "VkShaderStageFlags" => quote! { ShaderStages },
            "VkSubgroupFeatureFlags" => quote! { SubgroupFeatures },
            _ => unimplemented!("{}", ty),
        }
    }
}
