use std::{
    collections::BTreeMap,
    process::ExitCode,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc, Mutex,
    },
};

use cross::signal::set_sigint_handler;

use crate::{
    error::Error,
    event::{Event, EventData, WindowEvent, WindowManagementEvent},
    message::ClientMessage,
};

use self::{connection::Connection, window::Window};

pub mod connection;
pub mod window;

pub trait WindowManagementEventHandler = Fn(WindowManagementEvent) -> bool;

pub struct Application {
    pub(crate) connection: Arc<Mutex<Connection>>,
    windows: BTreeMap<u32, Window>,

    window_management_event_handler: Box<dyn WindowManagementEventHandler>,
}

static EXIT_SIGNAL: AtomicBool = AtomicBool::new(false);

fn sigint_handler() {
    EXIT_SIGNAL.store(true, Ordering::Release);
}

impl Application {
    pub fn new() -> Result<Self, Error> {
        set_sigint_handler(sigint_handler);

        let mut connection = Connection::new()?;
        connection.connect()?;

        Ok(Self {
            connection: Arc::new(Mutex::new(connection)),
            windows: BTreeMap::new(),

            window_management_event_handler: Box::new(|_| false),
        })
    }

    pub fn subscribe_to_window_management_events(&mut self) -> Result<(), Error> {
        let mut connection = self.connection.lock().unwrap();
        connection.send(&ClientMessage::SubscribeToWindowManagement)?;
        Ok(())
    }

    pub fn set_window_management_event_handler<H: WindowManagementEventHandler + 'static>(
        &mut self,
        handler: H,
    ) {
        self.window_management_event_handler = Box::new(handler);
    }

    pub fn add_window(&mut self, window: Window) {
        assert!(!self.windows.contains_key(&window.id()));
        self.windows.insert(window.id(), window);
    }

    pub fn is_running(&self) -> bool {
        !EXIT_SIGNAL.load(Ordering::Acquire)
    }

    fn run_inner(mut self) -> Result<ExitCode, Error> {
        while self.is_running() {
            self.wait_events()?;
        }
        Ok(ExitCode::SUCCESS)
    }

    pub fn handle_event(&mut self, event: Event) -> Result<(), Error> {
        match event.data {
            EventData::WindowManagementEvent(event) => {
                if (self.window_management_event_handler)(event) {
                    self.redraw()?;
                }
            }
            EventData::WindowEvent(window_id, ev) => {
                if let Some(window) = self.windows.get_mut(&window_id) {
                    window.handle_event(ev)?;
                } else {
                    log::warn!("Unknown window ID received: {window_id}");
                }
            }
            _ => (),
        }

        Ok(())
    }

    pub fn redraw(&mut self) -> Result<(), Error> {
        for (_, window) in self.windows.iter_mut() {
            // Inject RedrawRequested
            window.handle_event(WindowEvent::RedrawRequested)?;
        }
        Ok(())
    }

    pub fn wait_events(&mut self) -> Result<(), Error> {
        let event = {
            let mut connection = self.connection.lock().unwrap();
            connection.receive_event()?
        };

        self.handle_event(event)
    }

    pub fn poll_events(&mut self) -> Result<(), Error> {
        loop {
            let event = {
                let mut connection = self.connection.lock().unwrap();
                match connection.poll_event() {
                    Ok(Some(event)) => event,
                    Ok(None) => break,
                    Err(error) => return Err(error),
                }
            };

            self.handle_event(event)?;
        }
        Ok(())
    }

    pub fn run(self) -> ExitCode {
        match self.run_inner() {
            Ok(exit) => exit,
            Err(error) => {
                log::error!("Application finished with error: {error}");
                ExitCode::FAILURE
            }
        }
    }

    pub fn connection(&self) -> &Arc<Mutex<Connection>> {
        &self.connection
    }
}

impl Drop for Application {
    fn drop(&mut self) {
        let mut conn = self.connection.lock().unwrap();
        for (window_id, _) in self.windows.iter() {
            conn.send(&ClientMessage::DestroyWindow(*window_id)).ok();
        }
    }
}
