/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of On the Grid.

On the Grid is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/

pub mod components;

mod app_window;
pub use app_window::AppWindow;

mod views;

use async_std::task::yield_now;
use gio::resources_lookup_data;
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
use image::{io::Reader as ImageReader, ImageError};
use otg_core::{Core, CoreRequest, CoreResponse, Observable};
use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc};

#[derive(Clone)]
pub struct CoreApi {
    pub core: Core,
}

impl CoreApi {
    pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
        self.core.dispatch(request).await
    }
}

#[derive(Clone)]
pub enum Resource {
    Image(Pixbuf),
}

#[derive(Clone)]
pub struct ResourceManager {
    resources: Rc<RefCell<HashMap<String, Resource>>>,
}

impl Default for ResourceManager {
    fn default() -> Self {
        let mut resources = HashMap::new();

        for (path, xres, yres, transparency) in [
            (
                "/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
                840,
                840,
                false,
            ),
            (
                "/com/luminescent-dreams/otg-gtk/black_stone.png",
                40,
                40,
                true,
            ),
            (
                "/com/luminescent-dreams/otg-gtk/white_stone.png",
                40,
                40,
                true,
            ),
        ] {
            match perftrace(&format!("loading {}", path), || {
                Self::load_image(path, transparency, xres, yres)
            }) {
                Ok(Some(image)) => {
                    resources.insert(path.to_owned(), Resource::Image(image));
                }
                Ok(None) => println!("no image in resource bundle for {}", path),
                Err(err) => println!("failed to load image {}: {}", path, err),
            }
        }

        Self {
            resources: Rc::new(RefCell::new(resources)),
        }
    }
}

impl ResourceManager {
    pub fn resource(&self, path: &str) -> Option<Resource> {
        self.resources.borrow().get(path).cloned()
    }

    fn load_image(
        path: &str,
        transparency: bool,
        width: i32,
        height: i32,
    ) -> Result<Option<Pixbuf>, ImageError> {
        let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap();

        let image = ImageReader::new(Cursor::new(image_bytes))
            .with_guessed_format()
            .unwrap()
            .decode();
        image.map(|image| {
            let stride = if transparency {
                image.to_rgba8().sample_layout().height_stride
            } else {
                image.to_rgb8().sample_layout().height_stride
            };
            Pixbuf::from_bytes(
                &glib::Bytes::from(image.as_bytes()),
                Colorspace::Rgb,
                transparency,
                8,
                image.width() as i32,
                image.height() as i32,
                stride as i32,
            )
            .scale_simple(width, height, InterpType::Nearest)
        })
    }
}

pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
where
    F: FnOnce() -> A,
{
    let start = std::time::Instant::now();
    let result = f();
    let end = std::time::Instant::now();
    println!("[Trace: {}] {:?}", trace_name, end - start);
    result
}

/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
///
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.
#[allow(dead_code)]
struct LocalObserver<T> {
    join_handle: glib::JoinHandle<()>,
    handler: Rc<dyn Fn(T)>,
}

impl<T: 'static> LocalObserver<T> {
    /// Construct a new LocalObserver and start it running.
    ///
    /// observable -- any object which emits events
    /// handler -- a function which can process events
    #[allow(dead_code)]
    fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self {
        let listener = observable.subscribe();
        let handler = Rc::new(handler);
        let join_handle = glib::spawn_future_local({
            let handler = handler.clone();
            async move {
                loop {
                    match listener.recv().await {
                        Ok(msg) => handler(msg),
                        Err(_) => {
                            // recv only fails if the channel has been closed and no other notifications are pending. This will break out of the loop and terminate the observer.
                            return;
                        }
                    }
                    yield_now().await;
                }
            }
        });
        Self {
            join_handle,
            handler,
        }
    }
}

impl<T> Drop for LocalObserver<T> {
    fn drop(&mut self) {
        // Abort the task when the observer goes out of scope.
        self.join_handle.abort();
    }
}
