/* $Id$ */
/*
 * Copyright (c) 2007 Nick Schermer <nick@xfce.org>
 *
 * This program 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 program 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
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA  02111-1307  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 (0.50)
#define RELATIVE_DOTS  (3 * RELATIVE_SPACE)



/* prototypes */
static void      xfce_clock_lcd_finalize     (GObject           *object);
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_size_request (GtkWidget         *widget,
                                              GtkRequisition    *requisition);
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);



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

struct _XfceClockLcdClass
{
    GtkImageClass __parent__;
};

struct _XfceClockLcd
{
    GtkImage __parent__;

    /* whether we show seconds */
    guint    show_seconds : 1;

    /* whether it's a 24h clock */
    guint    show_military : 1;

    /* whether we display am/pm */
    guint    show_meridiem : 1;

    /* whether to flash the separators */
    guint    flash_separators : 1;
};



G_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->finalize = xfce_clock_lcd_finalize;
    gobject_class->set_property = xfce_clock_lcd_set_property;
    gobject_class->get_property = xfce_clock_lcd_get_property;

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

    /**
     * Whether we display seconds
     **/
    g_object_class_install_property (gobject_class,
                                     PROP_SHOW_SECONDS,
                                     g_param_spec_boolean ("show-seconds", "show-seconds", "show-seconds",
                                                           FALSE,
                                                           PANEL_PARAM_READWRITE));

    /**
     * Whether we show a 24h clock
     **/
    g_object_class_install_property (gobject_class,
                                     PROP_SHOW_MILITARY,
                                     g_param_spec_boolean ("show-military", "show-military", "show-military",
                                                           FALSE,
                                                           PANEL_PARAM_READWRITE));

    /**
     * Whether we show am or pm
     **/
    g_object_class_install_property (gobject_class,
                                     PROP_SHOW_MERIDIEM,
                                     g_param_spec_boolean ("show-meridiem", "show-meridiem", "show-meridiem",
                                                           TRUE,
                                                           PANEL_PARAM_READWRITE));

    /**
     * Whether to flash the time separators
     **/
    g_object_class_install_property (gobject_class,
                                     PROP_FLASH_SEPARATORS,
                                     g_param_spec_boolean ("flash-separators", "flash-separators", "flash-separators",
                                                           FALSE,
                                                           PANEL_PARAM_READWRITE));
}



static void
xfce_clock_lcd_init (XfceClockLcd *lcd)
{
    /* init */
    lcd->show_seconds = FALSE;
    lcd->show_meridiem = FALSE;
    lcd->show_military = TRUE;
    lcd->flash_separators = FALSE;
}



static void
xfce_clock_lcd_finalize (GObject *object)
{
    (*G_OBJECT_CLASS (xfce_clock_lcd_parent_class)->finalize) (object);
}



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

    switch (prop_id)
    {
        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;
    }
}



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

    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;

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



static void
xfce_clock_lcd_size_request (GtkWidget      *widget,
                             GtkRequisition *requisition)
{
    gint    width, height;
    gdouble ratio;

    /* get the current widget size */
    gtk_widget_get_size_request (widget, &width, &height);

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

    if (width == -1)
    {
        requisition->height = MAX (10, height - height % 10);
        requisition->width = requisition->height * ratio;
    }
    else
    {
        /* calc height */
        height = width / ratio;
        height = MAX (10, height - height % 10);

        requisition->height = height;
        requisition->width = height * ratio;
    }
}



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;

    g_return_val_if_fail (XFCE_CLOCK_IS_LCD (lcd), FALSE);

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

    /* size of a digit should be a fraction of 10 */
    size = widget->allocation.height - widget->allocation.height % 10;

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

    /* 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))
    {
        /* clip the drawing area */
        gdk_cairo_rectangle (cr, &event->area);
        cairo_clip (cr);
        
        /* get the local time */
        xfce_clock_util_get_localtime (&tm);

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

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

        /* queue a resize when the number of hour digits changed */
        if (G_UNLIKELY ((ticks == 10 || ticks == 0) && tm.tm_min == 0 && tm.tm_sec == 0))
          gtk_widget_queue_resize (widget);

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

        /* cleanup */
        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 */
    xfce_clock_util_get_localtime (&tm);

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

    ticks = tm.tm_hour;

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

    if (ticks >= 10)
        ratio += RELATIVE_DIGIT + RELATIVE_SPACE;

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



static gdouble
xfce_clock_lcd_draw_digit (cairo_t *cr,
                           guint    number,
                           gdouble  size,
                           gdouble  offset_x,
                           gdouble  offset_y)
{
    gint    i, j;
    gint    segment;
    gdouble x, y;
    gdouble rel_x, rel_y;

    /* ##1##
     * 6   2
     * ##7##
     * 5   3
     * ##4##
     */

    /* coordicates to draw for each segment */
    gdouble segments_x[][6] = { { 0.02, 0.48, 0.38, 0.12, -1.0, 0.00 },     /* 1x */
                                { 0.40, 0.505, 0.505, 0.40, -1.0, 0.00 },   /* 2x */
                                { 0.40, 0.505, 0.505, 0.40, -1.0, 0.00 },   /* 3x */
                                { 0.12, 0.38, 0.48, 0.02, -1.0, 0.00 },     /* 4x */
                                { 0.00, 0.105, 0.105, 0.00, -1.0, 0.00 },   /* 5x */
                                { 0.00, 0.105, 0.105, 0.00, -1.0, 0.00 },   /* 6x */
                                { 0.00, 0.10, 0.40, 0.50, 0.40, 0.10 } };   /* 7x */
    gdouble segments_y[][6] = { { 0.00, 0.00, 0.105, 0.105, -1.0, 0.00 },   /* 1y */
                                { 0.12, 0.02, 0.48, 0.43, -1.0, 0.00 },     /* 2y */
                                { 0.57, 0.52, 0.98, 0.88, -1.0, 0.00 },     /* 3y */
                                { 0.90, 0.90, 1.00, 1.00, -1.0, 0.00 },     /* 4y */
                                { 0.52, 0.57, 0.88, 0.98, -1.0, 0.00 },     /* 5y */
                                { 0.02, 0.12, 0.43, 0.48, -1.0, 0.00 },     /* 6y */
                                { 0.50, 0.445, 0.445, 0.50, 0.55, 0.55 } }; /* 7y */

    /* segment to draw for each number: 0, 1, ..., 9, A, P */
    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 } };
                             
    g_return_val_if_fail ((gint) number >= 0 || 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 = segments_x[segment][j];
            rel_y = segments_y[segment][j];

            /* 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;

            /* when 0.01 * size is larger then 1, round the numbers */
            if (size >= 10)
            {
                x = rint (x);
                y = rint (y);
            }

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

        /* close the line */
        cairo_close_path (cr);
    }

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

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



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



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

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

    return TRUE;
}
