use digest::DynDigest;

const TEST_TEXT: &str = "abcdefghijklmnopqrstuvwxyz";

struct GeneratedHasherImplMeta {
  crate_name: &'static str,
  hasher_type_name: &'static str,
  hasher_command: &'static str,
  hasher: Box<dyn DynDigest>,
}

#[cfg(any(
  feature = "ascon-hash",
  feature = "belt-hash",
  feature = "blake2",
  feature = "blake3",
  feature = "fsb",
  feature = "gost94",
  feature = "groestl",
  feature = "jh",
  feature = "md2",
  feature = "md4",
  feature = "ripemd",
  feature = "sha1",
  feature = "sha2",
  feature = "sha3",
  feature = "shabal",
  feature = "skein",
  feature = "sm3",
  feature = "streebog",
  feature = "tiger",
  feature = "whirlpool",
))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
  use std::{env, io::Write, path::Path};

  // MD5 and SHA256 are skipped on purpose
  let hasher_impls: Vec<GeneratedHasherImplMeta> = vec![
    #[cfg(feature = "ascon-hash")]
    GeneratedHasherImplMeta {
      crate_name: "ascon_hash",
      hasher_type_name: "AsconHash",
      hasher_command: "ascon",
      hasher: Box::new(ascon_hash::AsconHash::default()),
    },
    #[cfg(feature = "ascon-hash")]
    GeneratedHasherImplMeta {
      crate_name: "ascon_hash",
      hasher_type_name: "AsconAHash",
      hasher_command: "ascon-a",
      hasher: Box::new(ascon_hash::AsconAHash::default()),
    },
    #[cfg(feature = "belt-hash")]
    GeneratedHasherImplMeta {
      crate_name: "belt_hash",
      hasher_type_name: "BeltHash",
      hasher_command: "belt",
      hasher: Box::new(belt_hash::BeltHash::default()),
    },
    #[cfg(feature = "blake2")]
    GeneratedHasherImplMeta {
      crate_name: "blake2",
      hasher_type_name: "Blake2s256",
      hasher_command: "blake2s-256",
      hasher: Box::new(blake2::Blake2s256::default()),
    },
    #[cfg(feature = "blake2")]
    GeneratedHasherImplMeta {
      crate_name: "blake2",
      hasher_type_name: "Blake2b512",
      hasher_command: "blake2b-512",
      hasher: Box::new(blake2::Blake2b512::default()),
    },
    #[cfg(feature = "blake3")]
    GeneratedHasherImplMeta {
      crate_name: "blake3",
      hasher_type_name: "Hasher",
      hasher_command: "blake3",
      hasher: Box::new(blake3::Hasher::new()),
    },
    #[cfg(feature = "fsb")]
    GeneratedHasherImplMeta {
      crate_name: "fsb",
      hasher_type_name: "Fsb160",
      hasher_command: "fsb160",
      hasher: Box::new(fsb::Fsb160::default()),
    },
    #[cfg(feature = "fsb")]
    GeneratedHasherImplMeta {
      crate_name: "fsb",
      hasher_type_name: "Fsb224",
      hasher_command: "fsb224",
      hasher: Box::new(fsb::Fsb224::default()),
    },
    #[cfg(feature = "fsb")]
    GeneratedHasherImplMeta {
      crate_name: "fsb",
      hasher_type_name: "Fsb256",
      hasher_command: "fsb256",
      hasher: Box::new(fsb::Fsb256::default()),
    },
    #[cfg(feature = "fsb")]
    GeneratedHasherImplMeta {
      crate_name: "fsb",
      hasher_type_name: "Fsb384",
      hasher_command: "fsb384",
      hasher: Box::new(fsb::Fsb384::default()),
    },
    #[cfg(feature = "fsb")]
    GeneratedHasherImplMeta {
      crate_name: "fsb",
      hasher_type_name: "Fsb512",
      hasher_command: "fsb512",
      hasher: Box::new(fsb::Fsb512::default()),
    },
    #[cfg(feature = "gost94")]
    GeneratedHasherImplMeta {
      crate_name: "gost94",
      hasher_type_name: "Gost94CryptoPro",
      hasher_command: "gost94-crypto-pro",
      hasher: Box::new(gost94::Gost94CryptoPro::default()),
    },
    #[cfg(feature = "gost94")]
    GeneratedHasherImplMeta {
      crate_name: "gost94",
      hasher_type_name: "Gost94UA",
      hasher_command: "gost94-ua",
      hasher: Box::new(gost94::Gost94UA::default()),
    },
    #[cfg(feature = "gost94")]
    GeneratedHasherImplMeta {
      crate_name: "gost94",
      hasher_type_name: "Gost94s2015",
      hasher_command: "gost94-2015",
      hasher: Box::new(gost94::Gost94s2015::default()),
    },
    #[cfg(feature = "groestl")]
    GeneratedHasherImplMeta {
      crate_name: "groestl",
      hasher_type_name: "Groestl224",
      hasher_command: "groestl224",
      hasher: Box::new(groestl::Groestl224::default()),
    },
    #[cfg(feature = "groestl")]
    GeneratedHasherImplMeta {
      crate_name: "groestl",
      hasher_type_name: "Groestl256",
      hasher_command: "groestl256",
      hasher: Box::new(groestl::Groestl256::default()),
    },
    #[cfg(feature = "groestl")]
    GeneratedHasherImplMeta {
      crate_name: "groestl",
      hasher_type_name: "Groestl384",
      hasher_command: "groestl384",
      hasher: Box::new(groestl::Groestl384::default()),
    },
    #[cfg(feature = "groestl")]
    GeneratedHasherImplMeta {
      crate_name: "groestl",
      hasher_type_name: "Groestl512",
      hasher_command: "groestl512",
      hasher: Box::new(groestl::Groestl512::default()),
    },
    #[cfg(feature = "jh")]
    GeneratedHasherImplMeta {
      crate_name: "jh",
      hasher_type_name: "Jh224",
      hasher_command: "jh224",
      hasher: Box::new(jh::Jh224::default()),
    },
    #[cfg(feature = "jh")]
    GeneratedHasherImplMeta {
      crate_name: "jh",
      hasher_type_name: "Jh256",
      hasher_command: "jh256",
      hasher: Box::new(jh::Jh256::default()),
    },
    #[cfg(feature = "jh")]
    GeneratedHasherImplMeta {
      crate_name: "jh",
      hasher_type_name: "Jh384",
      hasher_command: "jh384",
      hasher: Box::new(jh::Jh384::default()),
    },
    #[cfg(feature = "jh")]
    GeneratedHasherImplMeta {
      crate_name: "jh",
      hasher_type_name: "Jh512",
      hasher_command: "jh512",
      hasher: Box::new(jh::Jh512::default()),
    },
    #[cfg(feature = "md2")]
    GeneratedHasherImplMeta {
      crate_name: "md2",
      hasher_type_name: "Md2",
      hasher_command: "md2",
      hasher: Box::new(md2::Md2::default()),
    },
    #[cfg(feature = "md4")]
    GeneratedHasherImplMeta {
      crate_name: "md4",
      hasher_type_name: "Md4",
      hasher_command: "md4",
      hasher: Box::new(md4::Md4::default()),
    },
    #[cfg(feature = "ripemd")]
    GeneratedHasherImplMeta {
      crate_name: "ripemd",
      hasher_type_name: "Ripemd128",
      hasher_command: "ripemd128",
      hasher: Box::new(ripemd::Ripemd128::default()),
    },
    #[cfg(feature = "ripemd")]
    GeneratedHasherImplMeta {
      crate_name: "ripemd",
      hasher_type_name: "Ripemd160",
      hasher_command: "ripemd160",
      hasher: Box::new(ripemd::Ripemd160::default()),
    },
    #[cfg(feature = "ripemd")]
    GeneratedHasherImplMeta {
      crate_name: "ripemd",
      hasher_type_name: "Ripemd256",
      hasher_command: "ripemd256",
      hasher: Box::new(ripemd::Ripemd256::default()),
    },
    #[cfg(feature = "ripemd")]
    GeneratedHasherImplMeta {
      crate_name: "ripemd",
      hasher_type_name: "Ripemd320",
      hasher_command: "ripemd320",
      hasher: Box::new(ripemd::Ripemd320::default()),
    },
    #[cfg(feature = "sha1")]
    GeneratedHasherImplMeta {
      crate_name: "sha1",
      hasher_type_name: "Sha1",
      hasher_command: "sha1",
      hasher: Box::new(sha1::Sha1::default()),
    },
    #[cfg(feature = "sha2")]
    GeneratedHasherImplMeta {
      crate_name: "sha2",
      hasher_type_name: "Sha224",
      hasher_command: "sha224",
      hasher: Box::new(sha2::Sha224::default()),
    },
    #[cfg(feature = "sha2")]
    GeneratedHasherImplMeta {
      crate_name: "sha2",
      hasher_type_name: "Sha384",
      hasher_command: "sha384",
      hasher: Box::new(sha2::Sha384::default()),
    },
    #[cfg(feature = "sha2")]
    GeneratedHasherImplMeta {
      crate_name: "sha2",
      hasher_type_name: "Sha512",
      hasher_command: "sha512",
      hasher: Box::new(sha2::Sha512::default()),
    },
    #[cfg(feature = "sha2")]
    GeneratedHasherImplMeta {
      crate_name: "sha2",
      hasher_type_name: "Sha512_224",
      hasher_command: "sha512-224",
      hasher: Box::new(sha2::Sha512_224::default()),
    },
    #[cfg(feature = "sha2")]
    GeneratedHasherImplMeta {
      crate_name: "sha2",
      hasher_type_name: "Sha512_256",
      hasher_command: "sha512-256",
      hasher: Box::new(sha2::Sha512_256::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Sha3_224",
      hasher_command: "sha3-224",
      hasher: Box::new(sha3::Sha3_224::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Sha3_256",
      hasher_command: "sha3-256",
      hasher: Box::new(sha3::Sha3_256::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Sha3_384",
      hasher_command: "sha3-384",
      hasher: Box::new(sha3::Sha3_384::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Sha3_512",
      hasher_command: "sha3-512",
      hasher: Box::new(sha3::Sha3_512::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Keccak224",
      hasher_command: "keccak224",
      hasher: Box::new(sha3::Keccak224::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Keccak256",
      hasher_command: "keccak256",
      hasher: Box::new(sha3::Keccak256::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Keccak384",
      hasher_command: "keccak384",
      hasher: Box::new(sha3::Keccak384::default()),
    },
    #[cfg(feature = "sha3")]
    GeneratedHasherImplMeta {
      crate_name: "sha3",
      hasher_type_name: "Keccak512",
      hasher_command: "keccak512",
      hasher: Box::new(sha3::Keccak512::default()),
    },
    #[cfg(feature = "shabal")]
    GeneratedHasherImplMeta {
      crate_name: "shabal",
      hasher_type_name: "Shabal192",
      hasher_command: "shabal192",
      hasher: Box::new(shabal::Shabal192::default()),
    },
    #[cfg(feature = "shabal")]
    GeneratedHasherImplMeta {
      crate_name: "shabal",
      hasher_type_name: "Shabal224",
      hasher_command: "shabal224",
      hasher: Box::new(shabal::Shabal224::default()),
    },
    #[cfg(feature = "shabal")]
    GeneratedHasherImplMeta {
      crate_name: "shabal",
      hasher_type_name: "Shabal256",
      hasher_command: "shabal256",
      hasher: Box::new(shabal::Shabal256::default()),
    },
    #[cfg(feature = "shabal")]
    GeneratedHasherImplMeta {
      crate_name: "shabal",
      hasher_type_name: "Shabal384",
      hasher_command: "shabal384",
      hasher: Box::new(shabal::Shabal384::default()),
    },
    #[cfg(feature = "shabal")]
    GeneratedHasherImplMeta {
      crate_name: "shabal",
      hasher_type_name: "Shabal512",
      hasher_command: "shabal512",
      hasher: Box::new(shabal::Shabal512::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein256::<skein::consts::U32>",
      hasher_command: "skein256-32",
      hasher: Box::new(skein::Skein256::<skein::consts::U32>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein256::<skein::consts::U64>",
      hasher_command: "skein256-64",
      hasher: Box::new(skein::Skein256::<skein::consts::U64>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein256::<skein::consts::U128>",
      hasher_command: "skein256-128",
      hasher: Box::new(skein::Skein256::<skein::consts::U128>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein512::<skein::consts::U32>",
      hasher_command: "skein512-32",
      hasher: Box::new(skein::Skein512::<skein::consts::U32>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein512::<skein::consts::U64>",
      hasher_command: "skein512-64",
      hasher: Box::new(skein::Skein512::<skein::consts::U64>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein512::<skein::consts::U128>",
      hasher_command: "skein512-128",
      hasher: Box::new(skein::Skein512::<skein::consts::U128>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein1024::<skein::consts::U32>",
      hasher_command: "skein1024-32",
      hasher: Box::new(skein::Skein1024::<skein::consts::U32>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein1024::<skein::consts::U64>",
      hasher_command: "skein1024-64",
      hasher: Box::new(skein::Skein1024::<skein::consts::U64>::default()),
    },
    #[cfg(feature = "skein")]
    GeneratedHasherImplMeta {
      crate_name: "skein",
      hasher_type_name: "Skein1024::<skein::consts::U128>",
      hasher_command: "skein1024-128",
      hasher: Box::new(skein::Skein1024::<skein::consts::U128>::default()),
    },
    #[cfg(feature = "sm3")]
    GeneratedHasherImplMeta {
      crate_name: "sm3",
      hasher_type_name: "Sm3",
      hasher_command: "sm3",
      hasher: Box::new(sm3::Sm3::default()),
    },
    #[cfg(feature = "streebog")]
    GeneratedHasherImplMeta {
      crate_name: "streebog",
      hasher_type_name: "Streebog256",
      hasher_command: "streebog256",
      hasher: Box::new(streebog::Streebog256::default()),
    },
    #[cfg(feature = "streebog")]
    GeneratedHasherImplMeta {
      crate_name: "streebog",
      hasher_type_name: "Streebog512",
      hasher_command: "streebog512",
      hasher: Box::new(streebog::Streebog512::default()),
    },
    #[cfg(feature = "tiger")]
    GeneratedHasherImplMeta {
      crate_name: "tiger",
      hasher_type_name: "Tiger",
      hasher_command: "tiger",
      hasher: Box::new(tiger::Tiger::default()),
    },
    #[cfg(feature = "tiger")]
    GeneratedHasherImplMeta {
      crate_name: "tiger",
      hasher_type_name: "Tiger2",
      hasher_command: "tiger2",
      hasher: Box::new(tiger::Tiger2::default()),
    },
    #[cfg(feature = "whirlpool")]
    GeneratedHasherImplMeta {
      crate_name: "whirlpool",
      hasher_type_name: "Whirlpool",
      hasher_command: "whirlpool",
      hasher: Box::new(whirlpool::Whirlpool::default()),
    },
  ];

  let out_dir = env::var_os("OUT_DIR").unwrap();
  let hashers_generated_path = Path::new(&out_dir).join("hashers_generated.rs");
  let commands_generated_path =
    Path::new(&out_dir).join("commands_generated.rs");
  let mut hashers_generated_file =
    std::fs::File::create(hashers_generated_path).unwrap();
  let mut commands_generated_file =
    std::fs::File::create(commands_generated_path).unwrap();

  write!(
    hashers_generated_file,
    "use nu_protocol::{{Example, Span, Value, ShellError}};
use nu_plugin::PluginCommand;
use crate::HashesPlugin;
use crate::hasher::{{Hasher, GenericHasher}};
"
  )?;

  write!(
    commands_generated_file,
    "use nu_plugin::PluginCommand;
use crate::{{HashesPlugin, hasher::GenericHasher}};

pub fn commands() -> Vec<Box<dyn PluginCommand<Plugin = HashesPlugin>>> {{
  vec![
"
  )?;

  for mut hasher_impl_meta in hasher_impls {
    let feature_name =
      hasher_impl_meta.crate_name.replace("-", "_").to_uppercase();
    if std::env::var(format!("CARGO_FEATURE_{feature_name}")).is_ok() {
      hashers_generated_file
        .write_all(build_impl_str(&mut hasher_impl_meta).as_bytes())?;
      hashers_generated_file
        .write_all(build_test_str(&hasher_impl_meta).as_bytes())?;

      let crate_name = hasher_impl_meta.crate_name;
      let hasher_type_name = hasher_impl_meta.hasher_type_name;
      writeln!(
        commands_generated_file,
        "    Box::new(GenericHasher::<{crate_name}::{hasher_type_name}>::default()),"
      )?;
    }
  }

  write!(
    commands_generated_file,
    "  ]
}}"
  )?;

  hashers_generated_file.flush()?;
  commands_generated_file.flush()?;

  Ok(())
}

#[cfg(any(
  feature = "ascon-hash",
  feature = "belt-hash",
  feature = "blake2",
  feature = "blake3",
  feature = "fsb",
  feature = "gost94",
  feature = "groestl",
  feature = "jh",
  feature = "md2",
  feature = "md4",
  feature = "ripemd",
  feature = "sha1",
  feature = "sha2",
  feature = "sha3",
  feature = "shabal",
  feature = "skein",
  feature = "sm3",
  feature = "streebog",
  feature = "tiger",
  feature = "whirlpool",
))]
fn build_impl_str(meta: &mut GeneratedHasherImplMeta) -> String {
  let crate_name = meta.crate_name;
  let hasher_type_name = meta.hasher_type_name;
  let command = meta.hasher_command;
  let mut hasher = meta.hasher.clone();
  hasher.update(TEST_TEXT.as_bytes());
  let hash = hasher.clone().finalize();
  format!(
    "
impl Hasher for {crate_name}::{hasher_type_name} {{
  fn name() -> &'static str {{
    \"{command}\"
  }}

  fn examples() -> Vec<Example<'static>> {{
    vec![
      Example {{
        description: \"Return the {command} hash of a string, hex-encoded\",
        example: \"'{TEST_TEXT}' | hash {command}\",
        result: Some(Value::string(
          \"{}\".to_owned(),
          Span::test_data(),
        )),
      }},
      Example {{
        description: \"Return the {command} hash of a string, as binary\",
        example: \"'{TEST_TEXT}' | hash {command} --binary\",
        result: Some(Value::binary(
          vec![{}],
          Span::test_data(),
        )),
      }},
      Example {{
        description: \"Return the {command} hash of a file's contents\",
        example: \"open ./nu_0_24_1_windows.zip | hash {command}\",
        result: None,
      }},
    ]
  }}
}}
",
    hash
      .iter()
      .map(|b| format!("{b:02x?}"))
      .collect::<Vec<_>>()
      .join(""),
    hash
      .iter()
      .map(|b| format!("0x{b:02x?}"))
      .collect::<Vec<_>>()
      .join(",")
  )
}

#[cfg(any(
  feature = "ascon-hash",
  feature = "belt-hash",
  feature = "blake2",
  feature = "blake3",
  feature = "fsb",
  feature = "gost94",
  feature = "groestl",
  feature = "jh",
  feature = "md2",
  feature = "md4",
  feature = "ripemd",
  feature = "sha1",
  feature = "sha2",
  feature = "sha3",
  feature = "shabal",
  feature = "skein",
  feature = "sm3",
  feature = "streebog",
  feature = "tiger",
  feature = "whirlpool",
))]
fn build_test_str(meta: &GeneratedHasherImplMeta) -> String {
  let crate_name = meta.crate_name;
  let hasher_type_name = meta.hasher_type_name;
  let command = meta.hasher_command;
  let test_name = command.replace("-", "_");
  format!(
    "
#[test]
fn test_{test_name}_examples() -> Result<(), ShellError> {{
  nu_plugin_test_support::PluginTest::new
    (
      \"hash {command}\",
      HashesPlugin.into()
    )?
    .test_examples(&GenericHasher::<{crate_name}::{hasher_type_name}>::default().examples())
}}
  "
  )
}

#[cfg(not(any(
  feature = "ascon-hash",
  feature = "belt-hash",
  feature = "blake2",
  feature = "blake3",
  feature = "fsb",
  feature = "gost94",
  feature = "groestl",
  feature = "jh",
  feature = "md2",
  feature = "md4",
  feature = "ripemd",
  feature = "sha1",
  feature = "sha2",
  feature = "sha3",
  feature = "shabal",
  feature = "skein",
  feature = "sm3",
  feature = "streebog",
  feature = "tiger",
  feature = "whirlpool",
)))]
fn main() {
  compile_error!("enable at least one feature to compile this plugin");
}
