use std::{
    env,
    fs::{self, File},
    io::{BufRead, BufReader, Result},
    path::Path,
};

use quote::{format_ident, quote};

fn main() {
    let out_dir = env::var_os("OUT_DIR").unwrap();
    gen_linux_errno(&Path::new(&out_dir).join("linux_errno.rs")).unwrap();
}

fn gen_linux_errno(dest_path: &Path) -> Result<()> {
    let mut variants = Vec::new();
    let mut try_from_arms = Vec::new();
    let mut as_str_arms = Vec::new();

    let file = File::open("src/errno.h")?;
    for line in BufReader::new(file).lines().map_while(Result::ok) {
        if line.starts_with("#define") {
            let mut iter = line.split_whitespace();
            if let Some(name) = iter.nth(1)
                && let Some(num) = iter.next()
            {
                let ident = format_ident!("{name}");
                let code = num.parse::<i32>().expect("invalid errno value");
                let description = if let Some(pos) = line.find("/* ") {
                    String::from(line[pos + 3..].trim_end_matches(" */"))
                } else {
                    format!("Error number {num}")
                };

                variants.push(quote! {
                    #[doc = #description]
                    #ident = #code,
                });
                try_from_arms.push(quote! {
                    #code => Ok(#ident),
                });
                as_str_arms.push(quote! {
                    #ident => #description,
                });
            }
        }
    }

    let tokens = quote! {
        // Generated by build.rs, DO NOT edit

        /// Linux specific error codes defined in `errno.h`.
        #[repr(i32)]
        #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
        pub enum LinuxError {
            #(#variants)*
        }

        impl TryFrom<i32> for LinuxError {
            type Error = i32;

            fn try_from(value: i32) -> Result<Self, Self::Error> {
                use self::LinuxError::*;
                match value {
                    #(#try_from_arms)*
                    _ => Err(value),
                }
            }
        }

        impl LinuxError {
            /// Returns the error description.
            pub const fn as_str(&self) -> &'static str {
                use self::LinuxError::*;
                match self {
                    #(#as_str_arms)*
                }
            }

            /// Returns the error code value in `i32`.
            pub const fn code(self) -> i32 {
                self as i32
            }
        }
    };

    fs::write(dest_path, tokens.to_string())?;

    Ok(())
}
