use glib::{ControlFlow, Properties, SourceId, clone, prelude::*, subclass::prelude::*, subclass::*};
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use std::{rc::Rc, time::Duration};

use super::drag_buffer::DragBuffer;

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type = super::DragScrollTracker)]
    pub struct DragScrollTracker {
        #[property(get, set, name = "drag-ongoing")]
        pub drag_ongoing: Cell<bool>,

        pub(super) drag_y_offset: Cell<f64>,
        pub(super) drag_momentum: Cell<f64>,
        pub(super) drag_buffer: RefCell<DragBuffer>,

        pub(super) drag_buffer_update_signal: RefCell<Option<SourceId>>,
        pub(super) drag_released_motion_signal: RefCell<Option<SourceId>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DragScrollTracker {
        const NAME: &'static str = "DragScrollTracker";
        type Type = super::DragScrollTracker;
    }

    #[glib::derived_properties]
    impl ObjectImpl for DragScrollTracker {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("scroll-to").param_types([f64::static_type()]).build()]);

            SIGNALS.as_ref()
        }
    }

    impl DragScrollTracker {
        pub(super) fn update_tick(&self) -> ControlFlow {
            if !self.drag_ongoing.get() {
                self.drag_buffer_update_signal.take();
                return ControlFlow::Break;
            }

            for i in (1..10).rev() {
                let value = self.drag_buffer.borrow().get(i - 1);
                self.drag_buffer.borrow_mut().set(i, value);
            }

            self.drag_buffer.borrow_mut().set(0, self.drag_y_offset.get());

            let momentum = self.drag_buffer.borrow().get(9) - self.drag_buffer.borrow().get(0);
            self.drag_momentum.set(momentum);
            ControlFlow::Continue
        }

        pub(super) fn released_tick(
            &self,
            scroll_pos: Rc<Cell<f64>>,
            scroll_upper: f64,
            page_size: f64,
        ) -> ControlFlow {
            self.drag_momentum.set(self.drag_momentum.get() / 1.2);

            let adjust_value = self.drag_momentum.get();
            let old_adjust = scroll_pos.get();
            let upper = scroll_upper;

            if (old_adjust + adjust_value) > (upper - page_size) || (old_adjust + adjust_value) < 0.0 {
                self.drag_momentum.set(0.0);
            }

            let new_scroll_pos = f64::min(old_adjust + adjust_value, upper - page_size);
            scroll_pos.set(new_scroll_pos);

            self.obj().emit_scroll_to(adjust_value);

            if self.drag_momentum.get().abs() < 1.0 || self.drag_ongoing.get() {
                self.drag_released_motion_signal.take();
                return ControlFlow::Break;
            }

            ControlFlow::Continue
        }
    }
}

glib::wrapper! {
    pub struct DragScrollTracker(ObjectSubclass<imp::DragScrollTracker>);
}

impl Default for DragScrollTracker {
    fn default() -> Self {
        glib::Object::new::<Self>()
    }
}

impl DragScrollTracker {
    pub fn begin_drag(&self) {
        let imp = self.imp();

        if let Some(a) = imp.drag_released_motion_signal.take() {
            SourceId::remove(a)
        }
        if let Some(a) = imp.drag_buffer_update_signal.take() {
            SourceId::remove(a)
        }
        imp.drag_buffer.replace(DragBuffer::default());
        imp.drag_ongoing.set(true);
        imp.drag_momentum.set(0.0);

        let tick_callback = clone!(
            #[weak]
            imp,
            #[upgrade_or]
            ControlFlow::Break,
            move || imp.update_tick()
        );

        let drag_tick_source_id = glib::timeout_add_local(Duration::from_millis(16), tick_callback);
        imp.drag_buffer_update_signal.replace(Some(drag_tick_source_id));
    }

    pub fn update_drag(&self, y_offset: f64) {
        let imp = self.imp();

        if !imp.drag_ongoing.get() {
            return;
        }

        for i in (1..10).rev() {
            let value = imp.drag_buffer.borrow().get(i - 1);
            imp.drag_buffer.borrow_mut().set(i, value);
        }

        imp.drag_buffer.borrow_mut().set(0, y_offset);

        let momentum = imp.drag_buffer.borrow().get(9) - imp.drag_buffer.borrow().get(0);
        imp.drag_momentum.set(momentum);

        let scroll = imp.drag_y_offset.get() - y_offset;
        imp.drag_y_offset.set(y_offset);

        self.emit_scroll_to(scroll);
    }

    pub fn end_drag(&self, scroll_pos: f64, scroll_upper: f64, page_size: f64) {
        let imp = self.imp();

        if !imp.drag_ongoing.get() {
            return;
        }

        imp.drag_ongoing.set(false);
        imp.drag_y_offset.set(0.0);

        let scroll_pos = Rc::new(Cell::new(scroll_pos));

        let released_callback = clone!(
            #[weak]
            imp,
            #[strong]
            scroll_pos,
            #[upgrade_or]
            ControlFlow::Break,
            move || imp.released_tick(scroll_pos.clone(), scroll_upper, page_size)
        );

        let released_tick_source_id = glib::timeout_add_local(Duration::from_millis(16), released_callback);
        imp.drag_released_motion_signal.replace(Some(released_tick_source_id));
    }

    pub fn stop(&self) {
        let imp = self.imp();

        if imp.drag_ongoing.get() {
            self.set_drag_ongoing(false);
            imp.drag_y_offset.set(0.0);
        }

        if let Some(a) = imp.drag_released_motion_signal.take() {
            SourceId::remove(a)
        }
        if let Some(a) = imp.drag_buffer_update_signal.take() {
            SourceId::remove(a)
        }
    }

    fn emit_scroll_to(&self, pos: f64) {
        self.emit_by_name::<()>("scroll-to", &[&pos]);
    }
}
