mod config_watching;
mod iced;
mod minimize;
mod notifications;
mod space;
mod space_container;
mod workspaces_dbus;
mod xdg_shell_wrapper;

use crate::iced::EVENT_LOOP_HANDLE;
use crate::xdg_shell_wrapper::client_state::ClientState;
use crate::xdg_shell_wrapper::run;
use crate::xdg_shell_wrapper::server_state::ServerState;
use crate::xdg_shell_wrapper::shared_state::GlobalState;
use anyhow::Result;
use calloop::channel::Sender;
use cctk::wayland_client::protocol::wl_output::WlOutput;
use cctk::wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1;
use config_watching::{watch_config, watch_cosmic_theme};
use cosmic_panel_config::CosmicPanelConfig;
use launch_pad::{ProcessKey, ProcessManager};
use minimize::MinimizeApplet;
use notifications::notifications_conn;
use smithay::reexports::calloop;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::wayland::background_effect::BackgroundEffectState;
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem;
use std::os::fd::{AsRawFd, OwnedFd};
use std::time::Duration;
use tokio::runtime;
use tokio::sync::mpsc;
use tracing::{error, info, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};

#[derive(Debug)]
pub enum PanelCalloopMsg {
    ClientSocketPair(ClientId),
    RestartSpace(CosmicPanelConfig, WlOutput),
    MinimizeRect { output: String, applet_info: MinimizeApplet },
    UpdateToplevel(ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1),
}

/// Access glibc malloc tunables.
#[cfg(target_env = "gnu")]
mod malloc {
    use std::os::raw::c_int;
    const M_MMAP_THRESHOLD: c_int = -3;

    unsafe extern "C" {
        fn malloc_trim(pad: usize);
        fn mallopt(param: c_int, value: c_int) -> c_int;
    }

    /// Prevents glibc from hoarding memory via memory fragmentation.
    pub fn limit_mmap_threshold() {
        unsafe {
            mallopt(M_MMAP_THRESHOLD, 65536);
        }
    }

    /// Asks glibc to trim malloc arenas.
    pub fn trim() {
        unsafe {
            malloc_trim(0);
        }
    }
}

fn main() -> Result<()> {
    // Prevents glibc from hoarding memory via memory fragmentation.
    #[cfg(target_env = "gnu")]
    malloc::limit_mmap_threshold();
    let fmt_layer = fmt::layer().with_target(false);
    let filter_layer =
        EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn")).unwrap();
    if let Ok(journal_layer) = tracing_journald::layer() {
        tracing_subscriber::registry().with(journal_layer).with(filter_layer).init();
    } else {
        tracing_subscriber::registry().with(fmt_layer).with(filter_layer).init();
    }

    log_panics::init();

    let arg = std::env::args().nth(1);
    let usage = "USAGE: cosmic-panel";
    let config = match arg.as_ref().map(|s| &s[..]) {
        Some(arg) if arg == "--help" || arg == "-h" => {
            std::process::exit(1);
        },
        None => match cosmic_panel_config::CosmicPanelContainerConfig::load() {
            Ok(c) => c,
            Err((errors, c)) => {
                for e in errors {
                    error!("Panel Entry Error: {:?}", e);
                }
                let _ = c.write_entries();
                c
            },
        },
        _ => {
            std::process::exit(1);
        },
    };

    // TODO: remove
    // migrate legacy configs
    if cosmic_panel_config::NEEDS_MIGRATION.swap(false, std::sync::atomic::Ordering::Relaxed) {
        let _ = config.write_entries();
    }

    let (applet_tx, mut applet_rx) = mpsc::channel(200);
    let (calloop_tx, calloop_rx): (Sender<PanelCalloopMsg>, _) = calloop::channel::channel();

    let event_loop = calloop::EventLoop::try_new()?;
    let handle = event_loop.handle();
    let _ = EVENT_LOOP_HANDLE.with(move |l| l.set(RefCell::new(handle)));

    let mut space = space_container::SpaceContainer::new(
        config,
        applet_tx.clone(),
        calloop_tx.clone(),
        event_loop.handle(),
    );

    let handle = event_loop.handle();
    match watch_config(&space.config, handle) {
        Ok(watchers) => {
            info!("Watching panel config successful");
            space.watchers = watchers;
        },
        Err(e) => warn!("Failed to watch config: {:?}", e),
    };
    match watch_cosmic_theme(event_loop.handle()) {
        Ok(w) => mem::forget(w),
        Err(e) => error!("Error while watching cosmic theme: {:?}", e),
    };

    event_loop
        .handle()
        .insert_source(
            calloop::timer::Timer::from_duration(Duration::from_secs(1)),
            |_, _, state: &mut GlobalState| {
                tracing::trace!("Updating hidden applets");

                state.space.update_hidden_applet_frame();
                calloop::timer::TimeoutAction::ToDuration(Duration::from_secs(1))
            },
        )
        .expect("failed to insert hidden applet frame timer");

    #[cfg(target_env = "gnu")]
    event_loop
        .handle()
        .insert_source(
            calloop::timer::Timer::from_duration(Duration::from_secs(600)),
            |_, _, _: &mut GlobalState| {
                tracing::trace!("Releasing free memory from the heap.");
                malloc::trim();
                calloop::timer::TimeoutAction::ToDuration(Duration::from_secs(600))
            },
        )
        .expect("failed to insert malloc trim timer");

    event_loop
        .handle()
        .insert_source(calloop_rx, move |e, _, state: &mut GlobalState| {
            tracing::trace!("Panel Event: {e:?}");
            match e {
                calloop::channel::Event::Msg(e) => match e {
                    PanelCalloopMsg::ClientSocketPair(client_id) => {
                        state.space.cleanup_client(client_id);
                    },
                    PanelCalloopMsg::RestartSpace(config, o) => {
                        state.space.update_space(
                            config,
                            &state.client_state.compositor_state,
                            state.client_state.fractional_scaling_manager.as_ref(),
                            state.client_state.viewporter_state.as_ref(),
                            &mut state.client_state.layer_state,
                            &state.client_state.queue_handle,
                            Some(o),
                            state.client_state.overlap_notify.clone(),
                        );
                    },
                    PanelCalloopMsg::UpdateToplevel(toplevel) => {
                        minimize::update_toplevel(state, toplevel)
                    },
                    PanelCalloopMsg::MinimizeRect { output, applet_info } => {
                        minimize::set_rectangles(state, output, applet_info)
                    },
                },
                calloop::channel::Event::Closed => {},
            };
        })
        .expect("failed to insert dbus event source");

    std::thread::spawn(move || -> anyhow::Result<()> {
        let rt = runtime::Builder::new_current_thread().enable_all().build()?;
        let mut process_ids: HashMap<String, Vec<ProcessKey>> = HashMap::new();

        rt.block_on(async move {
            let process_manager = ProcessManager::new().await;
            let _ = process_manager
                .set_restart_mode(launch_pad::RestartMode::ExponentialBackoff(
                    Duration::from_millis(2),
                ))
                .await;
            let _ = process_manager.set_max_restarts(999999).await;

            let mut notifications_proxy =
                match tokio::time::timeout(Duration::from_secs(5), notifications_conn()).await {
                    Ok(Ok(p)) => Some(p),
                    _ => {
                        error!("Failed to connect to the notifications daemon.");
                        None
                    },
                };

            while let Some(msg) = applet_rx.recv().await {
                tracing::trace!("Applet Message: {msg:?}");
                match msg {
                    space::AppletMsg::NewProcess(id, process) => {
                        if let Ok(key) = process_manager.start(process).await {
                            let entry = process_ids.entry(id).or_default();
                            entry.push(key);
                        }
                    },
                    space::AppletMsg::NewNotificationsProcess(
                        id,
                        mut process,
                        mut env,
                        mut fds,
                    ) => {
                        let Some(proxy) = notifications_proxy.as_mut() else {
                            notifications_proxy = match tokio::time::timeout(
                                Duration::from_secs(1),
                                notifications_conn(),
                            )
                            .await
                            {
                                Ok(Ok(p)) => Some(p),
                                _ => {
                                    error!("Failed to connect to the notifications daemon",);
                                    None
                                },
                            };
                            warn!("Can't start notifications applet without a connection");
                            continue;
                        };
                        info!("Getting fd for notifications applet");
                        let notif_fd = match tokio::time::timeout(
                            Duration::from_secs(1),
                            proxy.get_fd(),
                        )
                        .await
                        {
                            Ok(Ok(fd)) => fd,
                            Ok(Err(err)) => {
                                error!("Failed to get fd for the notifications applet {}", err);
                                continue;
                            },
                            Err(err) => {
                                error!("Failed to get fd for the notifications applet {}", err);
                                continue;
                            },
                        };
                        let notif_fd = OwnedFd::from(notif_fd);
                        env.push((
                            "COSMIC_NOTIFICATIONS".to_string(),
                            notif_fd.as_raw_fd().to_string(),
                        ));
                        fds.push(notif_fd);
                        process = process.with_fds(move || fds);
                        process = process.with_env(env);
                        info!("Starting notifications applet");
                        if let Ok(key) = process_manager.start(process).await {
                            let entry = process_ids.entry(id).or_default();
                            entry.push(key);
                        }
                    },
                    space::AppletMsg::ClientSocketPair(client_id) => {
                        let _ = calloop_tx.send(PanelCalloopMsg::ClientSocketPair(client_id));
                    },
                    space::AppletMsg::Cleanup(id) => {
                        for id in process_ids.remove(&id).unwrap_or_default() {
                            let _ = process_manager.stop_process(id).await;
                        }
                    },
                    space::AppletMsg::NeedNewNotificationFd(sender) => {
                        let Some(proxy) = notifications_proxy.as_mut() else {
                            warn!("Can't start notifications applet without a connection");
                            continue;
                        };
                        let fd = match proxy.get_fd().await {
                            Ok(fd) => fd,
                            Err(err) => {
                                error!("Failed to get fd for the notifications applet {}", err);
                                continue;
                            },
                        };
                        let fd = OwnedFd::from(fd);

                        _ = sender.send(fd);
                    },
                };
            }
        });

        Ok(())
    });

    let server_display = smithay::reexports::wayland_server::Display::new().unwrap();
    let s_dh = server_display.handle();

    let mut server_state = ServerState::new(s_dh.clone());

    let mut client_state = ClientState::new(event_loop.handle(), &mut space, &mut server_state)?;

    // gate background effect protocol for clients on whether it is available to the
    // panel
    if client_state.ext_background_effect_manager.is_some() {
        server_state.background_effect_state =
            Some(BackgroundEffectState::new::<GlobalState>(&s_dh));
    }

    space.corner_radius_manager = client_state.cosmic_corner_radius_manager.clone();
    client_state.init_workspace_state();
    client_state.init_toplevel_info_state();
    client_state.init_toplevel_manager_state();
    run(space, client_state, server_state, event_loop, server_display)?;
    Ok(())
}
