use std::collections::BTreeSet;
use std::ffi::{CStr, c_char};
use std::path::PathBuf;
use std::ptr::NonNull;
use std::sync::OnceLock;

use fontconfig_sys as fc;

pub fn cached_paths() -> &'static Option<BTreeSet<PathBuf>> {
    static DIRS: OnceLock<Option<BTreeSet<PathBuf>>> = OnceLock::new();

    DIRS.get_or_init(dirs)
}

fn dirs() -> Option<BTreeSet<PathBuf>> {
    unsafe {
        if fc::FcInit() != 1 {
            return None;
        }
        let config = NonNull::new(fc::FcConfigGetCurrent())?.as_ptr();

        let mut paths = cache_dirs(config)?;
        paths.append(&mut config_files_and_dirs(config)?);
        paths.append(&mut font_dirs(config)?);

        Some(paths)
    }
}

unsafe fn cache_dirs(config: *mut fc::FcConfig) -> Option<BTreeSet<PathBuf>> {
    unsafe { str_list_to_set(fc::FcConfigGetCacheDirs(config)) }
}

unsafe fn config_files_and_dirs(config: *mut fc::FcConfig) -> Option<BTreeSet<PathBuf>> {
    unsafe {
        let config_dirs = str_list_to_set(fc::FcConfigGetConfigDirs(config))?;
        let all_config_files = str_list_to_set(fc::FcConfigGetConfigFiles(config))?;

        let mut config_files_without_confd = all_config_files
            .into_iter()
            .filter(|x| {
                x.parent()
                    .is_some_and(|p| !config_dirs.contains(p) && !config_dirs.contains(x))
            })
            .collect();

        let mut all_paths = config_dirs;
        all_paths.append(&mut config_files_without_confd);
        Some(all_paths)
    }
}

unsafe fn font_dirs(config: *mut fc::FcConfig) -> Option<BTreeSet<PathBuf>> {
    unsafe {
        let font_dirs = str_list_to_set(fc::FcConfigGetFontDirs(config))?;

        Some(
            font_dirs
                .into_iter()
                .filter(|x| !x.starts_with("/usr/"))
                .collect(),
        )
    }
}

unsafe fn str_list_to_set(list: *mut fc::FcStrList) -> Option<BTreeSet<PathBuf>> {
    unsafe {
        let list = NonNull::new(list)?.as_ptr();
        let mut vec = BTreeSet::new();
        loop {
            let s = fc::FcStrListNext(list);
            if s.is_null() {
                break;
            } else if let Ok(cs) = CStr::from_ptr(s as *const c_char).to_str() {
                vec.insert(PathBuf::from(cs));
            } else {
                tracing::error!("fontconfig: Invalid path");
            }
        }

        fc::FcStrListDone(list);

        Some(vec)
    }
}
