use std::ptr;

use crate::annotation::PdfAnnotation;
use crate::error::{PdfKitError, Result};
use crate::ffi;
use crate::handle::ObjectHandle;
use crate::selection::PdfSelection;
use crate::types::{DisplayBox, PdfPageImageInitializationOptions, PdfPoint, PdfRect};
use crate::util::{c_string, take_string};

/// Wraps `PDFPage`.
#[derive(Debug, Clone)]
pub struct PdfPage {
    handle: ObjectHandle,
}

impl PdfPage {
    pub(crate) fn from_handle(handle: ObjectHandle) -> Self {
        Self { handle }
    }

    /// Wraps `PDFPage()`.
    pub fn new() -> Result<Self> {
        let mut out_page = ptr::null_mut();
        let mut out_error = ptr::null_mut();
        let status = unsafe { ffi::pdf_page_new(&mut out_page, &mut out_error) };
        crate::util::status_result(status, out_error)?;
        Ok(Self::from_handle(crate::util::required_handle(
            out_page, "PDFPage",
        )?))
    }

    /// Wraps the image-based `PDFPage` initializer.
    pub fn from_image_data(
        image_data: &[u8],
        options: &PdfPageImageInitializationOptions,
    ) -> Result<Self> {
        let options_json = serde_json::to_string(options).map_err(|error| {
            PdfKitError::new(
                ffi::status::FRAMEWORK,
                format!("failed to encode PDFPage image initialization options: {error}"),
            )
        })?;
        let options_json = c_string(&options_json)?;
        let mut out_page = ptr::null_mut();
        let mut out_error = ptr::null_mut();
        let status = unsafe {
            ffi::pdf_page_new_with_image_data(
                image_data.as_ptr(),
                image_data.len(),
                options_json.as_ptr(),
                &mut out_page,
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)?;
        Ok(Self::from_handle(crate::util::required_handle(
            out_page, "PDFPage",
        )?))
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn label(&self) -> Option<String> {
        take_string(unsafe { ffi::pdf_page_label_string(self.handle.as_ptr()) })
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn string(&self) -> Option<String> {
        take_string(unsafe { ffi::pdf_page_string(self.handle.as_ptr()) })
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn number_of_characters(&self) -> usize {
        unsafe { ffi::pdf_page_number_of_characters(self.handle.as_ptr()) as usize }
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn rotation(&self) -> i32 {
        unsafe { ffi::pdf_page_rotation(self.handle.as_ptr()) }
    }

    /// Wraps the corresponding `PDFPage` API.
    pub fn set_rotation(&self, rotation: i32) -> Result<()> {
        let mut out_error = ptr::null_mut();
        let status =
            unsafe { ffi::pdf_page_set_rotation(self.handle.as_ptr(), rotation, &mut out_error) };
        crate::util::status_result(status, out_error)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn bounds(&self, display_box: DisplayBox) -> PdfRect {
        let mut x = 0.0_f64;
        let mut y = 0.0_f64;
        let mut width = 0.0_f64;
        let mut height = 0.0_f64;
        unsafe {
            ffi::pdf_page_bounds(
                self.handle.as_ptr(),
                display_box.as_raw(),
                &mut x,
                &mut y,
                &mut width,
                &mut height,
            );
        }
        PdfRect {
            x,
            y,
            width,
            height,
        }
    }

    /// Wraps the corresponding `PDFPage` API.
    pub fn set_bounds(&self, display_box: DisplayBox, bounds: PdfRect) -> Result<()> {
        let mut out_error = ptr::null_mut();
        let status = unsafe {
            ffi::pdf_page_set_bounds(
                self.handle.as_ptr(),
                display_box.as_raw(),
                bounds.x,
                bounds.y,
                bounds.width,
                bounds.height,
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn document(&self) -> Option<crate::document::PdfDocument> {
        let ptr = unsafe { ffi::pdf_page_document(self.handle.as_ptr()) };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }
            .map(crate::document::PdfDocument::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn annotation_count(&self) -> usize {
        unsafe { ffi::pdf_page_annotation_count(self.handle.as_ptr()) as usize }
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn annotation(&self, index: usize) -> Option<PdfAnnotation> {
        let ptr = unsafe { ffi::pdf_page_annotation_at(self.handle.as_ptr(), index as u64) };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfAnnotation::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn annotations(&self) -> Vec<PdfAnnotation> {
        (0..self.annotation_count())
            .filter_map(|index| self.annotation(index))
            .collect()
    }

    /// Wraps the corresponding `PDFPage` API.
    pub fn add_annotation(&self, annotation: &PdfAnnotation) -> Result<()> {
        let mut out_error = ptr::null_mut();
        let status = unsafe {
            ffi::pdf_page_add_annotation(
                self.handle.as_ptr(),
                annotation.as_handle_ptr(),
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)
    }

    /// Wraps the corresponding `PDFPage` API.
    pub fn remove_annotation(&self, annotation: &PdfAnnotation) -> Result<()> {
        let mut out_error = ptr::null_mut();
        let status = unsafe {
            ffi::pdf_page_remove_annotation(
                self.handle.as_ptr(),
                annotation.as_handle_ptr(),
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn annotation_at_point(&self, point: PdfPoint) -> Option<PdfAnnotation> {
        let ptr =
            unsafe { ffi::pdf_page_annotation_at_point(self.handle.as_ptr(), point.x, point.y) };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfAnnotation::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn selection_for_range(&self, location: usize, length: usize) -> Option<PdfSelection> {
        let ptr = unsafe {
            ffi::pdf_page_selection_for_range(self.handle.as_ptr(), location as u64, length as u64)
        };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfSelection::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn selection_for_rect(&self, rect: PdfRect) -> Option<PdfSelection> {
        let ptr = unsafe {
            ffi::pdf_page_selection_for_rect(
                self.handle.as_ptr(),
                rect.x,
                rect.y,
                rect.width,
                rect.height,
            )
        };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfSelection::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn selection_for_word_at_point(&self, point: PdfPoint) -> Option<PdfSelection> {
        let ptr = unsafe {
            ffi::pdf_page_selection_for_word_at_point(self.handle.as_ptr(), point.x, point.y)
        };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfSelection::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn selection_for_line_at_point(&self, point: PdfPoint) -> Option<PdfSelection> {
        let ptr = unsafe {
            ffi::pdf_page_selection_for_line_at_point(self.handle.as_ptr(), point.x, point.y)
        };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfSelection::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn selection_from_points(&self, start: PdfPoint, end: PdfPoint) -> Option<PdfSelection> {
        let ptr = unsafe {
            ffi::pdf_page_selection_from_points(
                self.handle.as_ptr(),
                start.x,
                start.y,
                end.x,
                end.y,
            )
        };
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(PdfSelection::from_handle)
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn character_bounds_at(&self, index: usize) -> PdfRect {
        let mut x = 0.0_f64;
        let mut y = 0.0_f64;
        let mut width = 0.0_f64;
        let mut height = 0.0_f64;
        unsafe {
            ffi::pdf_page_character_bounds_at(
                self.handle.as_ptr(),
                index as u64,
                &mut x,
                &mut y,
                &mut width,
                &mut height,
            );
        }
        PdfRect {
            x,
            y,
            width,
            height,
        }
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn character_index_at_point(&self, point: PdfPoint) -> Option<usize> {
        let index = unsafe {
            ffi::pdf_page_character_index_at_point(self.handle.as_ptr(), point.x, point.y)
        };
        (index != i64::MAX)
            .then_some(index)
            .and_then(|index| usize::try_from(index).ok())
    }

    /// Wraps the corresponding `PDFPage` API.
    #[must_use]
    pub fn displays_annotations(&self) -> bool {
        unsafe { ffi::pdf_page_displays_annotations(self.handle.as_ptr()) != 0 }
    }

    /// Wraps the corresponding `PDFPage` API.
    pub fn set_displays_annotations(&self, value: bool) {
        unsafe { ffi::pdf_page_set_displays_annotations(self.handle.as_ptr(), i32::from(value)) };
    }

    pub(crate) fn as_handle_ptr(&self) -> *mut core::ffi::c_void {
        self.handle.as_ptr()
    }
}
