/*
 * This file is part of espanso.
 *
 * Copyright (C) 2021 Federico Terzi
 *
 * espanso is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * espanso is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::path::{PathBuf};

#[cfg(not(target_os = "windows"))]
use std::path::{Path};

#[cfg(not(target_os = "linux"))]
const WX_WIDGETS_ARCHIVE_NAME: &str = "wxWidgets-3.1.5.zip";

#[cfg(target_os = "windows")]
fn build_native() {
  use std::process::Command;

  let project_dir =
    PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
  let wx_archive = project_dir.join("vendor").join(WX_WIDGETS_ARCHIVE_NAME);
  if !wx_archive.is_file() {
    panic!("could not find wxWidgets archive!");
  }

  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
  let out_wx_dir = out_dir.join("wx");

  if !out_wx_dir.is_dir() {
    // Extract the wxWidgets archive
    let wx_archive =
      std::fs::File::open(&wx_archive).expect("unable to open wxWidgets source archive");
    let mut archive = zip::ZipArchive::new(wx_archive).expect("unable to read wxWidgets archive");
    archive
      .extract(&out_wx_dir)
      .expect("unable to extract wxWidgets source dir");

    // Compile wxWidgets
    let tool = cc::windows_registry::find_tool("msvc", "msbuild")
      .expect("unable to locate MSVC compiler, did you install Visual Studio?");
    let mut vcvars_path = None;
    let mut current_root = tool.path();
    while let Some(parent) = current_root.parent() {
      let target = parent
        .join("VC")
        .join("Auxiliary")
        .join("Build")
        .join("vcvars64.bat");
      if target.exists() {
        vcvars_path = Some(target);
        break;
      }
      current_root = parent;
    }

    let vcvars_path = vcvars_path.expect("unable to find vcvars64.bat file");
    let mut handle = Command::new("cmd")
      .current_dir(
        out_wx_dir
          .join("build")
          .join("msw")
          .to_string_lossy()
          .to_string(),
      )
      .args(&[
        "/k",
        &vcvars_path.to_string_lossy().to_string(),
        "&",
        "nmake",
        "/f",
        "makefile.vc",
        "BUILD=release",
        "TARGET_CPU=X64",
        "&",
        "exit",
      ])
      .spawn()
      .expect("failed to execute nmake");
    if !handle.wait().expect("unable to wait for nmake command").success() {
      panic!("nmake returned non-zero exit code!");
    }
  }

  // Make sure wxWidgets is compiled
  if !out_wx_dir
      .join("build")
      .join("msw")
      .join("vc_mswu_x64")
      .is_dir()
  {
    panic!("wxWidgets is not compiled correctly, missing 'build/msw/vc_mswu_x64' directory")
  }

  let wx_include_dir = out_wx_dir.join("include");
  let wx_include_msvc_dir = wx_include_dir.join("msvc");
  let wx_lib_dir = out_wx_dir.join("lib").join("vc_x64_lib");

  cc::Build::new()
    .cpp(true)
    .file("src/sys/form/form.cpp")
    .file("src/sys/search/search.cpp")
    .file("src/sys/common/common.cpp")
    .file("src/sys/wizard/wizard.cpp")
    .file("src/sys/wizard/wizard_gui.cpp")
    .file("src/sys/welcome/welcome.cpp")
    .file("src/sys/welcome/welcome_gui.cpp")
    .file("src/sys/troubleshooting/troubleshooting.cpp")
    .file("src/sys/troubleshooting/troubleshooting_gui.cpp")
    .flag("/EHsc")
    .include(wx_include_dir)
    .include(wx_include_msvc_dir)
    .compile("espansomodulosys");

  // Add resources (manifest)
  let mut resources = winres::WindowsResource::new();
  resources.set_manifest(include_str!("res/win.manifest"));
  resources
    .compile()
    .expect("unable to compile resource file");

  println!(
    "cargo:rustc-link-search=native={}",
    wx_lib_dir.to_string_lossy()
  );
}

#[cfg(target_os = "macos")]
fn build_native() {
  use std::process::Command;

  let project_dir =
    PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
  let wx_archive = project_dir.join("vendor").join(WX_WIDGETS_ARCHIVE_NAME);
  if !wx_archive.is_file() {
    panic!("could not find wxWidgets archive!");
  }

  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
  let out_wx_dir = out_dir.join("wx");

  if !out_wx_dir.is_dir() {
    // Extract the wxWidgets archive
    let wx_archive =
      std::fs::File::open(&wx_archive).expect("unable to open wxWidgets source archive");
    let mut archive = zip::ZipArchive::new(wx_archive).expect("unable to read wxWidgets archive");
    archive
      .extract(&out_wx_dir)
      .expect("unable to extract wxWidgets source dir");

    // Compile wxWidgets
    let build_dir = out_wx_dir.join("build-cocoa");
    std::fs::create_dir_all(&build_dir).expect("unable to create build-cocoa directory");

    let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").expect("unable to read target arch").as_str() {
      "x86_64" => "x86_64",
      "aarch64" => "arm64",
      arch => panic!("unsupported arch {}", arch),
    };

    let mut handle = Command::new(out_wx_dir.join("configure"))
      .current_dir(
        build_dir.to_string_lossy().to_string()
      )
      .args(&[
        "--disable-shared",
        "--without-libtiff",
        &format!("--enable-macosx_arch={}", target_arch),
      ])
      .spawn()
      .expect("failed to execute configure");
    if !handle.wait().expect("unable to wait for configure command").success() {
      panic!("configure returned non-zero exit code!");
    }

    let mut handle = Command::new("make")
      .current_dir(
        build_dir.to_string_lossy().to_string()
      )
      .args(&[
        "-j8",
      ])
      .spawn()
      .expect("failed to execute make");
    if !handle.wait().expect("unable to wait for make command").success() {
      panic!("make returned non-zero exit code!");
    }
  }

  // Make sure wxWidgets is compiled
  if !out_wx_dir.join("build-cocoa").is_dir() {
    panic!("wxWidgets is not compiled correctly, missing 'build-cocoa/' directory")
  }

  let config_path = out_wx_dir.join("build-cocoa").join("wx-config");
  let cpp_flags = get_cpp_flags(&config_path);

  let mut build = cc::Build::new();
  build
    .cpp(true)
    .file("src/sys/form/form.cpp")
    .file("src/sys/common/common.cpp")
    .file("src/sys/search/search.cpp")
    .file("src/sys/wizard/wizard.cpp")
    .file("src/sys/wizard/wizard_gui.cpp")
    .file("src/sys/welcome/welcome.cpp")
    .file("src/sys/welcome/welcome_gui.cpp")
    .file("src/sys/troubleshooting/troubleshooting.cpp")
    .file("src/sys/troubleshooting/troubleshooting_gui.cpp")
    .file("src/sys/common/mac.mm");
  build.flag("-std=c++17");

  for flag in cpp_flags {
    build.flag(&flag);
  }

  build.compile("espansomodulosys");

  // Render linker flags

  generate_linker_flags(&config_path);

  // On (older) OSX we need to link against the clang runtime,
  // which is hidden in some non-default path.
  //
  // More details at https://github.com/alexcrichton/curl-rust/issues/279.
  if let Some(path) = macos_link_search_path() {
    println!("cargo:rustc-link-lib=clang_rt.osx");
    println!("cargo:rustc-link-search={}", path);
  }
}

#[cfg(not(target_os = "windows"))]
fn get_cpp_flags(wx_config_path: &Path) -> Vec<String> {
  let config_output = std::process::Command::new(&wx_config_path)
    .arg("--cxxflags")
    .output()
    .expect("unable to execute wx-config");
  let config_libs =
    String::from_utf8(config_output.stdout).expect("unable to parse wx-config output");
  let cpp_flags: Vec<String> = config_libs
    .split(' ')
    .filter_map(|s| {
      if !s.trim().is_empty() {
        Some(s.trim().to_owned())
      } else {
        None
      }
    })
    .collect();
  cpp_flags
}

#[cfg(not(target_os = "windows"))]
fn generate_linker_flags(wx_config_path: &Path) {
  use regex::Regex;
  let config_output = std::process::Command::new(&wx_config_path)
    .arg("--libs")
    .output()
    .expect("unable to execute wx-config libs");
  let config_libs =
    String::from_utf8(config_output.stdout).expect("unable to parse wx-config libs output");
  let linker_flags: Vec<String> = config_libs
    .split(' ')
    .filter_map(|s| {
      if !s.trim().is_empty() {
        Some(s.trim().to_owned())
      } else {
        None
      }
    })
    .collect();

  let static_lib_extract = Regex::new(r"lib/lib(.*)\.a").unwrap();

  // Translate the flags generated by `wx-config` to commands
  // that cargo can understand.
  for (i, flag) in linker_flags.iter().enumerate() {
    if flag.starts_with("-L") {
      let path = flag.trim_start_matches("-L");
      println!("cargo:rustc-link-search=native={}", path);
    } else if flag.starts_with("-framework") {
      println!("cargo:rustc-link-lib=framework={}", linker_flags[i + 1]);
    } else if flag.starts_with('/') {
      let captures = static_lib_extract
        .captures(flag)
        .expect("unable to capture flag regex");
      let libname = captures.get(1).expect("unable to find static libname");
      println!("cargo:rustc-link-lib=static={}", libname.as_str());
    } else if flag.starts_with("-l") {
      let libname = flag.trim_start_matches("-l");
      println!("cargo:rustc-link-lib=dylib={}", libname);
    }
  }
}

// Taken from curl-rust: https://github.com/alexcrichton/curl-rust/pull/283/files
#[cfg(target_os = "macos")]
fn macos_link_search_path() -> Option<String> {
  let output = std::process::Command::new("clang")
    .arg("--print-search-dirs")
    .output()
    .ok()?;
  if !output.status.success() {
    println!("failed to run 'clang --print-search-dirs', continuing without a link search path");
    return None;
  }

  let stdout = String::from_utf8_lossy(&output.stdout);
  for line in stdout.lines() {
    if line.contains("libraries: =") {
      let path = line.split('=').skip(1).next()?;
      return Some(format!("{}/lib/darwin", path));
    }
  }

  println!("failed to determine link search path, continuing without it");
  None
}

// TODO: add documentation for linux
// Install wxWidgets:
// sudo apt install libwxgtk3.0-0v5 libwxgtk3.0-dev
//
// cargo run
#[cfg(target_os = "linux")]
fn build_native() {
  // Make sure wxWidgets is installed
  if std::process::Command::new("wx-config")
    .arg("--version")
    .output().is_err()
  {
    panic!("wxWidgets is not installed, as `wx-config` cannot be exectued")
  }

  let config_path = PathBuf::from("wx-config");
  let cpp_flags = get_cpp_flags(&config_path);

  let mut build = cc::Build::new();
  build
    .cpp(true)
    .file("src/sys/form/form.cpp")
    .file("src/sys/search/search.cpp")
    .file("src/sys/common/common.cpp")
    .file("src/sys/wizard/wizard.cpp")
    .file("src/sys/wizard/wizard_gui.cpp")
    .file("src/sys/welcome/welcome.cpp")
    .file("src/sys/welcome/welcome_gui.cpp")
    .file("src/sys/troubleshooting/troubleshooting.cpp")
    .file("src/sys/troubleshooting/troubleshooting_gui.cpp");
  build.flag("-std=c++17");

  for flag in cpp_flags {
    build.flag(&flag);
  }

  build.compile("espansomodulosys");

  // Render linker flags

  generate_linker_flags(&config_path);
}

fn main() {
  build_native();
}