/* -*- mode: C; c-file-style: "gnu"; c-basic-offset: 2; -*- */
/*
 * User interface for plug-in-maze.
 *
 * Implemented as a GIMP 0.99 Plugin by
 * Kevin Turner <acapnotic@users.sourceforge.net>
 * http://gimp-plug-ins.sourceforge.net/maze/
 */

/*
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <stdlib.h>

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include "maze.h"
#include "maze-dialog.h"

#include "libgimp/stdplugins-intl.h"


#define BORDER_TOLERANCE  1.00  /* maximum ratio of (max % divs) to width */
#define ENTRY_WIDTH       75

/* entscale stuff begin */
/* FIXME: Entry-Scale stuff is probably in libgimpui by now.
          Should use that instead.*/
/* Indeed! By using the gimp_scale_entry you could get along without the
   EntscaleIntData structure, since it has accessors to all objects  (Sven) */

#define ENTSCALE_INT_SCALE_WIDTH 125
#define ENTSCALE_INT_ENTRY_WIDTH 40

#define MORE  1
#define LESS -1

typedef void (* EntscaleIntCallback) (gint     value,
                                      gpointer data);

typedef struct
{
  GtkAdjustment       *adjustment;
  GtkWidget           *entry;
  gboolean             constraint;
  EntscaleIntCallback  callback;
  gpointer	       call_data;
} EntscaleIntData;
/* entscale stuff end */


/* one buffer fits all */
#define BUFSIZE 128
static gchar buffer[BUFSIZE];


/* Looking back, it would probably have been easier to completely
 * re-write the whole entry/scale thing to work with the divbox stuff.
 * It would undoubtably be cleaner code.  But since I already *had*
 * the entry/scale routines, I was under the (somewhat mistaken)
 * impression that it would be easier to work with them... */

/* Now, it goes like this:

   To update entscale (width) when div_entry changes:

    entscale_int_new has been slightly modified to return a pointer to
      its entry widget.

    This is fed to divbox_new as a "friend", which is in turn fed to
      the div_entry_callback routine.  And that's not really so bad,
      except...

    Oh, well, maybe it isn't so bad.  We can play with our friend's
      userdata to block his callbacks so we don't get feedback loops,
      that works nicely enough.

   To update div_entry when entscale (width) changes:

    The entry/scale setup graciously provides for callbacks.  However,
      this means we need to know about div_entry when we set up
      entry/scale, which we don't...  Chicken and egg problem.  So we
      set up a pointer to where div_entry will be, and pass this
      through to divbox_new when it happens.

    We need to block signal handlers for div_entry this time.  We
      happen to know that div_entry's callback data is our old
      "friend", so we pull our friend out from where we stuck him in
      the entry's userdata...  Hopefully that does it.
*/

static void        maze_message          (const gchar *message);
static void        div_button_callback   (GtkWidget   *button,
                                          GtkWidget   *entry);
static void        div_entry_callback    (GtkWidget   *entry,
                                          GtkWidget   *friend);
static void        height_width_callback (gint         width,
                                          GtkWidget  **div_entry);

static GtkWidget * divbox_new            (gint        *max,
                                          GtkWidget   *friend,
                                          GtkWidget  **div_entry);

/* entscale stuff begin */
static GtkWidget * entscale_int_new          (GtkWidget           *table,
                                              gint                 x,
                                              gint                 y,
                                              const gchar         *caption,
                                              gint                *intvar,
                                              gint                 min,
                                              gint                 max,
                                              gboolean             constraint,
                                              EntscaleIntCallback  callback,
                                              gpointer             data);

static void        entscale_int_scale_update (GtkAdjustment       *adjustment,
                                              gpointer             data);
static void        entscale_int_entry_update (GtkWidget           *widget,
                                              gpointer             data);


#define ISODD(X) ((X) & 1)
/* entscale stuff end */

static GtkWidget *msg_label;

gboolean
maze_dialog (void)
{
  GtkWidget    *dialog;
  GtkWidget    *vbox;
  GtkWidget    *vbox2;
  GtkWidget    *table;
  GtkWidget    *table2;
  GtkWidget    *tilecheck;
  GtkWidget    *width_entry;
  GtkWidget    *height_entry;
  GtkWidget    *hbox;
  GtkWidget    *frame;
  GtkSizeGroup *group;
  gboolean      run;
  gint          trow = 0;

  gimp_ui_init (PLUG_IN_BINARY, FALSE);

  dialog = gimp_dialog_new (_("Maze"), PLUG_IN_ROLE,
                            NULL, 0,
                            gimp_standard_help_func, PLUG_IN_PROC,

                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                            GTK_STOCK_OK,     GTK_RESPONSE_OK,

                            NULL);

  gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
                                           GTK_RESPONSE_OK,
                                           GTK_RESPONSE_CANCEL,
                                           -1);

  gimp_window_set_transient (GTK_WINDOW (dialog));

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 12);
  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
                      vbox, FALSE, FALSE, 0);

  /* The maze size frame */
  frame = gimp_frame_new (_("Maze Size"));
  gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
  gtk_widget_show (frame);

  table = gtk_table_new (6, 3, FALSE);

  gtk_table_set_col_spacings (GTK_TABLE (table), 6);
  gtk_table_set_row_spacings (GTK_TABLE (table), 6);
  gtk_container_add (GTK_CONTAINER (frame), table);
  gtk_widget_show (table);

  group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);

  /* entscale == Entry and Scale pair function found in pixelize.c */
  width_entry = entscale_int_new (table, 0, trow, _("Width (pixels):"),
                                  &mvals.width,
                                  1, sel_w/4, TRUE,
                                  (EntscaleIntCallback) height_width_callback,
                                  &width_entry);
  trow++;

  /* Number of Divisions entry */
  hbox = divbox_new (&sel_w, width_entry, &width_entry);
  g_snprintf (buffer, BUFSIZE, "%d", (sel_w / mvals.width));
  gtk_entry_set_text (GTK_ENTRY (width_entry), buffer);
  gimp_table_attach_aligned (GTK_TABLE (table), 0, trow,
			     _("Pieces:"), 0.0, 0.5,
			     hbox, 1, TRUE);
  gtk_table_set_row_spacing (GTK_TABLE (table), trow, 12);
  trow++;

  height_entry = entscale_int_new (table, 0, trow, _("Height (pixels):"),
                                   &mvals.height,
                                   1, sel_h/4, TRUE,
                                   (EntscaleIntCallback) height_width_callback,
                                   &height_entry);
  trow++;

  hbox = divbox_new (&sel_h, height_entry, &height_entry);
  g_snprintf (buffer, BUFSIZE, "%d", (sel_h / mvals.height));
  gtk_entry_set_text (GTK_ENTRY (height_entry), buffer);
  gimp_table_attach_aligned (GTK_TABLE (table), 0, trow,
			     _("Pieces:"), 0.0, 0.5,
			     hbox, 1, TRUE);
  gtk_table_set_row_spacing (GTK_TABLE (table), trow, 12);
  trow++;

  g_object_unref (group);

  /* The maze algorithm frame */
  frame = gimp_frame_new (_("Algorithm"));
  gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
  gtk_widget_show (frame);

  vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
  gtk_container_add (GTK_CONTAINER (frame), vbox2);
  gtk_widget_show (vbox2);

  /* Seed input box */
  table2 = gtk_table_new (3, 3, FALSE);
  gtk_table_set_col_spacings (GTK_TABLE (table2), 6);
  gtk_table_set_row_spacings (GTK_TABLE (table2), 6);
  gtk_box_pack_start (GTK_BOX (vbox2), table2, FALSE, FALSE, 0);
  gtk_widget_show (table2);

  hbox = gimp_random_seed_new (&mvals.seed, &mvals.random_seed);
  gimp_table_attach_aligned (GTK_TABLE (table2), 0, 0,
			     _("Seed:"), 0.0, 0.5,
			     hbox, 1, TRUE);

  /* Algorithm Choice */
  frame =
    gimp_int_radio_group_new (FALSE, NULL,
                              G_CALLBACK (gimp_radio_button_update),
                              &mvals.algorithm, mvals.algorithm,

                              _("Depth first"),      DEPTH_FIRST,     NULL,
                              _("Prim's algorithm"), PRIMS_ALGORITHM, NULL,

                              NULL);

  gtk_box_pack_start (GTK_BOX (vbox2), frame, FALSE, FALSE, 0);

  /* Tileable checkbox */
  tilecheck = gtk_check_button_new_with_label (_("Tileable"));
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (tilecheck), mvals.tile);
  g_signal_connect (tilecheck, "clicked",
                    G_CALLBACK (gimp_toggle_button_update),
                    &mvals.tile);

  gtk_box_pack_start (GTK_BOX (vbox2), tilecheck, FALSE, FALSE, 0);

  msg_label = gtk_label_new (NULL);
  gimp_label_set_attributes (GTK_LABEL (msg_label),
                             PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC,
                             -1);
  gtk_box_pack_start (GTK_BOX (vbox), msg_label, FALSE, FALSE, 0);

  gtk_widget_show_all (dialog);

  run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

  gtk_widget_destroy (dialog);

  return run;
}

static GtkWidget*
divbox_new (gint       *max,
	    GtkWidget  *friend,
	    GtkWidget **div_entry)
{
  GtkWidget *align;
  GtkWidget *hbox;
  GtkWidget *arrowl, *arrowr;
  GtkWidget *buttonl, *buttonr;
#if DIVBOX_LOOKS_LIKE_SPINBUTTON
  GtkWidget *buttonbox;
#endif

  align = gtk_alignment_new (0.5, 0.5, 0.0, 0.0);
  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_container_add (GTK_CONTAINER (align), hbox);

#if DIVBOX_LOOKS_LIKE_SPINBUTTON
  arrowl = gtk_arrow_new (GTK_ARROW_DOWN,  GTK_SHADOW_OUT);
  arrowr = gtk_arrow_new (GTK_ARROW_UP, GTK_SHADOW_OUT);
#else
  arrowl = gtk_arrow_new (GTK_ARROW_LEFT,  GTK_SHADOW_IN);
  arrowr = gtk_arrow_new (GTK_ARROW_RIGHT, GTK_SHADOW_IN);
#endif

  buttonl = gtk_button_new ();
  buttonr = gtk_button_new ();

  g_object_set_data (G_OBJECT (buttonl), "direction", GINT_TO_POINTER (LESS));
  g_object_set_data (G_OBJECT (buttonr), "direction", GINT_TO_POINTER (MORE));

  *div_entry = gtk_entry_new ();

  g_object_set_data (G_OBJECT (*div_entry), "max", max);
  g_object_set_data (G_OBJECT (*div_entry), "friend", friend);

  gtk_container_add (GTK_CONTAINER (buttonl), arrowl);
  gtk_container_add (GTK_CONTAINER (buttonr), arrowr);

  gtk_widget_set_size_request (*div_entry, ENTRY_WIDTH, -1);

#if DIVBOX_LOOKS_LIKE_SPINBUTTON
  buttonbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);

  gtk_box_pack_start (GTK_BOX (buttonbox), buttonr, FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (buttonbox), buttonl, FALSE, FALSE, 0);
  gtk_widget_show (buttonbox);

  gtk_box_pack_start (GTK_BOX (hbox), *div_entry, FALSE, FALSE, 2);
  gtk_box_pack_start (GTK_BOX (hbox), buttonbox, FALSE, FALSE, 0);
#else
  gtk_box_pack_start (GTK_BOX (hbox), buttonl, FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (hbox), *div_entry, FALSE, FALSE, 2);
  gtk_box_pack_start (GTK_BOX (hbox), buttonr, FALSE, FALSE, 0);
#endif

  gtk_widget_show_all (hbox);

  g_signal_connect (buttonl, "clicked",
                    G_CALLBACK (div_button_callback),
                    *div_entry);
  g_signal_connect (buttonr, "clicked",
                    G_CALLBACK (div_button_callback),
                    *div_entry);
  g_signal_connect (*div_entry, "changed",
                    G_CALLBACK (div_entry_callback),
                    friend);

  return align;
}

static void
div_button_callback (GtkWidget *button,
		     GtkWidget *entry)
{
  const gchar *text;
  gint         max, divs;
  gint         direction;

  direction = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
                                                  "direction"));
  max = *((gint*) g_object_get_data (G_OBJECT (entry), "max"));

  /* Tileable mazes shall have only an even number of divisions.
     Other mazes have odd. */

  /* Sanity check: */
  if (mvals.tile && ISODD(max))
    {
      maze_message (_("Selection size is not even.\n"
                      "Tileable maze won't work perfectly."));
      return;
    }

  text = gtk_entry_get_text (GTK_ENTRY (entry));

  divs = atoi (text);

  if (divs <= 3)
    {
      divs = mvals.tile ?
	max - (ISODD(max) ? 1 : 0) :
	max - (ISODD(max) ? 0 : 1);
    }
  else if (divs > max)
    {
      divs = mvals.tile ? 6 : 5;
    }

  /* Makes sure we're appropriately even or odd, adjusting in the
     proper direction. */

  divs += direction * (mvals.tile ? (ISODD(divs) ? 1 : 0) :
                                    (ISODD(divs) ? 0 : 1));

  if (mvals.tile)
    {
      if (direction > 0)
	{
	  do
	    {
	      divs += 2;
	      if (divs > max)
		divs = 4;
	    }
	  while (max % divs);
	}
      else
	{ /* direction < 0 */
	  do
	    {
	      divs -= 2;
	      if (divs < 4)
		divs = max - (max & 1);
	    }
	  while (max % divs);
	} /* endif direction < 0 */
    }
  else
    { /* If not tiling, having a non-zero remainder doesn't bother us much. */
      if (direction > 0)
	{
	  do
	    {
	      divs += 2;
	    }
	  while ((max % divs > max / divs * BORDER_TOLERANCE) && divs < max);
	}
      else
	{ /* direction < 0 */
	  do
	    {
	      divs -= 2;
	    }
	  while ((max % divs > max / divs * BORDER_TOLERANCE) && divs > 5);
	} /* endif direction < 0 */
    } /* endif not tiling */

  if (divs <= 3)
    {
      divs = mvals.tile ?
	max - (ISODD(max) ? 1 : 0) :
	max - (ISODD(max) ? 0 : 1);
    }
  else if (divs > max)
    {
      divs = mvals.tile ? 4 : 5;
    }

  g_snprintf (buffer, BUFSIZE, "%d", divs);
  gtk_entry_set_text (GTK_ENTRY (entry), buffer);

  return;
}

static void
div_entry_callback (GtkWidget *entry,
		    GtkWidget *friend)
{
  gint divs, width, max;
  EntscaleIntData *userdata;
  EntscaleIntCallback friend_callback;

  divs = atoi (gtk_entry_get_text (GTK_ENTRY (entry)));
  if (divs < 4) /* If this is under 4 (e.g. 0), something's weird.      */
    return;     /* But it'll probably be ok, so just return and ignore. */

  max = *((gint*) g_object_get_data (G_OBJECT (entry), "max"));

  /* I say "width" here, but it could be height.*/

  width = max / divs;
  g_snprintf (buffer, BUFSIZE, "%d", width);

  /* No tagbacks from our friend... */
  userdata = g_object_get_data (G_OBJECT (friend), "userdata");
  friend_callback = userdata->callback;
  userdata->callback = NULL;

  gtk_entry_set_text (GTK_ENTRY (friend), buffer);

  userdata->callback = friend_callback;
}

static void
height_width_callback (gint        width,
		       GtkWidget **div_entry)
{
  gint divs, max;
  gpointer data;

  max = *((gint*) g_object_get_data(G_OBJECT(*div_entry), "max"));
  divs = max / width;

  g_snprintf (buffer, BUFSIZE, "%d", divs );

  data = g_object_get_data (G_OBJECT(*div_entry), "friend");

  g_signal_handlers_block_by_func (*div_entry,
                                   entscale_int_entry_update,
                                   data);

  gtk_entry_set_text (GTK_ENTRY (*div_entry), buffer);

  g_signal_handlers_unblock_by_func (*div_entry,
                                     entscale_int_entry_update,
                                     data);
}

static void
maze_message (const gchar *message)
{
  gtk_label_set_text (GTK_LABEL (msg_label), message);
}

/* ==================================================================== */
/* As found in pixelize.c,
 * hacked to return a pointer to the entry widget. */

/*
  Entry and Scale pair 1.03

  TODO:
  - Do the proper thing when the user changes value in entry,
  so that callback should not be called when value is actually not changed.
  - Update delay
 */

/*
 *  entscale: create new entscale with label. (int)
 *  1 row and 2 cols of table are needed.
 *  Input:
 *    x, y:       starting row and col in table
 *    caption:    label string
 *    intvar:     pointer to variable
 *    min, max:   the boundary of scale
 *    constraint: (bool) true iff the value of *intvar should be constraint
 *                by min and max
 *    callback:	  called when the value is actually changed
 *    call_data:  data for callback func
 */
static GtkWidget*
entscale_int_new (GtkWidget           *table,
		  gint                 x,
		  gint                 y,
		  const gchar         *caption,
		  gint                *intvar,
		  gint                 min,
		  gint                 max,
		  gboolean             constraint,
		  EntscaleIntCallback  callback,
		  gpointer             call_data)
{
  EntscaleIntData *userdata;
  GtkWidget       *hbox;
  GtkWidget       *label;
  GtkWidget       *entry;
  GtkWidget       *scale;
  GtkAdjustment   *adjustment;
  gint	           constraint_val;

  userdata = g_new (EntscaleIntData, 1);

  label = gtk_label_new (caption);
  gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);

  /*
    If the first arg of gtk_adjustment_new() isn't between min and
    max, it is automatically corrected by gtk later with
    "value-changed" signal. I don't like this, since I want to leave
    *intvar untouched when `constraint' is false.
    The lines below might look oppositely, but this is OK.
   */
  userdata->constraint = constraint;
  if( constraint )
    constraint_val = *intvar;
  else
    constraint_val = ( *intvar < min ? min : *intvar > max ? max : *intvar );

  userdata->adjustment = adjustment =
    GTK_ADJUSTMENT (gtk_adjustment_new (constraint_val, min, max, 1.0, 1.0, 0.0));
  scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, adjustment);
  gtk_widget_set_size_request (scale, ENTSCALE_INT_SCALE_WIDTH, -1);
  gtk_scale_set_draw_value (GTK_SCALE (scale), FALSE);

  userdata->entry = entry = gtk_entry_new ();
  gtk_widget_set_size_request (entry, ENTSCALE_INT_ENTRY_WIDTH, -1);
  g_snprintf (buffer, BUFSIZE, "%d", *intvar);
  gtk_entry_set_text (GTK_ENTRY (entry), buffer);

  userdata->callback = callback;
  userdata->call_data = call_data;

  /* userdata is done */
  g_object_set_data (G_OBJECT (adjustment), "userdata", userdata);
  g_object_set_data (G_OBJECT (entry), "userdata", userdata);

  /* now ready for signals */
  g_signal_connect (entry, "changed",
                    G_CALLBACK (entscale_int_entry_update),
                    intvar);
  g_signal_connect (adjustment, "value-changed",
                    G_CALLBACK (entscale_int_scale_update),
                    intvar);
  g_signal_connect_swapped (entry, "destroy",
                            G_CALLBACK (g_free),
                            userdata);

  /* start packing */
  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
  gtk_box_pack_start (GTK_BOX (hbox), scale, TRUE, TRUE, 0);
  gtk_box_pack_start (GTK_BOX (hbox), entry, FALSE, TRUE, 0);

  gtk_table_attach (GTK_TABLE (table), label, x, x+1, y, y+1,
		    GTK_FILL, GTK_FILL, 0, 0);
  gtk_table_attach (GTK_TABLE (table), hbox, x+1, x+2, y, y+1,
		    GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);

  gtk_widget_show (label);
  gtk_widget_show (entry);
  gtk_widget_show (scale);
  gtk_widget_show (hbox);

  return entry;
}


static void
entscale_int_scale_update (GtkAdjustment *adjustment,
			   gpointer       data)
{
  EntscaleIntData *userdata;
  GtkEntry *entry;
  gint     *intvar = data;
  gint      new_val;

  userdata = g_object_get_data (G_OBJECT (adjustment), "userdata");

  new_val = (gint) gtk_adjustment_get_value (adjustment);

  *intvar = new_val;

  entry = GTK_ENTRY (userdata->entry);
  g_snprintf (buffer, BUFSIZE, "%d", (int) new_val);

  /* avoid infinite loop (scale, entry, scale, entry ...) */
  g_signal_handlers_block_by_func (entry,
                                   entscale_int_entry_update,
                                   data);

  gtk_entry_set_text (entry, buffer);

  g_signal_handlers_unblock_by_func (entry,
                                     entscale_int_entry_update,
                                     data);

  if (userdata->callback)
    (*userdata->callback) (*intvar, userdata->call_data);
}

static void
entscale_int_entry_update (GtkWidget *widget,
			   gpointer   data)
{
  EntscaleIntData *userdata;
  GtkAdjustment	  *adjustment;
  gint		   new_val, constraint_val;
  gint		  *intvar = data;

  userdata = g_object_get_data (G_OBJECT (widget), "userdata");
  adjustment = userdata->adjustment;

  new_val = atoi (gtk_entry_get_text (GTK_ENTRY (widget)));
  constraint_val = new_val;

  if (constraint_val < gtk_adjustment_get_lower (adjustment))
    constraint_val = gtk_adjustment_get_lower (adjustment);

  if (constraint_val > gtk_adjustment_get_upper (adjustment))
    constraint_val = gtk_adjustment_get_upper (adjustment);

  if ( userdata->constraint )
    *intvar = constraint_val;
  else
    *intvar = new_val;

  g_signal_handlers_block_by_func (adjustment,
                                   entscale_int_scale_update,
                                   data);

  gtk_adjustment_set_value (adjustment, constraint_val);

  g_signal_handlers_unblock_by_func (adjustment,
                                     entscale_int_scale_update,
                                     data);

  if (userdata->callback)
    (*userdata->callback) (*intvar, userdata->call_data);
}
