/* -*- mode:C; indent-tabs-mode:t; tab-width:8; c-basic-offset:8; -*- */

/*
 * Color lines for GNOME
 * Copyright © 1999 Free Software Foundation
 * Authors: Robert Szokovacs <szo@appaloosacorp.hu>
 *          Szabolcs Ban <shooby@gnome.hu>
 *
 * Copyright © 2007 Christian Persch
 *
 * This game 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, 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
 */

#include <config.h>

#include <stdlib.h>
#include <locale.h>
#include <math.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk/gdkkeysyms.h>

#include <libgames-support/games-file-list.h>
#include <libgames-support/games-gridframe.h>
#include <libgames-support/games-preimage.h>
#include <libgames-support/games-scores.h>
#include <libgames-support/games-scores-dialog.h>
#include <libgames-support/games-settings.h>
#include <libgames-support/games-stock.h>

#include "glines.h"

#define KEY_BACKGROUND_COLOR  "background-color"
#define KEY_BALL_THEME        "ball-theme"
#define KEY_MOVE_TIMEOUT      "move-timeout"
#define KEY_SIZE              "size"

#define KEY_SAVED_SCORE   "score"
#define KEY_SAVED_FIELD   "field"
#define KEY_SAVED_PREVIEW "preview"

#define MAXNPIECES 10
#define MAXFIELDSIZE 30
#define DEFAULT_GAME_SIZE MEDIUM
#define DEFAULT_BALL_THEME "balls.svg"

enum {
  UNSET = 0,
  SMALL = 1,
  MEDIUM,
  LARGE,
  MAX_SIZE,
};

/* Keep these in sync with the enum above. */
static const gint field_sizes[MAX_SIZE][4] = {
  {-1, -1, -1, -1}, /* This is a dummy entry. */
  {7, 7, 5, 3},     /* SMALL */
  {9, 9, 7, 3},     /* MEDIUM */
  {20, 15, 7, 7}    /* LARGE */
};

static const GamesScoresCategory scorecats[] = {
  { "Small",  NC_("board size", "Small")  },
  { "Medium", NC_("board size", "Medium") },
  { "Large",  NC_("board size", "Large")  }
};

static GamesScores *highscores;
static GSettings *settings;
static GtkBuilder *builder;
static GtkBuilder *builder_preferences;

static gint hfieldsize;
static gint vfieldsize;
static gint ncolors;
static gint npieces;
static gint game_size = UNSET;
static gboolean pref_dialog_done = FALSE;

static GRand *rgen;

static GtkWidget *draw_area;
static GtkWidget *app, *statusbar, *pref_dialog, *gridframe;
static GtkWidget *preview_widgets[MAXNPIECES];

static field_props field[MAXFIELDSIZE * MAXFIELDSIZE];

/* Pre-rendering image data prepared from file. */
static GamesPreimage *ball_preimage = NULL;
/* The tile images with balls rendered on them. */
static cairo_surface_t *ball_surface = NULL;
/* The balls rendered to a size appropriate for the preview. */
static cairo_surface_t *preview_surfaces[7] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL };

/* A cairo_surface_t of a blank tile. */
static cairo_surface_t *blank_surface = NULL;
static cairo_surface_t *blank_preview_surface = NULL;

static GamesFileList *theme_file_list = NULL;

static int active = -1;
static int target = -1;
static int inmove = 0;
static guint score = 0;
static int cursor_x = MAXFIELDSIZE / 2;
static int cursor_y = MAXFIELDSIZE / 2;
static gboolean show_cursor = FALSE;

static int boxsize;

static int preview_height = 0;
static int preview_width = 0;

static int move_timeout = 100;
static int animate_id = 0;
static int preview[MAXNPIECES];
static char *ball_filename;
static GtkWidget *scorelabel;
static scoretable sctab[] =
  { {5, 10}, {6, 12}, {7, 18}, {8, 28}, {9, 42}, {10, 82}, {11, 108}, {12,
                                                                       138},
  {13, 172}, {14, 210}, {0, 0} };

static struct {
  GdkRGBA color;
  gchar *name;
  gint set;
} backgnd = { { 0, 0, 0, 0}, NULL, 0 };

static gchar *warning_message = NULL;

static void
set_statusbar_message (gchar * message)
{
  guint context_id;
  context_id = gtk_statusbar_get_context_id (GTK_STATUSBAR (statusbar),
                                             "message");
  gtk_statusbar_pop (GTK_STATUSBAR (statusbar), context_id);
  gtk_statusbar_push (GTK_STATUSBAR (statusbar), context_id, message);
}

static void
show_image_warning (gchar * message)
{
  GtkWidget *dialog;
  GtkWidget *button;

  dialog = gtk_message_dialog_new (GTK_WINDOW (app),
                                   GTK_DIALOG_MODAL,
                                   GTK_MESSAGE_WARNING,
                                   GTK_BUTTONS_CLOSE,
                                   "%s",_("Could not load theme"));

  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                            "%s", message);

  button = gtk_dialog_add_button (GTK_DIALOG (dialog),
                                  _("Preferences"), GTK_RESPONSE_ACCEPT);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);

  g_signal_connect (button, "clicked", G_CALLBACK (game_props_callback),
                    NULL);

  gtk_dialog_run (GTK_DIALOG (dialog));
  gtk_widget_destroy (dialog);
}

static GamesPreimage *
load_image (gchar * fname)
{
  GamesPreimage *preimage;
  gchar *path;
  GError *error = NULL;

  path = g_build_filename (DATA_DIRECTORY, "themes", fname, NULL);
  if (!g_file_test (path, G_FILE_TEST_EXISTS)) {
    warning_message = g_strdup_printf (_("Unable to locate file:\n%s\n\n"
                                         "The default theme will be loaded instead."),
                                       fname);

    path = g_build_filename (DATA_DIRECTORY, "themes", "balls.svg", NULL);
    if (!g_file_test (path, G_FILE_TEST_EXISTS)) {
      g_free (warning_message);
      warning_message = g_strdup_printf (_("Unable to locate file:\n%s\n\n"
                                           "Please check that Five or More is installed correctly."),
                                         fname);
    }
  }

  preimage = games_preimage_new_from_file (path, &error);
  g_free (path);

  if (error) {
    warning_message = g_strdup (error->message);
    g_error_free (error);
  }

  return preimage;
}

static void
refresh_pixmaps (void)
{
  cairo_t *cr, *cr_blank;
  GdkPixbuf *ball_pixbuf = NULL;

  /* Since we get called both by configure and after loading an image.
   * it is possible the pixmaps aren't initialised. If they aren't
   * we don't do anything. */
  if (!ball_surface)
    return;

  if (ball_preimage) {
    ball_pixbuf = games_preimage_render (ball_preimage, 4 * boxsize,
                                         7 * boxsize);

    /* Handle rendering problems. */
    if (!ball_pixbuf) {
      g_object_unref (ball_preimage);
      ball_preimage = NULL;

      if (!warning_message) {
        warning_message = g_strdup ("The selected theme failed to render.\n\n"
                                    "Please check that Five or More is installed correctly.");
      }
    }
  }

  if (!ball_pixbuf) {
    ball_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8,
                                  4 * boxsize, 7 * boxsize);
    gdk_pixbuf_fill (ball_pixbuf, 0x00000000);
  }

  if (warning_message)
    show_image_warning (warning_message);
  g_free (warning_message);
  warning_message = NULL;

  cr = cairo_create (ball_surface);
  gdk_cairo_set_source_rgba (cr, &backgnd.color);

  cairo_rectangle (cr, 0, 0, boxsize * 4, boxsize * 7);
  cairo_fill (cr);

  gdk_cairo_set_source_pixbuf (cr, ball_pixbuf, 0, 0);
  cairo_mask (cr, cairo_get_source (cr));

  cr_blank = cairo_create (blank_surface);
  gdk_cairo_set_source_rgba (cr_blank, &backgnd.color);

  cairo_rectangle (cr_blank, 0, 0, boxsize, boxsize);
  cairo_fill (cr_blank);

  g_object_unref (ball_pixbuf);
  cairo_destroy (cr);
  cairo_destroy (cr_blank);
}

static void
refresh_preview_surfaces (void)
{
  guint i;
  GdkPixbuf *scaled = NULL;
  GtkWidget *widget = preview_widgets[0];
  GtkStyleContext *context;
  GdkRGBA bg;
  cairo_t *cr;
  GdkRectangle preview_rect;

  context = gtk_widget_get_style_context (widget);
  gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);

  /* Like the refresh_pixmaps() function, we may be called before
   * the window is ready. */
  if (preview_height == 0)
    return;

  preview_rect.x = 0;
  preview_rect.y = 0;
  preview_rect.width = preview_width;
  preview_rect.height = preview_height;

  /* We create pixmaps for each of the ball colours and then
   * set them as the background for each widget in the preview array.
   * This code assumes that each preview window is identical. */

  for (i = 0; i < 7; i++)
    if (preview_surfaces[i])
      cairo_surface_destroy (preview_surfaces[i]);

  if (ball_preimage)
    scaled = games_preimage_render (ball_preimage, 4 * preview_width,
                                    7 * preview_height);

  if (!scaled) {
    scaled = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8,
                             4 * preview_width, 7 * preview_height);
    gdk_pixbuf_fill (scaled, 0x00000000);
  }

  for (i = 0; i < 7; i++) {
    preview_surfaces[i] = gdk_window_create_similar_surface (gtk_widget_get_window (widget),
                                                             CAIRO_CONTENT_COLOR_ALPHA,
                                                             preview_width, preview_height);
    cr = cairo_create (preview_surfaces[i]);
    gdk_cairo_set_source_rgba (cr, &bg);
    gdk_cairo_rectangle (cr, &preview_rect);
    cairo_fill (cr);

    gdk_cairo_set_source_pixbuf (cr, scaled, 0, -1.0 * preview_height * i);
    cairo_mask (cr, cairo_get_source (cr));

    cairo_destroy (cr);
  }

  if (blank_preview_surface)
    cairo_surface_destroy (blank_preview_surface);

  blank_preview_surface = gdk_window_create_similar_surface (gtk_widget_get_window (widget),
                                                             CAIRO_CONTENT_COLOR_ALPHA,
                                                             preview_width, preview_height);
  cr = cairo_create (blank_preview_surface);
  gdk_cairo_set_source_rgba (cr, &bg);
  gdk_cairo_rectangle (cr, &preview_rect);
  cairo_fill (cr);

  cairo_destroy (cr);
  g_object_unref (scaled);
}

static void
draw_all_balls (GtkWidget * widget)
{
  gdk_window_invalidate_rect (gtk_widget_get_window (widget), NULL, FALSE);
}

static void
start_animation (void)
{
  int ms;

  ms = (inmove ? move_timeout : 100);
  if (animate_id == 0)
    animate_id = g_timeout_add (ms, animate, draw_area);
}

void
set_inmove (int i)
{
  if (inmove != i) {
    inmove = i;
    if (animate_id)
      g_source_remove (animate_id);
    animate_id = 0;
    start_animation ();
  }
}

static void
reset_game (void)
{
  int i;

  for (i = 0; i < hfieldsize * vfieldsize; i++) {
    field[i].color = 0;
    field[i].phase = 0;
    field[i].active = 0;
    field[i].pathsearch = -1;
  }
  score = 0;
  init_preview ();
  init_new_balls (npieces, -1);
}

static void
refresh_screen (void)
{
  draw_all_balls (draw_area);
  draw_preview ();
}

static void
start_game (void)
{
  char string[20];

  set_statusbar_message (_
                         ("Match five objects of the same type in a row to score!"));
  refresh_screen ();
  active = -1;
  target = -1;
  inmove = -1;
  g_snprintf (string, 19, "%d", score);
  gtk_label_set_text (GTK_LABEL (scorelabel), string);
  set_inmove (0);
}

static void
reset_pathsearch (void)
{
  int i;

  for (i = 0; i < hfieldsize * vfieldsize; i++)
    field[i].pathsearch = -1;
}

void
init_preview (void)
{
  int i;

  for (i = 0; i < npieces; i++) {
    preview[i] = g_rand_int_range (rgen, 1, ncolors + 1);
  }
}

void
draw_preview (void)
{
  guint i;
  cairo_pattern_t *pattern;

  for (i = 0; i < MAXNPIECES; i++) {

    if (i < npieces)
      pattern = cairo_pattern_create_for_surface (preview_surfaces[preview[i] - 1]);
    else
      pattern = cairo_pattern_create_for_surface (blank_preview_surface);

    gdk_window_set_background_pattern (gtk_widget_get_window (preview_widgets[i]),
                                       pattern);
    cairo_pattern_destroy (pattern);
    gtk_widget_queue_draw (preview_widgets[i]);
  }
}

void
game_new_callback (void)
{
  reset_game ();
  start_game ();
}

static void
show_scores (gint pos, gboolean new_game)
{
  static GtkWidget *dialog;

  if (dialog == NULL) {
    dialog = games_scores_dialog_new (GTK_WINDOW (app), highscores, _("GNOME Five or More"));
    games_scores_dialog_set_category_description (GAMES_SCORES_DIALOG
                                                  (dialog), _("_Board size:"));
  }

  if (pos > 0) {
    games_scores_dialog_set_hilight (GAMES_SCORES_DIALOG (dialog), pos);
  }

  gtk_window_present (GTK_WINDOW (dialog));
  gtk_dialog_run (GTK_DIALOG (dialog));
  gtk_widget_hide (dialog);
}

static void
game_over (void)
{
  int pos;

  set_statusbar_message (_("Game Over!"));
  pos = games_scores_add_plain_score (highscores, score);
  show_scores (pos, TRUE);
  return;
}

static int
spaces_left (void)
{
  int i, j;

  j = 0;

  for (i = 0; i < hfieldsize * vfieldsize; i++) {
    if (field[i].color == 0)
      j++;
  }

  return j;
}

static int
check_gameover (void)
{
  if (spaces_left () > 0)
    return 1;

  game_over ();

  return -1;
}

int
init_new_balls (int num, int prev)
{
  int i, j = -1;
  gfloat num_boxes = hfieldsize * vfieldsize;
  for (i = 0; i < num;) {
    j = g_rand_int_range (rgen, 0, num_boxes);
    if (field[j].color == 0) {
      field[j].color = (prev == -1) ?
        g_rand_int_range (rgen, 1, ncolors + 1) : preview[prev];
      i++;
    }
  }
  return j;
}

void
draw_box (GtkWidget * widget, int x, int y)
{
  gtk_widget_queue_draw_area (widget, x * boxsize, y * boxsize,
                              boxsize, boxsize);
}

static int
route (int num)
{
  int i;
  int flag = 0;

  if (field[target].pathsearch == num)
    return 1;
  for (i = 0; i < hfieldsize * vfieldsize; i++) {
    if (field[i].pathsearch == num) {
      flag = 1;
      if ((i / hfieldsize > 0) && (field[i - hfieldsize].pathsearch == -1)
          && (field[i - hfieldsize].color == 0))
        field[i - hfieldsize].pathsearch = num + 1;
      if ((i / hfieldsize < vfieldsize - 1)
          && (field[i + hfieldsize].pathsearch == -1)
          && (field[i + hfieldsize].color == 0))
        field[i + hfieldsize].pathsearch = num + 1;
      if ((i % hfieldsize > 0) && (field[i - 1].pathsearch == -1)
          && (field[i - 1].color == 0))
        field[i - 1].pathsearch = num + 1;
      if ((i % hfieldsize < hfieldsize - 1) && (field[i + 1].pathsearch == -1)
          && (field[i + 1].color == 0)) {
        field[i + 1].pathsearch = num + 1;
      }
    }
  }
  if (flag == 0)
    return 0;
  return 2;
}

static void
fix_route (int num, int pos)
{
  int i;

  for (i = 0; i < hfieldsize * vfieldsize; i++)
    if ((i != pos) && (field[i].pathsearch == num))
      field[i].pathsearch = -1;
  if (num < 2)
    return;
  if ((pos / hfieldsize > 0)
      && (field[pos - hfieldsize].pathsearch == num - 1))
    fix_route (num - 1, pos - hfieldsize);
  if ((pos % hfieldsize > 0) && (field[pos - 1].pathsearch == num - 1))
    fix_route (num - 1, pos - 1);
  if ((pos / hfieldsize < vfieldsize - 1)
      && (field[pos + hfieldsize].pathsearch == num - 1))
    fix_route (num - 1, pos + hfieldsize);
  if ((pos % hfieldsize < hfieldsize - 1)
      && (field[pos + 1].pathsearch == num - 1))
    fix_route (num - 1, pos + 1);
}

int
find_route (void)
{
  int i = 0;
  int j = 0;

  field[active].pathsearch = i;
  while ((j = route (i))) {
    if (j == 1) {
      fix_route (i, target);
      return 1;
    }
    i++;
  }
  return 0;
}

static void
deactivate (GtkWidget * widget, int x, int y)
{
  field[active].active = 0;
  field[active].phase = 0;
  draw_box (widget, x, y);
  active = -1;
}

static void
cell_clicked (GtkWidget * widget, int fx, int fy)
{
  int x, y;

  set_statusbar_message ("");
  if (field[fx + fy * hfieldsize].color == 0) {
    /* Clicked on an empty field */

    if ((active != -1) && (inmove != 1)) {
      /* We have an active ball, and it's not
       * already in move. Therefore we should begin
       * the moving sequence */

      target = fx + fy * hfieldsize;
      if (find_route ()) {
        /* We found a route to the new position */

        set_inmove (1);
      } else {
        /* Can't move there! */
        set_statusbar_message (_("You can't move there!"));
        reset_pathsearch ();
        target = -1;
      }
    }
  } else {
    /* Clicked on a ball */

    if ((fx + fy * hfieldsize) == active) {
      /* It's active, so let's deactivate it! */

      deactivate (widget, fx, fy);
    } else {
      /* It's not active, so let's activate it! */

      if (active != -1) {
        /* There is an other active ball, we should deactivate it first! */

        x = active % hfieldsize;
        y = active / hfieldsize;
        deactivate (widget, x, y);
      }

      active = fx + fy * hfieldsize;
      field[active].active = 1;
      start_animation();
    }
  }

}

static gint
button_press_event (GtkWidget * widget, GdkEvent * event)
{
  int x, y;
  int fx, fy;

  if (inmove)
    return TRUE;

  /* Ignore the 2BUTTON and 3BUTTON events. */
  if (event->type != GDK_BUTTON_PRESS)
    return TRUE;

  if (show_cursor) {
    show_cursor = FALSE;
    draw_box (draw_area, cursor_x, cursor_y);
  }

  /* FIXMEchpe: why not use event->[xy] here? */
  gtk_widget_get_pointer (widget, &x, &y);
  fx = x / boxsize;
  fy = y / boxsize;

  /* If we click on the outer border pixels, the previous calculation
   * will be wrong and we must correct. */
  fx = CLAMP (fx, 0, hfieldsize - 1);
  fy = CLAMP (fy, 0, vfieldsize - 1);

  cursor_x = fx;
  cursor_y = fy;

  cell_clicked (widget, fx, fy);

  return TRUE;
}

static void
move_cursor (int dx, int dy)
{
  int old_x = cursor_x;
  int old_y = cursor_y;

  if (!show_cursor) {
    show_cursor = TRUE;
    draw_box (draw_area, cursor_x, cursor_y);
  }

  cursor_x = cursor_x + dx;
  if (cursor_x < 0)
    cursor_x = 0;
  if (cursor_x >= hfieldsize)
    cursor_x = hfieldsize - 1;
  cursor_y = cursor_y + dy;
  if (cursor_y < 0)
    cursor_y = 0;
  if (cursor_y >= vfieldsize)
    cursor_y = vfieldsize - 1;

  if (cursor_x == old_x && cursor_y == old_y)
    return;

  draw_box (draw_area, old_x, old_y);
  draw_box (draw_area, cursor_x, cursor_y);
}

static gboolean
key_press_event (GtkWidget * widget, GdkEventKey * event, void *d)
{
  guint key;

  key = event->keyval;

  switch (key) {
  case GDK_KEY_Left:
  case GDK_KEY_KP_Left:
    move_cursor (-1, 0);
    break;
  case GDK_KEY_Right:
  case GDK_KEY_KP_Right:
    move_cursor (1, 0);
    break;
  case GDK_KEY_Up:
  case GDK_KEY_KP_Up:
    move_cursor (0, -1);
    break;
  case GDK_KEY_Down:
  case GDK_KEY_KP_Down:
    move_cursor (0, 1);
    break;
  case GDK_KEY_Home:
  case GDK_KEY_KP_Home:
    move_cursor (-999, 0);
    break;
  case GDK_KEY_End:
  case GDK_KEY_KP_End:
    move_cursor (999, 0);
    break;
  case GDK_KEY_Page_Up:
  case GDK_KEY_KP_Page_Up:
    move_cursor (0, -999);
    break;
  case GDK_KEY_Page_Down:
  case GDK_KEY_KP_Page_Down:
    move_cursor (0, 999);
    break;

  case GDK_KEY_space:
  case GDK_KEY_Return:
  case GDK_KEY_KP_Enter:
    if (show_cursor)
      cell_clicked (widget, cursor_x, cursor_y);
    break;
  }

  return TRUE;
}

static void
draw_grid (cairo_t *cr)
{
  GdkRGBA color;
  guint w, h;
  guint i;

  w = gtk_widget_get_allocated_width(draw_area);
  h = gtk_widget_get_allocated_height(draw_area);

  gdk_rgba_parse (&color, "#525F6C");
  gdk_cairo_set_source_rgba (cr, &color);
  cairo_set_line_width (cr, 1.0);

  for (i = boxsize; i < w; i = i + boxsize)
  {
    cairo_move_to (cr, i + 0.5, 0 + 0.5);
    cairo_line_to (cr, i + 0.5, h + 0.5);
  }

  for (i = boxsize; i < h; i = i + boxsize)
  {
    cairo_move_to (cr, 0 + 0.5, i + 0.5);
    cairo_line_to (cr, w + 0.5, i + 0.5);
  }

  cairo_rectangle (cr, 0.5, 0.5, w - 0.5, h - 0.5);
  cairo_stroke (cr);
}

static gboolean
field_draw_callback (GtkWidget * widget, cairo_t *cr)
{
  guint i, j, idx;
  GdkRGBA cursorColor;

  for (i = 0; i < vfieldsize; i++) {
    for (j = 0; j < hfieldsize; j++) {
      int phase, color;

      idx = j + i * hfieldsize;

      cairo_rectangle (cr, j * boxsize, i * boxsize, boxsize, boxsize);

      if (field[idx].color != 0) {
        phase = field[idx].phase;
        color = field[idx].color - 1;

        phase = ABS (ABS (3 - phase) - 3);

        cairo_set_source_surface (cr, ball_surface,
                                 (1.0 * j - phase) * boxsize,
                                 (1.0 * i - color) * boxsize);
      } else {
        cairo_set_source_surface (cr, blank_surface, 1.0 * j * boxsize, 1.0 * i * boxsize);
      }

      cairo_fill (cr);
    }
  }

  /* Cursor */
  if (show_cursor) {
    if (((backgnd.color.red + backgnd.color.green + backgnd.color.blue) / 3) > 0.5)
      gdk_rgba_parse (&cursorColor, "#000000");
    else
      gdk_rgba_parse (&cursorColor, "#FFFFFF");

    gdk_cairo_set_source_rgba (cr, &cursorColor);
    cairo_set_line_width (cr, 1.0);
    cairo_rectangle (cr,
                     cursor_x * boxsize + 1.5, cursor_y * boxsize + 1.5,
                     boxsize - 2.5, boxsize - 2.5);
    cairo_stroke (cr);
  }

  draw_grid (cr);

  return FALSE;
}

static int
addscore (int num)
{
  gchar string[20];
  int i = 0;
  int retval;

  while ((sctab[i].num != num) && (sctab[i].num != 0))
    i++;
  if (sctab[i].num == 0) {
    num-=5;
    retval = sctab[num%5].score + 72 * (num/5) + (num-5)*24 ;
  } else
    retval = sctab[i].score;

  score += retval;
  g_snprintf (string, 19, "%d", score);
  gtk_label_set_text (GTK_LABEL (scorelabel), string);

  return retval;
}


static void
tag_list (int *list, int len)
{
  int i;

  for (i = 0; i < len; i++)
    field[list[i]].tag = 1;
}

static int
find_lines (int num)
{
  int count = 1;
  int subcount = 0;
  int x = num % hfieldsize;
  int y = num / hfieldsize;
  int list[hfieldsize];

  /* Horizontal */

  x++;
  while ((x <= hfieldsize - 1)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    x++;
  }
  x = num % hfieldsize - 1;
  while ((x >= 0) && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    x--;
  }
  if (subcount >= 4) {
    field[num].tag = 1;
    tag_list (list, subcount);
    count += subcount;
  }
  subcount = 0;

  /* Vertical */

  x = num % hfieldsize;
  y++;
  while ((y <= vfieldsize - 1)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y++;
  }
  y = num / hfieldsize - 1;
  while ((y >= 0) && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y--;
  }
  if (subcount >= 4) {
    field[num].tag = 1;
    tag_list (list, subcount);
    count += subcount;
  }
  subcount = 0;

  /* Diagonal ++ */

  x = num % hfieldsize + 1;
  y = num / hfieldsize + 1;
  while ((y <= vfieldsize - 1) && (x <= hfieldsize - 1)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y++;
    x++;
  }
  x = num % hfieldsize - 1;
  y = num / hfieldsize - 1;
  while ((y >= 0) && (x >= 0)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y--;
    x--;
  }
  if (subcount >= 4) {
    field[num].tag = 1;
    tag_list (list, subcount);
    count += subcount;
  }
  subcount = 0;

  /* Diagonal +- */

  x = num % hfieldsize + 1;
  y = num / hfieldsize - 1;
  while ((y >= 0) && (x <= hfieldsize - 1)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y--;
    x++;
  }
  x = num % hfieldsize - 1;
  y = num / hfieldsize + 1;
  while ((y <= vfieldsize - 1) && (x >= 0)
         && (field[x + y * hfieldsize].color == field[num].color)) {
    list[subcount] = x + y * hfieldsize;
    subcount++;
    y++;
    x--;
  }
  if (subcount >= 4) {
    field[num].tag = 1;
    tag_list (list, subcount);
    count += subcount;
  }

  return count;
}

static void
clear_tags (void)
{
  int i;

  for (i = 0; i < hfieldsize * vfieldsize; i++)
    field[i].tag = 0;
}

static int
kill_tags (GtkWidget * widget)
{
  int i, j;

  j = 0;
  for (i = 0; i < hfieldsize * vfieldsize; i++) {
    if (field[i].tag == 1) {
      j++;
      field[i].color = 0;
      field[i].phase = 0;
      field[i].active = 0;
      draw_box (widget, i % hfieldsize, i / hfieldsize);
    }
  }

  return j;
}

static gboolean
check_goal (GtkWidget * w, int target)
{
  gboolean lines_cleared;
  int count;

  clear_tags ();

  count = find_lines (target);
  lines_cleared = count >= 5;

  if (lines_cleared) {
    kill_tags (w);
    addscore (count);
  }
  reset_pathsearch ();

  return lines_cleared;
}

gint
animate (gpointer gp)
{
  GtkWidget *widget = GTK_WIDGET (gp);
  int x, y;
  int newactive = 0;
    
  x = active % hfieldsize;
  y = active / hfieldsize;

  if (active == -1) {
    animate_id = 0;
    return FALSE;
  }

  if (inmove != 0) {
    if ((x > 0)
        && (field[active - 1].pathsearch == field[active].pathsearch + 1))
      newactive = active - 1;
    else if ((x < hfieldsize - 1)
             && (field[active + 1].pathsearch ==
                 field[active].pathsearch + 1))
      newactive = active + 1;
    else if ((y > 0)
             && (field[active - hfieldsize].pathsearch ==
                 field[active].pathsearch + 1))
      newactive = active - hfieldsize;
    else if ((y < vfieldsize - 1)
             && (field[active + hfieldsize].pathsearch ==
                 field[active].pathsearch + 1))
      newactive = active + hfieldsize;
    else {
      set_inmove (0);
    }
    draw_box (widget, x, y);
    x = newactive % hfieldsize;
    y = newactive / hfieldsize;
    field[newactive].phase = field[active].phase;
    field[newactive].color = field[active].color;
    field[active].phase = 0;
    field[active].color = 0;
    field[active].active = 0;
    if (newactive == target) {
      target = -1;
      set_inmove (0);
      active = -1;
      field[newactive].phase = 0;
      field[newactive].active = 0;
      draw_box (widget, x, y);
      reset_pathsearch ();
      if (!check_goal (widget, newactive)) {
        gboolean fullline;
        int balls[npieces];
        int spaces;

        clear_tags ();
        fullline = FALSE;
        spaces = spaces_left ();
        if (spaces > npieces)
          spaces = npieces;
        for (x = 0; x < spaces; x++) {
          int tmp = init_new_balls (1, x);
          draw_box (widget, tmp % hfieldsize, tmp / hfieldsize);
          balls[x] = tmp;
          fullline = (find_lines (balls[x]) >= 5) || fullline;
        }
        if (fullline)
          addscore (kill_tags (widget));
        if (check_gameover () == -1)
          return FALSE;
        init_preview ();
        draw_preview ();
      }
      return TRUE;
    }
    field[newactive].active = 1;
    active = newactive;
  }

  field[active].phase++;
  if (field[active].phase >= 4)
    field[active].phase = 0;
  draw_box (widget, x, y);

  return TRUE;
}

void
game_top_ten_callback (GtkAction * action, gpointer data)
{
  show_scores (0, FALSE);
}

void
game_about_callback (GtkAction * action, gpointer * data)
{
  const gchar *authors[] = { "Robert Szokovacs <szo@appaloosacorp.hu>",
    "Szabolcs B\xc3\xa1n <shooby@gnome.hu>",
    NULL
  };

  const gchar *documenters[] = { "Tiffany Antopolski",
                                 "Lanka Rathnayaka",
    NULL
  };
  gchar *license = games_get_license (_("Five or More"));

  gtk_show_about_dialog (GTK_WINDOW (app),
                         "program-name", _("Five or More"),
                         "version", VERSION,
                         "comments", _("GNOME port of the once-popular Color Lines game.\n\nFive or More is a part of GNOME Games."),
                         "copyright",
                         "Copyright \xc2\xa9 1997-2008 Free Software Foundation, Inc.",
                         "license", license,
                         "authors", authors,
                         "documenters", documenters,
                         "translator-credits", _("translator-credits"),
                         "logo-icon-name", "glines",
                         "website", "http://www.gnome.org/projects/gnome-games/",
                         "website-label", _("GNOME Games web site"),
                         "wrap-license", TRUE, NULL);
  g_free (license);
}

static void
set_backgnd_color (const gchar * str)
{
  if (!str)
    str = g_strdup ("#000000");

  if (str != backgnd.name) {
    g_free (backgnd.name);
    backgnd.name = g_strdup (str);
  }

  if (!gdk_rgba_parse (&backgnd.color, backgnd.name)) {
    gdk_rgba_parse (&backgnd.color, "#000000");
  }
}

static void
set_sizes (gint size)
{
  hfieldsize = field_sizes[size][0];
  vfieldsize = field_sizes[size][1];
  ncolors = field_sizes[size][2];
  npieces = field_sizes[size][3];
  game_size = size;
  games_scores_set_category (highscores, scorecats[size - 1].key);

  g_settings_set_int (settings, KEY_SIZE, size);

  if (gridframe)
    games_grid_frame_set (GAMES_GRID_FRAME (gridframe),
                          hfieldsize, vfieldsize);
}

static void
load_theme (void)
{
  if (ball_preimage)
    g_object_unref (ball_preimage);
  ball_preimage = load_image (ball_filename);

  refresh_pixmaps ();
  refresh_preview_surfaces ();
}

static void
conf_value_changed_cb (GSettings *settings, gchar *key)
{
  if (strcmp (key, KEY_BACKGROUND_COLOR) == 0) {
    gchar *color;

    color = g_settings_get_string (settings, KEY_BACKGROUND_COLOR);
    if (color != NULL) {
      set_backgnd_color (color);
      g_free (color);
    }
  } else if (strcmp (key, KEY_BALL_THEME) == 0) {
    gchar *theme_tmp = NULL;

    theme_tmp = g_settings_get_string (settings, KEY_BALL_THEME);
    if (theme_tmp) {
      if (strcmp (theme_tmp, ball_filename) != 0) {
        g_free (ball_filename);
        ball_filename = theme_tmp;
        load_theme ();
        refresh_screen ();
      } else
        g_free (theme_tmp);
    }
    /* FIXME apply in the prefs dialog GUI */
  } else if (strcmp (key, KEY_MOVE_TIMEOUT) == 0) {
    gint timeout_tmp;

    timeout_tmp = g_settings_get_int (settings, KEY_MOVE_TIMEOUT);
    timeout_tmp = CLAMP (timeout_tmp, 1, 1000);
    if (timeout_tmp != move_timeout)
      move_timeout = timeout_tmp;

  } else if (strcmp (key, KEY_SIZE) == 0) {
    gint size_tmp;
    size_tmp = g_settings_get_int (settings, KEY_SIZE);

    if (size_tmp != game_size) {
      set_sizes (size_tmp);
      reset_game ();
      start_game ();
    }
  }
}

static void
bg_color_callback (GtkWidget * widget, gpointer data)
{
  GdkRGBA c;
  char str[64];

  gtk_color_button_get_rgba (GTK_COLOR_BUTTON (widget), &c);

  g_snprintf (str, sizeof (str), "#%04x%04x%04x", (int) (c.red * 65535 + 0.5), (int) (c.green * 65535 + 0.5), (int) (c.blue * 65535 + 0.5));

  g_settings_set_string (settings, KEY_BACKGROUND_COLOR, str);

  load_theme ();
  refresh_screen ();
}

static void
size_callback (GtkWidget * widget, gpointer data)
{
  if (pref_dialog_done)
    g_settings_set_int (settings, KEY_SIZE, GPOINTER_TO_INT (data));
}

static void
set_selection (GtkWidget * widget, char *data)
{
  const gchar *entry;

  entry = games_file_list_get_nth (theme_file_list,
                                   gtk_combo_box_get_active (GTK_COMBO_BOX
                                                             (widget)));
  g_settings_set_string (settings, KEY_BALL_THEME, entry);
}

static GtkWidget *
fill_menu (void)
{
  gchar *pixmap_dir;

  if (theme_file_list)
    g_object_unref (theme_file_list);

  pixmap_dir = g_build_filename (DATA_DIRECTORY, "themes", NULL);
  theme_file_list = games_file_list_new_images (pixmap_dir, NULL);
  g_free (pixmap_dir);
  games_file_list_transform_basename (theme_file_list);

  return games_file_list_create_widget (theme_file_list, ball_filename,
                                        GAMES_FILE_LIST_REMOVE_EXTENSION |
                                        GAMES_FILE_LIST_REPLACE_UNDERSCORES);
}

static void
set_fast_moves_callback (GtkWidget * widget, gpointer * data)
{
  gboolean is_on = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget));
  gint timeout = is_on ? 10 : 100;
  g_settings_set_int (settings, KEY_MOVE_TIMEOUT, timeout);
}

void
pref_dialog_response (GtkDialog * dialog, gint response, gpointer data)
{
  gtk_widget_hide (GTK_WIDGET (dialog));
}

void
game_props_callback (void)
{
  gchar *ui_path;
  GError *error = NULL;
  GtkWidget *omenu;
  GtkWidget *grid;
  GtkWidget *color_button;
  GtkWidget *size_radio;
  GtkWidget *fast_moves_checkbutton;

  if (!pref_dialog) {
    ui_path = g_build_filename (DATA_DIRECTORY, "glines-preferences.ui", NULL);
    builder_preferences = gtk_builder_new ();
    gtk_builder_add_from_file (builder_preferences, ui_path, &error);
    g_free (ui_path);

    if (error) {
      g_critical ("Unable to load the user interface file: %s", error->message);
      g_error_free (error);
      g_assert_not_reached ();
    }

    pref_dialog = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "preferences_dialog"));
    g_signal_connect (pref_dialog, "response",
                      G_CALLBACK (pref_dialog_response), NULL);
    g_signal_connect (pref_dialog, "delete-event",
                      G_CALLBACK (gtk_widget_hide), NULL);

    grid = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "grid1"));
    omenu = fill_menu ();
    gtk_widget_show_all (GTK_WIDGET (omenu));
    gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET (omenu), 1, 0, 1, 1);
    g_signal_connect (omenu, "changed",
                      G_CALLBACK (set_selection), NULL);

    color_button = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "colorbutton1"));
    gtk_color_button_set_rgba (GTK_COLOR_BUTTON (color_button), &backgnd.color);
    g_signal_connect (color_button, "color-set",
                      G_CALLBACK (bg_color_callback), NULL);

    size_radio = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "radiobutton_small"));
    g_signal_connect (size_radio, "clicked",
                      G_CALLBACK (size_callback), GINT_TO_POINTER (1));

    size_radio = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "radiobutton_medium"));
    g_signal_connect (size_radio, "clicked",
                      G_CALLBACK (size_callback), GINT_TO_POINTER (2));

    size_radio = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "radiobutton_large"));
    g_signal_connect (size_radio, "clicked",
                      G_CALLBACK (size_callback), GINT_TO_POINTER (3));

    fast_moves_checkbutton = GTK_WIDGET (gtk_builder_get_object (builder_preferences, "checkbutton_fast_moves"));
    if (move_timeout == 10) {
      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (fast_moves_checkbutton), TRUE);
    }

    g_signal_connect (fast_moves_checkbutton, "clicked",
                      G_CALLBACK (set_fast_moves_callback), NULL);

    pref_dialog_done = TRUE;
  }

  gtk_window_present (GTK_WINDOW (pref_dialog));
}

int
game_quit_callback (GtkAction * action, gpointer data)
{
  if (animate_id)
    g_source_remove (animate_id);
  gtk_main_quit ();
  return FALSE;
}

void
game_help_callback (GtkAction * action, gpointer data)
{
  GError *error = NULL;

  gtk_show_uri (gtk_widget_get_screen (GTK_WIDGET (app)), "help:glines", gtk_get_current_event_time (), &error);
  if (error)
    g_warning ("Failed to show help: %s", error->message);
  g_clear_error (&error);
}

static int
preview_configure_cb (GtkWidget * widget, GdkEventConfigure * event)
{
  preview_width = event->width;
  preview_height = event->height;

  refresh_preview_surfaces ();

  draw_preview ();

  return TRUE;
}

static int
configure_event_callback (GtkWidget * widget, GdkEventConfigure * event)
{
  if (ball_surface)
    cairo_surface_destroy(ball_surface);

  if (blank_surface)
    cairo_surface_destroy (blank_surface);

  boxsize = (event->width - 1) / hfieldsize;
  ball_surface = gdk_window_create_similar_surface (gtk_widget_get_window (draw_area),
                                                    CAIRO_CONTENT_COLOR_ALPHA,
                                                    boxsize * 4, boxsize * 7);
  blank_surface = gdk_window_create_similar_surface (gtk_widget_get_window (draw_area),
                                                     CAIRO_CONTENT_COLOR_ALPHA,
                                                     boxsize, boxsize);
  refresh_pixmaps ();

  refresh_screen ();

  return TRUE;
}

static void
load_properties (void)
{
  gchar *buf;

  ball_filename = g_settings_get_string (settings, KEY_BALL_THEME);

  move_timeout = g_settings_get_int (settings, KEY_MOVE_TIMEOUT);
  if (move_timeout <= 0)
    move_timeout = 100;

  buf = g_settings_get_string (settings, KEY_BACKGROUND_COLOR); /* FIXMEchpe? */
  set_backgnd_color (buf);
  g_free (buf);

  load_theme ();
}

static void
init_config (void)
{
  g_signal_connect (settings, "changed",
                    G_CALLBACK (conf_value_changed_cb), NULL);

  game_size = g_settings_get_int (settings, KEY_SIZE);
  if (game_size == UNSET)
    game_size = DEFAULT_GAME_SIZE;

  game_size = CLAMP (game_size, SMALL, MAX_SIZE - 1);
  set_sizes (game_size);
}

int
main (int argc, char *argv[])
{
  GOptionContext *context;
  gchar *ui_path;
  GtkWidget *vbox, *hbox;
  GtkWidget *preview_hbox;
  guint i;
  gboolean retval;
  GError *error = NULL;

  setlocale (LC_ALL, "");
  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
  textdomain (GETTEXT_PACKAGE);

  games_scores_startup ();

  rgen = g_rand_new ();

  context = g_option_context_new (NULL);
  g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
  g_option_context_add_group (context, gtk_get_option_group (TRUE));

  retval = g_option_context_parse (context, &argc, &argv, &error);
  g_option_context_free (context);
  if (!retval) {
    g_print ("%s", error->message);
    g_error_free (error);
    exit (1);
  }

  g_set_application_name (_("Five or More"));

  settings = g_settings_new ("org.gnome.glines");

  highscores = games_scores_new ("glines",
                                 scorecats, G_N_ELEMENTS (scorecats),
                                 "board size", NULL,
                                 0 /* default category */,
                                 GAMES_SCORES_STYLE_PLAIN_DESCENDING);

  init_config ();

  games_stock_init ();

  gtk_window_set_default_icon_name ("glines");

  ui_path = g_build_filename (DATA_DIRECTORY, "glines.ui", NULL);
  builder = gtk_builder_new ();
  gtk_builder_add_from_file (builder, ui_path, &error);
  g_free (ui_path);

  if (error) {
    g_critical ("Unable to load the user interface file: %s", error->message);
    g_error_free (error);
    g_assert_not_reached ();
  }

  app = GTK_WIDGET (gtk_builder_get_object (builder, "glines_window"));
  games_settings_bind_window_state ("/org/gnome/glines/", GTK_WINDOW (app));

  hbox = GTK_WIDGET (gtk_builder_get_object (builder, "top_box"));

  gridframe = games_grid_frame_new (MAXNPIECES, 1);
  games_grid_frame_set_alignment (GAMES_GRID_FRAME (gridframe), 0, 0.5);
  gtk_box_pack_start (GTK_BOX (hbox), gridframe, TRUE, TRUE, 0);

  preview_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_container_add (GTK_CONTAINER (gridframe), preview_hbox);

  for (i = 0; i < MAXNPIECES; i++) {
    preview_widgets[i] = gtk_drawing_area_new ();
    gtk_box_pack_start (GTK_BOX (preview_hbox),
                        preview_widgets[i], TRUE, TRUE, 0);
    /* So we have all the windows at configure time since
     * we only hook into one of the configure events. */
    /* Yes, this is an evil hack. */
    gtk_widget_realize (preview_widgets[i]);
  }
  g_signal_connect (preview_widgets[0], "configure-event",
                    G_CALLBACK (preview_configure_cb), NULL);

  scorelabel = GTK_WIDGET (gtk_builder_get_object (builder, "scorelabel"));

  vbox = GTK_WIDGET (gtk_builder_get_object (builder, "vbox"));
  draw_area = gtk_drawing_area_new ();
  g_signal_connect (draw_area, "button-press-event",
                    G_CALLBACK (button_press_event), NULL);
  g_signal_connect (draw_area, "key-press-event",
                    G_CALLBACK (key_press_event), NULL);
  g_signal_connect (draw_area, "configure-event",
                    G_CALLBACK (configure_event_callback), NULL);
  g_signal_connect (draw_area, "draw",
                    G_CALLBACK (field_draw_callback), NULL);
  gridframe = games_grid_frame_new (hfieldsize, vfieldsize);
  games_grid_frame_set_padding (GAMES_GRID_FRAME (gridframe), 1, 1);
  gtk_container_add (GTK_CONTAINER (gridframe), draw_area);
  gtk_box_pack_start (GTK_BOX (vbox), gridframe, TRUE, TRUE, 0);

  statusbar = GTK_WIDGET (gtk_builder_get_object (builder, "statusbar"));

  gtk_widget_set_events (draw_area,
                         gtk_widget_get_events (draw_area) |
                         GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK);

  gtk_widget_set_can_focus (draw_area, TRUE);
  gtk_widget_grab_focus (draw_area);

  load_properties ();

  gtk_builder_connect_signals (builder, NULL);

  gtk_widget_show_all (app);

  start_game ();

  /* Enter the event loop */
  gtk_main ();

  if (ball_preimage)
    g_object_unref (ball_preimage);

  g_settings_sync();

  return 0;
}
