use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
  let target = env::var("TARGET").unwrap_or_default();
  let is_wasm = target.contains("wasm32") || target.contains("emscripten");

  let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
  let vendor_src_dir = manifest_dir.join("vendor/libherb/src");
  let vendor_include_dir = manifest_dir.join("vendor/libherb/src/include");

  let (src_dir, include_dir, root_dir) = if vendor_src_dir.exists() {
    (vendor_src_dir, vendor_include_dir, manifest_dir.clone())
  } else {
    let root = manifest_dir.parent().unwrap();
    (root.join("src"), root.join("src/include"), root.to_path_buf())
  };

  let prism_include = if is_wasm {
    let vendor_prism_include = manifest_dir.join("vendor/prism/include");

    if vendor_prism_include.exists() {
      vendor_prism_include
    } else {
      let prism_path = get_prism_path(&root_dir);
      prism_path.join("include")
    }
  } else {
    let vendor_prism_src = manifest_dir.join("vendor/prism/src");
    let vendor_prism_include = manifest_dir.join("vendor/prism/include");

    let prism_include = if vendor_prism_src.exists() {
      let mut prism_sources = Vec::new();
      for path in glob::glob(vendor_prism_src.join("**/*.c").to_str().unwrap()).unwrap().flatten() {
        prism_sources.push(path);
      }

      let mut prism_build = cc::Build::new();
      prism_build
        .flag("-std=c99")
        .flag("-fPIC")
        .opt_level(2)
        .define("HERB_EXCLUDE_PRETTYPRINT", None)
        .define("PRISM_EXCLUDE_PRETTYPRINT", None)
        .define("PRISM_EXCLUDE_JSON", None)
        .define("PRISM_EXCLUDE_PACK", None)
        .include(&vendor_prism_include)
        .files(&prism_sources)
        .warnings(false);

      prism_build.compile("prism");

      vendor_prism_include
    } else {
      let prism_path = get_prism_path(&root_dir);
      let prism_build = prism_path.join("build");
      println!("cargo:rustc-link-search=native={}", prism_build.display());
      println!("cargo:rustc-link-lib=static=prism");
      prism_path.join("include")
    };

    let mut c_sources = Vec::new();

    for path in glob::glob(src_dir.join("**/*.c").to_str().unwrap()).unwrap().flatten() {
      if !path.ends_with("main.c") {
        c_sources.push(path);
      }
    }

    let mut build = cc::Build::new();
    build
      .flag("-std=c99")
      .flag("-Wall")
      .flag("-Wextra")
      .flag("-Wno-unused-parameter")
      .flag("-fPIC")
      .opt_level(2)
      .define("HERB_EXCLUDE_PRETTYPRINT", None)
      .define("PRISM_EXCLUDE_JSON", None)
      .define("PRISM_EXCLUDE_PACK", None)
      .define("PRISM_EXCLUDE_SERIALIZATION", None)
      .include(&include_dir)
      .include(&prism_include)
      .files(&c_sources);

    build.compile("herb");

    for source in &c_sources {
      println!("cargo:rerun-if-changed={}", source.display());
    }

    prism_include
  };

  let mut builder = bindgen::Builder::default()
    .header(include_dir.join("analyze/analyze.h").to_str().unwrap())
    .header(include_dir.join("herb.h").to_str().unwrap())
    .header(include_dir.join("ast/ast_nodes.h").to_str().unwrap())
    .header(include_dir.join("errors.h").to_str().unwrap())
    .header(include_dir.join("extract.h").to_str().unwrap())
    .header(include_dir.join("lexer/token_struct.h").to_str().unwrap())
    .header(include_dir.join("lib/hb_allocator.h").to_str().unwrap())
    .header(include_dir.join("lib/hb_string.h").to_str().unwrap())
    .header(include_dir.join("lib/hb_array.h").to_str().unwrap())
    .header(include_dir.join("lib/hb_buffer.h").to_str().unwrap())
    .clang_arg(format!("-I{}", include_dir.display()))
    .clang_arg(format!("-I{}", prism_include.display()))
    .allowlist_function("herb_.*")
    .allowlist_function("hb_allocator_.*")
    .allowlist_function("hb_array_.*")
    .allowlist_function("hb_buffer_.*")
    .allowlist_function("token_type_to_string")
    .allowlist_function("ast_node_free")
    .allowlist_type("AST_.*")
    .allowlist_type("ERROR_.*")
    .allowlist_type(".*_ERROR_T")
    .allowlist_type("ast_node_type_T")
    .allowlist_type("error_type_T")
    .allowlist_type("hb_allocator")
    .allowlist_type("hb_allocator_T")
    .allowlist_type("hb_array_T")
    .allowlist_type("hb_buffer_T")
    .allowlist_type("hb_string_T")
    .allowlist_type("token_T")
    .allowlist_type("position_T")
    .allowlist_type("location_T")
    .allowlist_type("herb_extract_language_T")
    .allowlist_type("herb_extract_ruby_options_T")
    .allowlist_type("parser_options_T")
    .allowlist_type("prism_serialized_T")
    .allowlist_type("herb_prism_node_T")
    .allowlist_type("pm_buffer_t")
    .allowlist_function("pm_prettyprint")
    .allowlist_function("pm_buffer_free")
    .allowlist_var("AST_.*")
    .allowlist_var("ERROR_.*")
    .allowlist_var("ELEMENT_SOURCE_.*")
    .allowlist_var("HB_ALLOCATOR_.*")
    .allowlist_var("HERB_EXTRACT_.*")
    .derive_debug(true)
    .derive_default(false)
    .prepend_enum_name(false)
    .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));

  if is_wasm {
    let host = env::var("HOST").unwrap_or_default();

    if !host.is_empty() {
      builder = builder.clang_arg(format!("--target={}", host));
    }

    builder = builder.layout_tests(false);
  }

  let bindings = builder.generate().expect("Unable to generate bindings");

  let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
  bindings.write_to_file(out_path.join("bindings.rs")).expect("Couldn't write bindings!");

  println!("cargo:rerun-if-changed={}", include_dir.display());
  println!("cargo:rerun-if-changed=build.rs");
}

fn get_prism_path(root_dir: &Path) -> PathBuf {
  let output = Command::new("bundle")
    .args(["show", "prism"])
    .current_dir(root_dir)
    .output()
    .expect("Failed to run `bundle show prism`");

  let output_string = String::from_utf8(output.stdout).expect("Failed to parse bundle output");

  let path_string = output_string.lines().last().expect("No output from bundle show prism").trim().to_string();

  PathBuf::from(path_string)
}
