use std::error::Error;
use std::path::PathBuf;
use std::rc::Rc;

#[derive(Clone)]
pub enum CargoTargetType {
    Executable,
    Library {
        has_staticlib: bool,
        has_cdylib: bool,
    },
}

#[derive(Clone)]
pub struct CargoTarget {
    cargo_package: Rc<cargo_metadata::Package>,
    cargo_target: cargo_metadata::Target,
    target_type: CargoTargetType,
    workspace_manifest_path: Rc<PathBuf>,
}

impl CargoTargetType {
    fn to_string(&self) -> String {
        let mut s = String::new();
        match self {
            Self::Executable => {
                s.push_str("bin");
            }
            Self::Library {
                has_staticlib,
                has_cdylib,
            } => {
                if *has_staticlib {
                    s.push_str("staticlib")
                }
                if *has_cdylib {
                    s.push_str(" cdylib")
                }
            }
        }
        s
    }
}

impl CargoTarget {
    pub fn from_metadata(
        cargo_package: Rc<cargo_metadata::Package>,
        cargo_target: cargo_metadata::Target,
        workspace_manifest_path: Rc<PathBuf>,
        // If Some, only import crates if the kind variant is given in crate_kinds.
        crate_kinds: &Option<Vec<&str>>,
    ) -> Option<Self> {
        let filtered_kinds: Vec<String> = cargo_target
            .kind
            .clone()
            .into_iter()
            .filter(|kind| match crate_kinds {
                None => true,
                Some(allowed_kinds_subset) => allowed_kinds_subset.contains(&&**kind),
            })
            .collect();

        let target_type = if filtered_kinds
            .iter()
            .any(|k| k.as_str() == "staticlib" || k.as_str() == "cdylib")
        {
            CargoTargetType::Library {
                has_staticlib: filtered_kinds.iter().any(|k| k == "staticlib"),
                has_cdylib: filtered_kinds.iter().any(|k| k == "cdylib"),
            }
        } else if filtered_kinds.iter().any(|k| k == "bin") {
            CargoTargetType::Executable
        } else {
            return None;
        };

        Some(Self {
            cargo_package,
            cargo_target,
            target_type,
            workspace_manifest_path,
        })
    }

    /// Cargo / Rust 1.78 and newer replace dashes with underscores in libraries
    /// To make the names consistent across versions we also do the replacement here.
    pub(crate) fn target_name(&self) -> String {
        match self.target_type {
            CargoTargetType::Library { .. } => self.cargo_target.name.replace("-", "_"),
            _ => self.cargo_target.name.to_string(),
        }
    }

    pub fn emit_cmake_target(
        &self,
        out_file: &mut dyn std::io::Write,
        passthrough_add_cargo_build: &str,
    ) -> Result<(), Box<dyn Error>> {
        writeln!(
            out_file,
            "set(byproducts \"\")
                            set(cargo_build_out_dir \"\")
                            set(archive_byproducts \"\")
                            set(shared_lib_byproduct \"\")
                            set(pdb_byproduct \"\")
                            set(bin_byproduct \"\")
        "
        )?;
        let ws_manifest = self
            .workspace_manifest_path
            .to_str()
            .expect("Non-utf8 path encountered")
            .replace("\\", "/");

        match self.target_type {
            CargoTargetType::Library {
                has_staticlib,
                has_cdylib,
            } => {
                assert!(has_staticlib || has_cdylib);
                let ws_manifest = self
                    .workspace_manifest_path
                    .to_str()
                    .expect("Non-utf8 path encountered")
                    .replace("\\", "/");
                let mut lib_kinds = if has_staticlib { "staticlib" } else { "" }.to_string();
                if has_cdylib {
                    if has_staticlib {
                        lib_kinds.push(' ');
                    }
                    lib_kinds.push_str("cdylib")
                }

                writeln!(
                    out_file,
                    "
                    add_library({target_name} INTERFACE)
                    _corrosion_initialize_properties({target_name})
                    _corrosion_add_library_target(
                            WORKSPACE_MANIFEST_PATH \"{workspace_manifest_path}\"
                            TARGET_NAME \"{target_name}\"
                            LIB_KINDS {lib_kinds}
                            OUT_ARCHIVE_OUTPUT_BYPRODUCTS archive_byproducts
                            OUT_SHARED_LIB_BYPRODUCTS shared_lib_byproduct
                            OUT_PDB_BYPRODUCT pdb_byproduct
                    )
                    list(APPEND byproducts
                            \"${{archive_byproducts}}\"
                            \"${{shared_lib_byproduct}}\"
                            \"${{pdb_byproduct}}\"
                    )
                    ",
                    workspace_manifest_path = ws_manifest,
                    target_name = self.target_name(),
                    lib_kinds = lib_kinds,
                )?;
            }
            CargoTargetType::Executable => {
                writeln!(
                    out_file,
                    "
                    add_executable({target_name} IMPORTED GLOBAL)
                    _corrosion_initialize_properties({target_name})
                    _corrosion_add_bin_target(\"{workspace_manifest_path}\" \"{target_name}\"
                        bin_byproduct pdb_byproduct
                    )
                    set(byproducts \"\")
                    list(APPEND byproducts \"${{bin_byproduct}}\" \"${{pdb_byproduct}}\")
                    ",
                    workspace_manifest_path = ws_manifest,
                    target_name = self.target_name(),
                )?;
            }
        };
        let target_kinds = self.target_type.to_string();
        writeln!(out_file,
            "
            set(cargo_build_out_dir \"\")
            _add_cargo_build(
                cargo_build_out_dir
                PACKAGE \"{package_name}\"
                TARGET \"{target_name}\"
                MANIFEST_PATH \"{package_manifest_path}\"
                WORKSPACE_MANIFEST_PATH \"{workspace_manifest_path}\"
                TARGET_KINDS {target_kinds}
                BYPRODUCTS \"${{byproducts}}\"
                {passthrough_add_cargo_build}
            )

            set_target_properties({target_name} PROPERTIES
                INTERFACE_COR_PACKAGE_MANIFEST_PATH \"{package_manifest_path}\"
            )

            if(archive_byproducts)
                _corrosion_copy_byproducts(
                    {target_name} INTERFACE_ARCHIVE_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{archive_byproducts}}\" FALSE
                )
            endif()
            if(shared_lib_byproduct)
                _corrosion_copy_byproducts(
                    {target_name} INTERFACE_LIBRARY_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{shared_lib_byproduct}}\" FALSE
                )
            endif()
            if(pdb_byproduct)
                _corrosion_copy_byproducts(
                    {target_name} INTERFACE_PDB_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{pdb_byproduct}}\" FALSE
                )
            endif()
            if(bin_byproduct)
                _corrosion_copy_byproducts(
                    {target_name} INTERFACE_RUNTIME_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{bin_byproduct}}\" TRUE
                )
            endif()
            set_property(TARGET {target_name} PROPERTY INTERFACE_COR_CARGO_PACKAGE_NAME {package_name} )
            ",
            package_name = self.cargo_package.name,
            target_name = self.target_name(),
            package_manifest_path = self.cargo_package.manifest_path.as_str().replace("\\", "/"),
            workspace_manifest_path = ws_manifest,
            target_kinds = target_kinds,
            passthrough_add_cargo_build = passthrough_add_cargo_build,

        )?;
        Ok(())
    }
}
