//! C FFI for cpdf metadata operations.

use super::document::CPDF_DOCS;
use super::{clear_error, set_error};
use crate::cpdf::metadata;
use crate::ffi::Handle;
use std::ffi::{CStr, CString, c_char, c_int};

macro_rules! get_doc_clone {
    ($handle:expr) => {
        match CPDF_DOCS.get($handle) {
            Some(arc) => arc.lock().unwrap().clone(),
            None => {
                set_error(1, &format!("invalid document handle: {}", $handle));
                return 0;
            }
        }
    };
}

macro_rules! get_doc_ref {
    ($handle:expr) => {
        match CPDF_DOCS.get($handle) {
            Some(arc) => arc,
            None => {
                set_error(1, &format!("invalid document handle: {}", $handle));
                return std::ptr::null_mut();
            }
        }
    };
}

macro_rules! ok_or_error {
    ($result:expr) => {
        match $result {
            Ok(doc) => CPDF_DOCS.insert(doc),
            Err(e) => {
                set_error(1, &e.to_string());
                0
            }
        }
    };
}

fn cstr_to_str(ptr: *const c_char) -> &'static str {
    if ptr.is_null() {
        ""
    } else {
        unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or("")
    }
}

/// Get info dict value by key. Free with `cpdf_freeString`.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_getInfo(doc: Handle, key: *const c_char) -> *mut c_char {
    clear_error();
    let d = get_doc_ref!(doc);
    let guard = d.lock().unwrap();
    let k = cstr_to_str(key);
    match metadata::get_info(&guard, k) {
        Ok(s) => CString::new(s)
            .map(|cs| cs.into_raw())
            .unwrap_or(std::ptr::null_mut()),
        Err(e) => {
            set_error(1, &e.to_string());
            std::ptr::null_mut()
        }
    }
}

/// Set info dict value. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_setInfo(doc: Handle, key: *const c_char, value: *const c_char) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    let k = cstr_to_str(key);
    let v = cstr_to_str(value);
    ok_or_error!(metadata::set_info(d, k, v))
}

/// Remove info dict. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_removeInfo(doc: Handle) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    ok_or_error!(metadata::remove_info(d))
}

/// Get XMP metadata. Caller frees with `cpdf_free`.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_getXMP(doc: Handle, len_out: *mut usize) -> *mut u8 {
    clear_error();
    let d = match CPDF_DOCS.get(doc) {
        Some(arc) => arc,
        None => {
            set_error(1, &format!("invalid document handle: {}", doc));
            return std::ptr::null_mut();
        }
    };
    let guard = d.lock().unwrap();
    match metadata::get_xmp(&guard) {
        Ok(Some(xml)) => {
            let bytes = xml.into_bytes();
            let len = bytes.len();
            let ptr = unsafe {
                let layout = std::alloc::Layout::from_size_align_unchecked(len, 1);
                let p = std::alloc::alloc(layout);
                if p.is_null() {
                    set_error(1, "allocation failed");
                    return std::ptr::null_mut();
                }
                std::ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
                p
            };
            if !len_out.is_null() {
                unsafe { *len_out = len };
            }
            ptr
        }
        Ok(None) => {
            if !len_out.is_null() {
                unsafe { *len_out = 0 };
            }
            std::ptr::null_mut()
        }
        Err(e) => {
            set_error(1, &e.to_string());
            std::ptr::null_mut()
        }
    }
}

/// Set XMP metadata. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_setXMP(doc: Handle, data: *const u8, len: usize) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    if data.is_null() || len == 0 {
        set_error(1, "null or empty XMP data");
        return 0;
    }
    let slice = unsafe { std::slice::from_raw_parts(data, len) };
    let xml = match std::str::from_utf8(slice) {
        Ok(s) => s,
        Err(e) => {
            set_error(1, &format!("invalid UTF-8: {e}"));
            return 0;
        }
    };
    ok_or_error!(metadata::set_xmp(d, xml))
}

/// Remove XMP metadata. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_removeXMP(doc: Handle) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    ok_or_error!(metadata::remove_xmp(d))
}

/// Set PDF version. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_setPDFVersion(doc: Handle, major: c_int, minor: c_int) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    ok_or_error!(metadata::set_pdf_version(d, major as u8, minor as u8))
}

/// Get page layout. Free with `cpdf_freeString`.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_getPageLayout(doc: Handle) -> *mut c_char {
    clear_error();
    let d = match CPDF_DOCS.get(doc) {
        Some(arc) => arc,
        None => {
            set_error(1, &format!("invalid document handle: {}", doc));
            return std::ptr::null_mut();
        }
    };
    let guard = d.lock().unwrap();
    match metadata::get_page_layout(&guard) {
        Ok(s) => CString::new(s)
            .map(|cs| cs.into_raw())
            .unwrap_or(std::ptr::null_mut()),
        Err(e) => {
            set_error(1, &e.to_string());
            std::ptr::null_mut()
        }
    }
}

/// Set page layout. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_setPageLayout(doc: Handle, layout: *const c_char) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    let l = cstr_to_str(layout);
    ok_or_error!(metadata::set_page_layout(d, l))
}

/// Get page mode. Free with `cpdf_freeString`.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_getPageMode(doc: Handle) -> *mut c_char {
    clear_error();
    let d = match CPDF_DOCS.get(doc) {
        Some(arc) => arc,
        None => {
            set_error(1, &format!("invalid document handle: {}", doc));
            return std::ptr::null_mut();
        }
    };
    let guard = d.lock().unwrap();
    match metadata::get_page_mode(&guard) {
        Ok(s) => CString::new(s)
            .map(|cs| cs.into_raw())
            .unwrap_or(std::ptr::null_mut()),
        Err(e) => {
            set_error(1, &e.to_string());
            std::ptr::null_mut()
        }
    }
}

/// Set page mode. Returns new doc handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_setPageMode(doc: Handle, mode: *const c_char) -> Handle {
    clear_error();
    let d = get_doc_clone!(doc);
    let m = cstr_to_str(mode);
    ok_or_error!(metadata::set_page_mode(d, m))
}
