use std::cell::Cell;

use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{cairo, gdk, glib, graphene};
use rsvg::prelude::{HandleExt, HandleExtManual};

mod imp {
    use super::*;

    use std::cell::OnceCell;

    use glib::Properties;

    #[derive(Default, Properties)]
    #[properties(wrapper_type = super::SvgPaintable)]
    pub struct SvgPaintable {
        pub handle: OnceCell<rsvg::Handle>,
        pub width: Cell<i32>,
        pub height: Cell<i32>,

        #[property(get, set, construct_only)]
        pub is_symbolic: Cell<bool>,
        #[property(get, set = Self::set_scale_factor, default = 1, explicit_notify)]
        pub scale_factor: Cell<i32>,
    }

    impl SvgPaintable {
        fn set_scale_factor(&self, scale_factor: i32) {
            let obj = self.obj();
            if scale_factor != self.scale_factor.replace(scale_factor) {
                obj.notify_scale_factor();

                obj.invalidate_contents();
            }
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for SvgPaintable {
        const NAME: &'static str = "SvgPaintable";
        type Type = super::SvgPaintable;
        type ParentType = glib::Object;
        type Interfaces = (gdk::Paintable, gtk::SymbolicPaintable);

        fn new() -> Self {
            Self {
                handle: OnceCell::new(),
                scale_factor: Cell::new(1),
                width: Cell::new(0),
                height: Cell::new(0),
                is_symbolic: Cell::new(false),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for SvgPaintable {}

    impl SymbolicPaintableImpl for SvgPaintable {
        fn snapshot_symbolic(
            &self,
            snapshot: &gdk::Snapshot,
            width: f64,
            height: f64,
            colors: &[gdk::RGBA],
        ) {
            let paintable = self.obj();
            let rect = graphene::Rect::new(0.0, 0.0, width as f32, height as f32);
            let cr = snapshot.append_cairo(&rect);

            if self.is_symbolic.get() {
                let fg = colors[0];
                let color_offset =
                    graphene::Vec4::from_float([fg.red(), fg.green(), fg.blue(), 0.0]);
                let color_matrix =
                    color_matrix(colors.first(), colors.get(1), colors.get(2), colors.get(3));
                snapshot.push_color_matrix(&color_matrix, &color_offset)
            }

            paintable
                .render(&cr, width as i32, height as i32)
                .unwrap_or_else(|err| log::debug!("Failed to render svg: {:?}", err));

            if self.is_symbolic.get() {
                snapshot.pop();
            }
        }
    }
    impl PaintableImpl for SvgPaintable {
        fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) {
            self.snapshot_symbolic(snapshot, width, height, &[]);
        }

        fn intrinsic_width(&self) -> i32 {
            let handle = self.handle.get().unwrap();

            handle.intrinsic_dimensions().0.length() as i32
        }

        fn intrinsic_height(&self) -> i32 {
            let handle = self.handle.get().unwrap();

            handle.intrinsic_dimensions().1.length() as i32
        }
    }
}

glib::wrapper! {
    pub struct SvgPaintable(ObjectSubclass<imp::SvgPaintable>)
        @implements gdk::Paintable, gtk::SymbolicPaintable;
}

impl SvgPaintable {
    pub fn new(bytes: &[u8]) -> anyhow::Result<Self> {
        use anyhow::Context;

        let paintable: Self = glib::Object::new();
        let handle = rsvg::Handle::from_data(bytes)?.context("No SVG handle")?;

        paintable
            .imp()
            .handle
            .set(handle)
            .unwrap_or_else(|_| log::debug!("Could not set handle"));

        Ok(paintable)
    }

    pub fn render(&self, cr: &cairo::Context, width: i32, height: i32) -> anyhow::Result<()> {
        let imp = self.imp();
        let handle = imp.handle.get().unwrap();

        // FIXME Remove if https://gitlab.gnome.org/GNOME/librsvg/-/issues/826 is fixed
        let scale = imp.scale_factor.get();
        let surface =
            cairo::ImageSurface::create(cairo::Format::ARgb32, scale * width, scale * height)?;

        let cr2 = cairo::Context::new(&surface)?;

        let viewport =
            rsvg::Rectangle::new(0.0, 0.0, (scale * width) as f64, (scale * height) as f64);
        handle.render_document(&cr2, &viewport)?;

        cr.scale(1.0 / scale as f64, 1.0 / scale as f64);
        cr.set_source_surface(&surface, 0.0, 0.0)?;
        cr.paint()?;

        Ok(())
    }
}

// Taken from gtkicontheme.c's init_color_matrix
pub fn color_matrix(
    foreground_color: Option<&gdk::RGBA>,
    error: Option<&gdk::RGBA>,
    warning: Option<&gdk::RGBA>,
    success: Option<&gdk::RGBA>,
) -> graphene::Matrix {
    let fg = foreground_color
        .cloned()
        .unwrap_or_else(|| gdk::RGBA::new(0.74509805, 0.74509805, 0.74509805, 1.0));
    let sc = success
        .cloned()
        .unwrap_or_else(|| gdk::RGBA::new(0.30469215, 0.6015717, 0.023437858, 1.0));
    let ec = error
        .cloned()
        .unwrap_or_else(|| gdk::RGBA::new(0.79688716, 0.0, 0.0, 1.0));
    let wc = warning
        .cloned()
        .unwrap_or_else(|| gdk::RGBA::new(0.95704585, 0.47266346, 0.2421912, 1.0));

    #[rustfmt::skip]
    let matrix = graphene::Matrix::from_float([
        sc.red() - fg.red(), sc.green() - fg.green(), sc.blue() - fg.blue(), 0.0,
        wc.red() - fg.red(), wc.green() - fg.green(), wc.blue() - fg.blue(), 0.0,
        ec.red() - fg.red(), ec.green() - fg.green(), ec.blue() - fg.blue(), 0.0,
        0.0,                 0.0,                     0.0,                   fg.alpha(),
    ]);
    matrix
}
