/*
 * Copyright (C) 2007-2010 Nick Schermer <nick@xfce.org>
 *
 * This library 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 2 of the License, or (at your option)
 * any later version.
 *
 * This library 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 Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_MATH_H
#include <math.h>
#endif

#include <gtk/gtk.h>
#include <cairo/cairo.h>

#include "clock.h"
#include "clock-lcd.h"

#define RELATIVE_SPACE (0.10)
#define RELATIVE_DIGIT (5 * RELATIVE_SPACE)
#define RELATIVE_DOTS  (3 * RELATIVE_SPACE)



static void      xfce_clock_lcd_set_property (GObject           *object,
                                              guint              prop_id,
                                              const GValue      *value,
                                              GParamSpec        *pspec);
static void      xfce_clock_lcd_get_property (GObject           *object,
                                              guint              prop_id,
                                              GValue            *value,
                                              GParamSpec        *pspec);
static void      xfce_clock_lcd_finalize     (GObject           *object);
static gboolean  xfce_clock_lcd_expose_event (GtkWidget         *widget,
                                              GdkEventExpose    *event);
static gdouble   xfce_clock_lcd_get_ratio    (XfceClockLcd      *lcd);
static gdouble   xfce_clock_lcd_draw_dots    (cairo_t           *cr,
                                              gdouble            size,
                                              gdouble            offset_x,
                                              gdouble            offset_y);
static gdouble   xfce_clock_lcd_draw_digit   (cairo_t           *cr,
                                              guint              number,
                                              gdouble            size,
                                              gdouble            offset_x,
                                              gdouble            offset_y);
static gboolean  xfce_clock_lcd_update       (gpointer           user_data);



enum
{
  PROP_0,
  PROP_SHOW_SECONDS,
  PROP_SHOW_MILITARY,
  PROP_SHOW_MERIDIEM,
  PROP_FLASH_SEPARATORS,
  PROP_SIZE_RATIO,
  PROP_ORIENTATION
};

struct _XfceClockLcdClass
{
  GtkImageClass __parent__;
};

struct _XfceClockLcd
{
  GtkImage __parent__;

  ClockPluginTimeout *timeout;

  guint               show_seconds : 1;
  guint               show_military : 1; /* 24-hour clock */
  guint               show_meridiem : 1; /* am/pm */
  guint               flash_separators : 1;
};

typedef struct
{
  gdouble x;
  gdouble y;
}
LcdPoint;



XFCE_PANEL_DEFINE_TYPE (XfceClockLcd, xfce_clock_lcd, GTK_TYPE_IMAGE)



static void
xfce_clock_lcd_class_init (XfceClockLcdClass *klass)
{
  GObjectClass   *gobject_class;
  GtkWidgetClass *gtkwidget_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->set_property = xfce_clock_lcd_set_property;
  gobject_class->get_property = xfce_clock_lcd_get_property;
  gobject_class->finalize = xfce_clock_lcd_finalize;

  gtkwidget_class = GTK_WIDGET_CLASS (klass);
  gtkwidget_class->expose_event = xfce_clock_lcd_expose_event;

  g_object_class_install_property (gobject_class,
                                   PROP_SIZE_RATIO,
                                   g_param_spec_double ("size-ratio", NULL, NULL,
                                                        -1, G_MAXDOUBLE, -1.0,
                                                        G_PARAM_READABLE
                                                        | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
                                   PROP_ORIENTATION,
                                   g_param_spec_enum ("orientation", NULL, NULL,
                                                      GTK_TYPE_ORIENTATION,
                                                      GTK_ORIENTATION_HORIZONTAL,
                                                      G_PARAM_WRITABLE
                                                      | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
                                   PROP_SHOW_SECONDS,
                                   g_param_spec_boolean ("show-seconds", NULL, NULL,
                                                         FALSE,
                                                         G_PARAM_READWRITE
                                                         | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
                                   PROP_SHOW_MILITARY,
                                   g_param_spec_boolean ("show-military", NULL, NULL,
                                                         FALSE,
                                                         G_PARAM_READWRITE
                                                         | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
                                   PROP_SHOW_MERIDIEM,
                                   g_param_spec_boolean ("show-meridiem", NULL, NULL,
                                                         TRUE,
                                                         G_PARAM_READWRITE
                                                         | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
                                   PROP_FLASH_SEPARATORS,
                                   g_param_spec_boolean ("flash-separators", NULL, NULL,
                                                         FALSE,
                                                         G_PARAM_READWRITE
                                                         | G_PARAM_STATIC_STRINGS));
}



static void
xfce_clock_lcd_init (XfceClockLcd *lcd)
{
  lcd->show_seconds = FALSE;
  lcd->show_meridiem = FALSE;
  lcd->show_military = TRUE;
  lcd->flash_separators = FALSE;
  lcd->timeout = clock_plugin_timeout_new (CLOCK_INTERVAL_MINUTE,
                                           xfce_clock_lcd_update,
                                           lcd);
}



static void
xfce_clock_lcd_set_property (GObject      *object,
                             guint         prop_id,
                             const GValue *value,
                             GParamSpec   *pspec)
{
  XfceClockLcd *lcd = XFCE_CLOCK_LCD (object);
  gboolean      show_seconds;

  switch (prop_id)
    {
    case PROP_ORIENTATION:
      break;

    case PROP_SHOW_SECONDS:
      lcd->show_seconds = g_value_get_boolean (value);
      break;

    case PROP_SHOW_MILITARY:
      lcd->show_military = g_value_get_boolean (value);
      break;

    case PROP_SHOW_MERIDIEM:
      lcd->show_meridiem = g_value_get_boolean (value);
      break;

    case PROP_FLASH_SEPARATORS:
      lcd->flash_separators = g_value_get_boolean (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }

  g_object_notify (object, "size-ratio");

  /* reschedule the timeout and resize */
  show_seconds = lcd->show_seconds || lcd->flash_separators;
  clock_plugin_timeout_set_interval (lcd->timeout,
      show_seconds ? CLOCK_INTERVAL_SECOND : CLOCK_INTERVAL_MINUTE);
  gtk_widget_queue_resize (GTK_WIDGET (lcd));
}



static void
xfce_clock_lcd_get_property (GObject    *object,
                             guint       prop_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  XfceClockLcd *lcd = XFCE_CLOCK_LCD (object);
  gdouble       ratio;

  switch (prop_id)
    {
    case PROP_SHOW_SECONDS:
      g_value_set_boolean (value, lcd->show_seconds);
      break;

    case PROP_SHOW_MILITARY:
      g_value_set_boolean (value, lcd->show_military);
      break;

    case PROP_SHOW_MERIDIEM:
      g_value_set_boolean (value, lcd->show_meridiem);
      break;

    case PROP_FLASH_SEPARATORS:
      g_value_set_boolean (value, lcd->flash_separators);
      break;

    case PROP_SIZE_RATIO:
      ratio = xfce_clock_lcd_get_ratio (lcd);
      g_value_set_double (value, ratio);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}



static void
xfce_clock_lcd_finalize (GObject *object)
{
  /* stop the timeout */
  clock_plugin_timeout_free (XFCE_CLOCK_LCD (object)->timeout);

  (*G_OBJECT_CLASS (xfce_clock_lcd_parent_class)->finalize) (object);
}



static gboolean
xfce_clock_lcd_expose_event (GtkWidget      *widget,
                             GdkEventExpose *event)
{
  XfceClockLcd *lcd = XFCE_CLOCK_LCD (widget);
  cairo_t      *cr;
  gdouble       offset_x, offset_y;
  gint          ticks, i;
  gdouble       size;
  gdouble       ratio;
  struct tm     tm;

  panel_return_val_if_fail (XFCE_CLOCK_IS_LCD (lcd), FALSE);

  /* get the width:height ratio */
  ratio = xfce_clock_lcd_get_ratio (XFCE_CLOCK_LCD (widget));

  /* make sure we also fit on small vertical panels */
  size = MIN ((gdouble) widget->allocation.width / ratio, widget->allocation.height);

  /* begin offsets */
  offset_x = rint ((widget->allocation.width - (size * ratio)) / 2.00);
  offset_y = rint ((widget->allocation.height - size) / 2.00);

  /* only allow positive values from the base point */
  offset_x = widget->allocation.x + MAX (0.00, offset_x);
  offset_y = widget->allocation.y + MAX (0.00, offset_y);

  /* get the cairo context */
  cr = gdk_cairo_create (widget->window);

  if (G_LIKELY (cr != NULL))
    {
      gdk_cairo_set_source_color (cr, &widget->style->fg[GTK_WIDGET_STATE (widget)]);
      gdk_cairo_rectangle (cr, &event->area);
      cairo_clip (cr);
      cairo_push_group (cr);

      /* width of the clear line */
      cairo_set_line_width (cr, MAX (size * 0.05, 1.5));

      /* get the local time */
      clock_plugin_get_localtime (&tm);

      /* draw the hours */
      ticks = tm.tm_hour;

      /* convert 24h clock to 12h clock */
      if (!lcd->show_military && ticks > 12)
        ticks -= 12;

      if (ticks == 1 || (ticks >= 10 && ticks < 20))
        offset_x -= size * (RELATIVE_SPACE * 4);

      /* queue a resize when the number of hour digits changed,
       * because we might miss the exact second (due to slightly delayed
       * timeout) we queue a resize the first 3 seconds or anything in
       * the first minute */
      if ((ticks == 10 || ticks == 0) && tm.tm_min == 0
          && (!lcd->show_seconds || tm.tm_sec < 3))
        g_object_notify (G_OBJECT (lcd), "size-ratio");

      if (ticks >= 10)
        {
          /* draw the number and increase the offset */
          offset_x = xfce_clock_lcd_draw_digit (cr, ticks >= 20 ? 2 : 1, size, offset_x, offset_y);
        }

      /* draw the other number of the hour and increase the offset */
      offset_x = xfce_clock_lcd_draw_digit (cr, ticks % 10, size, offset_x, offset_y);

      for (i = 0; i < 2; i++)
        {
          /* get the time */
          if (i == 0)
            {
              /* get the minutes */
              ticks = tm.tm_min;
            }
          else
            {
              /* leave when we don't want seconds */
              if (!lcd->show_seconds)
                break;

              /* get the seconds */
              ticks = tm.tm_sec;
            }

          /* draw the dots */
          if (lcd->flash_separators && (tm.tm_sec % 2) == 1)
            offset_x += size * RELATIVE_SPACE * 2;
          else
            offset_x = xfce_clock_lcd_draw_dots (cr, size, offset_x, offset_y);

          /* draw the first digit */
          offset_x = xfce_clock_lcd_draw_digit (cr, (ticks - (ticks % 10)) / 10, size, offset_x, offset_y);

          /* draw the second digit */
          offset_x = xfce_clock_lcd_draw_digit (cr, ticks % 10, size, offset_x, offset_y);
        }

      if (lcd->show_meridiem)
        {
          /* am or pm? */
          ticks = tm.tm_hour >= 12 ? 11 : 10;

          /* draw the digit */
          offset_x = xfce_clock_lcd_draw_digit (cr, ticks, size, offset_x, offset_y);
        }

      /* drop the pushed group */
      cairo_pop_group_to_source (cr);
      cairo_paint (cr);
      cairo_destroy (cr);
    }

  return FALSE;
}



static gdouble
xfce_clock_lcd_get_ratio (XfceClockLcd *lcd)
{
  gdouble   ratio;
  gint      ticks;
  struct tm tm;

  /* get the local time */
  clock_plugin_get_localtime (&tm);

  /* 8:8(space)8 */
  ratio = (3 * RELATIVE_DIGIT) + RELATIVE_DOTS + RELATIVE_SPACE;

  ticks = tm.tm_hour;

  if (!lcd->show_military && ticks > 12)
    ticks -= 12;

  if (ticks == 1)
    ratio -= RELATIVE_SPACE * 4; /* only show 1 */
  else if (ticks >= 10 && ticks < 20)
    ratio += RELATIVE_SPACE * 2; /* 1 + space */
  else if (ticks >= 20)
    ratio += RELATIVE_DIGIT + RELATIVE_SPACE;

  /* (space):88 */
  if (lcd->show_seconds)
    ratio += (2 * RELATIVE_DIGIT) + RELATIVE_SPACE + RELATIVE_DOTS;

  if (lcd->show_meridiem)
    ratio += RELATIVE_DIGIT + RELATIVE_SPACE;

  return ratio;
}



static gdouble
xfce_clock_lcd_draw_dots (cairo_t *cr,
                          gdouble  size,
                          gdouble  offset_x,
                          gdouble  offset_y)
{
  gint i;

  if (size >= 10)
    {
      /* draw the dots (with rounding) */
      for (i = 1; i < 3; i++)
        cairo_rectangle (cr, rint (offset_x), rint (offset_y + size * RELATIVE_DOTS * i),
                         rint (size * RELATIVE_SPACE), rint (size * RELATIVE_SPACE));
    }
  else
    {
      /* draw the dots */
      for (i = 1; i < 3; i++)
        cairo_rectangle (cr, offset_x, offset_y + size * RELATIVE_DOTS * i,
                         size * RELATIVE_SPACE, size * RELATIVE_SPACE);
    }

  /* fill the dots */
  cairo_fill (cr);

  return (offset_x + size * RELATIVE_SPACE * 2);
}



/*
 * number:
 *
 * ##1##
 * 6   2
 * ##7##
 * 5   3
 * ##4##
 */
static gdouble
xfce_clock_lcd_draw_digit (cairo_t *cr,
                           guint    number,
                           gdouble  size,
                           gdouble  offset_x,
                           gdouble  offset_y)
{
  guint   i, j;
  gint    segment;
  gdouble x, y;
  gdouble rel_x, rel_y;

  /* coordicates to draw for each segment */
  const LcdPoint segment_points[][6] = {
    /* 1 */ { { 0, 0 }, { 0.5, 0 }, { 0.4, 0.1 }, { 0.1, 0.1 }, { -1, }, { -1, } },
    /* 2 */ { { 0.4, 0.1 }, { 0.5, 0.0 }, { 0.5, 0.5 }, { 0.4, 0.45 }, { -1, },  { -1, } },
    /* 3 */ { { 0.4, 0.55 }, { 0.5, 0.5 }, { 0.5, 1 }, { 0.4, 0.9 }, { -1, },  { -1, } },
    /* 4 */ { { 0.1, 0.9 }, { 0.4, 0.9 }, { 0.5, 1 }, { 0.0, 1 }, { -1, },  { -1, } },
    /* 5 */ { { 0.0, 0.5 }, { 0.1, 0.55 }, { 0.1, 0.90 }, { 0.0, 1}, { -1, },  { -1, } },
    /* 6 */ { { 0.0, 0.0 }, { 0.1, 0.1 }, { 0.1, 0.45 }, { 0.0, 0.5 }, { -1, },  { -1, } },
    /* 7 */ { { 0.0, 0.5 }, { 0.1, 0.45 }, { 0.4, 0.45 }, { 0.5, 0.5 }, { 0.4, 0.55 }, { 0.1, 0.55 } },
  };

  /* space line, mirrored to other side */
  const LcdPoint clear_points[] = {
    { 0, 0 }, { 0.25, 0.25 }, { 0.25, 0.375 }, { 0, 0.5 },
    { 0.25, 0.625 }, { 0.25, 0.75 }, { 0, 1 }
  };

  /* segment to draw for each number: 0, 1, ..., 9, A, P */
  const gint numbers[][8] = {
    { 0, 1, 2, 3, 4, 5, -1 },
    { 1, 2, -1 },
    { 0, 1, 6, 4, 3, -1 },
    { 0, 1, 6, 2, 3, -1 },
    { 5, 6, 1, 2, -1 },
    { 0, 5, 6, 2, 3, -1 },
    { 0, 5, 4, 3, 2, 6, -1 },
    { 0, 1, 2, -1 },
    { 0, 1, 2, 3, 4, 5, 6, -1 },
    { 3, 2, 1, 0, 5, 6, -1 },
    { 4, 5, 0, 1, 2, 6, -1 },
    { 4, 5, 0, 1, 6, -1 }
  };

  panel_return_val_if_fail (number <= 11, offset_x);

  for (i = 0; i < 9; i++)
    {
      /* get the segment we're going to draw */
      segment = numbers[number][i];

      /* leave when there are no more segments left */
      if (segment == -1)
        break;

      /* walk through the coordinate points */
      for (j = 0; j < 6; j++)
        {
          /* get the relative sizes */
          rel_x = segment_points[segment][j].x;
          rel_y = segment_points[segment][j].y;

          /* leave when there are no valid coordinates */
          if (rel_x == -1.00 || rel_y == -1.00)
            break;

          /* get x and y coordinates for this point */
          x = rel_x * size + offset_x;
          y = rel_y * size + offset_y;

          if (G_UNLIKELY (j == 0))
            cairo_move_to (cr, x, y);
          else
            cairo_line_to (cr, x, y);
        }

      cairo_close_path (cr);
    }

  cairo_fill (cr);

  /* clear the space between the segments to get the lcd look */
  cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
  for (j = 0; j < 2; j++)
    {
      for (i = 0; i < G_N_ELEMENTS (clear_points); i++)
        {
          x = (j == 0 ? clear_points[i].x : 0.5 - clear_points[i].x) * size + offset_x;
          y = clear_points[i].y * size + offset_y;

          if (G_UNLIKELY (i == 0))
            cairo_move_to (cr, x, y);
          else
            cairo_line_to (cr, x, y);
        }

      cairo_stroke (cr);
    }
  cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);

  return (offset_x + size * (RELATIVE_DIGIT + RELATIVE_SPACE));
}



static gboolean
xfce_clock_lcd_update (gpointer user_data)
{
  GtkWidget *widget = GTK_WIDGET (user_data);

  panel_return_val_if_fail (XFCE_CLOCK_IS_LCD (user_data), FALSE);

  /* update if the widget if visible */
  if (G_LIKELY (GTK_WIDGET_VISIBLE (widget)))
    gtk_widget_queue_draw (widget);

  return TRUE;
}



GtkWidget *
xfce_clock_lcd_new (void)
{
  return g_object_new (XFCE_CLOCK_TYPE_LCD, NULL);
}
