use std::ffi::CString;
use std::path::Path;

use pdfium_bind_sys as ffi;

use crate::error::{Error, OpenError};
use crate::page::Page;

/// An opened PDF document.
///
/// The document is closed automatically when this value is dropped.
/// All [`Page`] references borrowed from this document must be dropped first.
///
/// # Example
///
/// ```no_run
/// # let _lib = pdfium_bind::Library::init();
/// let doc = pdfium_bind::Document::open("input.pdf").unwrap();
/// println!("pages: {}", doc.page_count());
/// ```
pub struct Document {
    pub(crate) handle: ffi::FPDF_DOCUMENT,
}

impl Document {
    /// Open a PDF file at the given path.
    ///
    /// Returns an error if the path is invalid, the file cannot be read,
    /// or the PDF is malformed / password-protected.
    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
        let path = path.as_ref();
        let path_str = path.to_str().ok_or_else(|| Error::InvalidPath(path.to_owned()))?;
        let c_path =
            CString::new(path_str).map_err(|_| Error::InvalidPath(path.to_owned()))?;

        let handle = unsafe { ffi::FPDF_LoadDocument(c_path.as_ptr(), std::ptr::null()) };
        if handle.is_null() {
            let code = unsafe { ffi::FPDF_GetLastError() };
            return Err(Error::OpenFailed {
                path: path.to_owned(),
                cause: OpenError::from_code(code),
            });
        }

        Ok(Self { handle })
    }

    /// Return the number of pages in this document.
    pub fn page_count(&self) -> u32 {
        unsafe { ffi::FPDF_GetPageCount(self.handle) as u32 }
    }

    /// Load a page by zero-based index.
    ///
    /// The returned [`Page`] borrows this document and must be dropped
    /// before the document itself.
    pub fn page(&self, index: u32) -> Result<Page<'_>, Error> {
        let count = self.page_count();
        if index >= count {
            return Err(Error::PageOutOfRange { index, count });
        }

        let handle = unsafe { ffi::FPDF_LoadPage(self.handle, index as i32) };
        if handle.is_null() {
            return Err(Error::PageOutOfRange { index, count });
        }

        Ok(Page {
            handle,
            _doc: self,
        })
    }
}

// SAFETY: FPDF_DOCUMENT is an opaque pointer to a heap-allocated object.
// PDFium operations are not thread-safe for the *same* document, but moving
// a Document to another thread (Send) is safe.
unsafe impl Send for Document {}

impl Drop for Document {
    fn drop(&mut self) {
        unsafe { ffi::FPDF_CloseDocument(self.handle) }
    }
}
