use adw::{prelude::*, subclass::prelude::*};
use gtk::{gio, glib, glib::clone, CompositeTemplate};
use tracing::{error, warn};

use super::video_player_renderer::VideoPlayerRenderer;
use crate::utils::LoadingState;

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

    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/components/media/video_player.ui")]
    #[properties(wrapper_type = super::VideoPlayer)]
    pub struct VideoPlayer {
        #[template_child]
        video: TemplateChild<gtk::Picture>,
        #[template_child]
        timestamp: TemplateChild<gtk::Label>,
        #[template_child]
        player: TemplateChild<gst_play::Play>,
        /// The file that is currently played.
        file: RefCell<Option<gio::File>>,
        /// Whether the player is displayed in its compact form.
        #[property(get, set = Self::set_compact, explicit_notify)]
        compact: Cell<bool>,
        /// The state of the video in this player.
        #[property(get, builder(LoadingState::default()))]
        state: Cell<LoadingState>,
        /// The current error, if any.
        pub(super) error: RefCell<Option<glib::Error>>,
        /// The duration of the video, if it is known.
        duration: Cell<Option<gst::ClockTime>>,
        bus_guard: OnceCell<gst::bus::BusWatchGuard>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for VideoPlayer {
        const NAME: &'static str = "VideoPlayer";
        type Type = super::VideoPlayer;
        type ParentType = adw::Bin;

        fn class_init(klass: &mut Self::Class) {
            VideoPlayerRenderer::ensure_type();

            Self::bind_template(klass);
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for VideoPlayer {
        fn constructed(&self) {
            self.parent_constructed();

            let bus_guard = self
                .player
                .message_bus()
                .add_watch_local(clone!(
                    #[weak(rename_to = imp)]
                    self,
                    #[upgrade_or]
                    glib::ControlFlow::Break,
                    move |_, message| {
                        if let Ok(message) = gst_play::PlayMessage::parse(message) {
                            imp.handle_message(message);
                        }

                        glib::ControlFlow::Continue
                    }
                ))
                .expect("adding message bus watch succeeds");
            self.bus_guard
                .set(bus_guard)
                .expect("bus guard is uninitialized");
        }

        fn dispose(&self) {
            self.player.message_bus().set_flushing(true);
        }
    }

    impl WidgetImpl for VideoPlayer {
        fn map(&self) {
            self.parent_map();

            // Avoid more errors in the logs.
            if self.state.get() != LoadingState::Error {
                self.player.play();
            }
        }

        fn unmap(&self) {
            self.player.stop();
            self.parent_unmap();
        }
    }

    impl BinImpl for VideoPlayer {}

    impl VideoPlayer {
        /// Set whether this player should be displayed in a compact format.
        fn set_compact(&self, compact: bool) {
            if self.compact.get() == compact {
                return;
            }

            self.compact.set(compact);

            self.update_timestamp();
            self.obj().notify_compact();
        }

        /// Set the state of the media.
        fn set_state(&self, state: LoadingState) {
            if self.state.get() == state {
                return;
            }

            self.state.set(state);
            self.obj().notify_state();
        }

        /// Set the video file to play.
        pub(super) fn play_video_file(&self, file: gio::File) {
            let uri = file.uri();
            self.file.replace(Some(file));

            self.set_duration(None);
            self.set_state(LoadingState::Loading);

            self.player.set_uri(Some(uri.as_ref()));
            self.player.set_audio_track_enabled(false);

            if self.obj().is_mapped() {
                self.player.play();
            } else {
                // Pause, unlike stop, loads the info of the video.
                self.player.pause();
            }
        }

        /// Handle a message from the player.
        fn handle_message(&self, message: gst_play::PlayMessage) {
            match message {
                gst_play::PlayMessage::StateChanged { state } => {
                    if matches!(
                        state,
                        gst_play::PlayState::Playing | gst_play::PlayState::Paused
                    ) {
                        // Files that fail to play go from `Buffering` to `Stopped`.
                        self.set_state(LoadingState::Ready);
                    }
                }
                gst_play::PlayMessage::DurationChanged { duration } => {
                    self.set_duration(duration);
                }
                gst_play::PlayMessage::Warning { error, .. } => {
                    warn!("Warning playing video: {error}");
                }
                gst_play::PlayMessage::Error { error, .. } => {
                    error!("Error playing video: {error}");
                    self.error.replace(Some(error));
                    self.set_state(LoadingState::Error);
                }
                _ => {}
            }
        }

        /// Set the duration of the video.
        fn set_duration(&self, duration: Option<gst::ClockTime>) {
            if self.duration.get() == duration {
                return;
            }

            self.duration.set(duration);
            self.update_timestamp();
        }

        /// Update the timestamp for the current state.
        fn update_timestamp(&self) {
            // We show the duration if we know it and if we are not in compact mode.
            let visible_duration = self.duration.get().filter(|_| !self.compact.get());
            let is_visible = visible_duration.is_some();

            if let Some(duration) = visible_duration {
                let mut time = duration.seconds();

                let sec = time % 60;
                time -= sec;
                let min = (time % (60 * 60)) / 60;
                time -= min * 60;
                let hour = time / (60 * 60);

                let label = if hour > 0 {
                    // FIXME: Find how to localize this.
                    // hour:minutes:seconds
                    format!("{hour}:{min:02}:{sec:02}")
                } else {
                    // FIXME: Find how to localize this.
                    // minutes:seconds
                    format!("{min:02}:{sec:02}")
                };

                self.timestamp.set_label(&label);
            }

            self.timestamp.set_visible(is_visible);
        }
    }
}

glib::wrapper! {
    /// A widget to preview a video file without controls or sound.
    pub struct VideoPlayer(ObjectSubclass<imp::VideoPlayer>)
        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

impl VideoPlayer {
    /// Create a new video player.
    pub fn new() -> Self {
        glib::Object::new()
    }

    /// Set the video file to play.
    pub(crate) fn play_video_file(&self, file: gio::File) {
        self.imp().play_video_file(file);
    }

    /// The current error, if any.
    pub(crate) fn error(&self) -> Option<glib::Error> {
        self.imp().error.borrow().clone()
    }
}
