use cosmic::app::{Core, Task};
use cosmic::iced::core::SmolStr;
use cosmic::iced::event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent};
use cosmic::iced::event::{self};
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
use cosmic::iced::platform_specific::shell::commands::blur::blur;
use cosmic::iced::runtime::core::window::Id as SurfaceId;
use cosmic::iced::runtime::platform_specific::wayland::CornerRadius;
use cosmic::iced::{self, Rectangle, Size, Subscription};
use cosmic::surface::corner_radius::rounded_rect_strips;
use cosmic::widget::rectangle_tracker::{RectangleUpdate, rectangle_tracker_subscription};
use cosmic::widget::{self, RectangleTracker};
use cosmic_greeter_daemon::{BgSource, UserData};
use cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::{Connection, Proxy};

use crate::keyboard_layout_wayland;

pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.;

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ActiveLayout {
    pub layout: String,
    pub description: String,
    pub variant: String,
}

pub struct Common<M> {
    pub wayland_connection: Option<Connection>,
    pub keyboard_layout: Option<ZcosmicKeyboardLayoutV1>,
    pub current_keyboard_layout: usize,
    pub active_layouts: Vec<ActiveLayout>,
    pub active_surface_id_opt: Option<SurfaceId>,
    pub on_battery: bool,
    pub battery_percent: f64,
    pub charging_limit: Option<bool>,
    pub caps_lock: bool,
    pub core: Core,
    pub error_opt: Option<String>,
    pub fallback_background: widget::image::Handle,
    pub layouts_opt: Option<Arc<xkb_data::KeyboardLayouts>>,
    pub network_icon_opt: Option<widget::Icon>,
    pub on_output_event: Option<Box<dyn Fn(OutputEvent, WlOutput) -> M>>,
    pub on_session_lock_event: Option<Box<dyn Fn(SessionLockEvent) -> M>>,
    pub output_names: HashMap<WlOutput, String>,
    pub power_info_opt: Option<(widget::Icon, f64)>,
    pub prompt_opt: Option<(String, bool, Option<String>)>,
    pub rectangle_tracker: Option<RectangleTracker<(SurfaceId, bool)>>,
    pub rectangles: HashMap<(SurfaceId, bool), iced::Rectangle>,
    pub include_menu: bool,
    pub subsurface_rects: HashMap<WlOutput, Rectangle>,
    pub surface_ids: HashMap<WlOutput, SurfaceId>,
    pub subsurface_outputs: HashMap<SurfaceId, WlOutput>,
    pub surface_images: HashMap<SurfaceId, widget::image::Handle>,
    pub surface_names: HashMap<SurfaceId, String>,
    pub text_input_ids: HashMap<String, widget::Id>,
    pub time: crate::time::Time,
    pub window_size: HashMap<SurfaceId, Size>,
}

#[derive(Clone, Debug)]
pub enum Message {
    CapsLock(bool),
    Focus(SurfaceId),
    Key(Modifiers, Key, Option<SmolStr>),
    NetworkIcon(Option<&'static str>),
    SubsurfaceOpened(SurfaceId),
    OutputEvent(OutputEvent, WlOutput),
    PowerInfo(Option<(f64, bool, bool)>),
    Prompt(String, bool, Option<String>),
    SessionLockEvent(SessionLockEvent),
    Tick,
    Tz(jiff::tz::TimeZone),
    Rectangle(RectangleUpdate<(SurfaceId, bool)>),
    KeyboardLayoutWayland(keyboard_layout_wayland::Event),
}

impl<M: From<Message> + Send + 'static> Common<M> {
    pub fn init(mut core: Core) -> (Self, Task<M>) {
        core.window.show_window_menu = false;
        core.window.show_headerbar = false;
        // XXX must be false or define custom style to have transparent bg
        core.window.sharp_corners = false;
        core.window.show_maximize = false;
        core.window.show_minimize = false;
        core.window.use_template = false;

        let layouts_opt = match xkb_data::all_keyboard_layouts() {
            Ok(ok) => Some(Arc::new(ok)),
            Err(err) => {
                tracing::warn!("failed to load keyboard layouts: {}", err);
                None
            }
        };

        let app = Self {
            wayland_connection: None,
            keyboard_layout: None,
            current_keyboard_layout: 0,
            active_layouts: Vec::new(),
            active_surface_id_opt: None,
            caps_lock: false,
            core,
            error_opt: None,
            fallback_background: widget::image::Handle::from_bytes(
                include_bytes!("../res/background.jpg").as_slice(),
            ),
            layouts_opt,
            network_icon_opt: None,
            on_output_event: None,
            on_session_lock_event: None,
            output_names: HashMap::new(),
            power_info_opt: None,
            prompt_opt: None,
            subsurface_rects: HashMap::new(),
            surface_ids: HashMap::new(),
            surface_images: HashMap::new(),
            surface_names: HashMap::new(),
            text_input_ids: HashMap::new(),
            time: crate::time::Time::new(),
            window_size: HashMap::new(),
            battery_percent: 0.0,
            on_battery: false,
            charging_limit: None,
            subsurface_outputs: HashMap::new(),
            rectangle_tracker: None,
            rectangles: HashMap::new(),
            include_menu: false,
        };
        (
            app,
            Task::batch(vec![
                crate::time::tick().map(|_| cosmic::Action::App(Message::Tick.into())),
                crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz).into())),
            ]),
        )
    }

    pub fn update_wallpapers(&mut self, user_data: &UserData) {
        for (_output, surface_id) in self.surface_ids.iter() {
            if self.surface_images.contains_key(surface_id) {
                continue;
            }

            let Some(output_name) = self.surface_names.get(surface_id) else {
                continue;
            };

            tracing::info!("updating wallpaper for {:?}", output_name);

            for (wallpaper_output_name, wallpaper_source) in user_data.bg_state.wallpapers.iter() {
                if wallpaper_output_name == output_name {
                    match wallpaper_source {
                        BgSource::Path(path) => {
                            match user_data.bg_path_data.get(path) {
                                Some(bytes) => {
                                    let image = widget::image::Handle::from_bytes(bytes.clone());
                                    self.surface_images.insert(*surface_id, image);
                                    //TODO: what to do about duplicates?
                                }
                                None => {
                                    tracing::warn!(
                                        "output {}: failed to find wallpaper data for source {:?}",
                                        output_name,
                                        path
                                    );
                                }
                            }
                            break;
                        }
                        BgSource::Color(color) => {
                            //TODO: support color sources
                            tracing::warn!(
                                "output {}: unsupported source {:?}",
                                output_name,
                                color
                            );
                        }
                    }
                }
            }
        }
    }

    pub fn update_user_data(&mut self, user_data: &UserData) {
        self.update_wallpapers(user_data);

        // From cosmic-applet-input-sources
        if let Some(keyboard_layouts) = &self.layouts_opt
            && let Some(xkb_config) = &user_data.xkb_config_opt
        {
            self.active_layouts.clear();
            let config_layouts = xkb_config.layout.split_terminator(',');
            let config_variants = xkb_config
                .variant
                .split_terminator(',')
                .chain(std::iter::repeat(""));
            'outer: for (config_layout, config_variant) in config_layouts.zip(config_variants) {
                for xkb_layout in keyboard_layouts.layouts() {
                    if config_layout != xkb_layout.name() {
                        continue;
                    }
                    if config_variant.is_empty() {
                        let active_layout = ActiveLayout {
                            description: xkb_layout.description().to_owned(),
                            layout: config_layout.to_owned(),
                            variant: config_variant.to_owned(),
                        };
                        self.active_layouts.push(active_layout);
                        continue 'outer;
                    }

                    let Some(xkb_variants) = xkb_layout.variants() else {
                        continue;
                    };
                    for xkb_variant in xkb_variants {
                        if config_variant != xkb_variant.name() {
                            continue;
                        }
                        let active_layout = ActiveLayout {
                            description: xkb_variant.description().to_owned(),
                            layout: config_layout.to_owned(),
                            variant: config_variant.to_owned(),
                        };
                        self.active_layouts.push(active_layout);
                        continue 'outer;
                    }
                }
            }
            tracing::info!("{:?}", self.active_layouts);
        }
    }

    pub fn update(&mut self, message: Message) -> Task<M> {
        match message {
            Message::CapsLock(caps_lock) => {
                self.caps_lock = caps_lock;
            }
            Message::Focus(surface_id) => {
                self.active_surface_id_opt = Some(surface_id);
                if let Some(text_input_id) = self
                    .surface_names
                    .get(&surface_id)
                    .and_then(|id| self.text_input_ids.get(id))
                {
                    return widget::text_input::focus(text_input_id.clone());
                }
            }
            Message::Key(modifiers, key, text) => {
                // Uncaptured keys with only shift modifiers go to the password box
                if !modifiers.logo()
                    && !modifiers.control()
                    && !modifiers.alt()
                    && matches!(key, Key::Character(_))
                {
                    if let Some(text) = text
                        && let Some((_, _, Some(value))) = &mut self.prompt_opt
                    {
                        value.push_str(&text);
                    }

                    if let Some(surface_id) = self.active_surface_id_opt
                        && let Some(text_input_id) = self
                            .surface_names
                            .get(&surface_id)
                            .and_then(|id| self.text_input_ids.get(id))
                    {
                        return widget::text_input::focus(text_input_id.clone());
                    }
                }
            }
            Message::NetworkIcon(network_icon_opt) => {
                self.network_icon_opt =
                    network_icon_opt.map(|name| widget::icon::from_name(name).into());
            }
            Message::OutputEvent(output_event, output) => {
                if self.wayland_connection.is_none() {
                    if let Some(backend) = output.backend().upgrade() {
                        self.wayland_connection = Some(Connection::from_backend(backend));
                    }
                }
                if let Some(on_output_event) = &self.on_output_event {
                    return Task::done(cosmic::Action::App(on_output_event(output_event, output)));
                }
            }
            Message::PowerInfo(power_info_opt) => {
                if let Some((level, on_battery, threshold_enabled)) = power_info_opt {
                    self.charging_limit = Some(threshold_enabled);
                    self.update_battery(level, on_battery);
                }
            }
            Message::Prompt(prompt, secret, value_opt) => {
                let prompt_was_none = self.prompt_opt.is_none();
                self.prompt_opt = Some((prompt, secret, value_opt));
                if prompt_was_none
                    && let Some(surface_id) = self.active_surface_id_opt
                    && let Some(text_input_id) = self
                        .surface_names
                        .get(&surface_id)
                        .and_then(|id| self.text_input_ids.get(id))
                {
                    tracing::info!("focus surface found id {:?}", text_input_id);
                    return widget::text_input::focus(text_input_id.clone());
                }
            }
            Message::SessionLockEvent(lock_event) => {
                if let Some(on_session_lock_event) = &self.on_session_lock_event {
                    return Task::done(cosmic::Action::App(on_session_lock_event(lock_event)));
                }
            }
            Message::Tick => {
                self.time.tick();
            }
            Message::Tz(tz) => {
                self.time.set_tz(tz);
            }
            Message::SubsurfaceOpened(id) => {
                return self.blur_rects(id);
            }
            Message::Rectangle(u) => match u {
                RectangleUpdate::Rectangle(r) => {
                    self.rectangles.insert(r.0, r.1);

                    return self.blur_rects(r.0.0);
                }
                RectangleUpdate::Init(tracker) => {
                    self.rectangle_tracker.replace(tracker);
                }
            },
            Message::KeyboardLayoutWayland(w) => match w {
                keyboard_layout_wayland::Event::KeyboardLayout(keyboard_layout) => {
                    self.keyboard_layout = Some(keyboard_layout);
                }
                keyboard_layout_wayland::Event::Group(group) => {
                    self.current_keyboard_layout = group;
                }
            },
        }
        Task::none()
    }

    pub(crate) fn dropdown_blur_rects(&mut self, enable: bool) -> Task<M> {
        let mut ids = HashSet::new();
        for r in self.rectangles.keys() {
            ids.insert(r.0);
        }
        self.include_menu = enable;
        Task::batch(
            ids.into_iter()
                .map(|i| self.blur_rects(i))
                .collect::<Vec<_>>(),
        )
    }

    pub fn blur_rects(&mut self, id: SurfaceId) -> Task<M> {
        if let Some(output) = self.subsurface_outputs.get(&id) {
            if let Some(rect) = self.subsurface_rects.get(output) {
                let x = rect.x;
                let y = rect.y;
                if let Some(rect) = self.rectangles.get(&(id, false)) {
                    let mut offset_rect = *rect;
                    offset_rect.x += x;
                    offset_rect.y += y;
                    let rad = CornerRadius {
                        top_left: 16,
                        top_right: 16,
                        bottom_left: 16,
                        bottom_right: 16,
                    };
                    let mut rects = rounded_rect_strips(offset_rect, rad);
                    if let Some(other_rect) = self.rectangles.get(&(id, true))
                        && self.include_menu
                    {
                        let mut other = *other_rect;
                        other.x += x;
                        other.y += y;
                        let rad = CornerRadius {
                            top_left: 8,
                            top_right: 8,
                            bottom_left: 8,
                            bottom_right: 8,
                        };
                        rects.append(&mut rounded_rect_strips(other, rad));
                    }

                    return blur(id, Some(rects)).discard();
                } else {
                    tracing::error!("no rectangle for surface {id:?}");
                }
            }
        }
        Task::none()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let mut subscriptions = Vec::with_capacity(3);
        subscriptions
            .push(rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)));
        subscriptions.push(event::listen_with(|event, status, id| match event {
            iced::Event::Keyboard(KeyEvent::KeyPressed {
                key,
                modifiers,
                text,
                ..
            }) => match status {
                event::Status::Ignored => Some(Message::Key(modifiers, key, text)),
                event::Status::Captured => None,
            },
            iced::Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
                Some(Message::CapsLock(modifiers.contains(Modifiers::CAPS_LOCK)))
            }
            iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
                wayland_event,
            )) => match wayland_event {
                WaylandEvent::Output(output_event, output) => {
                    Some(Message::OutputEvent(output_event, output))
                }
                WaylandEvent::SessionLock(lock_event) => {
                    Some(Message::SessionLockEvent(lock_event))
                }
                iced::event::wayland::Event::Subsurface(
                    iced::event::wayland::SubsurfaceEvent::Created,
                ) => Some(Message::SubsurfaceOpened(id)),
                _ => None,
            },
            iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)),
            _ => None,
        }));

        if let Some(conn) = self.wayland_connection.clone() {
            subscriptions.push(
                keyboard_layout_wayland::subscription(conn).map(Message::KeyboardLayoutWayland),
            );
        }

        #[cfg(feature = "networkmanager")]
        {
            subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon));
        }

        #[cfg(feature = "upower")]
        {
            subscriptions.push(crate::upower::subscription().map(Message::PowerInfo));
        }

        Subscription::batch(subscriptions)
    }
}

impl<M> Common<M> {
    fn update_battery(&mut self, mut percent: f64, on_battery: bool) {
        percent = percent.clamp(0.0, 100.0);
        self.on_battery = on_battery;
        self.battery_percent = percent;
        let battery_percent =
            if self.battery_percent > 95.0 && !self.charging_limit.unwrap_or_default() {
                100
            } else if self.battery_percent > 80.0 && !self.charging_limit.unwrap_or_default() {
                90
            } else if self.battery_percent > 65.0 {
                80
            } else if self.battery_percent > 35.0 {
                50
            } else if self.battery_percent > 20.0 {
                35
            } else if self.battery_percent > 14.0 {
                20
            } else if self.battery_percent > 9.0 {
                10
            } else if self.battery_percent > 5.0 {
                5
            } else {
                0
            };
        let limited = if self.charging_limit.unwrap_or_default() {
            "limited-"
        } else {
            ""
        };
        let charging = if on_battery { "" } else { "charging-" };
        self.power_info_opt = Some((
            widget::icon::from_name(format!(
                "cosmic-applet-battery-level-{battery_percent}-{limited}{charging}symbolic",
            ))
            .into(),
            percent,
        ));
    }
}
