mod interfaces;

use self::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};

const APP_ID:       ffi::NT = c_str!("net.greyserv.maraiah.tycho");
const B_ADJ_M_HORZ: ffi::NT = c_str!("adj-map-horz");
const B_ADJ_M_VERT: ffi::NT = c_str!("adj-map-vert");
const B_BTN_ABOUT:  ffi::NT = c_str!("btn-about");
const B_BTN_M_PROP: ffi::NT = c_str!("btn-show-map-prop");
const B_BTN_M_TOOL: ffi::NT = c_str!("btn-show-map-tools");
const B_BTN_M_VIEW: ffi::NT = c_str!("btn-show-map-view");
const B_BTN_NEW:    ffi::NT = c_str!("btn-new");
const B_BTN_OPEN:   ffi::NT = c_str!("btn-open");
const B_BTN_QUIT:   ffi::NT = c_str!("btn-quit");
const B_CON_F_ENT:  ffi::NT = c_str!("con-f-ent");
const B_CON_F_ENV:  ffi::NT = c_str!("con-f-env");
const B_CON_F_MSN:  ffi::NT = c_str!("con-f-msn");
const B_DLG_ABOUT:  ffi::NT = c_str!("dlg-about");
const B_DRAW_AREA:  ffi::NT = c_str!("draw-area");
const B_WIN_MAIN:   ffi::NT = c_str!("win-main");
const B_WIN_M_PROP: ffi::NT = c_str!("win-map-prop");
const B_WIN_M_TOOL: ffi::NT = c_str!("win-map-tools");
const B_WIN_M_VIEW: ffi::NT = c_str!("win-map-view");
const E_ACTIVATE:   ffi::NT = c_str!("activate");
const E_DELETE:     ffi::NT = c_str!("delete-event");
const E_DESTROY:    ffi::NT = c_str!("destroy");
const E_DRAW:       ffi::NT = c_str!("draw");
const E_SHUTDOWN:   ffi::NT = c_str!("shutdown");
const E_TOGGLE:     ffi::NT = c_str!("toggled");
const IM_ABOUT:     ffi::NT = c_str!("/net/greyserv/maraiah/tycho/tycho2.png");
const IM_NOMAP:     ffi::NT = c_str!("/net/greyserv/maraiah/tycho/tycho1.png");
const PATH_BUILDER: ffi::NT = c_str!("/net/greyserv/maraiah/tycho/ui");
const PATH_CSS:     ffi::NT = c_str!("/net/greyserv/maraiah/tycho/css");

// Called when the application activates in order to set everything up.
unsafe extern "C" fn app_activate(app: *mut GtkApplication, _: gpointer)
{
   // Callback to finalize the reference.
   unsafe extern "C" fn c_done(_: *mut GApplication, ptr: gpointer)
   {
      let edit = Rc::from_raw(ptr as *const MapEditorRef);
      assert_eq!(Rc::strong_count(&edit), 1);
   }

   let b = Refc::new(gtk_builder_new_from_resource(PATH_BUILDER));

   let edit = MapEditor{edit: Default::default(),
                        draw: Refc::own(get_obj(&b, B_DRAW_AREA)),
                        fent: get_flag_fields(&b, B_CON_F_ENT),
                        fenv: get_flag_fields(&b, B_CON_F_ENV),
                        fmsn: get_flag_fields(&b, B_CON_F_MSN)};

   let edit = RefCell::new(edit);
   let edit = Rc::new(edit);

   setup_css();
   setup_toggles(edit.clone());
   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());

   connect(app, E_SHUTDOWN, c_done as _, Rc::into_raw(edit));
}

// Gets all of the toggle buttons from a container.
unsafe fn get_flag_fields(b: &Refc<GtkBuilder>, name: ffi::NT) -> Vec<PropFlag>
{
   let mut flags = Vec::new();

   let head = get_obj(b, name);
   let head = ListD::new(gtk_container_get_children(head));
   let gtyp = gtk_toggle_button_get_type();

   get_typed_from(head, gtyp, |obj| {
      flags.push(PropFlag{w: Refc::own(obj as _), h: 0});
   });

   flags
}

// Connects toggle events to every flag in the map properties window.
unsafe fn setup_toggles(edit: Rc<MapEditorRef>)
{
   let mut ed = edit.borrow_mut();

   connect_toggle(edit.clone(), ed.fent.iter_mut());
   connect_toggle(edit.clone(), ed.fenv.iter_mut());
   connect_toggle(edit.clone(), ed.fmsn.iter_mut());
}

// Connects toggle events to each of the widgets in `it`.
unsafe fn connect_toggle<'a, I>(edit: Rc<MapEditorRef>, it: I)
   where I: Iterator<Item = &'a mut PropFlag>
{
   // Callback for when the button is toggled, which causates an update.
   unsafe extern "C" fn c_toggled(_: *mut GtkToggleButton, edit: gpointer)
   {
      let edit = &*(edit as *const MapEditorRef);
      edit.borrow_mut().cause_update_props();
   }

   // go over each widget, set its object and callback handle
   for flg in it {
      let erf = connect_ref(*flg.w, edit.clone());
      flg.h = connect(*flg.w, E_TOGGLE, c_toggled as _, erf);
   }
}

// Sets up the map view window's drawing area.
unsafe fn setup_draw_area(b: &Refc<GtkBuilder>, edit: Rc<MapEditorRef>)
{
   // All of the state necessary for the drawing area.
   struct RenderState
   {
      im_nomap: Refc<'static, GdkPixbuf>,
      ax:       Refc<'static, GtkAdjustment>,
      ay:       Refc<'static, GtkAdjustment>,
      edit:     Rc<MapEditorRef>,
   }

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

   // 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 = &mut *(rend as *mut RenderState);

      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 = get_obj::<GtkDrawingArea>(b, B_DRAW_AREA);

   // get all of the necessary state and related objects
   let ax = Refc::own(get_obj(b, B_ADJ_M_HORZ));
   let ay = Refc::own(get_obj(b, B_ADJ_M_VERT));

   let im_nomap = Refc::new(load_img(IM_NOMAP));

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

   connect(wid, E_DESTROY, c_done as _, rend);
   connect(wid, E_DRAW, c_draw as _, rend);
}

// Sets up the map view window.
unsafe fn setup_win_map_view(b: &Refc<GtkBuilder>)
{
   let win = get_obj::<GtkWindow  >(b, B_WIN_M_VIEW);
   let btn = get_obj::<GtkMenuItem>(b, B_BTN_M_VIEW);

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

// Sets up the map tools window.
unsafe fn setup_win_map_tools(b: &Refc<GtkBuilder>)
{
   let win = get_obj::<GtkWindow  >(b, B_WIN_M_TOOL);
   let btn = get_obj::<GtkMenuItem>(b, B_BTN_M_TOOL);

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

// Sets up the map properties window.
unsafe fn setup_win_map_prop(b: &Refc<GtkBuilder>)
{
   let win = get_obj::<GtkWindow  >(b, B_WIN_M_PROP);
   let btn = get_obj::<GtkMenuItem>(b, B_BTN_M_PROP);

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

// Sets up the about dialogue.
unsafe fn setup_about_dlg(b: &Refc<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 = get_obj::<GtkAboutDialog>(b, B_DLG_ABOUT);
   let btn = get_obj::<GtkMenuItem   >(b, B_BTN_ABOUT);

   let it    = env!("CARGO_PKG_AUTHORS").split(';');
   let mut v = ffi::CStringVec::new_from_iter(it).unwrap();
   let img   = Refc::new(load_img(IM_ABOUT));

   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);
   connect(btn, E_ACTIVATE, c_show_act as _, dlg);
}

// Sets up explicit window finalization for the main window.
unsafe fn setup_explicit_drop(b: &Refc<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 = ListS::new(gtk_builder_get_objects(**b));

   get_typed_from(head, gtk_window_get_type(), |obj| {
      let obj = obj as *mut GtkWindow;

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

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

   connect(win, E_DESTROY, c_done as _, exp_del);
}

// Get objects of type `ty` from `it`.
unsafe fn get_typed_from<I, F>(it: I, ty: GType, mut f: F)
   where I: Iterator<Item = gpointer>,
         F: FnMut(*mut GObject)
{
   for obj in it {
      // 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 *mut GTypeInstance, ty) != 0 {
         f(obj as _);
      }
   }
}

// Sets up the main menu window.
unsafe fn setup_win_main(b: &Refc<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 map when the "New" button is pressed.
   unsafe extern "C" fn c_new_act(_: *mut GtkWidget, edit: gpointer)
   {
      let edit = &*(edit as *const MapEditorRef);
      let mut edit = edit.borrow_mut();

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

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

      edit.open_new();
      edit.cause_refresh();
   }

   // Callback to open an existing map when the "Open" button is pressed.
   unsafe extern "C" fn c_open_act(_: *mut GtkWidget, edit: gpointer)
   {
      let edit = &*(edit as *const MapEditorRef);
      let mut edit = edit.borrow_mut();

      if edit.unclean() {
         let titl = c_str!("Confirm");
         let text = c_str!("Are you sure you want to open this project? \
                            Unsaved data may be lost.");

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

      if let Some(path) = run_file_chooser_open() {
         // TODO: handle errors gracefully
         let fp = std::fs::File::open(&path).unwrap();
         let mm = memmap::Mmap::map(&fp).unwrap();
         edit.open_buf(&mm);
         edit.cause_refresh();
      }
   }

   // set up main window
   let win = get_obj(b, B_WIN_MAIN);

   setup_explicit_drop(b, win);

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

   // set up buttons
   let btn = get_obj::<GtkMenuItem>(b, B_BTN_QUIT);
   connect(btn, E_ACTIVATE, c_quit_act as _, win);

   let btn = get_obj::<GtkMenuItem>(b, B_BTN_NEW);
   connect(btn, E_ACTIVATE, c_new_act as _, connect_ref(btn, edit.clone()));

   let btn = get_obj::<GtkMenuItem>(b, B_BTN_OPEN);
   connect(btn, E_ACTIVATE, c_open_act as _, connect_ref(btn, edit.clone()));
}

// Sets up the CSS styling providers.
unsafe fn setup_css()
{
   let css  = Refc::new(gtk_css_provider_new());
   gtk_css_provider_load_from_resource(*css, PATH_CSS);

   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);
}

// 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_void());

   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);

   res == GTK_RESPONSE_ACCEPT
}

// Runs a modal Open File dialogue.
unsafe fn run_file_chooser_open() -> Option<String>
{
   let action = GTK_FILE_CHOOSER_ACTION_OPEN;
   let dlg = gtk_file_chooser_dialog_new(c_str!("Open File"),
                                         ffi::null_mut(),
                                         action,
                                         c_str!("_Cancel"),
                                         GTK_RESPONSE_CANCEL,
                                         c_str!("_Open"),
                                         GTK_RESPONSE_ACCEPT,
                                         ffi::null_mut_void());

   let res = gtk_dialog_run(dlg as _);

   let ret = if res == GTK_RESPONSE_ACCEPT {
      let fna = gtk_file_chooser_get_filename(dlg as _);

      let own = ffi::CStr::from_ptr(fna);
      let own = own.to_str().ok()?.to_owned();

      g_free(fna as _);

      Some(own)
   } else {
      None
   };

   gtk_widget_destroy(dlg);

   ret
}

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

   connect(wid, E_DELETE, c_hide_del as _, ffi::null_void());
}

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

   connect(btn, E_ACTIVATE, c_show_act as _, wid);
}

// Connects the map editor reference to a widget.
unsafe fn connect_ref<T>(obj: *mut T, rc: Rc<MapEditorRef>)
   -> *const MapEditorRef
{
   // Callback to finalize the reference.
   unsafe extern "C" fn c_done(_: *mut GtkWidget, ptr: gpointer)
   {
      Rc::from_raw(ptr as *const MapEditorRef);
   }

   let ptr = Rc::into_raw(rc);

   connect(obj, E_DESTROY, c_done as _, ptr);

   ptr
}

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

// Connects a signal handler.
unsafe fn connect<T, U>(obj: *mut T, name: ffi::NT, cb: gpointer, d: *const U)
   -> ffi::c_ulong
{
   let cb = std::mem::transmute(cb);
   g_signal_connect_data(obj as _, name, cb, d as _, 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 and run the app
         let app = Refc::new(gtk_application_new(APP_ID, 0));

         connect(*app, E_ACTIVATE, app_activate as _, ffi::null_void());

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

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

// EOF
