/* -*- Mode: C; indent-tabs-mode: nil; tab-width: 2 -*-
 *
 * Copyright (C) 2009 Canonical Ltd.
 * Authors:
 *  Gustavo Sverzut Barbieri <gustavo.barbieri@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 "netbook-launcher.h"
#include <gconf/gconf.h>
#include <gconf/gconf-client.h>
#include <stdio.h>
#include <string.h>

static const char BG_PATH[] = "/desktop/gnome/background";
static const char BG_FILE[] = "/desktop/gnome/background/picture_filename";
static const char BG_OPTION[] = "/desktop/gnome/background/picture_options";
static const char BG_COLOR[] = "/desktop/gnome/background/primary_color";
static const char BG_DRAW[] = "/desktop/gnome/background/draw_background";

enum {
  BG_OPTION_NONE,
  BG_OPTION_WALLPAPER,
  BG_OPTION_CENTERED,
  BG_OPTION_SCALED,
  BG_OPTION_ZOOM,
  BG_OPTION_STRETCHED
};

struct bg_ctxt
{
  Evas_Object *obj;
  struct bg_smart_data *sd;
  GConfClient *client;
  const char *file;
  unsigned int option;
  unsigned int color;
  Eina_Bool draw;
  struct
  {
    guint bg_file;
    guint bg_option;
    guint bg_color;
    guint bg_draw;
  } notify;
};

struct bg_smart_data
{
  Evas_Object_Smart_Clipped_Data base;
  Evas_Object *rect;
  Evas_Object *img;
  struct bg_ctxt *ctxt;
};

static Evas_Smart_Class _parent_sc = EVAS_SMART_CLASS_INIT_NULL;

static void
_bg_recalc_size(struct bg_ctxt *ctxt, Evas_Coord w, Evas_Coord h)
{
  Evas_Object *img = ctxt->sd->img;
  Evas_Coord ox, oy, ow, oh, fx, fy, fw, fh;
  const char *file = NULL;
  double phoriz, pvert;

  if (!ctxt->draw)
  {
    evas_object_hide(img);
    return;
  }

  if ((w <= 0) || (h <= 0))
    return;

  if (evas_object_image_load_error_get(img))
  {
    ERR("could not load background, error: %d\n",
        evas_object_image_load_error_get(img));
    evas_object_hide(img);
    return;
  }
  evas_object_image_file_get(img, &file, NULL);
  if (!file)
  {
    DBG("no background image set!\n");
    evas_object_hide(img);
    return;
  }

  switch (ctxt->option)
  {
  case BG_OPTION_NONE:
    /* exact image size at top-left */
    evas_object_image_size_get(img, &ow,  &oh);
    fx = ox = 0;
    fy = oy = 0;
    fw = ow;
    fh = oh;
    break;
  case BG_OPTION_WALLPAPER:
    /* exact image size at top-left, tiled to fill screen */
    evas_object_image_size_get(img, &fw,  &fh);
    fx = ox = 0;
    fy = oy = 0;
    ow = w;
    oh = h;
    break;
  case BG_OPTION_CENTERED:
    evas_object_image_size_get(img, &ow,  &oh);
    ox = (w - ow) / 2;
    oy = (h - oh) / 2;
    fx = 0;
    fy = 0;
    fw = ow;
    fh = oh;
    break;
  case BG_OPTION_SCALED:
    /* image scaled keeping aspect, do NOT overflow screen boundaries */
    evas_object_image_size_get(img, &ow, &oh);
    if ((ow <= 0) || (oh <= 0))
    {
      evas_object_hide(img);
      return;
    }

    phoriz = (double)ow / (double)w;
    pvert = (double)oh / (double)h;
    if (phoriz < pvert)
    {
      ow = (h * ow) / oh;
      oh = h;
    }
    else
    {
      oh = (w * oh) / ow;
      ow = w;
    }

    ox = (w - ow) / 2;
    oy = (h - oh) / 2;
    fx = 0;
    fy = 0;
    fw = ow;
    fh = oh;
    break;
  case BG_OPTION_ZOOM:
    /* image scaled keeping aspect, do overflow screen boundaries */
    evas_object_image_size_get(img, &ow, &oh);
    if ((ow <= 0) || (oh <= 0))
    {
      evas_object_hide(img);
      return;
    }

    phoriz = (double)ow / (double)w;
    pvert = (double)oh / (double)h;
    if (phoriz > pvert) /* inverted */
    {
      ow = (h * ow) / oh;
      oh = h;
    }
    else
    {
      oh = (w * oh) / ow;
      ow = w;
    }

    ox = (w - ow) / 2;
    oy = (h - oh) / 2;
    fx = 0;
    fy = 0;
    fw = ow;
    fh = oh;
    break;
  case BG_OPTION_STRETCHED:
  default:
    fx = ox = 0;
    fy = oy = 0;
    fw = ow = w;
    fh = oh = h;
  }

  evas_object_image_fill_set(img, fx, fy, fw, fh);
  evas_object_move(img, ox, oy);
  evas_object_resize(img, ow, oh);
  evas_object_show(img);

  DBG("background image geometry: %d,%d+%dx%d, fill: %d,%d+%dx%d\n",
      ox, oy, ow, oh, fx, fy, fw, fh);
}

static void
_bg_recalc(struct bg_ctxt *ctxt)
{
  Evas_Coord w, h;
  evas_object_geometry_get(ctxt->obj, NULL, NULL, &w, &h);
  _bg_recalc_size(ctxt, w, h);
}

static void
_bg_color_update(struct bg_ctxt *ctxt)
{
  unsigned int color, r, g, b;

  color = ctxt->color;
  r = (color & 0xff0000) >> 16;
  g = (color & 0x00ff00) >> 8;
  b = color & 0x0000ff;

  evas_object_color_set(ctxt->sd->rect, r, g, b, 255);
}

static void
_bg_conf_save(const struct bg_ctxt *ctxt)
{
  conf_bg_set(ctxt->file, ctxt->option, ctxt->color, ctxt->draw);
}

static void
_bg_ecore_file_changed(void *data, const char *file)
{
  struct bg_ctxt *ctxt = data;
  int err;

  if (!eina_stringshare_replace(&ctxt->file, file))
    return;

  evas_object_image_file_set(ctxt->sd->img, ctxt->file, NULL);
  err = evas_object_image_load_error_get(ctxt->sd->img);
  if (err)
  {
    ERR("error loading background %s: %d\n", file, err);
    return;
  }
  _bg_recalc(ctxt);
  _bg_conf_save(ctxt);
}

static void
_bg_glib_file_changed(GConfClient *client __UNUSED__, guint id __UNUSED__, GConfEntry *entry, gpointer data)
{
  struct bg_ctxt *ctxt = data;
  GConfValue *value;
  const char *file;

  value = gconf_entry_get_value(entry);
  file = gconf_value_get_string(value);
  gstuff_ecore_run_ptr_string(_bg_ecore_file_changed, ctxt, file);
}

static void
_bg_ecore_option_changed(void *data)
{
  struct bg_ctxt *ctxt = data;
  _bg_recalc(ctxt);
  _bg_conf_save(ctxt);
}

static int
_bg_parse_option(const char *option)
{
  if (!option)
    return BG_OPTION_STRETCHED;
  else if (strcmp(option, "none") == 0)
    return BG_OPTION_NONE;
  else if (strcmp(option, "wallpaper") == 0)
    return BG_OPTION_WALLPAPER;
  else if ((strcmp(option, "centred") == 0) ||
           (strcmp(option, "centered") == 0))
    return BG_OPTION_CENTERED;
  else if (strcmp(option, "scaled") == 0)
    return BG_OPTION_SCALED;
  else if (strcmp(option, "zoom") == 0)
    return BG_OPTION_ZOOM;
  else if (strcmp(option, "stretched") == 0)
    return BG_OPTION_STRETCHED;
  else
  {
    g_warning("unknown background option: '%s'", option);
    return BG_OPTION_STRETCHED;
  }
}

static void
_bg_glib_option_changed(GConfClient *client __UNUSED__, guint id __UNUSED__, GConfEntry *entry, gpointer data)
{
  struct bg_ctxt *ctxt = data;
  GConfValue *value;
  const char *option;
  unsigned int newval;

  value = gconf_entry_get_value(entry);
  option = gconf_value_get_string(value);
  newval = _bg_parse_option(option);

  if (newval == ctxt->option)
    return;
  ctxt->option = newval;
  gstuff_ecore_run_simple(_bg_ecore_option_changed, ctxt);
}

static void
_bg_ecore_color_changed(void *data)
{
  struct bg_ctxt *ctxt = data;
  _bg_color_update(ctxt);
  _bg_conf_save(ctxt);
}

static inline unsigned int
_bg_parse_hex(const char *hex, size_t size)
{
  unsigned int value = 0;
  for (; size > 0; size--, hex++)
  {
    char n, c = *hex;
    if ((c >= '0') && (c <= '9'))
      n = c - '0';
    else if ((c >= 'a') && (c <= 'f'))
      n = 10 + (c - 'a');
    else if ((c >= 'A') && (c <= 'F'))
      n = 10 + (c - 'A');
    else break;
    value = (value << 4) | n;
  }
  return value;
}

static unsigned int
_bg_parse_color(const char *color)
{
  unsigned int value;

  if (!color)
    goto default_color;

  if (color[0] != '#')
  {
    g_warning("unsupported color string: %s", color);
    goto default_color;
  }
  else
  {
    size_t len;
    unsigned char r, g, b;

    color++;

    len = strlen(color);
    switch (len)
    {
    case 3 * 4:
      r = (_bg_parse_hex(color,     4) >> 8) & 0xff;
      g = (_bg_parse_hex(color + 4, 4) >> 8) & 0xff;
      b = (_bg_parse_hex(color + 8, 4) >> 8) & 0xff;
      break;
    case 3 * 3:
      r = (_bg_parse_hex(color,     3) >> 4) & 0xff;
      g = (_bg_parse_hex(color + 3, 3) >> 4) & 0xff;
      b = (_bg_parse_hex(color + 6, 3) >> 4) & 0xff;
      break;
    case 3 * 2:
      r = _bg_parse_hex(color,     2);
      g = _bg_parse_hex(color + 2, 2);
      b = _bg_parse_hex(color + 4, 2);
      break;
    case 3 * 1:
      r = _bg_parse_hex(color,     1);
      g = _bg_parse_hex(color + 1, 1);
      b = _bg_parse_hex(color + 2, 1);

      r = (r << 4) | r;
      g = (g << 4) | g;
      b = (b << 4) | b;
      break;
    default:
      g_warning("unsupported color format: %s (%zd bytes)", color, len);
      goto default_color;
    }

    value = (r << 16) | (g << 8) | b;
    return value;
  }

default_color:
  return 0x000000;
}

static void
_bg_glib_color_changed(GConfClient *client __UNUSED__, guint id __UNUSED__, GConfEntry *entry, gpointer data)
{
  struct bg_ctxt *ctxt = data;
  GConfValue *value;
  const char *color;
  unsigned int newval;

  value = gconf_entry_get_value(entry);
  color = gconf_value_get_string(value);
  newval = _bg_parse_color(color);

  if (newval == ctxt->color)
    return;

  ctxt->color = newval;
  gstuff_ecore_run_simple(_bg_ecore_color_changed, ctxt);
}

static void
_bg_ecore_draw_changed(void *data)
{
  struct bg_ctxt *ctxt = data;
  _bg_recalc(ctxt);
  _bg_conf_save(ctxt);
}

static void
_bg_glib_draw_changed(GConfClient *client __UNUSED__, guint id __UNUSED__, GConfEntry *entry, gpointer data)
{
  struct bg_ctxt *ctxt = data;
  GConfValue *value;
  unsigned char newval;

  value = gconf_entry_get_value(entry);
  newval = gconf_value_get_bool(value);

  if (newval == ctxt->draw)
    return;
  ctxt->draw = newval;
  gstuff_ecore_run_simple(_bg_ecore_draw_changed, ctxt);
}

static void
_bg_glib_stop(void *data)
{
  struct bg_ctxt *ctxt = data;

  if (!ctxt->client)
    goto end;

  gconf_client_remove_dir(ctxt->client, BG_PATH, NULL);

  if (ctxt->notify.bg_file)
    gconf_client_notify_remove(ctxt->client, ctxt->notify.bg_file);
  if (ctxt->notify.bg_option)
    gconf_client_notify_remove(ctxt->client, ctxt->notify.bg_option);
  if (ctxt->notify.bg_color)
    gconf_client_notify_remove(ctxt->client, ctxt->notify.bg_color);
  if (ctxt->notify.bg_draw)
    gconf_client_notify_remove(ctxt->client, ctxt->notify.bg_draw);

  g_object_unref(ctxt->client);

end:
  free(ctxt);
}

static void
_bg_glib_start(void *data)
{
  struct bg_ctxt *ctxt = data;
  GError *err = NULL;
  char *str;
  unsigned int newuint;
  unsigned char newbool;

  ctxt->client = gconf_client_get_default();
  if (!ctxt->client)
  {
    g_critical("could not get default gconf client.");
    return;
  }

  gconf_client_add_dir(ctxt->client, BG_PATH, GCONF_CLIENT_PRELOAD_NONE, &err);
  if (err)
  {
    g_critical("could not add gconf dir to be monitored '%s': %s",
               BG_PATH, err->message);
    g_error_free(err);
    return;
  }

  ctxt->notify.bg_option = gconf_client_notify_add
    (ctxt->client, BG_OPTION, _bg_glib_option_changed, ctxt, NULL, &err);
  if (err)
  {
    g_critical("could not add gconf notify path '%s': %s",
               BG_OPTION, err->message);
    g_error_free(err);
    return;
  }

  ctxt->notify.bg_file = gconf_client_notify_add
    (ctxt->client, BG_FILE, _bg_glib_file_changed, ctxt, NULL, &err);
  if (err)
  {
    g_critical("could not add gconf notify path '%s': %s",
               BG_FILE, err->message);
    g_error_free(err);
    return;
  }

  ctxt->notify.bg_color = gconf_client_notify_add
    (ctxt->client, BG_COLOR, _bg_glib_color_changed, ctxt, NULL, &err);
  if (err)
  {
    g_critical("could not add gconf notify path '%s': %s",
               BG_COLOR, err->message);
    g_error_free(err);
    return;
  }

  ctxt->notify.bg_draw = gconf_client_notify_add
    (ctxt->client, BG_DRAW, _bg_glib_draw_changed, ctxt, NULL, &err);
  if (err)
  {
    g_critical("could not add gconf notify path '%s': %s",
               BG_DRAW, err->message);
    g_error_free(err);
    return;
  }

  str = gconf_client_get_string(ctxt->client, BG_FILE, &err);
  if (err)
  {
    g_critical("could not get gconf key as string '%s': %s",
               BG_FILE, err->message);
    g_error_free(err);
    return;
  }
  gstuff_ecore_run_ptr_string(_bg_ecore_file_changed, ctxt, str);
  g_free(str);

  str = gconf_client_get_string(ctxt->client, BG_OPTION, &err);
  if (err)
  {
    g_critical("could not get gconf key as string '%s': %s",
               BG_OPTION, err->message);
    g_error_free(err);
    return;
  }
  newuint = _bg_parse_option(str);
  if (newuint != ctxt->option)
  {
    ctxt->option = newuint;
    gstuff_ecore_run_simple(_bg_ecore_option_changed, ctxt);
  }
  g_free(str);

  str = gconf_client_get_string(ctxt->client, BG_COLOR, &err);
  if (err)
  {
    g_critical("couldnot get gconf key as string '%s': %s",
               BG_COLOR, err->message);
    g_error_free(err);
    return;
  }
  newuint = _bg_parse_color(str);
  if (newuint != ctxt->color)
  {
    ctxt->color = newuint;
    gstuff_ecore_run_simple(_bg_ecore_color_changed, ctxt);
  }
  g_free(str);

  newbool = gconf_client_get_bool(ctxt->client, BG_DRAW, &err);
  if (err)
  {
    g_critical("couldnot get gconf key as string '%s': %s",
               BG_DRAW, err->message);
    g_error_free(err);
    return;
  }
  if (newbool != ctxt->draw)
  {
    ctxt->draw = newbool;
    gstuff_ecore_run_simple(_bg_ecore_draw_changed, ctxt);
  }
}

static void
_bg_smart_del(Evas_Object *obj)
{
  struct bg_smart_data *sd = evas_object_smart_data_get(obj);
  struct bg_ctxt *ctxt;
  if (!sd)
  {
    ERR("could not get background smart data!\n");
    return;
  }

  ctxt = sd->ctxt;
  ctxt->obj = NULL;
  ctxt->sd = NULL;
  eina_stringshare_del(ctxt->file);
  sd->ctxt = NULL;

  _parent_sc.del(obj);

  conf_shutdown();

  gstuff_glib_run_simple(_bg_glib_stop, ctxt);
}

static void
_bg_smart_resize(Evas_Object *obj, int w, int h)
{
  struct bg_smart_data *sd = evas_object_smart_data_get(obj);
  if (!sd)
  {
    ERR("could not get background smart data!\n");
    return;
  }
  evas_object_resize(sd->rect, w, h);
  _bg_recalc_size(sd->ctxt, w, h);
}

static void
_bg_conf_load(struct bg_ctxt *ctxt)
{
  if (conf_bg_get(&ctxt->file, &ctxt->option, &ctxt->color, &ctxt->draw))
  {
    evas_object_image_file_set(ctxt->sd->img, ctxt->file, NULL);
    _bg_color_update(ctxt);
    _bg_recalc(ctxt);
  }
}

static void
_bg_smart_add(Evas_Object *obj)
{
  struct bg_smart_data *sd = calloc(1, sizeof(*sd));
  if (!sd)
  {
    ERR("could not allocate background smart data!\n");
    return;
  }

  sd->ctxt = calloc(1, sizeof(*sd->ctxt));
  if (!sd->ctxt)
  {
    ERR("could not allocate background context!\n");
    free(sd);
    return;
  }

  sd->ctxt->obj = obj;
  sd->ctxt->sd = sd;

  evas_object_smart_data_set(obj, sd);
  _parent_sc.add(obj);

  sd->rect = evas_object_rectangle_add(sd->base.evas);
  evas_object_color_set(sd->rect, 0, 0, 0, 255);
  evas_object_smart_member_add(sd->rect, obj);
  evas_object_show(sd->rect);

  sd->img = evas_object_image_add(sd->base.evas);
  evas_object_smart_member_add(sd->img, obj);

  if (conf_init())
    _bg_conf_load(sd->ctxt);

  gstuff_glib_run_simple(_bg_glib_start, sd->ctxt);
}

Evas_Object *
bg_add(Evas_Object *parent)
{
  static Evas_Smart *smart = NULL;
  if (!smart)
  {
    static Evas_Smart_Class sc = EVAS_SMART_CLASS_INIT_NAME_VERSION("BG");
    evas_object_smart_clipped_smart_set(&sc);
    _parent_sc = sc;
    sc.add = _bg_smart_add;
    sc.del = _bg_smart_del;
    sc.resize = _bg_smart_resize;
    smart = evas_smart_class_new(&sc);
  }

  return evas_object_smart_add(evas_object_evas_get(parent), smart);
}
