mod editor;
mod interfaces;

use crate::interfaces::*;
use gdk_pixbuf_sys::*;
use gdk_sys::*;
use gio_sys::*;
use glib_sys::*;
use gobject_sys::*;
use gtk_sys::*;
use maraiah::{c_str, durandal::ffi};
use std::{cell::RefCell, rc::Rc};

/// Called whne the application activates in order to set everything up.
unsafe extern "C" fn app_activate(app: *mut GtkApplication, edit: gpointer)
{
   // this ref will be cloned around a bit, but will ultimately be dropped at
   // the end of this function.
   let edit = Rc::from_raw(edit as *const MapEditorRef);

   setup_css();

   let path = c_str!("/net/greyserv/maraiah/tycho/ui");
   let b    = gtk_builder_new_from_resource(path);

   setup_draw_area(b, edit.clone());
   setup_win_map_view(b);
   setup_win_map_tools(b);
   setup_win_map_prop(b);
   setup_about_dlg(b);
   setup_win_main(b, app, edit.clone());

   g_object_unref(b as _);
}

/// Sets up the map view window's drawing area.
unsafe fn setup_draw_area(b: *mut GtkBuilder, edit: Rc<MapEditorRef>)
{
   struct RenderState
   {
      im_nomap: *mut GdkPixbuf,
      ax:       *mut GtkAdjustment,
      ay:       *mut GtkAdjustment,
      edit:     Rc<MapEditorRef>,
   }

   /// Callback to finalize the drawing area.
   unsafe extern "C" fn c_done(_: *mut GtkWidget, rend: gpointer)
   {
      let rend = Box::from_raw(rend as *mut RenderState);

      g_object_unref(rend.im_nomap as gpointer as _);
      g_object_unref(rend.ax as _);
      g_object_unref(rend.ay as _);

      // data is dropped and freed here
   }

   /// Callback to draw on the drawing area.
   unsafe extern "C" fn c_draw(wid: *mut GtkWidget,
                               ctx: *mut cairo_sys::cairo_t,
                               rend: gpointer)
      -> gboolean
   {
      let rend = rend as *mut RenderState;
      let rend = &mut *rend;

      let w = f64::from(gtk_widget_get_allocated_width(wid));
      let h = f64::from(gtk_widget_get_allocated_height(wid));

      gtk_adjustment_set_lower(rend.ax, 0.0);
      gtk_adjustment_set_upper(rend.ax, w);

      gtk_adjustment_set_lower(rend.ay, 0.0);
      gtk_adjustment_set_upper(rend.ay, h);

      let im = CrImage(rend.im_nomap);
      let mut dr = CrDrawArea::new(ctx, w, h);

      rend.edit.borrow().draw(&mut dr, &im);

      1
   }

   let wid: *mut GtkDrawingArea = get_obj(b, c_str!("draw-area"));

   {
      let mut edit = edit.borrow_mut();
      edit.call.push(Box::new(move || gtk_widget_queue_draw(wid as _)));
   }

   let ax: *mut GtkAdjustment = get_obj(b, c_str!("adj-map-horz"));
   let ay: *mut GtkAdjustment = get_obj(b, c_str!("adj-map-vert"));

   g_object_ref(ax as _);
   g_object_ref(ay as _);

   let im_nomap = load_img(c_str!("/net/greyserv/maraiah/tycho/tycho1.png"));

   let rend = RenderState{im_nomap, ax, ay, edit};
   let rend = Box::into_raw(Box::new(rend));

   connect(wid as _, c_str!("destroy"), c_done as _, rend as _);
   connect(wid as _, c_str!("draw"), c_draw as _, rend as _);
}

/// Sets up the map view window.
unsafe fn setup_win_map_view(b: *mut GtkBuilder)
{
   let win: *mut GtkWindow   = get_obj(b, c_str!("win-map-view"));
   let btn: *mut GtkMenuItem = get_obj(b, c_str!("btn-show-map-view"));

   connect_hide(win as _);
   connect_show(btn as _, win as _);
}

/// Sets up the map tools window.
unsafe fn setup_win_map_tools(b: *mut GtkBuilder)
{
   let win: *mut GtkWindow   = get_obj(b, c_str!("win-map-tools"));
   let btn: *mut GtkMenuItem = get_obj(b, c_str!("btn-show-map-tools"));

   connect_hide(win as _);
   connect_show(btn as _, win as _);
}

/// Sets up the map properties window.
unsafe fn setup_win_map_prop(b: *mut GtkBuilder)
{
   let win: *mut GtkWindow   = get_obj(b, c_str!("win-map-prop"));
   let btn: *mut GtkMenuItem = get_obj(b, c_str!("btn-show-map-prop"));

   connect_hide(win as _);
   connect_show(btn as _, win as _);
}

/// Sets up the about dialogue.
unsafe fn setup_about_dlg(b: *mut GtkBuilder)
{
   /// Callback to show the dialogue when the "About" button is pressed, and
   /// hide it when the "Close" button is pressed on it.
   unsafe extern "C" fn c_show_act(_: *mut GtkWidget, dlg: gpointer)
   {
      gtk_dialog_run(dlg as _);
      gtk_widget_hide(dlg as _);
   }

   let dlg: *mut GtkAboutDialog = get_obj(b, c_str!("dlg-about"));
   let btn: *mut GtkMenuItem    = get_obj(b, c_str!("btn-about"));

   let it    = env!("CARGO_PKG_AUTHORS").split(';');
   let mut v = ffi::CStringVec::new_from_iter(it).unwrap();
   let img   = load_img(c_str!("/net/greyserv/maraiah/tycho/tycho2.png"));

   gtk_about_dialog_set_authors(dlg, v.as_mut_ptr());
   gtk_about_dialog_set_version(dlg, c_str!(env!("CARGO_PKG_VERSION")));
   gtk_about_dialog_set_website(dlg, c_str!(env!("CARGO_PKG_HOMEPAGE")));
   gtk_about_dialog_set_logo(dlg, img);

   connect_hide(dlg as _);
   connect(btn as _, c_str!("activate"), c_show_act as _, dlg as _);

   g_object_unref(img as gpointer as _);
}

/// Sets up explicit window finalization for the main window.
unsafe fn setup_explicit_drop(b: *mut GtkBuilder, win: *mut GtkWindow)
{
   /// Callback to explicitly finalize all windows on exit.
   unsafe extern "C" fn c_done(_: *mut GtkWidget, exp_del: gpointer)
   {
      let exp_del = Box::from_raw(exp_del as *mut Vec<*mut GtkWindow>);

      for win in *exp_del {
         gtk_widget_destroy(win as _);
      }
   }

   // we need to explicitly drop other windows on exit, which means we need to
   // create a list of them to send to the callback
   let mut exp_del = Vec::new();

   // so, we get all of the objects from the builder, and iterate through them
   let head    = gtk_builder_get_objects(b);
   let mut lst = &*head;

   loop {
      let obj = lst.data as *mut GObject;

      // while this is well-defined, it is a weird way of doing it, because
      // this exact method of checking types isn't fully documented. we can't
      // use the macros for this functionality because we're not using C, so we
      // use the underlying function calls. again, get jacked, punk.
      if g_type_check_instance_is_a(obj as _, gtk_window_get_type()) != 0 {
         let owin = obj as *mut GtkWindow;

         if owin != win {
            exp_del.push(owin);
         }
      }

      let nx = lst.next;

      if nx != ffi::null_mut() {
         lst = &*nx;
      } else {
         break;
      }
   }

   g_slist_free(head);

   let exp_del = Box::into_raw(Box::new(exp_del));

   connect(win as _, c_str!("destroy"), c_done as _, exp_del as _);
}

/// Sets up the main menu window.
unsafe fn setup_win_main(b: *mut GtkBuilder,
                         app: *mut GtkApplication,
                         edit: Rc<MapEditorRef>)
{
   /// Callback to close the window when the "Quit" button is pressed.
   unsafe extern "C" fn c_quit_act(_: *mut GtkWidget, win: gpointer)
   {
      gtk_window_close(win as _);
   }

   /// Callback to create a new editor state when the "New" button is pressed.
   unsafe extern "C" fn c_new_act(_: *mut GtkWidget, edit: gpointer)
   {
      let edit = edit as *const MapEditorRef;
      let edit = &*edit;
      let mut edit = edit.borrow_mut();

      if edit.is_opened() {
         let titl = c_str!("Confirm");
         let text = c_str!("Are you sure you want to create a new project? \
                            Data may be lost.");

         if !run_ok_cancel_dlg(titl, text) {
            return;
         }
      }

      edit.open_new();
      edit.cause_update();
   }

   /// Callback to finalize the editor state reference.
   unsafe extern "C" fn c_new_done(_: *mut GtkWidget, edit: gpointer)
   {
      let edit = edit as *const MapEditorRef;
      Rc::from_raw(edit);
   }

   // set up main window
   let win: *mut GtkWindow = get_obj(b, c_str!("win-main"));

   setup_explicit_drop(b, win);

   gtk_window_set_application(win, app);
   gtk_widget_show_all(win as _);

   // set up buttons
   let btn: *mut GtkMenuItem = get_obj(b, c_str!("btn-quit"));

   connect(btn as _, c_str!("activate"), c_quit_act as _, win as _);

   let btn: *mut GtkMenuItem = get_obj(b, c_str!("btn-new"));

   let eptr = Rc::into_raw(edit.clone());

   connect(btn as _, c_str!("activate"), c_new_act as _, eptr as _);
   connect(btn as _, c_str!("destroy"), c_new_done as _, eptr as _);
}

/// Sets up the CSS styling providers.
unsafe fn setup_css()
{
   let path = c_str!("/net/greyserv/maraiah/tycho/css");
   let css  = gtk_css_provider_new();
   gtk_css_provider_load_from_resource(css, path);

   let scr = gdk_screen_get_default();
   let pri = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION as u32;
   gtk_style_context_add_provider_for_screen(scr, css as _, pri);

   g_object_unref(css as _);
}

/// Runs a modal OK/Cancel dialogue.
unsafe fn run_ok_cancel_dlg(title: ffi::NT, text: ffi::NT) -> bool
{
   let flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
   let dlg = gtk_dialog_new_with_buttons(title,
                                         ffi::null_mut(),
                                         flags,
                                         c_str!("_OK"),
                                         GTK_RESPONSE_ACCEPT,
                                         c_str!("_Cancel"),
                                         GTK_RESPONSE_REJECT,
                                         ffi::null_mut::<gpointer>());

   let area = gtk_dialog_get_content_area(dlg as _);
   let labl = gtk_label_new(text);

   gtk_container_add(area as _, labl);
   gtk_widget_show_all(area as _);

   let res = gtk_dialog_run(dlg as _);

   gtk_widget_destroy(dlg);

   match res {
      GTK_RESPONSE_ACCEPT => true,
      _                   => false,
   }
}

/// Connects a handler that hides a toplevel widget when deleted.
unsafe fn connect_hide(wid: *mut GtkWidget)
{
   /// Callback to hide the widget.
   unsafe extern "C" fn c_hide_del(wid: *mut GtkWidget,
                                   _: *mut GdkEvent,
                                   _: gpointer)
   {
      gtk_widget_hide(wid);
   }

   connect(wid as _, c_str!("delete-event"), c_hide_del as _, ffi::null_mut());
}

/// Connects a handler that shows a widget when activated.
unsafe fn connect_show(btn: *mut GtkWidget, wid: *mut GtkWidget)
{
   /// Callback to show the widget.
   unsafe extern "C" fn c_show_act(_: *mut GtkWidget, wid: gpointer)
   {
      gtk_widget_show_all(wid as _);
   }

   connect(btn as _, c_str!("activate"), c_show_act as _, wid as _);
}

/// Gets an object from a `GtkBuilder`.
unsafe fn get_obj<T>(b: *mut GtkBuilder, name: ffi::NT) -> *mut T
{
   let obj = gtk_builder_get_object(b, name);
   obj as _
}

/// Connects a signal handler.
unsafe fn connect(obj: *mut GObject, name: ffi::NT, cb: gpointer, d: gpointer)
{
   let cb = std::mem::transmute(cb);
   g_signal_connect_data(obj, name, cb, d, None, 0);
}

/// Loads a `Pixbuf` from a resource.
unsafe fn load_img(path: ffi::NT) -> *mut GdkPixbuf
{
   gdk_pixbuf_new_from_resource(path, ffi::null_mut())
}

/// Entry point.
fn main()
{
   unsafe {
      // get jacked, punk. opaque data structures are for nerds.
      const RESOURCE_DATA: &[u8] =
         include_bytes!(concat!(env!("OUT_DIR"), "/resources"));

      // first we create the static resource header, which is really simple
      let mut resource = GStaticResource{data:     RESOURCE_DATA.as_ptr(),
                                         data_len: RESOURCE_DATA.len(),
                                         resource: ffi::null_mut(),
                                         next:     ffi::null_mut(),
                                         padding:  ffi::null_mut()};

      // init it, now we can use it throughout the entire app without copying!
      g_static_resource_init(&mut resource);

      // create a container for the editor state
      let edit = MapEditor{edit: editor::MapEditor::new_closed(),
                           call: Vec::new(),
                           done: Vec::new()};
      let edit = Rc::new(RefCell::new(edit));
      let eptr = Rc::into_raw(edit.clone());

      // create and run the app
      let name = c_str!("net.greyserv.maraiah.tycho");
      let app  = gtk_application_new(name, 0);

      connect(app as _, c_str!("activate"), app_activate as _, eptr as _);

      g_application_run(app as _, 0, ffi::null_mut());

      // ok, clean up all this crap now
      g_object_unref(app as _);

      assert_eq!(Rc::strong_count(&edit), 1);
      drop(edit);

      // deinit the "static" data, and everything will be done
      g_static_resource_fini(&mut resource);
   }
}

impl MapEditor
{
   fn cause_update(&mut self) {for cb in &mut self.call {cb();}}
}

impl Drop for MapEditor
{
   fn drop(&mut self) {for cb in &mut self.done {cb();}}
}

impl std::ops::Deref for MapEditor
{
   type Target = editor::MapEditor;

   fn deref(&self) -> &editor::MapEditor {&self.edit}
}

impl std::ops::DerefMut for MapEditor
{
   fn deref_mut(&mut self) -> &mut editor::MapEditor {&mut self.edit}
}

struct MapEditor
{
   edit: editor::MapEditor,
   call: Vec<Box<dyn FnMut()>>,
   done: Vec<Box<dyn FnMut()>>,
}

type MapEditorRef = RefCell<MapEditor>;

// EOF
