/* -*- Mode: C; indent-tabs-mode: nil; tab-width: 2 -*-
 *
 * Copyright (C) 2009,2010 Canonical Ltd.
 * Authors:
 *  Gustavo Sverzut Barbieri <gustavo.barbieri@canonical.com>
 *  Michael Terry <michael.terry@canonical.com>
 *
 * This file is part of Netbook Launcher EFL.
 *
 * Netbook Launcher EFL 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, version 3 of the License.
 *
 * Netbook Launcher EFL 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 Netbook Launcher EFL.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <gtk/gtk.h>
#include <Elementary.h>
#include <cairo/cairo.h>
#include <libgadget/libgadget.h>
#include "netbook-launcher.h"

#define SMART_DATA_GET(o, ptr)  \
    struct _data *ptr = evas_object_smart_data_get(o)

#define SMART_DATA_GET_OR_RETURN_VAL(o, ptr, rv) \
  SMART_DATA_GET(o, ptr);              \
  if (!ptr) {                          \
      ERR("no private data for object %p (%s)\n", \
          o, evas_object_type_get(o));            \
      return rv;                          \
  }

#define SMART_DATA_GET_OR_RETURN(o, ptr) \
  SMART_DATA_GET_OR_RETURN_VAL(o, ptr, )

struct _data {
  Evas_Object_Smart_Clipped_Data base;
  Evas_Object *cairo;
  GadgetProxy *proxy;
  GadgetManager *manager;
  guint redraw_id;
  guint resize_id;
  const char *uid;
  const char *path;
  gboolean faking;
};

/* Keep just one instance of them */
static Evas_Smart_Class _parent_sc = EVAS_SMART_CLASS_INIT_NULL;
static unsigned int gtk_init_count = 0;
static unsigned int g_type_init_count = 0;
static GadgetManager *manager = NULL;
Evas_Object *prev_focus = NULL;

static void *
_setup_private_data(Evas_Object *o, unsigned int size)
{
  struct _data *priv;

  priv = evas_object_smart_data_get(o);
  if (!priv) {
    priv = calloc(1, size);
    if (!priv) {
      ERR("could not malloc private data.\n");
      return NULL;
    }
    evas_object_smart_data_set(o, priv);
  }

  return priv;
}

static void
_smart_add(Evas_Object *o)
{
  struct _data *priv;

  priv = _setup_private_data(o, sizeof(struct _data));
  if (!priv) {
    ERR("Could not add new smart object\n");
    return;
  }

  _parent_sc.add(o);
}

static void
_smart_del(Evas_Object *o)
{
    SMART_DATA_GET(o, priv);

    if (priv->cairo)
        evas_object_del(priv->cairo);
    if (priv->manager)
        g_object_unref(priv->manager);
    if (priv->proxy)
        g_object_unref(priv->proxy);
    if (priv->path)
        eina_stringshare_del(priv->path);
    if (priv->uid)
        eina_stringshare_del(priv->uid);

    priv->cairo = NULL;
    priv->manager = NULL;
    priv->proxy = NULL;

    _parent_sc.del(o);
}

static void
_smart_calculate(Evas_Object *o)
{
    Evas_Coord x, y, w, h;
    SMART_DATA_GET_OR_RETURN(o, priv);
    evas_object_geometry_get(o, &x, &y, &w, &h);
    evas_object_image_data_update_add(priv->cairo, 0, 0, w, h);
}

static void
_smart_resize(Evas_Object *o, Evas_Coord w, Evas_Coord h)
{
    SMART_DATA_GET_OR_RETURN(o, priv);

    /* Resize smart object */
    evas_object_image_size_set(priv->cairo, w, h);
    cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
    evas_image_cairo_surface_set(priv->cairo, s);

    evas_object_smart_changed(o);

    return;
}

static Evas_Smart_Class *
_smart_class_new(void)
{
    static Evas_Smart_Class sc =
        EVAS_SMART_CLASS_INIT_NAME_VERSION("EFLGADGET");

    if (!_parent_sc.name) {
        evas_object_smart_clipped_smart_set(&sc);
        _parent_sc = sc;
        sc.add = _smart_add;
        sc.del = _smart_del;
        sc.resize = _smart_resize;
        sc.calculate = _smart_calculate;
    }

    return &sc;
}

static guint
_evas_to_gdk_state(int buttons, Evas_Modifier *modifiers)
{
  guint rv = 0;
  rv |= ((buttons & 0x1f) * GDK_BUTTON1_MASK); // shift buttons left
  if (evas_key_modifier_is_set(modifiers, "Shift"))
    rv |= GDK_SHIFT_MASK;
  if (evas_key_modifier_is_set(modifiers, "Control"))
    rv |= GDK_CONTROL_MASK;
  if (evas_key_modifier_is_set(modifiers, "Alt"))
    rv |= GDK_MOD1_MASK;
  return rv;
}

static GdkEventType
_evas_to_gdk_press(Evas_Button_Flags flags)
{
  if (flags & EVAS_BUTTON_TRIPLE_CLICK)
    return GDK_3BUTTON_PRESS;
  else if (flags & EVAS_BUTTON_DOUBLE_CLICK)
    return GDK_2BUTTON_PRESS;
  else
    return GDK_BUTTON_PRESS;
}

static void
_shift_focus(Evas *evas, Evas_Object *o, Eina_Bool on)
{
  if (on)
    prev_focus = evas_focus_get(evas);
  evas_object_focus_set(o, on);
  if (!on && prev_focus)
    evas_object_focus_set(prev_focus, 1);
}

static void
_key_down(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Event_Key_Down *kd = (Evas_Event_Key_Down *)ev;
    GdkEventKey e;
    Evas_Object *gadget = data;
    char *string;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    string = g_strdup(kd->string);

    e.window = NULL;
    e.type = GDK_KEY_PRESS;
    e.time = kd->timestamp;
    e.keyval = gstuff_keyname_to_key(kd->key);
    e.hardware_keycode = 0;
    e.string = string;
    e.state = _evas_to_gdk_state(0, kd->modifiers);

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
    g_free(string);
}

static void
_key_up(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Event_Key_Up *ku = (Evas_Event_Key_Up *)ev;
    GdkEventKey e;
    Evas_Object *gadget = data;
    char *string;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    string = g_strdup(ku->string);

    e.window = NULL;
    e.type = GDK_KEY_RELEASE;
    e.time = ku->timestamp;
    e.keyval = gstuff_keyname_to_key(ku->key);
    e.hardware_keycode = 0;
    e.string = string;
    e.state = _evas_to_gdk_state(0, ku->modifiers);

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
    g_free(string);
}

static void
_mouse_down(void *data, Evas *evas, Evas_Object *o, void *ev)
{
  Evas_Coord x, y;
  Evas_Event_Mouse_Down *md = (Evas_Event_Mouse_Down *)ev;
  GdkEventButton e;
  Evas_Object *gadget = data;
  SMART_DATA_GET_OR_RETURN(data, priv);

  evas_object_geometry_get(o, &x, &y, NULL, NULL);

  // the x coordinate of the pointer relative to the window.
  e.window = NULL;
  e.x = md->canvas.x - x;
  e.y = md->canvas.y - y;
  e.time = md->timestamp;
  e.type = _evas_to_gdk_press(md->flags);
  e.device = NULL;
  e.button = md->button;
  e.state = _evas_to_gdk_state(md->button, md->modifiers);
  // the x coordinate of the pointer relative to the root of the screen.
  e.x_root = md->output.x;
  e.y_root = md->output.x;

  /* first, cancel our parent's mouse down, so they don't scroll with us */
  priv->faking = TRUE;
  evas_event_feed_mouse_cancel(evas, md->timestamp, NULL);
  priv->faking = FALSE;

  gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_mouse_up(void *data, Evas *evas, Evas_Object *o, void *ev)
{
  Evas_Coord x, y;
  Evas_Event_Mouse_Up *mu = (Evas_Event_Mouse_Up *)ev;
  GdkEventButton e;
  Evas_Object *gadget = data;
  SMART_DATA_GET_OR_RETURN(gadget, priv);

  if (priv->faking)
    return;

  evas_object_geometry_get(o, &x, &y, NULL, NULL);

  e.window = NULL;
  e.x = mu->canvas.x - x;
  e.y = mu->canvas.y - y;
  e.time = mu->timestamp;
  e.type = GDK_BUTTON_RELEASE;
  e.device = NULL;
  e.button = mu->button;
  e.x_root = mu->output.x;
  e.y_root = mu->output.x;
  e.state = _evas_to_gdk_state(mu->button, mu->modifiers);

  gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_mouse_move(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Coord x, y;
    Evas_Event_Mouse_Move *mm = (Evas_Event_Mouse_Move *)ev;
    GdkEventMotion e;
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    evas_object_geometry_get(o, &x, &y, NULL, NULL);

    e.window = NULL;
    e.type = GDK_MOTION_NOTIFY;
    e.time = mm->timestamp;
    e.x = mm->cur.canvas.x - x;
    e.y = mm->cur.canvas.y - y;
    e.x_root = mm->cur.output.x;
    e.y_root = mm->cur.output.x;
    e.state = _evas_to_gdk_state(mm->buttons, mm->modifiers);

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_mouse_in(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Coord x, y;
    Evas_Event_Mouse_In *mi = (Evas_Event_Mouse_In *)ev;
    GdkEventCrossing e;
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    evas_object_geometry_get(o, &x, &y, NULL, NULL);

    e.window = NULL;
    e.type = GDK_ENTER_NOTIFY;
    e.time = mi->timestamp;
    e.x = mi->canvas.x - x;
    e.y = mi->canvas.y - y;
    e.state = _evas_to_gdk_state(mi->buttons, mi->modifiers);
    e.x_root = mi->output.x;
    e.y_root = mi->output.x;
    e.mode = GDK_CROSSING_NORMAL;

    /* Focus-follows-mouse for now */
    _shift_focus(evas, priv->cairo, 1);

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_mouse_out(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Coord x, y;
    Evas_Event_Mouse_Out *mo = (Evas_Event_Mouse_Out *)ev;
    GdkEventCrossing e;
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    evas_object_geometry_get(o, &x, &y, NULL, NULL);

    e.window = NULL;
    e.type = GDK_LEAVE_NOTIFY;
    e.time = mo->timestamp;
    e.x = mo->canvas.x - x;
    e.y = mo->canvas.y - y;
    e.state = _evas_to_gdk_state(mo->buttons, mo->modifiers);
    e.x_root = mo->output.x;
    e.y_root = mo->output.x;
    e.mode = GDK_CROSSING_NORMAL;

    /* Focus-follows-mouse for now */
    _shift_focus(evas, priv->cairo, 0);

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_mouse_wheel(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Coord x, y;
    Evas_Event_Mouse_Wheel *mw = (Evas_Event_Mouse_Wheel *)ev;
    GdkEventScroll e;
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);

    evas_object_geometry_get(o, &x, &y, NULL, NULL);

    e.window = NULL;
    e.type = GDK_SCROLL;
    e.time = mw->timestamp;
    e.x = mw->canvas.x - x;
    e.y = mw->canvas.y - y;
    e.x_root = mw->output.x;
    e.y_root = mw->output.x;
    e.state = _evas_to_gdk_state(0, mw->modifiers);
    e.direction =
        mw->direction == 0 && mw->z < 0 ? GDK_SCROLL_UP :
        mw->direction == 0 && mw->z > 0 ? GDK_SCROLL_DOWN :
        mw->direction != 0 && mw->z < 0 ? GDK_SCROLL_LEFT :
        GDK_SCROLL_RIGHT;

    gadget_proxy_event(priv->proxy, (GdkEvent *)&e);
}

static void
_focus_in(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);
    gadget_proxy_set_focused(priv->proxy, 1);
}

static void
_focus_out(void *data, Evas *evas, Evas_Object *o, void *ev)
{
    Evas_Object *gadget = data;
    SMART_DATA_GET_OR_RETURN(gadget, priv);
    gadget_proxy_set_focused(priv->proxy, 0);
}

static gboolean
_idle_redraw(Evas_Object *gadget)
{
  cairo_t *cr;
  cairo_surface_t *cs;
  SMART_DATA_GET_OR_RETURN_VAL(gadget, priv, FALSE);

  priv->redraw_id = 0;

  if (!priv->cairo || !priv->proxy)
    return FALSE;

  cs = evas_image_cairo_surface_get(priv->cairo);
  if (!cs) {
    ERR("Could not get cairo surface\n");
    return FALSE;
  }
  cr = cairo_create(cs);
  if (!cr) {
    ERR("Could not get cairo from surface\n");
    return FALSE;
  }

  cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
  cairo_paint(cr);
  cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
  gadget_cairo_draw(GADGET_CAIRO(priv->proxy), cr);
  cairo_destroy(cr);

  evas_object_smart_changed(gadget);
  return FALSE;
}

static void
_on_queue_redraw(GadgetProxy *proxy, Evas_Object *gadget)
{
  SMART_DATA_GET_OR_RETURN(gadget, priv);
  if (!priv->redraw_id && !priv->resize_id) // We redraw after resizes
    priv->redraw_id = g_idle_add((GSourceFunc)_idle_redraw, gadget);
}

static gboolean
_idle_resize(Evas_Object *gadget)
{
  int w, h;
  SMART_DATA_GET_OR_RETURN_VAL(gadget, priv, FALSE);

  priv->resize_id = 0;

  w = gadget_proxy_get_width(priv->proxy);
  h = gadget_proxy_get_height(priv->proxy);

  evas_object_resize(gadget, w, h);

  // Force redraw now that we've resized, in case widget doesn't
  _on_queue_redraw(priv->proxy, gadget);

  return FALSE;
}

static void
_on_queue_resize(GadgetProxy *proxy, Evas_Object *gadget)
{
  SMART_DATA_GET_OR_RETURN(gadget, priv);
  if (!priv->resize_id) {
    priv->resize_id = g_idle_add((GSourceFunc)_idle_resize, gadget);
    if (priv->redraw_id) { // we redraw after resizing
      g_source_remove(priv->redraw_id);
      priv->redraw_id = 0;
    }
  }
}

static void
_on_remove_me(GadgetProxy *proxy, Evas_Object *gadget)
{
  SMART_DATA_GET(gadget, priv);

  if (priv->proxy)
      gadget_proxy_removed(priv->proxy);

  evas_object_smart_callback_call(gadget, "remove-me", NULL);
}

static void
_on_begin_resize(GadgetProxy *proxy, GadgetResizePoint point, Evas_Object *gadget)
{
}

static void
_on_begin_drag_move(Evas_Object *gadget)
{
  INF("begin drag move\n");
}

static void
_on_open_url(GadgetProxy *proxy, const gchar *url, Evas_Object *gadget)
{
  GError *error = NULL;
  gtk_show_uri (gdk_screen_get_default(), url, gtk_get_current_event_time (),
                &error);
  if (error)
  {
    ERR("Could not open %s: %s\n", url, error->message);
    g_error_free(error);
  }
}

static void
_signals_connect(GadgetProxy *proxy, Evas_Object *gadget)
{
    if (!proxy || !gadget)
        return;

    g_signal_connect(proxy, "queue-redraw",
            G_CALLBACK(_on_queue_redraw), gadget);
    g_signal_connect(proxy, "queue-resize",
            G_CALLBACK(_on_queue_resize), gadget);
    g_signal_connect(proxy, "open-url",
            G_CALLBACK(_on_open_url), gadget);
    g_signal_connect(proxy, "begin-resize",
            G_CALLBACK(_on_begin_resize), gadget);
    g_signal_connect(proxy, "begin-drag-move",
            G_CALLBACK(_on_begin_drag_move), gadget);
    g_signal_connect(proxy, "remove-me",
            G_CALLBACK(_on_remove_me), gadget);
}

static void
_callbacks_add(Evas_Object *cairo, Evas_Object *gadget)
{
    if (!cairo || !gadget)
        return;

    evas_object_event_callback_add(cairo, EVAS_CALLBACK_FOCUS_IN,
            _focus_in, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_FOCUS_OUT,
            _focus_out, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_KEY_DOWN,
            _key_down, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_KEY_UP,
            _key_up, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_DOWN,
            _mouse_down, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_UP,
            _mouse_up, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_MOVE,
            _mouse_move, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_IN,
            _mouse_in, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_OUT,
            _mouse_out, gadget);
    evas_object_event_callback_add(cairo, EVAS_CALLBACK_MOUSE_WHEEL,
            _mouse_wheel, gadget);
}

static Evas_Object *
_create_gadget(Evas *evas, Evas_Smart *smart, const char *path, const char *uid)
{
  Evas_Object *gadget = evas_object_smart_add(evas, smart);
  if (!gadget) {
    ERR("Could not add smart gadget\n");
    return NULL;
  }

  SMART_DATA_GET(gadget, priv);
  if (!priv)
    goto on_error;

  priv->manager = gadget_manager_get_default();
  if (!priv->manager) {
    ERR("Could not get a gadget manager\n");
    goto on_error;
  }
  gadget_manager_load_sources(priv->manager);

  if (!gadget_manager_can_handle_path(priv->manager, path)) {
    ERR("Cannot handle path: %s\n", path);
    goto on_error;
  }

  priv->proxy = gadget_manager_load_gadget(priv->manager, path, uid);
  if (!priv->proxy) {
    ERR("Unable to load gadget %s path %s\n", uid, path);
    goto on_error;
  }

  priv->cairo = evas_image_cairo_new(evas, 1, 1, 1);
  if (!priv->cairo)
    goto on_error;

  evas_object_smart_member_add(priv->cairo, gadget);
  evas_object_show(priv->cairo);

  priv->uid = eina_stringshare_add(uid);
  priv->path = eina_stringshare_add(path);

  _signals_connect(priv->proxy, gadget);
  _callbacks_add(priv->cairo, gadget);

  evas_object_image_filled_set(priv->cairo, 1);
  evas_image_cairo_fill_auto_set(priv->cairo, 1);
  _on_queue_resize(priv->proxy, gadget);

  return gadget;

on_error:
  evas_object_del(gadget);
  return NULL;
}

Evas_Object *
efl_gadget_new(Evas *evas, const char *path, const char *uid)
{
    Evas_Smart *smart;
    Evas_Object *gadget;

    if (!evas) {
        ERR("No canvas to add the gadget to\n");
        return NULL;
    }

    smart = evas_smart_class_new(_smart_class_new());
    if (!smart) {
        ERR("Could not create new smart class\n");
        return NULL;
    }

    gadget = _create_gadget(evas, smart, path, uid);
    if (!gadget) {
        ERR("Could not create new gadget\n");
        return NULL;
    }

    return gadget;
}

const char *
efl_gadget_path_get(Evas_Object *gadget)
{
    SMART_DATA_GET_OR_RETURN_VAL(gadget, priv, NULL);
    return priv->path;
}

const char *
efl_gadget_uid_get(Evas_Object *gadget)
{
    SMART_DATA_GET_OR_RETURN_VAL(gadget, priv, NULL);
    return priv->uid;
}

char *
efl_gadget_uid_make()
{
  struct timeval tv;
  char *uid;

  if (!gettimeofday(&tv, NULL)) {
    uid = g_strdup_printf("gadget-%u-%u", (guint)tv.tv_sec, (guint)tv.tv_usec);
  } else {
    uid = g_strdup_printf("gadget-%d", (gint)time(NULL));
  }

  return uid;
}

void
efl_gadget_init(int argc, char *argv[])
{
    if (!g_type_init_count)
        g_type_init();
    g_type_init_count++;

    if (!gtk_init_count)
        gtk_init(&argc, &argv);
    gtk_init_count++;

    /* Keep an extra reference around to the gadget manager, because if it ever
       actually has 0 references left, it will free itself and future calls to
       get_manager will misbehave. */
    manager = gadget_manager_get_default();
}

void
efl_gadget_shutdown(void)
{
    if (g_type_init_count - 1 >= 0)
        g_type_init_count--;

    if (gtk_init_count - 1 >= 0)
        gtk_init_count--;

    g_object_unref(manager);
}
