#![allow(clippy::trivial_regex)]
#![allow(clippy::useless_format)]

extern crate gl_generator;

use std::env::var;
use std::fs::File;
use std::io::{Error as IoError, Write};
use std::path::Path;

use gl_generator::{
    generators::{gen_struct_name, gen_symbol_name, gen_types},
    Api, Cmd, Fallbacks, Generator as GlGenerator, Profile, Registry,
};
use lazy_static::lazy_static;
use regex::{Captures, Regex};

fn main() {
    let out_dir = var("OUT_DIR").unwrap();
    let mut file = File::create(&Path::new(&out_dir).join("bindings.rs")).unwrap();

    let generator = Generator {
        generate_debug: var("CARGO_FEATURE_GENERATE_DEBUG").is_ok(),
        generate_global: var("CARGO_FEATURE_GENERATE_GLOBAL").is_ok(),
        generate_struct: var("CARGO_FEATURE_GENERATE_STRUCT").is_ok(),
        log_fn: if var("CARGO_FEATURE_USE_LOG_CRATE").is_ok() {
            "log::trace!"
        } else {
            "println!"
        },
    };

    Registry::new(Api::Gl, (4, 5), Profile::Compatibility, Fallbacks::All, [])
        .write_bindings(generator, &mut file)
        .unwrap();
}

pub struct Generator {
    generate_debug: bool,
    generate_global: bool,
    generate_struct: bool,
    log_fn: &'static str,
}

impl GlGenerator for Generator {
    fn write<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        self.write_header(dest)?;
        self.write_type_aliases(registry, dest)?;
        self.write_enums(registry, dest)?;
        self.write_fnptr_struct_def(registry, dest)?;

        if self.generate_global {
            self.write_load_fn(registry, dest)?;
            self.write_fns(registry, dest)?;
            self.write_ptrs(registry, dest)?;
            self.write_fn_mods(registry, dest)?;
        }

        if self.generate_struct {
            self.write_struct_def(registry, dest)?;
            self.write_struct_impl(registry, dest)?;
        }

        Ok(())
    }
}

impl Generator {
    fn write_header<W>(&self, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        writeln!(
            dest,
            r#"
            mod __gl_imports {{
                pub use std::os::raw;
                pub use std::mem::transmute;
            }}"#
        )?;

        Ok(())
    }

    fn write_type_aliases<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        writeln!(
            dest,
            r#"
            pub mod types {{"#
        )?;

        gen_types(registry.api, dest)?;

        writeln!(
            dest,
            r#"
            }}"#
        )?;

        Ok(())
    }

    fn write_enums<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        for enm in &registry.enums {
            writeln!(
                dest,
                r#"pub const {ident}: {types_prefix}{ty} = {value}{cast_suffix};"#,
                ident = enm.ident,
                types_prefix = if enm.ty == "&'static str" {
                    ""
                } else {
                    "types::"
                },
                ty = enm.ty,
                value = enm.value,
                cast_suffix = match enm.cast {
                    true => format!(" as types::{}", enm.ty),
                    false => String::new(),
                },
            )?;
        }

        Ok(())
    }

    fn write_fnptr_struct_def<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        writeln!(
            dest,
            r#"
            #[derive(Clone)]
            pub struct FnPtr {{
                f: *const std::os::raw::c_void,
                is_loaded: bool,
            }}

            impl FnPtr {{
                fn new(ptr: *const std::os::raw::c_void) -> FnPtr {{
                    if ptr.is_null() {{
                        FnPtr {{
                            f: missing_fn_panic as *const std::os::raw::c_void,
                            is_loaded: false
                        }}
                    }} else {{
                        FnPtr {{ f: ptr, is_loaded: true }}
                    }}
                }}

                #[inline]
                pub fn is_loaded(&self) -> bool {{
                    self.is_loaded
                }}
            }}

            #[inline(never)]
            fn missing_fn_panic() -> ! {{
                panic!("{api} function was not loaded")
            }}

            #[inline(never)]
            fn metaloadfn(
                loadfn: &mut dyn FnMut(&'static str) -> *const std::os::raw::c_void,
                symbol: &'static str,
                fallbacks: &[&'static str]) -> *const std::os::raw::c_void
            {{
                let mut ptr = loadfn(symbol);

                if ptr.is_null() {{
                    for &sym in fallbacks {{
                        ptr = loadfn(sym);
                        if !ptr.is_null() {{ break; }}
                    }}
                }}

                ptr
            }}"#,
            api = registry.api,
        )
    }

    fn write_load_fn<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        write!(
            dest,
            r#"
            pub fn load_with<F>(mut loadfn: F) where F: FnMut(&'static str) -> *const __gl_imports::raw::c_void {{
                #[inline(never)]
                fn inner(loadfn: &mut dyn FnMut(&'static str) -> *const __gl_imports::raw::c_void) {{"#
        )?;

        for c in &registry.cmds {
            write!(
                dest,
                r#"
                    {cmd_name}::load_with(&mut *loadfn);"#,
                cmd_name = to_snake_case(&c.proto.ident),
            )?;
        }

        writeln!(
            dest,
            r#"
                }}

                inner(&mut loadfn)
            }}"#
        )?;

        Ok(())
    }

    fn write_fns<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        let has_get_error = registry
            .cmds
            .iter()
            .any(|cmd| cmd.proto.ident == "GetError");

        for cmd in &registry.cmds {
            self.gen_func(dest, cmd, has_get_error, true)?;
        }

        Ok(())
    }

    fn write_ptrs<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        writeln!(
            dest,
            r#"
            mod storage {{
                use super::__gl_imports::raw;
                use super::FnPtr;"#
        )?;

        for c in &registry.cmds {
            writeln!(
                dest,
                r#"
                pub static mut {name}: FnPtr = FnPtr {{
                    f: super::missing_fn_panic as *const raw::c_void,
                    is_loaded: false
                }};"#,
                name = to_snake_case(&c.proto.ident)
            )?;
        }

        writeln!(
            dest,
            r#"
            }}"#
        )?;

        Ok(())
    }

    fn write_fn_mods<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        for c in &registry.cmds {
            let fallbacks = match registry.aliases.get(&c.proto.ident) {
                Some(v) => {
                    let names = v
                        .iter()
                        .map(|name| format!("\"{}\"", gen_symbol_name(registry.api, &name)))
                        .collect::<Vec<_>>();
                    format!("&[{}]", names.join(", "))
                }
                None => "&[]".to_string(),
            };
            let fnname = to_snake_case(&c.proto.ident);
            let symbol = gen_symbol_name(registry.api, &c.proto.ident);
            let symbol = &symbol;

            writeln!(
                dest,
                r##"
                pub mod {fnname} {{
                    use super::{{storage, metaloadfn}};
                    use super::__gl_imports::raw;
                    use super::FnPtr;

                    #[inline]
                    pub fn is_loaded() -> bool {{
                        unsafe {{ storage::{fnname}.is_loaded }}
                    }}

                    pub fn load_with<F>(mut loadfn: F) where F: FnMut(&'static str) -> *const raw::c_void {{
                        unsafe {{
                            storage::{fnname} = FnPtr::new(metaloadfn(&mut loadfn, "{symbol}", {fallbacks}))
                        }}
                    }}
                }}"##,
                fnname = fnname,
                fallbacks = fallbacks,
                symbol = symbol
            )?;
        }

        Ok(())
    }

    fn write_struct_def<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        writeln!(
            dest,
            r#"
            #[derive(Clone)]
            pub struct {api} {{"#,
            api = gen_struct_name(registry.api)
        )?;

        for cmd in &registry.cmds {
            writeln!(
                dest,
                r#"                pub {name}: FnPtr,"#,
                name = to_snake_case(&cmd.proto.ident)
            )?;
        }

        writeln!(dest, r#"            }}"#)?;

        Ok(())
    }

    fn write_struct_impl<W>(&self, registry: &Registry, dest: &mut W) -> Result<(), IoError>
    where
        W: Write,
    {
        write!(
            dest,
            r#"
            impl {api} {{
                pub fn load_with<F>(mut loadfn: F) -> {api} where F: FnMut(&'static str) -> *const std::os::raw::c_void {{
                    let mut do_loadfn = |symbol: &'static str, symbols: &[&'static str]| {{
                        metaloadfn(&mut loadfn, symbol, symbols)
                    }};

                    {api} {{"#,
            api = gen_struct_name(registry.api)
        )?;

        for cmd in &registry.cmds {
            write!(
                dest,
                r#"
                       {name}: FnPtr::new(do_loadfn("{symbol}", &[{fallbacks}])),"#,
                name = to_snake_case(&cmd.proto.ident),
                symbol = gen_symbol_name(registry.api, &cmd.proto.ident),
                fallbacks = match registry.aliases.get(&cmd.proto.ident) {
                    Some(fbs) => fbs
                        .iter()
                        .map(|name| format!(r#""{}""#, gen_symbol_name(registry.api, &name)))
                        .collect::<Vec<_>>()
                        .join(", "),
                    None => format!(""),
                },
            )?
        }

        writeln!(
            dest,
            r#"
                    }}
                }}"#
        )?;

        let has_get_error = registry
            .cmds
            .iter()
            .any(|cmd| cmd.proto.ident == "GetError");

        for cmd in &registry.cmds {
            self.gen_func(dest, cmd, has_get_error, false)?;
        }

        writeln!(
            dest,
            r#"
            }}

            unsafe impl std::marker::Send for {api} {{}}"#,
            api = gen_struct_name(registry.api)
        )
    }

    fn gen_func<W>(
        &self,
        dest: &mut W,
        cmd: &Cmd,
        has_get_error: bool,
        is_global: bool,
    ) -> Result<(), IoError>
    where
        W: Write,
    {
        let ident = to_snake_case(&cmd.proto.ident);
        let idents = gen_parameters(cmd, true, false);
        let typed_params = gen_parameters(cmd, false, true);
        let mut print_ln = String::new();
        let mut print_err = String::new();

        if self.generate_debug {
            print_ln = format!(
                r#"
                    {log_fn}("[OpenGL] {}({})" {});
                    "#,
                ident,
                (0..idents.len())
                    .map(|_| "{:?}".to_string())
                    .collect::<Vec<_>>()
                    .join(", "),
                idents
                    .iter()
                    .zip(typed_params.iter())
                    .map(|(name, ty)| if ty.contains("GLDEBUGPROC") {
                        format!(r#", "<callback>""#)
                    } else {
                        format!(r#", {}"#, name)
                    })
                    .collect::<Vec<_>>()
                    .concat(),
                log_fn = self.log_fn,
            );

            if cmd.proto.ident != "GetError" && has_get_error {
                print_err = format!(
                    r#"
                    let __get_err = __gl_imports::transmute::<_, extern "system" fn() -> u32>({this}get_error.f);
                    match __get_err() {{
                        0 => (),
                        r => {log_fn}("[OpenGL] ^ GL error triggered: {{}}", r),
                    }}
                    "#,
                    this = if is_global { "storage::" } else { "self." },
                    log_fn = self.log_fn,
                );
            }
        }

        write!(
            dest,
            r#"
            #[inline]
            pub fn {name}({this_ty}{params}) -> {return_suffix} {{
                unsafe {{{print_ln}
                    let __f = __gl_imports::transmute::<_, extern "system" fn({typed_params}) -> {return_suffix}>({this}{name}.f);
                    let __r = __f({idents});
                    {print_err}
                    __r
                }}
            }}"#,
            this_ty = if is_global { "" } else { "&self, " },
            this = if is_global { "storage::" } else { "self." },
            name = ident,
            params = gen_parameters(cmd, true, true).join(", "),
            typed_params = typed_params.join(", "),
            return_suffix = cmd.proto.ty,
            idents = idents.join(", "),
            print_ln = print_ln,
            print_err = print_err,
        )
    }
}

pub fn gen_parameters(cmd: &Cmd, with_idents: bool, with_types: bool) -> Vec<String> {
    cmd.params
        .iter()
        .map(|binding| {
            if with_idents && with_types {
                format!("{}: {}", to_snake_case(&binding.ident), binding.ty)
            } else if with_types {
                format!("{}", binding.ty)
            } else if with_idents {
                format!("{}", to_snake_case(&binding.ident))
            } else {
                panic!()
            }
        })
        .collect()
}

fn to_snake_case(s: &str) -> String {
    lazy_static! {
        static ref RX01: Regex = Regex::new(r"([a-z])([A-Z])").unwrap();
        static ref RX02: Regex = Regex::new(r"([a-z])([0-9][0-9A-Za-z]+)$").unwrap();
        static ref RX03: Regex = Regex::new(r"([A-Z])([A-Z][a-z]+)").unwrap();
        static ref RX04: Regex = Regex::new(r"([A-Z])([0-9])").unwrap();
        static ref RX05: Regex = Regex::new(r"([A-Z][a-z]*)(uiv|usv)$").unwrap();
        static ref RX06: Regex = Regex::new(r"([A-Z][a-z]*)(dv|fv|iv)$").unwrap();
        static ref RX07: Regex = Regex::new(r"([A-Z][a-z]*)(v|f|i)$").unwrap();
        static ref RX08: Regex = Regex::new(r"i_64").unwrap();
        static ref RX09: Regex = Regex::new(r"u_i64").unwrap();
        static ref RX10: Regex = Regex::new(r"f_i").unwrap();
        static ref RX11: Regex = Regex::new(r"([a-z]{3,})i$").unwrap();
        static ref RX12: Regex = Regex::new(r"([a-z])i_v").unwrap();
        static ref RX13: Regex = Regex::new(r"([a-z])i64_v").unwrap();
        static ref RX14: Regex = Regex::new(r"[Ii]nteger_64").unwrap();
    }

    let s = RX01.replace_all(s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX02.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX03.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX04.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX05.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX06.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX07.replace_all(&s, |c: &Captures| format!("{}_{}", &c[1], &c[2]));
    let s = RX08.replace_all(&s, "_i64");
    let s = RX09.replace_all(&s, "_ui64");
    let s = RX10.replace_all(&s, "_fi");
    let s = RX11.replace_all(&s, |c: &Captures| format!("{}_i", &c[1]));
    let s = RX12.replace_all(&s, |c: &Captures| format!("{}_i_v", &c[1]));
    let s = RX13.replace_all(&s, |c: &Captures| format!("{}_i64_v", &c[1]));
    let s = RX14.replace_all(&s, "Integer64");

    s.into_owned().to_lowercase()
}
