use std::cell::Cell;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::sync::OnceLock;

use adw::prelude::*;
use adw::subclass::prelude::*;
use bytes::Bytes;
use futures::AsyncWriteExt;
use futures::StreamExt;
use futures::future;
use gettextrs::gettext;
use glib::Properties;
use glib::clone;
use glib::closure;
use glib::subclass::Signal;
use gtk::CompositeTemplate;
use gtk::gdk;
use gtk::gio;
use gtk::glib;
use vte4::TerminalExt;
use vte4::TerminalExtManual;

use crate::engine;
use crate::model;
use crate::rt;
use crate::utils;
use crate::view;

const ACTION_START_OR_RESUME: &str = "container-terminal.start-or-resume";
const ACTION_COPY: &str = "container-terminal.copy";
const ACTION_COPY_HTML: &str = "container-terminal.copy-html";
const ACTION_PASTE: &str = "container-terminal.paste";

#[derive(Debug)]
enum ExecInput {
    Data(Vec<u8>),
    Resize { columns: usize, rows: usize },
    Terminate,
}

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties, CompositeTemplate)]
    #[properties(wrapper_type = super::ContainerTerminal)]
    #[template(resource = "/com/github/marhkb/Pods/ui/view/container_terminal.ui")]
    pub(crate) struct ContainerTerminal {
        pub(super) settings: utils::PodsSettings,
        pub(super) tx_input: RefCell<Option<tokio::sync::mpsc::UnboundedSender<ExecInput>>>,
        pub(super) keep_alive_on_next_unroot: Cell<bool>,

        #[property(get, set, nullable)]
        pub(super) container: glib::WeakRef<model::Container>,

        #[property(get = Self::font_scale, set = Self::set_font_scale, explicit_notify)]
        _font_scale: PhantomData<f64>,

        #[template_child]
        pub(super) popover_menu: TemplateChild<gtk::PopoverMenu>,
        #[template_child]
        pub(super) stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub(super) terminal: TemplateChild<vte4::Terminal>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for ContainerTerminal {
        const NAME: &'static str = "PdsContainerTerminal";
        type Type = super::ContainerTerminal;
        type ParentType = gtk::Widget;

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

            klass.install_action(ACTION_START_OR_RESUME, None, |widget, _, _| {
                let Some(container) = widget.container() else {
                    return;
                };

                if container.status().can_start() {
                    view::container::start(widget, widget.container());
                    widget.action_set_enabled(ACTION_START_OR_RESUME, false);
                } else if container.status().can_resume() {
                    view::container::resume(widget, widget.container());
                    widget.action_set_enabled(ACTION_START_OR_RESUME, false);
                }
            });

            klass.install_action(ACTION_COPY, None, |widget, _, _| widget.copy_plain());
            klass.install_action(ACTION_COPY_HTML, None, |widget, _, _| widget.copy_html());
            klass.install_action(ACTION_PASTE, None, |widget, _, _| widget.paste());

            klass.add_binding_action(
                gdk::Key::C,
                gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::SHIFT_MASK,
                ACTION_COPY,
            );
            klass.add_binding_action(
                gdk::Key::V,
                gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::SHIFT_MASK,
                ACTION_PASTE,
            );
        }

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

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

        fn properties() -> &'static [glib::ParamSpec] {
            Self::derived_properties()
        }

        fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
            self.derived_set_property(id, value, pspec);
        }

        fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
            self.derived_property(id, pspec)
        }

        fn constructed(&self) {
            self.parent_constructed();

            let obj = &*self.obj();

            self.settings
                .bind("terminal-font-scale", obj, "font-scale")
                .build();

            self.popover_menu.set_parent(obj);

            self.terminal.set_colors(
                None,
                None,
                &[
                    &rgba_from_hex(0x17, 0x14, 0x21),
                    &rgba_from_hex(0xc0, 0x1c, 0x28),
                    &rgba_from_hex(0x26, 0xa2, 0x69),
                    &rgba_from_hex(0xa2, 0x73, 0x4c),
                    &rgba_from_hex(0x12, 0x48, 0x8b),
                    &rgba_from_hex(0xa3, 0x47, 0xba),
                    &rgba_from_hex(0x2a, 0xa1, 0xb3),
                    &rgba_from_hex(0xd0, 0xcf, 0xcc),
                    &rgba_from_hex(0x5e, 0x5c, 0x64),
                    &rgba_from_hex(0xf6, 0x61, 0x51),
                    &rgba_from_hex(0x33, 0xd1, 0x7a),
                    &rgba_from_hex(0xe9, 0xad, 0x0c),
                    &rgba_from_hex(0x2a, 0x7b, 0xde),
                    &rgba_from_hex(0xc0, 0x61, 0xcb),
                    &rgba_from_hex(0x33, 0xc7, 0xde),
                    &rgba_from_hex(0xff, 0xff, 0xff),
                ],
            );

            let status_expr = Self::Type::this_expression("container")
                .chain_property::<model::Container>("status");

            status_expr
                .chain_closure::<String>(closure!(
                    |_: Self::Type, status: model::ContainerStatus| {
                        match status {
                            model::ContainerStatus::Running => "running",
                            _ => "not-running",
                        }
                    }
                ))
                .bind(&*self.stack, "visible-child-name", Some(obj));

            status_expr.watch(
                Some(obj),
                clone!(
                    #[weak]
                    obj,
                    move || {
                        obj.action_set_enabled(
                            ACTION_START_OR_RESUME,
                            match obj.container().filter(|container| {
                                container.status() == model::ContainerStatus::Running
                            }) {
                                Some(container) => {
                                    obj.setup_tty_connection(&container);
                                    false
                                }
                                None => true,
                            },
                        );
                    }
                ),
            );

            self.on_terminal_selection_changed();

            adw::StyleManager::default().connect_dark_notify(clone!(
                #[weak]
                obj,
                move |_| {
                    glib::idle_add_local_once(clone!(
                        #[weak]
                        obj,
                        move || {
                            obj.imp().on_notify_dark();
                        }
                    ));
                }
            ));
            self.on_notify_dark();
        }

        fn dispose(&self) {
            utils::unparent_children(&*self.obj());
        }
    }

    impl WidgetImpl for ContainerTerminal {
        fn grab_focus(&self) -> bool {
            self.terminal.grab_focus()
        }

        fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
            self.stack.measure(orientation, for_size);
            self.parent_measure(orientation, for_size)
        }

        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
            self.stack.allocate(width, height, baseline, None);
            if let Some(tx_tokio) = &*self.tx_input.borrow() {
                _ = tx_tokio.send(ExecInput::Resize {
                    columns: self.terminal.column_count() as usize,
                    rows: self.terminal.row_count() as usize,
                });
            }
        }

        fn unroot(&self) {
            if !self.keep_alive_on_next_unroot.get()
                && let Some(tx_input) = &*self.tx_input.borrow()
            {
                _ = tx_input.send(ExecInput::Terminate);
            }
            self.keep_alive_on_next_unroot.set(false);
            self.parent_unroot();
        }
    }

    #[gtk::template_callbacks]
    impl ContainerTerminal {
        pub(crate) fn font_scale(&self) -> f64 {
            self.terminal.font_scale()
        }

        pub(crate) fn set_font_scale(&self, value: f64) {
            if self.font_scale() == value {
                return;
            }
            self.terminal.set_font_scale(value);
        }

        #[template_callback]
        fn on_terminal_notify_font_scale(&self) {
            self.obj().notify_font_scale();
        }

        #[template_callback]
        fn on_terminal_selection_changed(&self) {
            let has_selection = self.terminal.has_selection();

            let obj = &*self.obj();
            obj.action_set_enabled(ACTION_COPY, has_selection);
            obj.action_set_enabled(ACTION_COPY_HTML, has_selection);
        }

        #[template_callback]
        fn on_terminal_key_pressed(
            &self,
            key: gdk::Key,
            _: u32,
            modifier: gdk::ModifierType,
            _: &gtk::EventControllerKey,
        ) -> glib::ControlFlow {
            glib::ControlFlow::from(if modifier == gdk::ModifierType::CONTROL_MASK {
                if key == gdk::Key::minus || key == gdk::Key::KP_Subtract {
                    self.obj().zoom_out();
                    true
                } else if key == gdk::Key::plus || key == gdk::Key::KP_Add || key == gdk::Key::equal
                {
                    self.obj().zoom_in();
                    true
                } else if key == gdk::Key::_0 {
                    self.obj().zoom_normal();
                    true
                } else {
                    false
                }
            } else if modifier == gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::SHIFT_MASK {
                if key == gdk::Key::C {
                    self.obj().copy_plain();
                    true
                } else if key == gdk::Key::V {
                    self.obj().paste();
                    true
                } else {
                    false
                }
            } else {
                false
            })
        }

        #[template_callback]
        fn on_mouse_3_pressed(&self, _: i32, x: f64, y: f64) {
            let popover_menu = &*self.popover_menu;
            popover_menu.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0)));
            popover_menu.popup();
        }

        #[template_callback]
        fn on_scroll(
            &self,
            _dx: f64,
            dy: f64,
            scroll: gtk::EventControllerScroll,
        ) -> glib::Propagation {
            if scroll.current_event_state() == gdk::ModifierType::CONTROL_MASK {
                if dy.is_sign_negative() {
                    self.obj().zoom_in();
                } else {
                    self.obj().zoom_out();
                }
            }

            glib::Propagation::Proceed
        }

        fn on_notify_dark(&self) {
            let style_context = self.obj().style_context();

            self.terminal.set_color_background(
                &style_context
                    .lookup_color("view_bg_color")
                    .unwrap_or_else(|| {
                        if adw::StyleManager::default().is_dark() {
                            gdk::RGBA::new(0.118, 0.118, 0.118, 1.0)
                        } else {
                            gdk::RGBA::new(1.0, 1.0, 1.0, 1.0)
                        }
                    }),
            );
            self.terminal.set_color_foreground(
                &style_context
                    .lookup_color("view_fg_color")
                    .unwrap_or_else(|| {
                        if adw::StyleManager::default().is_dark() {
                            gdk::RGBA::new(1.0, 1.0, 1.0, 1.0)
                        } else {
                            gdk::RGBA::new(0.0, 0.0, 0.0, 0.8)
                        }
                    }),
            );
        }
    }
}

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

impl ContainerTerminal {
    pub(crate) fn keep_alive_on_next_unroot(&self) {
        self.imp().keep_alive_on_next_unroot.set(true);
    }

    fn setup_tty_connection(&self, container: &model::Container) {
        let imp = self.imp();

        let container = container.api().unwrap();

        let (tx_output, mut rx_output) = tokio::sync::mpsc::channel::<Bytes>(5);

        glib::spawn_future_local(clone!(
            #[weak(rename_to = obj)]
            self,
            async move {
                while let Some(buf) = rx_output.recv().await {
                    obj.imp().terminal.feed(&buf);
                }
            }
        ));

        let (tx_input, mut rx_input) = tokio::sync::mpsc::unbounded_channel::<ExecInput>();
        imp.tx_input.replace(Some(tx_input.clone()));
        imp.terminal.connect_commit(move |_, data, _| {
            _ = tx_input.send(ExecInput::Data(data.as_bytes().to_vec()));
        });

        let width = imp.terminal.column_count();
        let height = imp.terminal.row_count();

        self.grab_focus();

        rt::Promise::new(async move {
            let exec = container
                .create_exec(engine::opts::ExecCreateOpts {
                    attach_stderr: Some(true),
                    attach_stdout: Some(true),
                    attach_stdin: Some(true),
                    tty: Some(true),
                    command: vec!["/bin/sh".to_owned()],
                })
                .await
                .unwrap();

            let (mut reader, mut writer) = exec
                .start(true)
                .await
                .unwrap()
                .into_attached()
                .unwrap()
                .split();

            exec.resize(width as usize, height as usize).await?;

            loop {
                match future::select(Box::pin(rx_input.recv()), reader.next()).await {
                    future::Either::Left((buf, _)) => match buf {
                        Some(input) => match input {
                            ExecInput::Data(buf) => {
                                if let Err(e) = writer.write_all(&buf).await {
                                    log::error!("Error on writing to terminal: {e}");
                                    break;
                                }
                            }
                            ExecInput::Resize { columns, rows } => {
                                if let Err(e) = exec.resize(columns, rows).await {
                                    log::error!("Error on resizing terminal: {e}");
                                    break;
                                }
                            }

                            ExecInput::Terminate => break,
                        },
                        None => break,
                    },
                    future::Either::Right((chunk, _)) => match chunk {
                        Some(chunk) => match chunk {
                            Ok(chunk) => {
                                tx_output.send(chunk.into()).await.unwrap();
                            }
                            Err(e) => {
                                log::error!("Error on reading from terminal: {e}");
                                break;
                            }
                        },
                        None => break,
                    },
                }
            }

            // Close remaining processes.
            log::info!(
                "Closing remaining processes of container {} for exec {}.",
                container.id(),
                exec.id()
            );
            while writer.write_all(&[3]).await.is_ok() && writer.write_all(&[4]).await.is_ok() {}

            Ok(())
        })
        .defer(clone!(
            #[weak(rename_to = obj)]
            self,
            move |result: anyhow::Result<_>| {
                if result.is_err() {
                    utils::show_error_toast(
                        &gio::Application::default()
                            .unwrap()
                            .downcast::<crate::Application>()
                            .unwrap()
                            .main_window()
                            .toast_overlay(),
                        &gettext("Terminal error"),
                        &gettext("'/bin/sh' not found"),
                    );
                }
                obj.emit_by_name::<()>("terminated", &[]);
            }
        ));
    }

    pub(crate) fn zoom_out(&self) {
        self.set_font_scale(self.font_scale() - 0.1);
    }

    pub(crate) fn zoom_in(&self) {
        self.set_font_scale(self.font_scale() + 0.1);
    }

    pub(crate) fn zoom_normal(&self) {
        self.set_font_scale(1.0);
    }

    fn copy(&self, format: vte4::Format) {
        let terminal = &*self.imp().terminal;
        if terminal.has_selection() {
            terminal.copy_clipboard_format(format);
        }
    }

    fn copy_plain(&self) {
        self.copy(vte4::Format::Text);
    }

    fn copy_html(&self) {
        self.copy(vte4::Format::Html);
    }

    fn paste(&self) {
        if let Some(display) = gdk::Display::default() {
            display.clipboard().read_text_async(
                gio::Cancellable::NONE,
                clone!(
                    #[weak(rename_to = obj)]
                    self,
                    move |result| if let Some(text) = result.ok().flatten() {
                        obj.imp().terminal.paste_text(text.as_str());
                    }
                ),
            );
        }
    }
}

fn rgba_from_hex(r: i32, g: i32, b: i32) -> gdk::RGBA {
    gdk::RGBA::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 0.0)
}
