//! Main API for `RsvgHandle`.
//!
//! The C API of librsvg revolves around an `RsvgHandle` GObject class, which is
//! implemented as follows:
//!
//! * [`RsvgHandle`] and [`RsvgHandleClass`] are derivatives of `GObject` and
//! `GObjectClass`.  These are coded explicitly, instead of using
//! [`glib::subclass::prelude::InstanceStruct<T>`] and
//! [`glib::subclass::prelude::ClassStruct<T>`], as the structs need need to be kept
//! ABI-compatible with the traditional C API/ABI.
//!
//! * The actual data for a handle (e.g. the `RsvgHandle`'s private data, in GObject
//! parlance) is in [`CHandle`].
//!
//! * Public C ABI functions are the `#[no_mangle]` functions with an `rsvg_` prefix.
//!
//! The C API is implemented in terms of the Rust API in `librsvg_crate`.  In effect,
//! [`RsvgHandle`] is a rather convoluted builder or adapter pattern that translates all the
//! historical idiosyncrasies of the C API into the simple Rust API.

use std::cell::{Cell, Ref, RefCell, RefMut};
use std::ffi::{CStr, CString, OsStr};
use std::fmt;
use std::path::PathBuf;
use std::ptr;
use std::slice;
use std::str;
use std::{f64, i32};

use gdk_pixbuf::Pixbuf;
use gio::prelude::*;
use glib::error::ErrorDomain;
use url::Url;

use glib::subclass::prelude::*;
use glib::translate::*;
use glib::types::instance_of;
use glib::{ffi::gpointer, gobject_ffi};
use glib::{Bytes, Cast, StaticType, ToValue};

use crate::api::{self, CairoRenderer, IntrinsicDimensions, Loader, LoadingError, SvgHandle};

use crate::{
    length::RsvgLength,
    rsvg_log,
    session::Session,
    surface_utils::shared_surface::{SharedImageSurface, SurfaceType},
};

use super::dpi::Dpi;
use super::messages::{rsvg_g_critical, rsvg_g_warning};
use super::pixbuf_utils::{empty_pixbuf, pixbuf_from_surface};
use super::sizing::LegacySize;

// The C API exports global variables that contain the library's version number;
// those get autogenerated from `build.rs` and placed in this `version.rs` file.
include!(concat!(env!("OUT_DIR"), "/version.rs"));

// This is basically the same as api::RenderingError but with extra cases for
// the peculiarities of the C API.
enum RenderingError {
    RenderingError(api::RenderingError),

    // The RsvgHandle is created, but hasn't been loaded yet.
    HandleIsNotLoaded,
}

impl<T: Into<api::RenderingError>> From<T> for RenderingError {
    fn from(e: T) -> RenderingError {
        RenderingError::RenderingError(e.into())
    }
}

impl fmt::Display for RenderingError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            RenderingError::RenderingError(ref e) => e.fmt(f),
            RenderingError::HandleIsNotLoaded => write!(f, "SVG data is not loaded into handle"),
        }
    }
}

/// Rust version of the `RsvgHandleFlags` enum in C.
#[glib::flags(name = "RsvgHandleFlags")]
pub enum HandleFlags {
    #[flags_value(name = "RSVG_HANDLE_FLAGS_NONE", nick = "flags-none")]
    NONE = 0,

    #[flags_value(name = "RSVG_HANDLE_FLAG_UNLIMITED", nick = "flag-unlimited")]
    UNLIMITED = 1 << 0,

    #[flags_value(
        name = "RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA",
        nick = "flag-keep-image-data"
    )]
    KEEP_IMAGE_DATA = 1 << 1,
}

/// Type alias used to pass flags in the C API functions.
pub type RsvgHandleFlags = u32;

/// Internal representation of the loading flags, without bitflags.
#[derive(Default, Copy, Clone)]
struct LoadFlags {
    unlimited_size: bool,
    keep_image_data: bool,
}

impl From<HandleFlags> for LoadFlags {
    fn from(flags: HandleFlags) -> LoadFlags {
        LoadFlags {
            unlimited_size: flags.contains(HandleFlags::UNLIMITED),
            keep_image_data: flags.contains(HandleFlags::KEEP_IMAGE_DATA),
        }
    }
}

impl From<LoadFlags> for HandleFlags {
    fn from(lflags: LoadFlags) -> HandleFlags {
        let mut hflags = HandleFlags::empty();

        if lflags.unlimited_size {
            hflags.insert(HandleFlags::UNLIMITED);
        }

        if lflags.keep_image_data {
            hflags.insert(HandleFlags::KEEP_IMAGE_DATA);
        }

        hflags
    }
}

/// GObject class struct for RsvgHandle.
///
/// This is not done through [`glib::subclass::prelude::ClassStruct<T>`] because we need
/// to include the `_abi_padding` field for ABI compatibility with the C headers, and
/// `simple::ClassStruct` does not allow that.
#[repr(C)]
pub struct RsvgHandleClass {
    // Keep this in sync with rsvg.h:RsvgHandleClass
    parent: gobject_ffi::GObjectClass,

    _abi_padding: [gpointer; 15],
}

unsafe impl ClassStruct for RsvgHandleClass {
    type Type = imp::CHandle;
}

/// GObject instance struct for RsvgHandle.
///
/// This is not done through [`glib::subclass::prelude::InstanceStruct<T>`] because we need
/// to include the `_abi_padding` field for ABI compatibility with the C headers, and
/// `simple::InstanceStruct` does not allow that.
#[repr(C)]
pub struct RsvgHandle {
    // Keep this in sync with rsvg.h:RsvgHandle
    parent: gobject_ffi::GObject,

    _abi_padding: [gpointer; 16],
}

unsafe impl InstanceStruct for RsvgHandle {
    type Type = imp::CHandle;
}

/// State machine for `RsvgHandle`.
///
/// When an `RsvgHandled` is created it is empty / not loaded yet, and it does not know
/// whether the caller will feed it data gradually with the legacy `write()/close()` API,
/// or whether it will be given a `GInputStream` to read in a blocking fashion.  After the
/// handle is loaded (e.g. the SVG document is finished parsing), we make sure that no
/// further loading operations can be done.
#[allow(clippy::large_enum_variant)]
enum LoadState {
    /// Just created the CHandle; nothing loaded yet.
    Start,

    /// Being loaded using the legacy write()/close() API.
    ///
    /// We buffer all the data from `write()` calls until the time `close()` is called;
    /// then we run the buffer through a decompressor in case this is an SVGZ file.
    Loading { buffer: Vec<u8> },

    /// Loading finished successfully; the document is in the `SvgHandle`.
    ClosedOk { handle: SvgHandle },

    /// Loaded unsuccessfully.
    ClosedError,
}

impl LoadState {
    fn set_from_loading_result(
        &mut self,
        result: Result<SvgHandle, LoadingError>,
    ) -> Result<(), LoadingError> {
        match result {
            Ok(handle) => {
                *self = LoadState::ClosedOk { handle };
                Ok(())
            }

            Err(e) => {
                *self = LoadState::ClosedError;
                Err(e)
            }
        }
    }
}

impl Default for LoadState {
    fn default() -> Self {
        Self::Start
    }
}

/// Holds the base URL for loading a handle, and the C-accessible version of it
///
/// There is a public API to query the base URL, and we need to
/// produce a CString with it.  However, that API returns a borrowed
/// *const char, so we need to maintain a long-lived CString along with the
/// internal Url.
#[derive(Default)]
struct BaseUrl {
    inner: Option<BaseUrlInner>,
}

struct BaseUrlInner {
    url: Url,
    cstring: CString,
}

impl BaseUrl {
    fn set(&mut self, url: Url) {
        let cstring = CString::new(url.as_str()).unwrap();

        self.inner = Some(BaseUrlInner { url, cstring });
    }

    fn get(&self) -> Option<&Url> {
        self.inner.as_ref().map(|b| &b.url)
    }

    fn get_gfile(&self) -> Option<gio::File> {
        self.get().map(|url| gio::File::for_uri(url.as_str()))
    }

    fn get_ptr(&self) -> *const libc::c_char {
        self.inner
            .as_ref()
            .map(|b| b.cstring.as_ptr())
            .unwrap_or_else(ptr::null)
    }
}

#[derive(Clone, Copy)]
#[repr(C)]
pub struct RsvgRectangle {
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
}

impl From<cairo::Rectangle> for RsvgRectangle {
    fn from(r: cairo::Rectangle) -> RsvgRectangle {
        RsvgRectangle {
            x: r.x(),
            y: r.y(),
            width: r.width(),
            height: r.height(),
        }
    }
}

impl From<RsvgRectangle> for cairo::Rectangle {
    fn from(r: RsvgRectangle) -> cairo::Rectangle {
        cairo::Rectangle::new(r.x, r.y, r.width, r.height)
    }
}

mod imp {
    use super::*;
    use glib::{ParamSpec, ParamSpecDouble, ParamSpecFlags, ParamSpecInt, ParamSpecString};
    use once_cell::sync::Lazy;

    /// Contains all the interior mutability for a RsvgHandle to be called
    /// from the C API.
    #[derive(Default)]
    pub struct CHandle {
        pub(super) inner: RefCell<CHandleInner>,
        pub(super) load_state: RefCell<LoadState>,
        pub(super) session: Session,
    }

    #[derive(Default)]
    pub(super) struct CHandleInner {
        pub(super) dpi: Dpi,
        pub(super) load_flags: LoadFlags,
        pub(super) base_url: BaseUrl,
        pub(super) size_callback: SizeCallback,
        pub(super) is_testing: bool,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for CHandle {
        const NAME: &'static str = "RsvgHandle";

        type Type = super::CHandle;

        type Instance = RsvgHandle;
        type Class = RsvgHandleClass;
    }

    impl ObjectImpl for CHandle {
        fn properties() -> &'static [ParamSpec] {
            static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
                vec![
                    ParamSpecFlags::builder::<HandleFlags>("flags")
                        .construct_only()
                        .build(),
                    ParamSpecDouble::builder("dpi-x").construct().build(),
                    ParamSpecDouble::builder("dpi-y").construct().build(),
                    ParamSpecString::builder("base-uri").construct().build(),
                    ParamSpecInt::builder("width").read_only().build(),
                    ParamSpecInt::builder("height").read_only().build(),
                    ParamSpecDouble::builder("em").read_only().build(),
                    ParamSpecDouble::builder("ex").read_only().build(),
                    ParamSpecString::builder("title").read_only().build(),
                    ParamSpecString::builder("desc").read_only().build(),
                    ParamSpecString::builder("metadata").read_only().build(),
                ]
            });
            PROPERTIES.as_ref()
        }

        fn set_property(&self, id: usize, value: &glib::Value, pspec: &ParamSpec) {
            let obj = self.obj();
            match pspec.name() {
                "flags" => {
                    let v: HandleFlags = value.get().expect("flags value has incorrect type");
                    obj.set_flags(v);
                }

                "dpi-x" => {
                    let dpi_x: f64 = value.get().expect("dpi-x value has incorrect type");
                    obj.set_dpi_x(dpi_x);
                }

                "dpi-y" => {
                    let dpi_y: f64 = value.get().expect("dpi-y value has incorrect type");
                    obj.set_dpi_y(dpi_y);
                }

                "base-uri" => {
                    let v: Option<String> = value.get().expect("base-uri value has incorrect type");

                    // rsvg_handle_set_base_uri() expects non-NULL URI strings,
                    // but the "base-uri" property can be set to NULL due to a missing
                    // construct-time property.

                    if let Some(s) = v {
                        obj.set_base_url(&s);
                    }
                }

                _ => unreachable!("invalid property id {}", id),
            }
        }

        fn property(&self, id: usize, pspec: &ParamSpec) -> glib::Value {
            let obj = self.obj();
            match pspec.name() {
                "flags" => obj.get_flags().to_value(),
                "dpi-x" => obj.get_dpi_x().to_value(),
                "dpi-y" => obj.get_dpi_y().to_value(),
                "base-uri" => obj.get_base_url().to_value(),
                "width" => obj.get_dimensions_or_empty().width.to_value(),
                "height" => obj.get_dimensions_or_empty().height.to_value(),
                "em" => obj.get_dimensions_or_empty().em.to_value(),
                "ex" => obj.get_dimensions_or_empty().ex.to_value(),

                // the following three are deprecated
                "title" => None::<String>.to_value(),
                "desc" => None::<String>.to_value(),
                "metadata" => None::<String>.to_value(),

                _ => unreachable!("invalid property id={} for RsvgHandle", id),
            }
        }
    }
}

glib::wrapper! {
    // We don't use subclass:simple::InstanceStruct and ClassStruct
    // because we need to maintain the respective _abi_padding of each
    // of RsvgHandleClass and RsvgHandle.
    pub struct CHandle(ObjectSubclass<imp::CHandle>);
}

// Keep in sync with tests/src/reference.rs
pub(crate) fn checked_i32(x: f64) -> Result<i32, cairo::Error> {
    cast::i32(x).map_err(|_| cairo::Error::InvalidSize)
}

// Keep in sync with rsvg.h:RsvgPositionData
#[repr(C)]
pub struct RsvgPositionData {
    pub x: libc::c_int,
    pub y: libc::c_int,
}

// Keep in sync with rsvg.h:RsvgDimensionData
#[repr(C)]
pub struct RsvgDimensionData {
    pub width: libc::c_int,
    pub height: libc::c_int,
    pub em: f64,
    pub ex: f64,
}

impl RsvgDimensionData {
    // This is not #[derive(Default)] to make it clear that it
    // shouldn't be the default value for anything; it is actually a
    // special case we use to indicate an error to the public API.
    pub fn empty() -> RsvgDimensionData {
        RsvgDimensionData {
            width: 0,
            height: 0,
            em: 0.0,
            ex: 0.0,
        }
    }
}

// Keep in sync with rsvg.h:RsvgSizeFunc
pub type RsvgSizeFunc = Option<
    unsafe extern "C" fn(
        inout_width: *mut libc::c_int,
        inout_height: *mut libc::c_int,
        user_data: gpointer,
    ),
>;

struct SizeCallback {
    size_func: RsvgSizeFunc,
    user_data: gpointer,
    destroy_notify: glib::ffi::GDestroyNotify,
    in_loop: Cell<bool>,
}

impl SizeCallback {
    fn new(
        size_func: RsvgSizeFunc,
        user_data: gpointer,
        destroy_notify: glib::ffi::GDestroyNotify,
    ) -> Self {
        SizeCallback {
            size_func,
            user_data,
            destroy_notify,
            in_loop: Cell::new(false),
        }
    }

    fn call(&self, width: libc::c_int, height: libc::c_int) -> (libc::c_int, libc::c_int) {
        unsafe {
            let mut w = width;
            let mut h = height;

            if let Some(ref f) = self.size_func {
                f(&mut w, &mut h, self.user_data);
            };

            (w, h)
        }
    }

    fn start_loop(&self) {
        assert!(!self.in_loop.get());
        self.in_loop.set(true);
    }

    fn end_loop(&self) {
        assert!(self.in_loop.get());
        self.in_loop.set(false);
    }

    fn get_in_loop(&self) -> bool {
        self.in_loop.get()
    }
}

impl Default for SizeCallback {
    fn default() -> SizeCallback {
        SizeCallback {
            size_func: None,
            user_data: ptr::null_mut(),
            destroy_notify: None,
            in_loop: Cell::new(false),
        }
    }
}

impl Drop for SizeCallback {
    fn drop(&mut self) {
        unsafe {
            if let Some(ref f) = self.destroy_notify {
                f(self.user_data);
            };
        }
    }
}
pub trait CairoRectangleExt {
    fn from_size(width: f64, height: f64) -> Self;
}

impl CairoRectangleExt for cairo::Rectangle {
    fn from_size(width: f64, height: f64) -> Self {
        Self::new(0.0, 0.0, width, height)
    }
}

impl CHandle {
    fn set_base_url(&self, url: &str) {
        let imp = self.imp();
        let session = &imp.session;
        let state = imp.load_state.borrow();

        match *state {
            LoadState::Start => (),
            _ => {
                rsvg_g_critical(
                    "Please set the base file or URI before loading any data into RsvgHandle",
                );
                return;
            }
        }

        match Url::parse(url) {
            Ok(u) => {
                rsvg_log!(session, "setting base_uri to \"{}\"", u.as_str());
                let mut inner = imp.inner.borrow_mut();
                inner.base_url.set(u);
            }

            Err(e) => {
                rsvg_log!(
                    session,
                    "not setting base_uri to \"{}\" since it is invalid: {}",
                    url,
                    e
                );
            }
        }
    }

    fn set_base_gfile(&self, file: &gio::File) {
        self.set_base_url(&file.uri());
    }

    fn get_base_url(&self) -> Option<String> {
        let inner = self.imp().inner.borrow();
        inner.base_url.get().map(|url| url.as_str().to_string())
    }

    fn get_base_url_as_ptr(&self) -> *const libc::c_char {
        let inner = self.imp().inner.borrow();
        inner.base_url.get_ptr()
    }

    fn set_dpi_x(&self, dpi_x: f64) {
        let mut inner = self.imp().inner.borrow_mut();
        let dpi = inner.dpi;
        inner.dpi = Dpi::new(dpi_x, dpi.y());
    }

    fn set_dpi_y(&self, dpi_y: f64) {
        let mut inner = self.imp().inner.borrow_mut();
        let dpi = inner.dpi;
        inner.dpi = Dpi::new(dpi.x(), dpi_y);
    }

    fn get_dpi_x(&self) -> f64 {
        let inner = self.imp().inner.borrow();
        inner.dpi.x()
    }

    fn get_dpi_y(&self) -> f64 {
        let inner = self.imp().inner.borrow();
        inner.dpi.y()
    }

    fn set_flags(&self, flags: HandleFlags) {
        let mut inner = self.imp().inner.borrow_mut();
        inner.load_flags = LoadFlags::from(flags);
    }

    fn get_flags(&self) -> HandleFlags {
        let inner = self.imp().inner.borrow();
        HandleFlags::from(inner.load_flags)
    }

    fn set_size_callback(
        &self,
        size_func: RsvgSizeFunc,
        user_data: gpointer,
        destroy_notify: glib::ffi::GDestroyNotify,
    ) {
        let mut inner = self.imp().inner.borrow_mut();
        inner.size_callback = SizeCallback::new(size_func, user_data, destroy_notify);
    }

    fn write(&self, buf: &[u8]) {
        let mut state = self.imp().load_state.borrow_mut();

        match *state {
            LoadState::Start => {
                *state = LoadState::Loading {
                    buffer: Vec::from(buf),
                }
            }

            LoadState::Loading { ref mut buffer } => {
                buffer.extend_from_slice(buf);
            }

            _ => {
                rsvg_g_critical("Handle must not be closed in order to write to it");
            }
        }
    }

    fn close(&self) -> Result<(), LoadingError> {
        let imp = self.imp();

        let inner = imp.inner.borrow();
        let mut state = imp.load_state.borrow_mut();

        match *state {
            LoadState::Start => {
                *state = LoadState::ClosedError;
                Err(LoadingError::XmlParseError(String::from(
                    "caller did not write any data",
                )))
            }

            LoadState::Loading { ref buffer } => {
                let bytes = Bytes::from(buffer);
                let stream = gio::MemoryInputStream::from_bytes(&bytes);

                let base_file = inner.base_url.get_gfile();
                self.read_stream(state, &stream.upcast(), base_file.as_ref(), None)
            }

            // Closing is idempotent
            LoadState::ClosedOk { .. } => Ok(()),
            LoadState::ClosedError => Ok(()),
        }
    }

    fn read_stream_sync(
        &self,
        stream: &gio::InputStream,
        cancellable: Option<&gio::Cancellable>,
    ) -> Result<(), LoadingError> {
        let imp = self.imp();

        let state = imp.load_state.borrow_mut();
        let inner = imp.inner.borrow();

        match *state {
            LoadState::Start => {
                let base_file = inner.base_url.get_gfile();
                self.read_stream(state, stream, base_file.as_ref(), cancellable)
            }

            LoadState::Loading { .. } | LoadState::ClosedOk { .. } | LoadState::ClosedError => {
                rsvg_g_critical(
                    "handle must not be already loaded in order to call \
                     rsvg_handle_read_stream_sync()",
                );
                Err(LoadingError::Other(String::from("API ordering")))
            }
        }
    }

    fn read_stream(
        &self,
        mut load_state: RefMut<'_, LoadState>,
        stream: &gio::InputStream,
        base_file: Option<&gio::File>,
        cancellable: Option<&gio::Cancellable>,
    ) -> Result<(), LoadingError> {
        let loader = self.make_loader();

        load_state.set_from_loading_result(loader.read_stream(stream, base_file, cancellable))
    }

    fn get_handle_ref(&self) -> Result<Ref<'_, SvgHandle>, RenderingError> {
        let state = self.imp().load_state.borrow();

        match *state {
            LoadState::Start => {
                rsvg_g_critical("Handle has not been loaded");
                Err(RenderingError::HandleIsNotLoaded)
            }

            LoadState::Loading { .. } => {
                rsvg_g_critical("Handle is still loading; call rsvg_handle_close() first");
                Err(RenderingError::HandleIsNotLoaded)
            }

            LoadState::ClosedError => {
                rsvg_g_critical(
                    "Handle could not read or parse the SVG; did you check for errors during the \
                     loading stage?",
                );
                Err(RenderingError::HandleIsNotLoaded)
            }

            LoadState::ClosedOk { .. } => Ok(Ref::map(state, |s| match *s {
                LoadState::ClosedOk { ref handle } => handle,
                _ => unreachable!(),
            })),
        }
    }

    fn make_loader(&self) -> Loader {
        let imp = self.imp();
        let inner = imp.inner.borrow();
        let session = imp.session.clone();

        Loader::new_with_session(session)
            .with_unlimited_size(inner.load_flags.unlimited_size)
            .keep_image_data(inner.load_flags.keep_image_data)
    }

    fn has_sub(&self, id: &str) -> Result<bool, RenderingError> {
        let handle = self.get_handle_ref()?;
        Ok(handle.has_element_with_id(id)?)
    }

    fn get_dimensions_or_empty(&self) -> RsvgDimensionData {
        self.get_dimensions_sub(None)
            .unwrap_or_else(|_| RsvgDimensionData::empty())
    }

    fn get_dimensions_sub(&self, id: Option<&str>) -> Result<RsvgDimensionData, RenderingError> {
        let inner = self.imp().inner.borrow();

        // This function is probably called from the cairo_render functions,
        // or is being erroneously called within the size_func.
        // To prevent an infinite loop we are saving the state, and
        // returning a meaningless size.
        if inner.size_callback.get_in_loop() {
            return Ok(RsvgDimensionData {
                width: 1,
                height: 1,
                em: 1.0,
                ex: 1.0,
            });
        }

        inner.size_callback.start_loop();

        let res = self
            .get_geometry_sub(id)
            .and_then(|(ink_r, _)| {
                // Keep these in sync with tests/src/reference.rs
                let width = checked_i32(ink_r.width().round())?;
                let height = checked_i32(ink_r.height().round())?;

                Ok((ink_r, width, height))
            })
            .map(|(ink_r, width, height)| {
                let (w, h) = inner.size_callback.call(width, height);

                RsvgDimensionData {
                    width: w,
                    height: h,
                    em: ink_r.width(),
                    ex: ink_r.height(),
                }
            });

        inner.size_callback.end_loop();

        res
    }

    fn get_position_sub(&self, id: Option<&str>) -> Result<RsvgPositionData, RenderingError> {
        let inner = self.imp().inner.borrow();

        if id.is_none() {
            return Ok(RsvgPositionData { x: 0, y: 0 });
        }

        self.get_geometry_sub(id)
            .and_then(|(ink_r, _)| {
                let width = checked_i32(ink_r.width().round())?;
                let height = checked_i32(ink_r.height().round())?;

                Ok((ink_r, width, height))
            })
            .and_then(|(ink_r, width, height)| {
                inner.size_callback.call(width, height);

                Ok(RsvgPositionData {
                    x: checked_i32(ink_r.x())?,
                    y: checked_i32(ink_r.y())?,
                })
            })
    }

    fn make_renderer<'a>(&self, handle_ref: &'a Ref<'_, SvgHandle>) -> CairoRenderer<'a> {
        let inner = self.imp().inner.borrow();

        CairoRenderer::new(handle_ref)
            .with_dpi(inner.dpi.x(), inner.dpi.y())
            .test_mode(inner.is_testing)
    }

    fn get_geometry_sub(
        &self,
        id: Option<&str>,
    ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
        let handle = self.get_handle_ref()?;
        let renderer = self.make_renderer(&handle);

        Ok(renderer.legacy_layer_geometry(id)?)
    }

    fn set_stylesheet(&self, css: &str) -> Result<(), LoadingError> {
        match *self.imp().load_state.borrow_mut() {
            LoadState::ClosedOk { ref mut handle } => handle.set_stylesheet(css),

            _ => {
                rsvg_g_critical(
                    "handle must already be loaded in order to call \
                     rsvg_handle_set_stylesheet()",
                );
                Err(LoadingError::Other(String::from("API ordering")))
            }
        }
    }

    fn render_cairo_sub(
        &self,
        cr: *mut cairo::ffi::cairo_t,
        id: Option<&str>,
    ) -> Result<(), RenderingError> {
        let dimensions = self.get_dimensions_sub(None)?;
        if dimensions.width == 0 || dimensions.height == 0 {
            // nothing to render
            return Ok(());
        }

        let viewport = cairo::Rectangle::new(
            0.0,
            0.0,
            f64::from(dimensions.width),
            f64::from(dimensions.height),
        );

        self.render_layer(cr, id, &viewport)
    }

    fn get_pixbuf_sub(&self, id: Option<&str>) -> Result<Pixbuf, RenderingError> {
        let dimensions = self.get_dimensions_sub(None)?;

        if dimensions.width == 0 || dimensions.height == 0 {
            return Ok(empty_pixbuf()?);
        }

        let surface = cairo::ImageSurface::create(
            cairo::Format::ARgb32,
            dimensions.width,
            dimensions.height,
        )?;

        {
            let cr = cairo::Context::new(&surface)?;
            let cr_raw = cr.to_raw_none();
            self.render_cairo_sub(cr_raw, id)?;
        }

        let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;

        Ok(pixbuf_from_surface(&surface)?)
    }

    fn render_document(
        &self,
        cr: *mut cairo::ffi::cairo_t,
        viewport: &cairo::Rectangle,
    ) -> Result<(), RenderingError> {
        let cr = check_cairo_context(cr)?;

        let handle = self.get_handle_ref()?;

        let renderer = self.make_renderer(&handle);
        Ok(renderer.render_document(&cr, viewport)?)
    }

    fn get_geometry_for_layer(
        &self,
        id: Option<&str>,
        viewport: &cairo::Rectangle,
    ) -> Result<(RsvgRectangle, RsvgRectangle), RenderingError> {
        let handle = self.get_handle_ref()?;
        let renderer = self.make_renderer(&handle);

        Ok(renderer
            .geometry_for_layer(id, viewport)
            .map(|(i, l)| (RsvgRectangle::from(i), RsvgRectangle::from(l)))?)
    }

    fn render_layer(
        &self,
        cr: *mut cairo::ffi::cairo_t,
        id: Option<&str>,
        viewport: &cairo::Rectangle,
    ) -> Result<(), RenderingError> {
        let cr = check_cairo_context(cr)?;

        let handle = self.get_handle_ref()?;

        let renderer = self.make_renderer(&handle);

        Ok(renderer.render_layer(&cr, id, viewport)?)
    }

    fn get_geometry_for_element(
        &self,
        id: Option<&str>,
    ) -> Result<(RsvgRectangle, RsvgRectangle), RenderingError> {
        let handle = self.get_handle_ref()?;

        let renderer = self.make_renderer(&handle);

        Ok(renderer
            .geometry_for_element(id)
            .map(|(i, l)| (RsvgRectangle::from(i), RsvgRectangle::from(l)))?)
    }

    fn render_element(
        &self,
        cr: *mut cairo::ffi::cairo_t,
        id: Option<&str>,
        element_viewport: &cairo::Rectangle,
    ) -> Result<(), RenderingError> {
        let cr = check_cairo_context(cr)?;

        let handle = self.get_handle_ref()?;

        let renderer = self.make_renderer(&handle);

        Ok(renderer.render_element(&cr, id, element_viewport)?)
    }

    fn get_intrinsic_dimensions(&self) -> Result<IntrinsicDimensions, RenderingError> {
        let handle = self.get_handle_ref()?;
        let renderer = self.make_renderer(&handle);
        Ok(renderer.intrinsic_dimensions())
    }

    fn get_intrinsic_size_in_pixels(&self) -> Result<Option<(f64, f64)>, RenderingError> {
        let handle = self.get_handle_ref()?;
        let renderer = self.make_renderer(&handle);
        Ok(renderer.intrinsic_size_in_pixels())
    }

    fn set_testing(&self, is_testing: bool) {
        let mut inner = self.imp().inner.borrow_mut();
        inner.is_testing = is_testing;
    }
}

fn is_rsvg_handle(obj: *const RsvgHandle) -> bool {
    unsafe { instance_of::<CHandle>(obj as *const _) }
}

fn is_input_stream(obj: *mut gio::ffi::GInputStream) -> bool {
    unsafe { instance_of::<gio::InputStream>(obj as *const _) }
}

fn is_gfile(obj: *const gio::ffi::GFile) -> bool {
    unsafe { instance_of::<gio::File>(obj as *const _) }
}

fn is_cancellable(obj: *mut gio::ffi::GCancellable) -> bool {
    unsafe { instance_of::<gio::Cancellable>(obj as *const _) }
}

fn get_rust_handle(handle: *const RsvgHandle) -> CHandle {
    let handle = unsafe { &*handle };
    handle.imp().obj().to_owned()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_type() -> glib::ffi::GType {
    CHandle::static_type().into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_error_get_type() -> glib::ffi::GType {
    Error::static_type().into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_flags_get_type() -> glib::ffi::GType {
    HandleFlags::static_type().into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_base_uri(
    handle: *const RsvgHandle,
    uri: *const libc::c_char,
) {
    rsvg_return_if_fail! {
        rsvg_handle_set_base_uri;

        is_rsvg_handle(handle),
        !uri.is_null(),
    }

    let rhandle = get_rust_handle(handle);

    assert!(!uri.is_null());
    let uri: String = from_glib_none(uri);

    rhandle.set_base_url(&uri);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_base_gfile(
    handle: *const RsvgHandle,
    raw_gfile: *mut gio::ffi::GFile,
) {
    rsvg_return_if_fail! {
        rsvg_handle_set_base_gfile;

        is_rsvg_handle(handle),
        is_gfile(raw_gfile),
    }

    let rhandle = get_rust_handle(handle);

    assert!(!raw_gfile.is_null());

    let file: gio::File = from_glib_none(raw_gfile);

    rhandle.set_base_gfile(&file);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_base_uri(
    handle: *const RsvgHandle,
) -> *const libc::c_char {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_base_uri => ptr::null();

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    rhandle.get_base_url_as_ptr()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_dpi(handle: *const RsvgHandle, dpi: libc::c_double) {
    rsvg_return_if_fail! {
        rsvg_handle_set_dpi;

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);
    rhandle.set_dpi_x(dpi);
    rhandle.set_dpi_y(dpi);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_dpi_x_y(
    handle: *const RsvgHandle,
    dpi_x: libc::c_double,
    dpi_y: libc::c_double,
) {
    rsvg_return_if_fail! {
        rsvg_handle_set_dpi_x_y;

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);
    rhandle.set_dpi_x(dpi_x);
    rhandle.set_dpi_y(dpi_y);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_size_callback(
    handle: *const RsvgHandle,
    size_func: RsvgSizeFunc,
    user_data: gpointer,
    destroy_notify: glib::ffi::GDestroyNotify,
) {
    rsvg_return_if_fail! {
        rsvg_handle_set_size_callback;

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    rhandle.set_size_callback(size_func, user_data, destroy_notify);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_internal_set_testing(
    handle: *const RsvgHandle,
    testing: glib::ffi::gboolean,
) {
    rsvg_return_if_fail! {
        rsvg_handle_internal_set_testing;

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    rhandle.set_testing(from_glib(testing));
}

trait IntoGError {
    type GlibResult;

    fn into_gerror(self, session: &Session, error: *mut *mut glib::ffi::GError)
        -> Self::GlibResult;
}

impl<E: fmt::Display> IntoGError for Result<(), E> {
    type GlibResult = glib::ffi::gboolean;

    fn into_gerror(
        self,
        session: &Session,
        error: *mut *mut glib::ffi::GError,
    ) -> Self::GlibResult {
        match self {
            Ok(()) => true.into_glib(),

            Err(e) => {
                set_gerror(session, error, 0, &format!("{e}"));
                false.into_glib()
            }
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_read_stream_sync(
    handle: *const RsvgHandle,
    stream: *mut gio::ffi::GInputStream,
    cancellable: *mut gio::ffi::GCancellable,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_read_stream_sync => false.into_glib();

        is_rsvg_handle(handle),
        is_input_stream(stream),
        cancellable.is_null() || is_cancellable(cancellable),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let stream = gio::InputStream::from_glib_none(stream);
    let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable);

    rhandle
        .read_stream_sync(&stream, cancellable.as_ref())
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_write(
    handle: *const RsvgHandle,
    buf: *const u8,
    count: usize,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_write => false.into_glib();

        is_rsvg_handle(handle),
        error.is_null() || (*error).is_null(),
        !buf.is_null() || count == 0,
    }

    let rhandle = get_rust_handle(handle);
    let buffer = slice::from_raw_parts(buf, count);
    rhandle.write(buffer);

    true.into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_close(
    handle: *const RsvgHandle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_close => false.into_glib();

        is_rsvg_handle(handle),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    rhandle.close().into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_has_sub(
    handle: *const RsvgHandle,
    id: *const libc::c_char,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_has_sub => false.into_glib();

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    if id.is_null() {
        return false.into_glib();
    }

    let id: String = from_glib_none(id);
    rhandle.has_sub(&id).unwrap_or(false).into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_render_cairo(
    handle: *const RsvgHandle,
    cr: *mut cairo::ffi::cairo_t,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_render_cairo => false.into_glib();

        is_rsvg_handle(handle),
        !cr.is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    rhandle
        .render_cairo_sub(cr, None)
        .into_gerror(&session, ptr::null_mut())
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_render_cairo_sub(
    handle: *const RsvgHandle,
    cr: *mut cairo::ffi::cairo_t,
    id: *const libc::c_char,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_render_cairo_sub => false.into_glib();

        is_rsvg_handle(handle),
        !cr.is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let id: Option<String> = from_glib_none(id);

    rhandle
        .render_cairo_sub(cr, id.as_deref())
        .into_gerror(&session, ptr::null_mut())
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_pixbuf(
    handle: *const RsvgHandle,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_pixbuf => ptr::null_mut();

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    match rhandle.get_pixbuf_sub(None) {
        Ok(pixbuf) => pixbuf.to_glib_full(),
        Err(e) => {
            let session = &rhandle.imp().session;
            rsvg_log!(session, "could not render: {}", e);
            ptr::null_mut()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_pixbuf_sub(
    handle: *const RsvgHandle,
    id: *const libc::c_char,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_pixbuf_sub => ptr::null_mut();

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);
    let id: Option<String> = from_glib_none(id);

    match rhandle.get_pixbuf_sub(id.as_deref()) {
        Ok(pixbuf) => pixbuf.to_glib_full(),
        Err(e) => {
            let session = &rhandle.imp().session;
            rsvg_log!(session, "could not render: {}", e);
            ptr::null_mut()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_dimensions(
    handle: *const RsvgHandle,
    dimension_data: *mut RsvgDimensionData,
) {
    rsvg_handle_get_dimensions_sub(handle, dimension_data, ptr::null());
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_dimensions_sub(
    handle: *const RsvgHandle,
    dimension_data: *mut RsvgDimensionData,
    id: *const libc::c_char,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_dimensions_sub => false.into_glib();

        is_rsvg_handle(handle),
        !dimension_data.is_null(),
    }

    let rhandle = get_rust_handle(handle);

    let id: Option<String> = from_glib_none(id);

    match rhandle.get_dimensions_sub(id.as_deref()) {
        Ok(dimensions) => {
            *dimension_data = dimensions;
            true.into_glib()
        }

        Err(e) => {
            let session = &rhandle.imp().session;
            rsvg_log!(session, "could not get dimensions: {}", e);
            *dimension_data = RsvgDimensionData::empty();
            false.into_glib()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_position_sub(
    handle: *const RsvgHandle,
    position_data: *mut RsvgPositionData,
    id: *const libc::c_char,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_position_sub => false.into_glib();

        is_rsvg_handle(handle),
        !position_data.is_null(),
    }

    let rhandle = get_rust_handle(handle);

    let id: Option<String> = from_glib_none(id);

    match rhandle.get_position_sub(id.as_deref()) {
        Ok(position) => {
            *position_data = position;
            true.into_glib()
        }

        Err(e) => {
            let p = &mut *position_data;

            p.x = 0;
            p.y = 0;

            let session = &rhandle.imp().session;
            rsvg_log!(session, "could not get position: {}", e);
            false.into_glib()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new() -> *const RsvgHandle {
    let obj = glib::Object::new::<CHandle>();

    obj.to_glib_full()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new_with_flags(flags: RsvgHandleFlags) -> *const RsvgHandle {
    let obj = glib::Object::builder::<CHandle>()
        .property("flags", &HandleFlags::from_bits_truncate(flags))
        .build();

    obj.to_glib_full()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new_from_file(
    filename: *const libc::c_char,
    error: *mut *mut glib::ffi::GError,
) -> *const RsvgHandle {
    rsvg_return_val_if_fail! {
        rsvg_handle_new_from_file => ptr::null();

        !filename.is_null(),
        error.is_null() || (*error).is_null(),
    }

    let file = match PathOrUrl::new(filename) {
        Ok(p) => p.get_gfile(),

        Err(s) => {
            // Here we don't have a handle created yet, so it's fine to create a session
            // to log the error message.  We'll need to change this when we start logging
            // API calls, so that we can log the call to rsvg_handle_new_from_file() and
            // then pass *that* session to rsvg_handle_new_from_gfile_sync() below.
            let session = Session::default();
            set_gerror(&session, error, 0, &s);
            return ptr::null_mut();
        }
    };

    rsvg_handle_new_from_gfile_sync(file.to_glib_none().0, 0, ptr::null_mut(), error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new_from_gfile_sync(
    file: *mut gio::ffi::GFile,
    flags: RsvgHandleFlags,
    cancellable: *mut gio::ffi::GCancellable,
    error: *mut *mut glib::ffi::GError,
) -> *const RsvgHandle {
    rsvg_return_val_if_fail! {
        rsvg_handle_new_from_gfile_sync => ptr::null();

        is_gfile(file),
        cancellable.is_null() || is_cancellable(cancellable),
        error.is_null() || (*error).is_null(),
    }

    let raw_handle = rsvg_handle_new_with_flags(flags);

    let rhandle = get_rust_handle(raw_handle);
    let session = rhandle.imp().session.clone();

    let file = gio::File::from_glib_none(file);
    rhandle.set_base_gfile(&file);

    let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable);

    let res = file
        .read(cancellable.as_ref())
        .map_err(LoadingError::from)
        .and_then(|stream| rhandle.read_stream_sync(&stream.upcast(), cancellable.as_ref()));

    match res {
        Ok(()) => raw_handle,

        Err(e) => {
            set_gerror(&session, error, 0, &format!("{e}"));
            gobject_ffi::g_object_unref(raw_handle as *mut _);
            ptr::null_mut()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new_from_stream_sync(
    input_stream: *mut gio::ffi::GInputStream,
    base_file: *mut gio::ffi::GFile,
    flags: RsvgHandleFlags,
    cancellable: *mut gio::ffi::GCancellable,
    error: *mut *mut glib::ffi::GError,
) -> *const RsvgHandle {
    rsvg_return_val_if_fail! {
        rsvg_handle_new_from_stream_sync => ptr::null();

        is_input_stream(input_stream),
        base_file.is_null() || is_gfile(base_file),
        cancellable.is_null() || is_cancellable(cancellable),
        error.is_null() || (*error).is_null(),
    }

    let raw_handle = rsvg_handle_new_with_flags(flags);

    let rhandle = get_rust_handle(raw_handle);
    let session = rhandle.imp().session.clone();

    let base_file: Option<gio::File> = from_glib_none(base_file);
    if let Some(base_file) = base_file {
        rhandle.set_base_gfile(&base_file);
    }

    let stream: gio::InputStream = from_glib_none(input_stream);
    let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable);

    match rhandle.read_stream_sync(&stream, cancellable.as_ref()) {
        Ok(()) => raw_handle,

        Err(e) => {
            set_gerror(&session, error, 0, &format!("{e}"));
            gobject_ffi::g_object_unref(raw_handle as *mut _);
            ptr::null_mut()
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_new_from_data(
    data: *const u8,
    data_len: usize,
    error: *mut *mut glib::ffi::GError,
) -> *const RsvgHandle {
    rsvg_return_val_if_fail! {
        rsvg_handle_new_from_data => ptr::null();

        !data.is_null() || data_len == 0,
        data_len <= std::isize::MAX as usize,
        error.is_null() || (*error).is_null(),
    }

    // We create the MemoryInputStream without the gtk-rs binding because of this:
    //
    // - The binding doesn't provide _new_from_data().  All of the binding's ways to
    // put data into a MemoryInputStream involve copying the data buffer.
    //
    // - We can't use glib::Bytes from the binding either, for the same reason.
    //
    // - For now, we are using the other C-visible constructor, so we need a raw pointer to the
    //   stream, anyway.

    assert!(data_len <= std::isize::MAX as usize);
    let data_len = data_len as isize;

    let raw_stream = gio::ffi::g_memory_input_stream_new_from_data(data as *mut u8, data_len, None);

    let ret = rsvg_handle_new_from_stream_sync(
        raw_stream,
        ptr::null_mut(), // base_file
        0,
        ptr::null_mut(), // cancellable
        error,
    );

    gobject_ffi::g_object_unref(raw_stream as *mut _);
    ret
}

unsafe fn set_out_param<T: Copy>(
    out_has_param: *mut glib::ffi::gboolean,
    out_param: *mut T,
    value: &Option<T>,
) {
    let has_value = if let Some(ref v) = *value {
        if !out_param.is_null() {
            *out_param = *v;
        }

        true
    } else {
        false
    };

    if !out_has_param.is_null() {
        *out_has_param = has_value.into_glib();
    }
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_free(handle: *mut RsvgHandle) {
    gobject_ffi::g_object_unref(handle as *mut _);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_set_stylesheet(
    handle: *const RsvgHandle,
    css: *const u8,
    css_len: usize,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_set_stylesheet => false.into_glib();

        is_rsvg_handle(handle),
        !css.is_null() || (css.is_null() && css_len == 0),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let css = match (css, css_len) {
        (p, 0) if p.is_null() => "",
        (_, _) => {
            let s = slice::from_raw_parts(css, css_len);
            match str::from_utf8(s) {
                Ok(s) => s,
                Err(e) => {
                    set_gerror(&session, error, 0, &format!("CSS is not valid UTF-8: {e}"));
                    return false.into_glib();
                }
            }
        }
    };

    rhandle.set_stylesheet(css).into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_intrinsic_dimensions(
    handle: *const RsvgHandle,
    out_has_width: *mut glib::ffi::gboolean,
    out_width: *mut RsvgLength,
    out_has_height: *mut glib::ffi::gboolean,
    out_height: *mut RsvgLength,
    out_has_viewbox: *mut glib::ffi::gboolean,
    out_viewbox: *mut RsvgRectangle,
) {
    rsvg_return_if_fail! {
        rsvg_handle_get_intrinsic_dimensions;

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    let d = rhandle
        .get_intrinsic_dimensions()
        .unwrap_or_else(|_| panic!("API called out of order"));

    let w = d.width;
    let h = d.height;
    let r = d.vbox.map(RsvgRectangle::from);

    set_out_param(out_has_width, out_width, &Into::into(w));
    set_out_param(out_has_height, out_height, &Into::into(h));
    set_out_param(out_has_viewbox, out_viewbox, &r);
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_intrinsic_size_in_pixels(
    handle: *const RsvgHandle,
    out_width: *mut f64,
    out_height: *mut f64,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_intrinsic_size_in_pixels => false.into_glib();

        is_rsvg_handle(handle),
    }

    let rhandle = get_rust_handle(handle);

    let dim = rhandle
        .get_intrinsic_size_in_pixels()
        .unwrap_or_else(|_| panic!("API called out of order"));

    let (w, h) = dim.unwrap_or((0.0, 0.0));

    if !out_width.is_null() {
        *out_width = w;
    }

    if !out_height.is_null() {
        *out_height = h;
    }

    dim.is_some().into_glib()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_render_document(
    handle: *const RsvgHandle,
    cr: *mut cairo::ffi::cairo_t,
    viewport: *const RsvgRectangle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_render_document => false.into_glib();

        is_rsvg_handle(handle),
        !cr.is_null(),
        !viewport.is_null(),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    rhandle
        .render_document(cr, &(*viewport).into())
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_geometry_for_layer(
    handle: *mut RsvgHandle,
    id: *const libc::c_char,
    viewport: *const RsvgRectangle,
    out_ink_rect: *mut RsvgRectangle,
    out_logical_rect: *mut RsvgRectangle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_geometry_for_layer => false.into_glib();

        is_rsvg_handle(handle),
        !viewport.is_null(),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let id: Option<String> = from_glib_none(id);

    rhandle
        .get_geometry_for_layer(id.as_deref(), &(*viewport).into())
        .map(|(ink_rect, logical_rect)| {
            if !out_ink_rect.is_null() {
                *out_ink_rect = ink_rect;
            }

            if !out_logical_rect.is_null() {
                *out_logical_rect = logical_rect;
            }
        })
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_render_layer(
    handle: *const RsvgHandle,
    cr: *mut cairo::ffi::cairo_t,
    id: *const libc::c_char,
    viewport: *const RsvgRectangle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_render_layer => false.into_glib();

        is_rsvg_handle(handle),
        !cr.is_null(),
        !viewport.is_null(),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let id: Option<String> = from_glib_none(id);

    rhandle
        .render_layer(cr, id.as_deref(), &(*viewport).into())
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_geometry_for_element(
    handle: *const RsvgHandle,
    id: *const libc::c_char,
    out_ink_rect: *mut RsvgRectangle,
    out_logical_rect: *mut RsvgRectangle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_geometry_for_element => false.into_glib();

        is_rsvg_handle(handle),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let id: Option<String> = from_glib_none(id);

    rhandle
        .get_geometry_for_element(id.as_deref())
        .map(|(ink_rect, logical_rect)| {
            if !out_ink_rect.is_null() {
                *out_ink_rect = ink_rect;
            }

            if !out_logical_rect.is_null() {
                *out_logical_rect = logical_rect;
            }
        })
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_render_element(
    handle: *const RsvgHandle,
    cr: *mut cairo::ffi::cairo_t,
    id: *const libc::c_char,
    element_viewport: *const RsvgRectangle,
    error: *mut *mut glib::ffi::GError,
) -> glib::ffi::gboolean {
    rsvg_return_val_if_fail! {
        rsvg_handle_render_element => false.into_glib();

        is_rsvg_handle(handle),
        !cr.is_null(),
        !element_viewport.is_null(),
        error.is_null() || (*error).is_null(),
    }

    let rhandle = get_rust_handle(handle);
    let session = rhandle.imp().session.clone();

    let id: Option<String> = from_glib_none(id);

    rhandle
        .render_element(cr, id.as_deref(), &(*element_viewport).into())
        .into_gerror(&session, error)
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_desc(handle: *const RsvgHandle) -> *mut libc::c_char {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_desc => ptr::null_mut();

        is_rsvg_handle(handle),
    }

    ptr::null_mut()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_metadata(handle: *const RsvgHandle) -> *mut libc::c_char {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_metadata => ptr::null_mut();

        is_rsvg_handle(handle),
    }

    ptr::null_mut()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_handle_get_title(handle: *const RsvgHandle) -> *mut libc::c_char {
    rsvg_return_val_if_fail! {
        rsvg_handle_get_title => ptr::null_mut();

        is_rsvg_handle(handle),
    }

    ptr::null_mut()
}

#[no_mangle]
pub unsafe extern "C" fn rsvg_init() {}

#[no_mangle]
pub unsafe extern "C" fn rsvg_term() {}

#[no_mangle]
pub unsafe extern "C" fn rsvg_cleanup() {}

/// Detects whether a `*const libc::c_char` is a path or a URI
///
/// `rsvg_handle_new_from_file()` takes a `filename` argument, and advertises
/// that it will detect either a file system path, or a proper URI.  It will then use
/// `gio::File::for_path()` or `gio::File::for_uri()` as appropriate.
///
/// This enum does the magic heuristics to figure this out.
///
/// The `from_os_str` version is for using the same logic on rsvg-convert's command-line
/// arguments: we want `rsvg-convert http://example.com/foo.svg` to go to a URL, not to a
/// local file with that name.
#[derive(Clone, Debug)]
pub enum PathOrUrl {
    Path(PathBuf),
    Url(Url),
}

impl PathOrUrl {
    unsafe fn new(s: *const libc::c_char) -> Result<PathOrUrl, String> {
        let cstr = CStr::from_ptr(s);

        if cstr.to_bytes().is_empty() {
            return Err("invalid empty filename".to_string());
        }

        Ok(cstr
            .to_str()
            .map_err(|_| ())
            .and_then(Self::try_from_str)
            .unwrap_or_else(|_| PathOrUrl::Path(PathBuf::from_glib_none(s))))
    }

    fn try_from_str(s: &str) -> Result<PathOrUrl, ()> {
        assert!(!s.is_empty());

        Url::parse(s).map_err(|_| ()).and_then(|url| {
            if url.origin().is_tuple() || url.scheme() == "file" {
                Ok(PathOrUrl::Url(url))
            } else {
                Ok(PathOrUrl::Path(url.to_file_path()?))
            }
        })
    }

    pub fn from_os_str(osstr: &OsStr) -> Result<PathOrUrl, String> {
        if osstr.is_empty() {
            return Err("invalid empty filename".to_string());
        }

        Ok(osstr
            .to_str()
            .ok_or(())
            .and_then(Self::try_from_str)
            .unwrap_or_else(|_| PathOrUrl::Path(PathBuf::from(osstr.to_os_string()))))
    }

    pub fn get_gfile(&self) -> gio::File {
        match *self {
            PathOrUrl::Path(ref p) => gio::File::for_path(p),
            PathOrUrl::Url(ref u) => gio::File::for_uri(u.as_str()),
        }
    }
}

impl fmt::Display for PathOrUrl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            PathOrUrl::Path(ref p) => p.display().fmt(f),
            PathOrUrl::Url(ref u) => u.fmt(f),
        }
    }
}

fn check_cairo_context(cr: *mut cairo::ffi::cairo_t) -> Result<cairo::Context, RenderingError> {
    let status = unsafe { cairo::ffi::cairo_status(cr) };

    if status == cairo::ffi::STATUS_SUCCESS {
        Ok(unsafe { from_glib_none(cr) })
    } else {
        let status: cairo::Error = status.into();

        let msg = format!("cannot render on a cairo_t with a failure status (status={status:?})");

        rsvg_g_warning(&msg);

        Err(RenderingError::from(status))
    }
}

pub(crate) fn set_gerror(
    session: &Session,
    err: *mut *mut glib::ffi::GError,
    code: u32,
    msg: &str,
) {
    unsafe {
        // this is RSVG_ERROR_FAILED, the only error code available in RsvgError
        assert!(code == 0);

        // Log this, in case the calling program passes a NULL GError, so we can at least
        // diagnose things by asking for RSVG_LOG.
        //
        // See https://gitlab.gnome.org/GNOME/gtk/issues/2294 for an example of code that
        // passed a NULL GError and so we had no easy way to see what was wrong.
        rsvg_log!(session, "{}", msg);

        glib::ffi::g_set_error_literal(
            err,
            rsvg_error_quark(),
            code as libc::c_int,
            msg.to_glib_none().0,
        );
    }
}

#[derive(Debug, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "RsvgError")]
enum Error {
    #[enum_value(name = "RSVG_ERROR_FAILED", nick = "failed")]
    // Keep in sync with rsvg.h:RsvgError
    Failed = 0,
}

/// Used as a generic error to translate to glib::Error
///
/// This type implements `glib::error::ErrorDomain`, so it can be used
/// to obtain the error code while calling `glib::Error::new()`.  Unfortunately
/// the public librsvg API does not have detailed error codes yet, so we use
/// this single value as the only possible error code to return.
#[derive(Copy, Clone)]
struct RsvgError;

impl ErrorDomain for RsvgError {
    fn domain() -> glib::Quark {
        glib::Quark::from_str("rsvg-error-quark")
    }

    fn code(self) -> i32 {
        Error::Failed as i32
    }

    fn from(_code: i32) -> Option<Self> {
        // We don't have enough information from glib error codes
        Some(RsvgError)
    }
}

#[no_mangle]
pub extern "C" fn rsvg_error_quark() -> glib::ffi::GQuark {
    RsvgError::domain().into_glib()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn path_or_url_unix() {
        unsafe {
            match PathOrUrl::new(rsvg_c_str!("/foo/bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("unix filename should be a PathOrUrl::Path"),
            }

            match PathOrUrl::new(rsvg_c_str!("foo/bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("unix filename should be a PathOrUrl::Path"),
            }
        }
    }

    #[test]
    fn path_or_url_windows() {
        unsafe {
            match PathOrUrl::new(rsvg_c_str!("c:/foo/bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("windows filename should be a PathOrUrl::Path"),
            }

            match PathOrUrl::new(rsvg_c_str!("C:/foo/bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("windows filename should be a PathOrUrl::Path"),
            }

            match PathOrUrl::new(rsvg_c_str!("c:\\foo\\bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("windows filename should be a PathOrUrl::Path"),
            }

            match PathOrUrl::new(rsvg_c_str!("C:\\foo\\bar")).unwrap() {
                PathOrUrl::Path(_) => (),
                _ => panic!("windows filename should be a PathOrUrl::Path"),
            }
        }
    }

    #[test]
    fn path_or_url_unix_url() {
        unsafe {
            match PathOrUrl::new(rsvg_c_str!("file:///foo/bar")).unwrap() {
                PathOrUrl::Url(_) => (),
                _ => panic!("file:// unix filename should be a PathOrUrl::Url"),
            }
        }
    }

    #[test]
    fn path_or_url_windows_url() {
        unsafe {
            match PathOrUrl::new(rsvg_c_str!("file://c:/foo/bar")).unwrap() {
                PathOrUrl::Url(_) => (),
                _ => panic!("file:// windows filename should be a PathOrUrl::Url"),
            }

            match PathOrUrl::new(rsvg_c_str!("file://C:/foo/bar")).unwrap() {
                PathOrUrl::Url(_) => (),
                _ => panic!("file:// windows filename should be a PathOrUrl::Url"),
            }
        }
    }

    #[test]
    fn path_or_url_empty_str() {
        unsafe {
            assert!(PathOrUrl::new(rsvg_c_str!("")).is_err());
        }

        assert!(PathOrUrl::from_os_str(OsStr::new("")).is_err());
    }

    #[test]
    fn base_url_works() {
        let mut u = BaseUrl::default();

        assert!(u.get().is_none());
        assert_eq!(u.get_ptr(), ptr::null());

        u.set(Url::parse("file:///example.txt").unwrap());

        assert_eq!(u.get().unwrap().as_str(), "file:///example.txt");

        unsafe {
            let p = u.get_ptr();
            let cstr = CStr::from_ptr(p);
            assert_eq!(cstr.to_str().unwrap(), "file:///example.txt");
        }
    }
}
