use super::ArticleLoader;
use super::ArticleView;
use crate::app::App;
use crate::article_list::ArticleList;
use crate::article_view::article_widget::ArticleWidget;
use crate::content_page::ArticleViewColumn;
use crate::gobject_models::GRead;
use crate::infrastructure::TokioRuntime;
use crate::util::JOIN_ERROR;
use gdk4::Rectangle;
use glib::{Properties, clone, subclass::prelude::*};
use gsk4::graphene;
use gtk4::{Accessible, Buildable, ConstraintTarget, Orientation, Snapshot, Widget, prelude::*, subclass::prelude::*};
use libadwaita::{
    CallbackAnimationTarget, NavigationDirection, SpringAnimation, SpringParams, SwipeTracker, Swipeable, prelude::*,
    subclass::prelude::*,
};
use std::cell::{Cell, RefCell};

mod imp {

    use super::*;

    #[derive(Debug, Properties)]
    #[properties(wrapper_type = super::ArticleSwipeable)]
    pub struct ArticleSwipeable {
        pub progress: Cell<f64>,
        pub can_go_prev: Cell<bool>,
        pub can_go_next: Cell<bool>,
        pub tracker: RefCell<Option<SwipeTracker>>,
        pub animation: RefCell<Option<SpringAnimation>>,
        pub direction: Cell<NavigationDirection>,
        pub decoy: RefCell<Option<ArticleWidget>>,

        #[property(get, set = Self::set_child, nullable)]
        pub child: RefCell<Option<Widget>>,
    }

    impl Default for ArticleSwipeable {
        fn default() -> Self {
            Self {
                direction: Cell::new(NavigationDirection::Forward),
                progress: Default::default(),
                can_go_prev: Default::default(),
                can_go_next: Default::default(),
                tracker: Default::default(),
                animation: Default::default(),
                decoy: Default::default(),
                child: Default::default(),
            }
        }
    }

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

    #[glib::derived_properties]
    impl ObjectImpl for ArticleSwipeable {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            let tracker = SwipeTracker::new(&*obj);
            tracker.set_enabled(true);
            tracker.set_allow_mouse_drag(true);
            tracker.set_lower_overshoot(true);
            tracker.set_upper_overshoot(true);

            tracker.connect_prepare(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_tracker, direction| {
                    tracing::trace!("swipe prep");

                    imp.progress.set(0.0);
                    imp.direction.set(direction);
                    imp.can_go_next.set(ArticleList::instance().can_go_next());
                    imp.can_go_prev.set(ArticleList::instance().can_go_prev());
                }
            ));

            // 2. Capture the frozen snapshot when the swipe BEGINS
            tracker.connect_begin_swipe(clone!(
                #[weak]
                obj,
                move |_| {
                    let imp = obj.imp();

                    tracing::trace!("swipe begin");

                    if let Some(animation) = imp.animation.take() {
                        animation.skip();
                    }

                    if let Some(child) = imp.child.borrow().as_ref() {
                        child.add_css_class("hide-scroll");
                    }

                    // refresh if animation was still ongoing during prepare
                    imp.can_go_next.set(ArticleList::instance().can_go_next());
                    imp.can_go_prev.set(ArticleList::instance().can_go_prev());

                    let disallow_prev = imp.direction.get() == NavigationDirection::Back && !imp.can_go_prev.get();
                    let disallow_next = imp.direction.get() == NavigationDirection::Forward && !imp.can_go_next.get();

                    if disallow_prev || disallow_next {
                        if let Some(old_decoy) = imp.decoy.take() {
                            old_decoy.unparent();
                        }
                        return;
                    }

                    let widget = ArticleWidget::default();
                    widget.set_parent(&obj);
                    if let Some(article_widget) = ArticleView::instance().imp().article_widget.borrow().as_ref() {
                        widget.set_inner_margin(article_widget.inner_margin());
                    };
                    if let Some(old_decoy) = imp.decoy.replace(Some(widget)) {
                        old_decoy.unparent();
                    }

                    let article = match imp.direction.get() {
                        NavigationDirection::Back => ArticleList::instance().prev_article(),
                        _ => ArticleList::instance().next_article(),
                    };

                    let Some(article) = article else {
                        return;
                    };

                    glib::spawn_future_local(clone!(
                        #[weak]
                        imp,
                        async move {
                            // load content of next article
                            let article_id = article.article_id();

                            let future = TokioRuntime::spawn(async move {
                                let news_flash = App::news_flash();
                                let news_flash_guard = news_flash.read().await;
                                let news_flash = news_flash_guard.as_ref()?;
                                ArticleLoader::load(&article_id, news_flash)
                            });

                            let Some(loader) = future.await.expect(JOIN_ERROR) else {
                                return;
                            };

                            let new_article = loader.build();
                            new_article.set_read(GRead::Read);

                            let parse_result = ArticleView::instance().imp().build_article(&new_article, true).await;

                            if let Some(parse_result) = parse_result
                                && let Some(article_widget) = imp.decoy.borrow().as_ref()
                            {
                                article_widget.set_article(Some(new_article));
                                article_widget.set_parse_result_partial(parse_result, imp.obj().height());
                            }
                        }
                    ));
                }
            ));

            tracker.connect_update_swipe(glib::clone!(
                #[weak]
                obj,
                move |_, progress| {
                    let imp = obj.imp();

                    let rubber_band = |p: f64, min: f64, max: f64| {
                        if p < min {
                            let overshoot = p - min;
                            min + overshoot / (1.0 + overshoot.abs() * 4.0)
                        } else if p > max {
                            let overshoot = p - max;
                            max + overshoot / (1.0 + overshoot.abs() * 4.0)
                        } else {
                            p
                        }
                    };
                    let clamped_progress = match imp.direction.get() {
                        NavigationDirection::Back => {
                            let p = progress.min(0.0);
                            let (min, max) = if imp.can_go_prev.get() { (-1.0, 0.0) } else { (0.0, 0.0) };
                            rubber_band(p, min, max)
                        }
                        NavigationDirection::Forward => {
                            let p = progress.max(0.0);
                            let (min, max) = if imp.can_go_next.get() { (0.0, 1.0) } else { (0.0, 0.0) };
                            rubber_band(p, min, max)
                        }
                        _ => rubber_band(progress, -1.0, 1.0),
                    };
                    imp.progress.set(clamped_progress);
                    obj.queue_draw();
                }
            ));

            tracker.connect_end_swipe(clone!(
                #[weak]
                obj,
                move |_tracker, velocity, to| {
                    let imp = obj.imp();

                    tracing::trace!("swipe end");

                    let mut to = to;
                    match imp.direction.get() {
                        NavigationDirection::Back => {
                            if to > -0.5 || !imp.can_go_prev.get() {
                                to = 0.0;
                            }
                        }
                        NavigationDirection::Forward => {
                            if to < 0.5 || !imp.can_go_next.get() {
                                to = 0.0;
                            }
                        }
                        _ => {
                            if (to < -0.5 && !imp.can_go_prev.get()) || (to > 0.5 && !imp.can_go_next.get()) {
                                to = 0.0;
                            }
                        }
                    }

                    // 2. Create an animation target that updates our progress variable
                    let target = CallbackAnimationTarget::new(clone!(
                        #[weak]
                        obj,
                        move |value| {
                            obj.imp().progress.set(value);
                            obj.queue_draw();
                        }
                    ));

                    // 3. Setup realistic spring physics (damping ratio, mass, stiffness)
                    // A damping ratio of 1.0 means no bouncing (critical damping).
                    let spring_params = SpringParams::new(1.0, 1.0, 400.0);

                    let anim = SpringAnimation::new(&obj, imp.progress.get(), to, spring_params, target);

                    // Pass the physical flick velocity directly into the spring
                    anim.set_initial_velocity(velocity);
                    anim.set_clamp(false); // Allow natural overshooting

                    // 4. Cleanup when the animation finishes
                    anim.connect_done(clone!(
                        #[weak]
                        obj,
                        move |_| {
                            let imp = obj.imp();
                            imp.animation.replace(None);

                            tracing::trace!("animation finished");

                            if let Some(child) = imp.child.borrow().as_ref() {
                                child.remove_css_class("hide-scroll");
                            }

                            if (imp.progress.get().abs() - 1.0).abs() <= f64::EPSILON {
                                match imp.direction.get() {
                                    NavigationDirection::Back => ArticleList::instance().select_prev_article(),
                                    _ => ArticleList::instance().select_next_article(),
                                };

                                if let Some(article_widget) = imp.decoy.take() {
                                    article_widget.reload();
                                    article_widget.unparent();
                                    let article = article_widget.article();
                                    ArticleView::instance().replace_article_widget(article_widget);
                                    ArticleViewColumn::instance().set_article_without_signals(article);
                                }
                            } else {
                                if let Some(article_widget) = imp.decoy.take() {
                                    article_widget.unparent();
                                }
                            }

                            imp.progress.set(0.0);

                            obj.queue_draw();
                        }
                    ));

                    // Start the animation and store it
                    anim.play();
                    imp.animation.replace(Some(anim));
                }
            ));

            self.tracker.replace(Some(tracker));
        }

        fn dispose(&self) {
            if let Some(decoy) = self.decoy.take() {
                decoy.unparent();
            }
            if let Some(child) = self.child.take() {
                child.unparent();
            }
        }
    }

    impl WidgetImpl for ArticleSwipeable {
        fn measure(&self, orientation: Orientation, for_size: i32) -> (i32, i32, i32, i32) {
            if let Some(decoy) = self.decoy.borrow().as_ref() {
                decoy.measure(orientation, for_size);
            }

            if let Some(child) = self.child.borrow().as_ref() {
                child.measure(orientation, for_size)
            } else {
                (0, 0, -1, -1)
            }
        }

        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
            if let Some(decoy) = self.decoy.borrow().as_ref() {
                decoy.allocate(width, height, baseline, None);
            }
            if let Some(child) = self.child.borrow().as_ref() {
                child.allocate(width, height, baseline, None);
            }
        }

        // 5. The core visual logic: Draw the reveal transition
        fn snapshot(&self, snapshot: &Snapshot) {
            let progress = self.progress.get();
            let width = self.obj().width() as f32;

            // If we are actively swiping (not resting at an exact snap point)
            if progress != 0.0 {
                let offset = width * progress as f32;

                // Draw the OLD state (frozen node) sliding away
                if let Some(child) = &*self.child.borrow() {
                    snapshot.save();
                    // If progress is positive, -offset pushes it left.
                    // If progress is negative, -offset pushes it right.
                    snapshot.translate(&graphene::Point::new(-offset, 0.0));
                    self.obj().snapshot_child(child, snapshot);
                    snapshot.restore();
                }

                // Draw the NEW state (live child) sliding in
                if let Some(widget) = &*self.decoy.borrow() {
                    snapshot.save();

                    // Determine which side the new child comes from
                    let child_offset = if progress > 0.0 {
                        width - offset // Coming from the right
                    } else {
                        -width - offset // Coming from the left
                    };

                    snapshot.translate(&graphene::Point::new(child_offset, 0.0));
                    self.obj().snapshot_child(widget, snapshot);
                    snapshot.restore();
                }
            } else {
                // Widget is at rest, just draw the live child normally
                if let Some(child) = &*self.child.borrow() {
                    self.obj().snapshot_child(child, snapshot);
                }
            }
        }
    }

    impl SwipeableImpl for ArticleSwipeable {
        fn distance(&self) -> f64 {
            // Distance is usually the widget's width for horizontal swipes
            self.obj().width() as f64
        }

        fn snap_points(&self) -> Vec<f64> {
            vec![-1.0, 0.0, 1.0]
        }

        fn progress(&self) -> f64 {
            self.progress.get()
        }

        fn cancel_progress(&self) -> f64 {
            self.progress.get().round()
        }

        fn swipe_area(&self, _navigation_direction: NavigationDirection, _is_drag: bool) -> Rectangle {
            let obj = self.obj();
            Rectangle::new(0, 0, obj.width(), obj.height())
        }
    }

    impl ArticleSwipeable {
        fn set_child(&self, child: Option<Widget>) {
            let obj = self.obj();

            // Remove old child
            if let Some(old_child) = self.child.take() {
                old_child.unparent();
            }

            // Set and parent new child
            if let Some(new_child) = child {
                new_child.set_parent(&*obj);
                self.child.replace(Some(new_child));
            }

            obj.queue_resize();
        }
    }
}

glib::wrapper! {
    pub struct ArticleSwipeable(ObjectSubclass<imp::ArticleSwipeable>)
        @extends Widget,
        @implements Accessible, Buildable, ConstraintTarget, Swipeable;
}

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

impl ArticleSwipeable {
    pub fn new() -> Self {
        Self::default()
    }
}
