// SPDX-License-Identifier: GPL-3.0-or-later
use std::os::fd::OwnedFd;

use adw::subclass::prelude::*;
use ashpd::desktop::camera;
use gtk::{
    glib::{self, clone},
    prelude::*,
};

use crate::{screenshot, widgets::CameraRow};

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

    use glib::subclass::Signal;

    use super::*;

    #[derive(Debug, gtk::CompositeTemplate, Default, glib::Properties)]
    #[properties(wrapper_type = super::CameraPage)]
    #[template(resource = "/com/belmoussaoui/Decoder/ui/camera_page.ui")]
    pub struct CameraPage {
        #[template_child]
        pub viewfinder: TemplateChild<aperture::Viewfinder>,
        #[template_child]
        pub camera_menu_button: TemplateChild<gtk::MenuButton>,
        #[template_child]
        pub stack: TemplateChild<gtk::Stack>,

        #[property(get, set)]
        is_streaming: Cell<bool>,

        pub portal_failed: Cell<bool>,
        pub selection: gtk::SingleSelection,
        pub provider: RefCell<Option<aperture::DeviceProvider>>,
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();

            klass.install_action_async("camera.screenshot-scan", None, async |obj, _, _| {
                let root = obj.root().unwrap();
                match screenshot::capture(&root).await {
                    Ok(code) => obj.emit_code_detected(&code),
                    Err(err) => tracing::error!("Could not take screenshot: {err}"),
                }
            });
        }

        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }
    #[glib::derived_properties]
    impl ObjectImpl for CameraPage {
        fn constructed(&self) {
            let obj = self.obj();
            self.parent_constructed();

            self.viewfinder.connect_code_detected(clone!(
                #[weak]
                obj,
                move |_, code| {
                    match std::str::from_utf8(&code) {
                        Ok(code) => obj.emit_code_detected(code),
                        Err(err) => {
                            let code = String::from_utf8_lossy(&code);
                            tracing::error!("Could not decode QR code {code}: {err}");
                        }
                    }
                }
            ));
            self.viewfinder.connect_state_notify(clone!(
                #[weak]
                obj,
                move |_| {
                    obj.update_state();
                }
            ));
            obj.update_state();

            let popover = gtk::Popover::new();
            popover.add_css_class("menu");

            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(|_, item| {
                let item = item.downcast_ref::<gtk::ListItem>().unwrap();
                let camera_row = CameraRow::default();

                item.set_child(Some(&camera_row));
            });
            let selection = &self.selection;
            factory.connect_bind(clone!(
                #[weak]
                selection,
                move |_, item| {
                    let item = item.downcast_ref::<gtk::ListItem>().unwrap();
                    let child = item.child().unwrap();
                    let row = child.downcast_ref::<CameraRow>().unwrap();

                    let item = item.item().and_downcast::<aperture::Camera>().unwrap();
                    row.set_label(&item.display_name());
                    row.set_model(&selection);
                }
            ));
            let list_view = gtk::ListView::new(Some(self.selection.clone()), Some(factory));

            popover.set_child(Some(&list_view));

            self.selection.connect_selected_item_notify(clone!(
                #[weak]
                obj,
                #[weak]
                popover,
                move |selection| {
                    if let Some(selected_item) = selection.selected_item() {
                        let camera = selected_item.downcast::<aperture::Camera>().ok();
                        obj.imp().viewfinder.set_camera(camera);
                    }
                    popover.popdown();
                }
            ));

            self.camera_menu_button.set_popover(Some(&popover));
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
                vec![
                    Signal::builder("code-detected")
                        .param_types([String::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }
    impl WidgetImpl for CameraPage {}
    impl BinImpl for CameraPage {}
}

glib::wrapper! {
    pub struct CameraPage(ObjectSubclass<imp::CameraPage>)
        @extends gtk::Widget, adw::Bin,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl CameraPage {
    pub fn stop(&self) {
        self.set_is_streaming(false);
        self.imp().viewfinder.stop_stream();
    }

    pub fn start(&self) {
        use std::sync::Once;
        static STARTED: Once = Once::new();
        if STARTED.is_completed() {
            self.imp().viewfinder.start_stream();
            self.update_state();
        } else {
            STARTED.call_once(|| self.start_inner());
        }
    }

    fn start_inner(&self) {
        use ashpd::{Error::Portal, PortalError};

        glib::spawn_future_local(clone!(
            #[weak(rename_to = camera)]
            self,
            async move {
                let imp = camera.imp();

                let app_id = crate::config::APP_ID.try_into().unwrap();
                if let Err(err) = ashpd::register_host_app(app_id).await {
                    tracing::error!(
                        "Failed to run org.freedesktop.host.portal.Registry.Register: {err}"
                    );
                }
                if let Err(err) = camera.try_start_stream().await
                    && let Some(Portal(PortalError::NotAllowed(err))) =
                        err.downcast_ref::<ashpd::Error>()
                {
                    imp.portal_failed.set(true);
                    camera.update_state();
                    tracing::debug!("Missing portal permission for camera: {err}");
                }
            }
        ));
    }

    async fn try_start_stream(&self) -> anyhow::Result<()> {
        let imp = self.imp();
        let stream_fd = stream().await?;

        let provider = aperture::DeviceProvider::instance();
        provider.set_fd(stream_fd)?;
        provider.start_with_default(|camera| {
            matches!(camera.location(), aperture::CameraLocation::Back)
        })?;

        // We keep a reference so the fd is keep alive.

        imp.selection.set_model(Some(provider));

        imp.provider.replace(Some(provider.clone()));

        Ok(())
    }

    pub fn emit_code_detected(&self, code: &str) {
        self.emit_by_name::<()>("code-detected", &[&code]);
    }

    pub fn connect_code_detected<F: Fn(&Self, &str) + 'static>(&self, f: F) {
        self.connect_local(
            "code-detected",
            false,
            clone!(
                #[weak(rename_to = obj)]
                self,
                #[upgrade_or]
                None,
                move |args: &[glib::Value]| {
                    let code = args.get(1).unwrap().get::<&str>().unwrap();
                    f(&obj, code);

                    None
                }
            ),
        );
    }

    fn update_state(&self) {
        let imp = self.imp();
        let state = imp.viewfinder.state();
        let portal_failed = imp.portal_failed.get();
        match state {
            aperture::ViewfinderState::Ready => imp.stack.set_visible_child_name("stream"),
            aperture::ViewfinderState::Loading => imp.stack.set_visible_child_name("loading"),
            aperture::ViewfinderState::NoCameras => imp.stack.set_visible_child_name("not-found"),
            aperture::ViewfinderState::Error => imp.stack.set_visible_child_name("not-found"),
        }
        if portal_failed {
            imp.stack.set_visible_child_name("not-allowed");
        }
        self.set_is_streaming(matches!(state, aperture::ViewfinderState::Ready) && !portal_failed);
    }
}

async fn stream() -> ashpd::Result<OwnedFd> {
    let proxy = camera::Camera::new().await?;
    proxy.request_access().await?;

    proxy.open_pipe_wire_remote().await
}
