use alloc::ffi::CString;

use crate::error::{Error, Result};
use crate::paint::Paint;
use thorvg_sys as sys;

/// Image filtering method used during scaling or transformation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FilterMethod {
    /// Smooth interpolation using surrounding pixels.
    Bilinear,
    /// Fast filtering using nearest-neighbor sampling.
    Nearest,
}

impl FilterMethod {
    fn to_raw(self) -> sys::Tvg_Filter_Method {
        match self {
            FilterMethod::Bilinear => sys::Tvg_Filter_Method::TVG_FILTER_METHOD_BILINEAR,
            FilterMethod::Nearest => sys::Tvg_Filter_Method::TVG_FILTER_METHOD_NEAREST,
        }
    }
}

/// Picture data format passed to [`Picture::load_data`].
///
/// Maps to the mime strings thorvg's loader manager recognises (see
/// `tvgLoaderMgr.cpp`).  Runtime availability of each loader
/// depends on the `thorvg-sys` features enabled (e.g. `svg`,
/// `png`, `lottie`); selecting a format whose loader isn't
/// compiled returns `Error::NonSupport`.
///
/// GIF is intentionally not represented here: thorvg's loader
/// manager has no mime string or extension for it (it is detected
/// by content sniffing only), so there is no mime value a
/// `MimeType::Gif` could carry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MimeType {
    /// Scalable Vector Graphics (`svg`, `svg+xml`).
    Svg,
    /// PNG bitmap (`png`).
    Png,
    /// JPEG bitmap (`jpg`, `jpeg`).
    Jpg,
    /// WebP bitmap (`webp`).
    Webp,
    /// Lottie animation (`lot`, `lottie+json`).
    Lottie,
    /// Raw pixel buffer (`raw`).
    Raw,
}

impl MimeType {
    fn as_c_str(self) -> &'static core::ffi::CStr {
        match self {
            MimeType::Svg => c"svg",
            MimeType::Png => c"png",
            MimeType::Jpg => c"jpg",
            MimeType::Webp => c"webp",
            MimeType::Lottie => c"lottie+json",
            MimeType::Raw => c"raw",
        }
    }
}

/// A picture object for loading and displaying images (SVG, PNG, JPG, Lottie, etc.).
///
/// The lifetime `'eng` ties this picture to a [`Thorvg`](crate::Thorvg) engine
/// instance. Create pictures via [`Thorvg::picture()`](crate::Thorvg::picture).
///
/// # Thread Safety
///
/// `Picture` is [`Send`] but not [`Sync`].
pub struct Picture<'eng> {
    raw: sys::Tvg_Paint,
    owned: bool,
    /// Type-erased asset resolver, kept alive for the picture's
    /// lifetime.  The closure lives in its own `Box<F>` on the
    /// heap; `data` is a thin pointer to that allocation, which
    /// thorvg stores verbatim and feeds back to the monomorphized
    /// trampoline.  Moving the `Picture` does not invalidate the
    /// pointer because the closure stays at its heap address.
    /// `None` until [`set_asset_resolver`](Self::set_asset_resolver) is called.
    resolver: Option<ErasedResolver>,
    _engine: core::marker::PhantomData<&'eng ()>,
}

/// Owning, type-erased handle to a heap-allocated resolver closure.
///
/// Avoids the `Box<Box<dyn ...>>` double-indirection a `dyn` resolver
/// would require: each `set_asset_resolver::<F>` call monomorphizes
/// its own trampoline that casts the thin `data` pointer back to
/// `*mut F` directly, and `drop_fn` reconstructs the original
/// `Box<F>` so the right `Drop` runs.
struct ErasedResolver {
    /// Thin pointer to a heap-allocated `F` produced by
    /// `Box::into_raw(Box::<F>::new(...))`.
    data: core::ptr::NonNull<()>,
    /// Reconstructs and drops the original `Box<F>`.  Set to the
    /// monomorphized `drop_resolver::<F>` at construction so the
    /// concrete type is preserved across erasure.
    drop_fn: unsafe fn(core::ptr::NonNull<()>),
}

impl Drop for ErasedResolver {
    fn drop(&mut self) {
        // SAFETY: `data` was produced by `Box::<F>::into_raw` and
        // `drop_fn` was set to `drop_resolver::<F>` for the same
        // `F` at construction time, so the cast inside is sound.
        unsafe { (self.drop_fn)(self.data) }
    }
}

// SAFETY: `set_asset_resolver` requires `F: Send`, so the boxed
// closure behind `data` is `Send`.  `drop_fn` is a plain function
// pointer.
unsafe impl Send for ErasedResolver {}

/// Monomorphized destructor used by [`ErasedResolver`] to drop the
/// original `Box<F>` behind the erased `data` pointer.
unsafe fn drop_resolver<F>(data: core::ptr::NonNull<()>) {
    // SAFETY: caller guarantees `data` originated from
    // `Box::<F>::into_raw` for this same `F`.
    drop(unsafe { alloc::boxed::Box::from_raw(data.as_ptr().cast::<F>()) });
}

// SAFETY: `Picture` exclusively owns (or borrows) a heap-allocated ThorVG
// paint handle.  Shared global state is mutex-protected in C++.  Sole
// ownership transfer to another thread is safe.
unsafe impl Send for Picture<'_> {}

impl Picture<'_> {
    /// Creates a new Picture object.
    pub(crate) fn new() -> Result<Self> {
        let raw = unsafe { sys::tvg_picture_new() };
        if raw.is_null() {
            return Err(Error::FailedAllocation);
        }
        Ok(Self {
            raw,
            owned: true,
            resolver: None,
            _engine: core::marker::PhantomData,
        })
    }

    /// Wraps an existing raw paint pointer.
    ///
    /// # Safety
    /// The pointer must be a valid `Tvg_Paint` of type Picture.
    pub(crate) unsafe fn from_raw(raw: sys::Tvg_Paint, owned: bool) -> Self {
        Self {
            raw,
            owned,
            resolver: None,
            _engine: core::marker::PhantomData,
        }
    }

    /// Loads a picture from a file path string.
    ///
    /// # Runtime requirements
    ///
    /// thorvg reads the file with the C runtime (`fopen`/`fread`), so
    /// this requires a working filesystem at runtime even though it
    /// compiles under `no_std`. On bare-metal targets with no libc
    /// filesystem it returns an error; embed the asset and use
    /// [`load_data_static`](Self::load_data_static) instead.
    pub fn load_from_str(&mut self, path: &str) -> Result<()> {
        let c_path = CString::new(path)?;
        Error::from_raw(unsafe { sys::tvg_picture_load(self.raw, c_path.as_ptr()) })
    }

    /// Loads a picture from a file path.
    #[cfg(feature = "std")]
    pub fn load<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
        self.load_from_str(&path.as_ref().to_string_lossy())
    }

    /// Loads a picture from memory, copying `data` into thorvg.
    ///
    /// `mime` selects the loader (see [`MimeType`]). `resource_path`
    /// is the base directory for SVG external assets; pass `None`
    /// for self-contained content.
    ///
    /// For zero-copy loading of `'static` buffers (e.g.
    /// `include_bytes!(...)`), use [`load_data_static`](Self::load_data_static).
    pub fn load_data(
        &mut self,
        data: &[u8],
        mime: MimeType,
        resource_path: Option<&str>,
    ) -> Result<()> {
        load_data_inner(self.raw, data, mime, resource_path, /* copy = */ true)
    }

    /// Loads a picture from `'static` memory without copying.
    ///
    /// thorvg borrows `data`; the `'static` bound enforces at the type
    /// level that the buffer outlives the picture. Typical use:
    /// `pic.load_data_static(include_bytes!("logo.svg"), MimeType::Svg, None)`.
    ///
    /// # Compile-time safety
    ///
    /// ```compile_fail,E0597
    /// let engine = thorvg::Thorvg::init(0).unwrap();
    /// let mut pic = engine.picture().unwrap();
    /// let local = vec![0u8; 32];
    /// pic.load_data_static(&local, thorvg::MimeType::Svg, None).unwrap();
    /// // error[E0597]: `local` does not live long enough
    /// ```
    pub fn load_data_static(
        &mut self,
        data: &'static [u8],
        mime: MimeType,
        resource_path: Option<&str>,
    ) -> Result<()> {
        load_data_inner(self.raw, data, mime, resource_path, /* copy = */ false)
    }

    /// Loads raw image data (pixel buffer), copying `data` into thorvg.
    ///
    /// For zero-copy loading of `'static` buffers, use
    /// [`load_raw_static`](Self::load_raw_static).
    pub fn load_raw(
        &mut self,
        data: &[u32],
        w: u32,
        h: u32,
        cs: crate::ColorSpace,
    ) -> Result<()> {
        load_raw_inner(self.raw, data, w, h, cs, /* copy = */ true)
    }

    /// Loads raw image data from `'static` memory without copying.
    ///
    /// thorvg borrows `data`; the `'static` bound enforces at the type
    /// level that the buffer outlives the picture.
    pub fn load_raw_static(
        &mut self,
        data: &'static [u32],
        w: u32,
        h: u32,
        cs: crate::ColorSpace,
    ) -> Result<()> {
        load_raw_inner(self.raw, data, w, h, cs, /* copy = */ false)
    }

    /// Resizes the picture content.
    pub fn set_size(&mut self, w: f32, h: f32) -> Result<()> {
        Error::from_raw(unsafe { sys::tvg_picture_set_size(self.raw, w, h) })
    }

    /// Gets the size of the loaded picture.
    pub fn size(&self) -> Result<(f32, f32)> {
        let (mut w, mut h) = (0.0f32, 0.0f32);
        Error::from_raw(unsafe { sys::tvg_picture_get_size(self.raw, &raw mut w, &raw mut h) })?;
        Ok((w, h))
    }

    /// Sets the normalized origin point.
    pub fn set_origin(&mut self, x: f32, y: f32) -> Result<()> {
        Error::from_raw(unsafe { sys::tvg_picture_set_origin(self.raw, x, y) })
    }

    /// Gets the normalized origin point.
    pub fn origin(&self) -> Result<(f32, f32)> {
        let (mut x, mut y) = (0.0f32, 0.0f32);
        Error::from_raw(unsafe { sys::tvg_picture_get_origin(self.raw, &raw mut x, &raw mut y) })?;
        Ok((x, y))
    }

    /// Sets the image filtering method.
    pub fn set_filter(&mut self, method: FilterMethod) -> Result<()> {
        Error::from_raw(unsafe { sys::tvg_picture_set_filter(self.raw, method.to_raw()) })
    }

    /// Enables or disables accessible mode for efficient ID-based lookup.
    pub fn set_accessible(&mut self, accessible: bool) -> Result<()> {
        Error::from_raw(unsafe { sys::tvg_picture_set_accessible(self.raw, accessible) })
    }

    /// Installs a Rust closure as the external-asset resolver.
    ///
    /// thorvg invokes `resolver` from inside [`Picture::load`] /
    /// [`Picture::load_from_str`] whenever the loaded document
    /// references an external image, font, etc.  The closure
    /// receives the requested asset src and returns either the
    /// resolved bytes paired with a [`MimeType`], or `None` to
    /// signal that the asset cannot be supplied.
    ///
    /// The closure is stored inside this `Picture` and lives for
    /// the picture's lifetime; calling `set_asset_resolver` again
    /// replaces the previous closure (and drops it).
    ///
    /// Must be called **before** [`Picture::load`] /
    /// [`Picture::load_from_str`] for asset resolution to kick in.
    ///
    /// ```ignore
    /// pic.set_asset_resolver(|src| {
    ///     let bytes = my_loader.fetch(src)?;
    ///     Some((bytes, thorvg::MimeType::Png))
    /// })?;
    /// pic.load_from_str("scene.svg")?;
    /// ```
    pub fn set_asset_resolver<F>(&mut self, resolver: F) -> Result<()>
    where
        F: FnMut(&str) -> Option<(alloc::vec::Vec<u8>, MimeType)> + Send + 'static,
    {
        // Detach any previous resolver from the C side first.
        // Without this, replacing `self.resolver` would drop the
        // old Box while the C side still holds its address — any
        // asset resolution in between would dereference freed memory.
        if self.resolver.is_some() {
            unsafe {
                sys::tvg_picture_set_asset_resolver(
                    self.raw,
                    None,
                    core::ptr::null_mut(),
                );
            }
            self.resolver = None;
        }
        // Heap-allocate the concrete closure and hand thorvg a thin
        // pointer to it.  No `dyn`, no double box: the monomorphized
        // `resolver_trampoline::<F>` casts the pointer back to
        // `*mut F` and calls `F` directly.
        let boxed: alloc::boxed::Box<F> = alloc::boxed::Box::new(resolver);
        let raw_f: *mut F = alloc::boxed::Box::into_raw(boxed);
        // SAFETY: `Box::into_raw` never returns null.
        let data = unsafe { core::ptr::NonNull::new_unchecked(raw_f.cast::<()>()) };
        self.resolver = Some(ErasedResolver {
            data,
            drop_fn: drop_resolver::<F>,
        });
        // SAFETY: `data` references a heap allocation owned by
        // `self.resolver`; `Picture::Drop` unregisters the resolver
        // before that allocation is freed, so C never dereferences
        // a dangling pointer.
        Error::from_raw(unsafe {
            sys::tvg_picture_set_asset_resolver(
                self.raw,
                Some(resolver_trampoline::<F>),
                data.as_ptr().cast::<core::ffi::c_void>(),
            )
        })
    }

    /// Removes any previously installed asset resolver.
    pub fn clear_asset_resolver(&mut self) -> Result<()> {
        let r = Error::from_raw(unsafe {
            sys::tvg_picture_set_asset_resolver(self.raw, None, core::ptr::null_mut())
        });
        self.resolver = None;
        r
    }

    /// Retrieves a paint object from the picture scene by its unique ID.
    ///
    /// The returned [`BorrowedPaint`] is owned by the picture's scene
    /// graph; its lifetime is tied to `&self`, so it cannot outlive
    /// the picture.  Use [`BorrowedPaint::paint_type`] to dispatch on
    /// the runtime type.
    pub fn get_paint(&self, id: u32) -> Option<crate::paint::BorrowedPaint<'_>> {
        let raw = unsafe { sys::tvg_picture_get_paint(self.raw, id) };
        if raw.is_null() {
            None
        } else {
            // SAFETY: `raw` is owned by `self`'s scene graph and is
            // valid for as long as `&self` is borrowed.
            Some(unsafe { crate::paint::BorrowedPaint::from_raw(raw) })
        }
    }
}

impl crate::paint::sealed::Sealed for Picture<'_> {}

impl Paint for Picture<'_> {
    fn raw(&self) -> sys::Tvg_Paint {
        self.raw
    }

    fn into_raw(mut self) -> sys::Tvg_Paint {
        self.owned = false;
        self.raw
    }

    unsafe fn from_raw_paint(raw: sys::Tvg_Paint) -> Self {
        Self {
            raw,
            owned: true,
            resolver: None,
            _engine: core::marker::PhantomData,
        }
    }
}

impl Drop for Picture<'_> {
    fn drop(&mut self) {
        // Unregister any installed resolver BEFORE freeing the C
        // handle and our resolver Box.  The C side keeps a pointer
        // into our Box for asset resolution; if we let the Box drop
        // first, a subsequent resolution would dereference freed
        // memory.  This matters for the `owned == false` branch too
        // because the underlying paint may outlive this wrapper.
        if self.resolver.is_some() {
            unsafe {
                sys::tvg_picture_set_asset_resolver(self.raw, None, core::ptr::null_mut());
            }
        }
        if self.owned {
            unsafe {
                sys::tvg_paint_rel(self.raw);
            }
        }
    }
}

#[allow(clippy::cast_possible_truncation)]
fn load_data_inner(
    raw: sys::Tvg_Paint,
    data: &[u8],
    mime: MimeType,
    resource_path: Option<&str>,
    copy: bool,
) -> Result<()> {
    let c_rpath = resource_path.map(CString::new).transpose()?;
    let rpath_ptr = c_rpath.as_ref().map_or(core::ptr::null(), |c| c.as_ptr());
    Error::from_raw(unsafe {
        sys::tvg_picture_load_data(
            raw,
            data.as_ptr().cast::<core::ffi::c_char>(),
            data.len() as u32,
            mime.as_c_str().as_ptr(),
            rpath_ptr,
            copy,
        )
    })
}

fn load_raw_inner(
    raw: sys::Tvg_Paint,
    data: &[u32],
    w: u32,
    h: u32,
    cs: crate::ColorSpace,
    copy: bool,
) -> Result<()> {
    // thorvg's `RawLoader::open` does an unchecked `memcpy` of
    // `w * h * sizeof(u32)` from the supplied pointer (see upstream
    // `tvgRawLoader.cpp`).  Guarantee here that `data` actually has
    // that many elements before crossing the FFI boundary.
    //
    // Same pattern as `SwCanvas::set_target`: compute in `u64` so the
    // `u32 * u32` product cannot wrap and slip a too-small buffer past
    // the check.
    let Some(needed) = u64::from(w).checked_mul(u64::from(h)) else {
        return Err(Error::InvalidArguments);
    };
    if (data.len() as u64) < needed {
        return Err(Error::InvalidArguments);
    }
    Error::from_raw(unsafe { sys::tvg_picture_load_raw(raw, data.as_ptr(), w, h, cs.to_raw(), copy) })
}

/// FFI trampoline that bridges thorvg's C callback to the boxed
/// Rust closure stored in [`Picture::resolver`].  Monomorphized on
/// the concrete closure type `F`, so the call site dispatches
/// directly (no `dyn` vtable, no double indirection): `data` is the
/// thin `*mut F` produced by [`Picture::set_asset_resolver`].
unsafe extern "C" fn resolver_trampoline<F>(
    paint: sys::Tvg_Paint,
    src: *const core::ffi::c_char,
    data: *mut core::ffi::c_void,
) -> bool
where
    F: FnMut(&str) -> Option<(alloc::vec::Vec<u8>, MimeType)> + Send + 'static,
{
    if data.is_null() || src.is_null() {
        return false;
    }
    let f = unsafe { &mut *data.cast::<F>() };
    let src_str = unsafe { core::ffi::CStr::from_ptr(src) }.to_string_lossy();
    // SAFETY: user closure runs in Rust context; a panic here would
    // unwind across the C++ caller above us, which is UB.  Catch and
    // convert to a "not resolved" return.  In `no_std` builds the
    // crate-level docs require `panic = "abort"`, which makes panic
    // termination strictly safer (the process is gone before unwinding
    // could reach the FFI boundary).
    let resolved = invoke_resolver::<F>(f, &src_str);
    let Some((bytes, mime)) = resolved else {
        return false;
    };
    // Copy into thorvg so the consumer's Vec can drop after return.
    #[allow(clippy::cast_possible_truncation)]
    let r = unsafe {
        sys::tvg_picture_load_data(
            paint,
            bytes.as_ptr().cast::<core::ffi::c_char>(),
            bytes.len() as u32,
            mime.as_c_str().as_ptr(),
            core::ptr::null(),
            true,
        )
    };
    r == sys::Tvg_Result::TVG_RESULT_SUCCESS
}

#[cfg(feature = "std")]
fn invoke_resolver<F>(f: &mut F, src: &str) -> Option<(alloc::vec::Vec<u8>, MimeType)>
where
    F: FnMut(&str) -> Option<(alloc::vec::Vec<u8>, MimeType)> + Send + 'static,
{
    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(src))).unwrap_or(None)
}

#[cfg(not(feature = "std"))]
fn invoke_resolver<F>(f: &mut F, src: &str) -> Option<(alloc::vec::Vec<u8>, MimeType)>
where
    F: FnMut(&str) -> Option<(alloc::vec::Vec<u8>, MimeType)> + Send + 'static,
{
    // `no_std` users are required to build with `panic = "abort"`
    // (see crate docs).  An aborting panic cannot cross the FFI
    // boundary, so no `catch_unwind` is needed.
    f(src)
}

impl core::fmt::Debug for Picture<'_> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Picture").finish_non_exhaustive()
    }
}
