use gtk::{
    gdk, gio,
    glib::{self, clone},
    graphene, gsk,
    prelude::*,
    subclass::prelude::*,
};

use crate::widgets::Window;

#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "Filter")]
pub enum Filter {
    Blur = 0,
    #[default]
    Filled = 1,
}

mod imp {
    use std::{
        cell::{Cell, RefCell},
        sync::OnceLock,
    };

    use glib::subclass::Signal;

    use super::*;

    #[derive(Debug, glib::Properties, gtk::CompositeTemplate)]
    #[properties(wrapper_type = super::DrawingArea)]
    #[template(resource = "/com/belmoussaoui/Obfuscate/drawing_area.ui")]
    pub struct DrawingArea {
        pub start_position: Cell<Option<(f64, f64)>>,
        pub current_position: Cell<Option<(f64, f64)>>,
        pub pointer_position: Cell<Option<(f64, f64)>>,
        pub history: RefCell<Vec<gdk::Texture>>,
        pub future: RefCell<Vec<gdk::Texture>>,

        pub initial_zoom: Cell<f64>,
        pub initial_zoom_center: Cell<Option<(f64, f64)>>,
        pub queued_scroll: Cell<Option<(f64, f64)>>,
        pub scale_factor: Cell<i32>,
        #[property(get, set)]
        pub can_undo: Cell<bool>,
        #[property(get, set)]
        pub can_redo: Cell<bool>,
        #[property(get, set)]
        pub saved: Cell<bool>,
        #[property(get, set = Self::set_zoom, minimum = 0.0, default = 1.0, maximum = 8.0)]
        pub zoom: Cell<f64>,
        #[property(get, set, builder(Filter::default()))]
        pub filter: Cell<Filter>,
        // Scrollable properties
        #[property(get, set = Self::set_vadjustment, override_interface = gtk::Scrollable)]
        pub vadjustment: RefCell<Option<gtk::Adjustment>>,
        pub vadjustment_signal: RefCell<Option<glib::SignalHandlerId>>,
        #[property(get, set = Self::set_hadjustment, override_interface = gtk::Scrollable)]
        pub hadjustment: RefCell<Option<gtk::Adjustment>>,
        pub hadjustment_signal: RefCell<Option<glib::SignalHandlerId>>,
        #[property(get, set, override_interface = gtk::Scrollable)]
        pub hscroll_policy: Cell<gtk::ScrollablePolicy>,
        #[property(get, set, override_interface = gtk::Scrollable)]
        pub vscroll_policy: Cell<gtk::ScrollablePolicy>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DrawingArea {
        const NAME: &'static str = "DrawingArea";
        type ParentType = gtk::Widget;
        type Type = super::DrawingArea;
        type Interfaces = (gtk::Scrollable,);

        fn new() -> Self {
            Self {
                saved: Cell::new(true),
                start_position: Default::default(),
                current_position: Default::default(),
                pointer_position: Default::default(),
                history: Default::default(),
                future: Default::default(),
                initial_zoom: Cell::new(1.0),
                initial_zoom_center: Default::default(),
                queued_scroll: Default::default(),
                scale_factor: Cell::new(1),
                can_redo: Default::default(),
                can_undo: Default::default(),
                zoom: Cell::new(1.0),
                filter: Default::default(),
                hadjustment: Default::default(),
                hadjustment_signal: Default::default(),
                vadjustment: Default::default(),
                vadjustment_signal: Default::default(),
                hscroll_policy: Cell::new(gtk::ScrollablePolicy::Minimum),
                vscroll_policy: Cell::new(gtk::ScrollablePolicy::Minimum),
            }
        }

        fn class_init(klass: &mut Self::Class) {
            klass.set_accessible_role(gtk::AccessibleRole::Img);
            klass.bind_template();
            klass.bind_template_callbacks();
        }

        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for DrawingArea {
        fn signals() -> &'static [Signal] {
            static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
            SIGNALS.get_or_init(|| vec![Signal::builder("loaded").build()])
        }

        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            obj.set_overflow(gtk::Overflow::Hidden);
            obj.set_cursor(gdk::Cursor::from_name("crosshair", None).as_ref());

            let drop_target = gtk::DropTarget::new(gio::File::static_type(), gdk::DragAction::COPY);
            drop_target.connect_drop(
                clone!(@weak obj => @default-return false, move |_, value, _, _| {
                    if let Ok(file) = value.get::<gio::File>() {
                        let window = obj.root().and_downcast::<Window>().unwrap();
                        window.set_open_file(&file);
                        true
                    } else {
                        false
                    }
                }),
            );
            obj.add_controller(drop_target);
        }
    }

    impl WidgetImpl for DrawingArea {
        fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
            let widget = self.obj();
            let zoom = widget.effective_zoom();
            let (width, height) = if let Some(texture) = self.history.borrow().iter().last() {
                (
                    (texture.width() as f64 * zoom) as i32,
                    (texture.height() as f64 * zoom) as i32,
                )
            } else {
                // some default (width, height)
                (300, 300)
            };

            if orientation == gtk::Orientation::Horizontal {
                (0, width, -1, -1)
            } else {
                (0, height, -1, -1)
            }
        }

        fn snapshot(&self, snapshot: &gtk::Snapshot) {
            let widget = self.obj();
            let Some(texture) = widget.texture() else {
                return;
            };
            let hadj = widget.hadjustment().unwrap();
            let vadj = widget.vadjustment().unwrap();
            let zoom = widget.effective_zoom() as f32;
            let (translate_x, translate_y) = translate(
                widget.width() as f32,
                widget.height() as f32,
                &texture,
                zoom,
            );

            snapshot.save();
            snapshot.translate(&graphene::Point::new(
                (-hadj.value() as f32 + translate_x).round(),
                (-vadj.value() as f32 + translate_y).round(),
            ));
            snapshot.scale(zoom, zoom);
            snapshot.append_texture(&texture, &widget.bounds());
            snapshot.restore();

            let Some(start_pos) = self.start_position.get() else {
                return;
            };

            let Some(end_pos) = self.current_position.get() else {
                return;
            };

            let Some(view_start) = widget.convert_image_coords(start_pos) else {
                return;
            };
            let Some(view_end) = widget.convert_image_coords(end_pos) else {
                return;
            };

            let (x, y) = top_left_corner(view_start, view_end);
            let (width, height) = width_height(view_start, view_end);

            let selection_rect = graphene::Rect::new(x, y, width, height);

            snapshot.append_border(
                &gsk::RoundedRect::from_rect(selection_rect, 0.0),
                &[1.0, 1.0, 1.0, 1.0],
                &[
                    gdk::RGBA::BLACK,
                    gdk::RGBA::BLACK,
                    gdk::RGBA::BLACK,
                    gdk::RGBA::BLACK,
                ],
            );
        }

        fn size_allocate(&self, width: i32, height: i32, _baseline: i32) {
            let widget = self.obj();
            let Some(texture) = widget.texture() else {
                return;
            };
            let zoom = widget.effective_zoom();

            let hadjustment = widget.hadjustment().unwrap();
            let vadjustment = widget.vadjustment().unwrap();

            let (x, y) = self
                .queued_scroll
                .take()
                .unwrap_or((hadjustment.value(), vadjustment.value()));

            hadjustment.configure(
                x,
                0.0,
                (width as f64).max(texture.width() as f64 * zoom),
                0.1 * width as f64,
                0.9 * width as f64,
                width as f64,
            );
            vadjustment.configure(
                y,
                0.0,
                (height as f64).max(texture.height() as f64 * zoom),
                0.1 * height as f64,
                0.9 * height as f64,
                height as f64,
            );
        }
    }

    impl ScrollableImpl for DrawingArea {}

    #[gtk::template_callbacks]
    impl DrawingArea {
        fn set_hadjustment(&self, adj: Option<gtk::Adjustment>) {
            let obj = self.obj();
            if let (Some(signal_id), Some(obj)) =
                (self.hadjustment_signal.take(), self.hadjustment.take())
            {
                obj.disconnect(signal_id);
            }

            if let Some(ref adjustment) = adj {
                let signal_id = adjustment.connect_value_changed(clone!(@weak obj => move |_| {
                    obj.queue_draw();
                }));
                self.hadjustment_signal.replace(Some(signal_id));
            }
            self.hadjustment.replace(adj);
        }

        fn set_vadjustment(&self, adj: Option<gtk::Adjustment>) {
            let obj = self.obj();
            if let (Some(signal_id), Some(obj)) =
                (self.vadjustment_signal.take(), self.vadjustment.take())
            {
                obj.disconnect(signal_id);
            }

            if let Some(ref adjustment) = adj {
                let signal_id = adjustment.connect_value_changed(clone!(@weak obj => move |_| {
                    obj.queue_draw();
                }));
                self.vadjustment_signal.replace(Some(signal_id));
            }
            self.vadjustment.replace(adj);
        }

        fn set_zoom(&self, new_zoom: f64) {
            let obj = self.obj();
            if let Some(pos) = self.pointer_position.get() {
                obj.begin_zoom(pos);
                obj.set_zoom_at_center(new_zoom, pos);
                return;
            }

            let width = obj.width();
            let height = obj.height();
            let pos = (width as f64 / 2.0, height as f64 / 2.0);
            obj.begin_zoom(pos);
            obj.set_zoom_at_center(new_zoom, pos);
        }

        #[template_callback]
        fn on_drag_begin(&self, start_x: f64, start_y: f64) {
            let Some(image_coords) = self.obj().to_image_coords((start_x, start_y)) else {
                return;
            };
            self.start_position.replace(Some(image_coords));
        }

        #[template_callback]
        fn on_drag_update(&self, offset_x: f64, offset_y: f64, gesture: &gtk::GestureDrag) {
            let Some((start_x, start_y)) = gesture.start_point() else {
                return;
            };

            let obj = self.obj();
            let image_coords = obj.to_image_coords((start_x + offset_x, start_y + offset_y));
            self.current_position.replace(image_coords);
            obj.queue_draw();
        }

        #[template_callback]
        fn on_drag_end(&self, offset_x: f64, offset_y: f64, gesture: &gtk::GestureDrag) {
            let Some((start_x, start_y)) = gesture.start_point() else {
                return;
            };

            let obj = self.obj();
            let image_coords = obj.to_image_coords((start_x + offset_x, start_y + offset_y));
            self.current_position.replace(image_coords);
            obj.apply_filter();
        }

        #[template_callback]
        fn on_drag_cancel(&self) {
            self.start_position.replace(None);
            self.current_position.replace(None);
        }

        #[template_callback]
        fn on_scale_factor_notify(&self) {
            let obj = self.obj();
            let change = obj.scale_factor() as f64 / self.scale_factor.get() as f64;

            let hadj = obj.hadjustment().unwrap();
            let vadj = obj.vadjustment().unwrap();
            let new_scroll = (hadj.value(), vadj.value());

            obj.set_zoom(obj.zoom() * change);
            self.queued_scroll.replace(Some(new_scroll));

            self.scale_factor.set(obj.scale_factor());
            obj.queue_allocate();
        }

        #[template_callback]
        fn on_motion_enter(&self, x: f64, y: f64) {
            self.pointer_position.replace(Some((x, y)));
        }

        #[template_callback]
        fn on_motion(&self, x: f64, y: f64) {
            self.pointer_position.replace(Some((x, y)));
        }

        #[template_callback]
        fn on_motion_leave(&self) {
            self.pointer_position.replace(None);
        }

        #[template_callback]
        fn on_scroll(
            &self,
            _delta_x: f64,
            delta_y: f64,
            event: &gtk::EventControllerScroll,
        ) -> glib::Propagation {
            if event
                .current_event_state()
                .contains(gdk::ModifierType::CONTROL_MASK)
            {
                let obj = self.obj();
                let zoom = -delta_y * 0.1 + obj.zoom();
                obj.set_zoom(zoom.clamp(0f64, 8.0f64));
                glib::Propagation::Stop
            } else {
                glib::Propagation::Proceed
            }
        }

        #[template_callback]
        fn on_zoom_begin(&self, _: &gdk::EventSequence, gesture: &gtk::GestureZoom) {
            let Some(view_center) = gesture.bounding_box_center() else {
                return;
            };
            self.obj().begin_zoom(view_center);
            gesture.set_state(gtk::EventSequenceState::Claimed);
        }

        #[template_callback]
        fn on_zoom_scale_changed(&self, scale: f64, gesture: &gtk::GestureZoom) {
            let Some(view_center) = gesture.bounding_box_center() else {
                return;
            };
            self.obj()
                .set_zoom_at_center(self.initial_zoom.get() * scale, view_center);
        }
    }
}

glib::wrapper! {
    pub struct DrawingArea(ObjectSubclass<imp::DrawingArea>)
        @extends gtk::Widget,
        @implements gtk::Scrollable;
}

impl DrawingArea {
    fn effective_zoom(&self) -> f64 {
        self.zoom() / self.scale_factor() as f64
    }

    pub fn load_file(&self, file: &gio::File) -> anyhow::Result<()> {
        let texture = gdk::Texture::from_file(file)?;
        self.load_texture(texture);
        Ok(())
    }

    fn texture(&self) -> Option<gdk::Texture> {
        let history = self.imp().history.borrow().clone();
        history.into_iter().last()
    }

    fn bounds(&self) -> graphene::Rect {
        let (width, height) = if let Some(texture) = self.texture() {
            (texture.width() as f32, texture.height() as f32)
        } else {
            (0.0, 0.0)
        };
        graphene::Rect::new(0.0, 0.0, width, height)
    }

    fn load_texture(&self, texture: gdk::Texture) {
        let imp = self.imp();
        imp.history.replace(vec![texture]);
        imp.future.replace(Vec::new());
        imp.start_position.replace(None);
        imp.current_position.replace(None);
        imp.initial_zoom_center.replace(None);
        self.set_can_undo(false);
        self.set_can_redo(false);
        self.set_saved(true);
        self.set_zoom(1.0);
        imp.queued_scroll.replace(Some((0.0, 0.0)));
        self.queue_allocate();
    }

    fn begin_zoom(&self, zoom_center: (f64, f64)) {
        let imp = self.imp();
        imp.initial_zoom.set(self.zoom());
        imp.initial_zoom_center
            .replace(self.to_image_coords(zoom_center));
    }

    pub fn set_zoom_at_center(&self, new_zoom: f64, zoom_center: (f64, f64)) {
        let imp = self.imp();

        let zoom = self.zoom();
        imp.zoom
            .replace(new_zoom.clamp((0.1f64).min(zoom), (8.0f64).max(zoom)));

        let Some(hadj) = self.hadjustment() else {
            return;
        };
        let Some(vadj) = self.vadjustment() else {
            return;
        };

        let (center_x, center_y) = zoom_center;

        let Some((x, y)) = imp
            .initial_zoom_center
            .get()
            .and_then(|initial_zoom| self.convert_image_coords(initial_zoom))
        else {
            return;
        };

        let new_scroll = (x + hadj.value() - center_x, y + vadj.value() - center_y);
        imp.queued_scroll.replace(Some(new_scroll));

        self.queue_allocate();
        self.queue_draw();
    }

    pub async fn save_to(&self, dest: &gio::File) -> anyhow::Result<()> {
        if let Some(ref texture) = self.texture() {
            let bytes = texture.save_to_png_bytes();
            dest.replace_contents_future(
                bytes,
                None,
                false,
                gio::FileCreateFlags::REPLACE_DESTINATION,
            )
            .await
            .map_err(|e| e.1)?;
            self.set_saved(true);
        }
        Ok(())
    }

    pub async fn paste(&self) {
        let clipboard = self.clipboard();
        match clipboard.read_texture_future().await {
            Ok(Some(texture)) => {
                self.load_texture(texture);
                self.emit_by_name::<()>("loaded", &[]);
            }
            Ok(None) => {
                log::warn!("No texture found");
            }
            Err(err) => {
                log::error!("Failed to paste texture {err}");
            }
        }
    }

    pub fn copy(&self) {
        if let Some(ref texture) = self.texture() {
            let clipboard = self.clipboard();
            clipboard.set_texture(texture);
        }
    }

    pub fn undo(&self) {
        let imp = self.imp();
        if let Some(texture) = imp.history.borrow_mut().pop() {
            self.set_can_redo(true);
            imp.future.borrow_mut().push(texture);
        } else {
            self.set_can_redo(false);
        }
        let has_history_left = imp.history.borrow().len() > 1;
        self.set_can_undo(has_history_left);
        self.set_saved(!has_history_left);
        self.queue_allocate();
        self.queue_draw();
    }

    pub fn redo(&self) {
        let imp = self.imp();
        if let Some(texture) = imp.future.borrow_mut().pop() {
            imp.history.borrow_mut().push(texture);
            self.set_can_undo(true);
        } else {
            self.set_can_undo(true);
        }
        self.set_can_redo(imp.future.borrow().len() != 0);
        self.set_saved(false);
        self.queue_draw();
    }

    /// Takes current (start_position, current_position)
    /// and based on the selected filter, create an appropriate texture
    /// and add it to the history
    fn apply_filter(&self) {
        let imp = self.imp();
        let Some(start_pos) = imp.start_position.take() else {
            return;
        };
        let Some(end_pos) = imp.current_position.take() else {
            return;
        };
        let Some(texture) = self.texture() else {
            return;
        };

        let start_pos = self.clamp_to_image(start_pos);
        let end_pos = self.clamp_to_image(end_pos);

        let (x, y) = top_left_corner(start_pos, end_pos);
        let (width, height) = width_height(start_pos, end_pos);

        let filter_bounds = graphene::Rect::new(x, y, width, height);
        let texture_bounds = self.bounds();

        let Some(renderer) = self
            .root()
            .and_upcast::<gtk::Native>()
            .and_then(|n| n.renderer())
        else {
            return;
        };
        let snapshot = gtk::Snapshot::new();
        match self.filter() {
            Filter::Blur => {
                snapshot.append_texture(&texture, &texture_bounds);
                snapshot.push_clip(&filter_bounds);
                snapshot.push_blur(10.0);
                snapshot.push_clip(&filter_bounds.inset_r(-10.0, -10.0));
                snapshot.append_texture(&texture, &texture_bounds);
                snapshot.pop();
                snapshot.pop();
                snapshot.pop();
            }
            Filter::Filled => {
                snapshot.append_texture(&texture, &texture_bounds);
                snapshot.append_color(&gdk::RGBA::BLACK, &filter_bounds);
            }
        };

        let Some(node) = snapshot.to_node() else {
            return;
        };
        let texture = renderer.render_texture(&node, None);

        imp.history.borrow_mut().push(texture);
        self.set_saved(false);
        imp.current_position.replace(None);
        self.set_can_undo(true);
        self.queue_draw();
    }

    fn to_image_coords(&self, view_coords: (f64, f64)) -> Option<(f64, f64)> {
        let hadj = self.hadjustment()?;
        let vadj = self.vadjustment()?;
        let texture = self.texture()?;
        let zoom = self.effective_zoom();
        let (translate_x, translate_y) = translate(
            self.width() as f32,
            self.height() as f32,
            &texture,
            zoom as f32,
        );
        let (x, y) = view_coords;

        let image_x = (x + hadj.value() - translate_x as f64) / zoom;
        let image_y = (y + vadj.value() - translate_y as f64) / zoom;

        Some((image_x, image_y))
    }

    fn convert_image_coords(&self, image_coords: (f64, f64)) -> Option<(f64, f64)> {
        let hadj = self.hadjustment()?;
        let vadj = self.vadjustment()?;
        let texture = self.texture()?;
        let zoom = self.effective_zoom();
        let (translate_x, translate_y) = translate(
            self.width() as f32,
            self.height() as f32,
            &texture,
            zoom as f32,
        );
        let (x, y) = image_coords;

        let view_x = x * zoom - hadj.value() + translate_x as f64;
        let view_y = y * zoom - vadj.value() + translate_y as f64;

        Some((view_x, view_y))
    }

    fn clamp_to_image(&self, pos: (f64, f64)) -> (f64, f64) {
        let (x, y) = pos;

        let Some(texture) = self.texture() else {
            return (x, y);
        };

        let x = x.clamp(0.0, texture.width() as f64);
        let y = y.clamp(0.0, texture.height() as f64);

        (x, y)
    }
}

fn top_left_corner(pos1: (f64, f64), pos2: (f64, f64)) -> (f32, f32) {
    let (x0, y0) = pos1;
    let (x1, y1) = pos2;

    let width = x1 - x0;
    let height = y1 - y0;
    // figure out the real start position of the rectangle
    let (x, y) = if height < 0.0 && width > 0.0 {
        (x0, y1)
    } else if width < 0.0 && height > 0.0 {
        (x1, y0)
    } else if width < 0.0 && height < 0.0 {
        (x1, y1)
    } else {
        (x0, y0)
    };
    (x as f32, y as f32)
}

fn width_height(pos1: (f64, f64), pos2: (f64, f64)) -> (f32, f32) {
    let (x0, y0) = pos1;
    let (x1, y1) = pos2;

    let width = (x1 - x0).abs();
    let height = (y1 - y0).abs();

    ((width) as f32, (height) as f32)
}

fn translate(width: f32, height: f32, texture: &gdk::Texture, zoom: f32) -> (f32, f32) {
    let (mut translate_x, mut translate_y) = (0.0, 0.0);
    let texture_width = texture.width() as f32 * zoom;
    let texture_height = texture.height() as f32 * zoom;

    if width > texture_width {
        translate_x = (width - texture_width) / 2.0;
    }
    if height > texture_height {
        translate_y = (height - texture_height) / 2.0;
    }
    (translate_x, translate_y)
}
