/*
 * gnoclScale.c --
 *
 * This file implements the scale widget
 *
 * Copyright (c) 2001 - 2002 P.G. Baum
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 */

/*
   History:
        02: removed "invoke"
   2002-01: new command "invoke"
   2001-10: Begin of developement
 */

#include "gnocl.h"
#include <string.h>
#include <assert.h>

typedef struct
{
   GNOCL_STD_OPTIONS_VARS;

   Gnocl_StringBool command;
   Gnocl_StringBool variable;
   Gnocl_ObjBool    orientation;
   Gnocl_ObjBool    updatePolicy;
   Gnocl_DoubleBool lower;
   Gnocl_DoubleBool upper;
   Gnocl_DoubleBool value;
   Gnocl_DoubleBool stepInc;
   Gnocl_DoubleBool pageInc;
   Gnocl_IntBool    digits;
   Gnocl_BoolBool   drawValue;
   Gnocl_ObjBool    valuePos;
} ScaleOptions;

static const GnoclWidgetOptions widgetOptions[] =
{
   GNOCL_STD_OPTIONS_NAMES( ScaleOptions ),
   { "-command", GNOCL_STRING, GNOCL_OFFSET( ScaleOptions, command ) },
   { "-variable", GNOCL_STRING, GNOCL_OFFSET( ScaleOptions, variable ) },
   { "-orientation", GNOCL_OBJ, GNOCL_OFFSET( ScaleOptions, orientation ) },
   { "-updatePolicy", GNOCL_OBJ, GNOCL_OFFSET( ScaleOptions, updatePolicy ) },
   { "-lower", GNOCL_DOUBLE, GNOCL_OFFSET( ScaleOptions, lower ) },
   { "-upper", GNOCL_DOUBLE, GNOCL_OFFSET( ScaleOptions, upper ) },
   { "-value", GNOCL_DOUBLE, GNOCL_OFFSET( ScaleOptions, value ) },
   { "-stepInc", GNOCL_DOUBLE, GNOCL_OFFSET( ScaleOptions, stepInc ) },
   { "-pageInc", GNOCL_DOUBLE, GNOCL_OFFSET( ScaleOptions, pageInc ) },
   { "-digits", GNOCL_INT, GNOCL_OFFSET( ScaleOptions, digits ) },
   { "-drawValue", GNOCL_BOOL, GNOCL_OFFSET( ScaleOptions, drawValue ) },
   { "-valuePos", GNOCL_OBJ, GNOCL_OFFSET( ScaleOptions, valuePos ) },
   { NULL, 0, 0 }
};

/*
   This struct contains only the necessary states to remember.
   Option values can be inquired via gtk
*/
typedef enum {  EMPTY = 0, 
                DONT_COMMAND = 1 << 0, 
                DONT_SET     = 1 << 1, 
                DONT_GET     = 1 << 2,
                ALL          = 0xFF } InSignalType;

typedef struct
{
   char         *name;
   Tcl_Interp   *interp;
   GtkScale     *scale;
   char         *command;
   char         *variable;
   InSignalType inSignal;
} ScaleParams;

static void freeOpts( ScaleOptions *p )
{
   GNOCL_STD_OPTIONS_FREE( p );
   GNOCL_FREE_MEM_OPT( p->command );
   GNOCL_FREE_MEM_OPT( p->variable );
   GNOCL_FREE_TCL_OPT( p->orientation );
   GNOCL_FREE_TCL_OPT( p->updatePolicy );
   GNOCL_FREE_TCL_OPT( p->valuePos );
}

static int doCommand( ScaleParams *para, Tcl_Obj *val )
{
   if( para->command )
   {
      static Gnocl_PercSubst ps[] = {
         { 'w', GNOCL_STRING },  /* widget */
         { 'v', GNOCL_OBJ },     /* value */
         { 0 }
      };

      ps[0].val.str = para->name;
      ps[1].val.obj = val;

      return gnoclPercentSubstAndEval( para->interp, ps, para->command );
   }
   return TCL_OK;
}


static int setValue( ScaleParams *para, Tcl_Obj *val )
{
   double d;
   if( Tcl_GetDoubleFromObj( para->interp, val, &d ) != TCL_OK )
      return TCL_ERROR;

   para->inSignal |= DONT_SET|DONT_COMMAND;
   gtk_adjustment_set_value(
         gtk_range_get_adjustment( GTK_RANGE( para->scale ) ), d );
   para->inSignal &= ~(DONT_SET|DONT_COMMAND);

   return TCL_OK;
}


static char *traceFunc( ClientData data,
      Tcl_Interp *interp, char *name1, char *name2, int flags)
{
   ScaleParams *para = (ScaleParams *)data;

   if( ( para->inSignal & DONT_GET ) == 0 )
   {
      Tcl_Obj *val = Tcl_GetVar2Ex( interp, name1, name2, flags );
      if( val != NULL && setValue( para, val ) == TCL_OK )
      {
         doCommand( para, val );
      }

   }

   return NULL;
}

static int changedFuncRet( ScaleParams *para )
{
   GtkAdjustment *adjust = gtk_range_get_adjustment( 
         GTK_RANGE( para->scale ) );
   Tcl_Obj *valObj = Tcl_NewDoubleObj( adjust->value );
   int     ret = TCL_OK;

   if( ( para->inSignal & DONT_SET ) == 0 && para->variable )
   {
      para->inSignal |= DONT_GET;
      if( Tcl_SetVar2Ex( para->interp, para->variable, NULL, 
            valObj, 0 ) == NULL )
         ret = TCL_ERROR;
      para->inSignal &= ~DONT_GET;
   }

   if( ( para->inSignal & DONT_COMMAND ) == 0 )
   {
      if( doCommand( para, valObj ) != TCL_OK )
         ret = TCL_ERROR;
   }

   return ret;
}

static void changedFunc( GtkWidget *widget, gpointer data )
{
   changedFuncRet( (ScaleParams *)data );
}

static void scaleDestroyFunc( GtkWidget *widget, gpointer data )
{
   ScaleParams *para = (ScaleParams *)data;

   gnoclAttacheCmdAndVar( 
         1, &para->command, NULL,
         1, &para->variable, NULL,
         "value-changed", 
         GTK_OBJECT( gtk_range_get_adjustment( GTK_RANGE( para->scale ) ) ),
         changedFunc, para->interp, traceFunc, para );

   gnoclForgetWidgetFromName( para->name );
   Tcl_DeleteCommand( para->interp, para->name );

   g_free( para->name );
   g_free( para );
}

static int scaleConfigure( Tcl_Interp *interp, ScaleParams *para, 
      ScaleOptions *popt )
{
   GNOCL_STD_OPTIONS_HANDLE( popt, GTK_WIDGET( para->scale ) );

   gnoclAttacheCmdAndVar( 
         popt->command.changed, &para->command, &popt->command.val,
         popt->variable.changed, &para->variable, &popt->variable.val,
         "value-changed", 
         GTK_OBJECT( gtk_range_get_adjustment( GTK_RANGE( para->scale ) ) ),
         changedFunc, interp, traceFunc, para );

   if( popt->variable.changed && para->variable )
   {
         /* if variable does not exist -> set it, else set widget state */
      Tcl_Obj *var = Tcl_GetVar2Ex( interp, para->variable, NULL, 
            TCL_GLOBAL_ONLY );
      if( var == NULL )
      {
         GtkAdjustment *adjust = gtk_range_get_adjustment( 
               GTK_RANGE( para->scale ) );

         para->inSignal |= DONT_GET;
         Tcl_SetVar2Ex( para->interp, para->variable, NULL, 
               Tcl_NewDoubleObj( adjust->value ), TCL_GLOBAL_ONLY );
         para->inSignal &= ~DONT_GET;
      }
      else
      {
         if( setValue( para, var ) != TCL_OK )
            return TCL_ERROR;
      }
   }

   if( popt->valuePos.changed )
   {
      GtkPositionType type;
      if( gnoclGetPositionType( interp, popt->valuePos.val, &type )
            != TCL_OK )
         return TCL_ERROR;

      gtk_scale_set_value_pos( para->scale, type );
   }
   if( popt->updatePolicy.changed )
   {
      GtkUpdateType policy;
      if( gnoclGetUpdatePolicy( interp, popt->updatePolicy.val, &policy )
            != TCL_OK )
         return TCL_ERROR;
      gtk_range_set_update_policy( GTK_RANGE( para->scale ), policy );
   }

   if( popt->digits.changed )
   {
      gtk_scale_set_digits( para->scale, popt->digits.val );
   }

   if( popt->drawValue.changed )
   {
      gtk_scale_set_draw_value( para->scale, popt->drawValue.val );
   }

   /* see also spinButton.c */
   if( popt->lower.changed || popt->upper.changed 
         || popt->value.changed
         || popt->stepInc.changed || popt->pageInc.changed )
   {
      GtkAdjustment *old = gtk_range_get_adjustment( 
            GTK_RANGE( para->scale ) );


      gfloat lower = popt->lower.changed ? popt->lower.val : old->lower;
      gfloat upper = popt->upper.changed ? popt->upper.val : old->upper;
      gfloat value = popt->value.changed ? popt->value.val : old->value;
      gfloat stepInc = popt->stepInc.changed ? popt->stepInc.val : 
            old->step_increment;
      gfloat pageInc = popt->pageInc.changed ? popt->pageInc.val : 
            old->page_increment;

      GtkAdjustment *new = GTK_ADJUSTMENT( 
            gtk_adjustment_new( value, lower, upper, stepInc, pageInc, 0 ) );

      if( para->command || para->variable )
      {
         gtk_signal_disconnect_by_func( GTK_OBJECT( old ), changedFunc,
               para );
         gtk_signal_connect( GTK_OBJECT( new ), "value-changed", 
               changedFunc, para );
      }
      gtk_range_set_adjustment( GTK_RANGE( para->scale ), new );
   }

   return TCL_OK;
}

static int scaleFunc( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   static char *cmds[] = { GNOCL_STD_CMD_NAMES,
         "configure", "getValue", "setValue", "invoke",
         NULL };
   enum cmdIdx { GNOCL_STD_CMD_ENUMS, 
         ConfigureIdx, GetValueIdx, SetValueIdx };
   ScaleParams *para = (ScaleParams *)data;
   GtkWidget *widget = GTK_WIDGET( para->scale );
   int idx;

   if( Tcl_GetIndexFromObj( interp, objv[1], cmds, "command", 
         TCL_EXACT, &idx ) != TCL_OK )
      return TCL_ERROR;

   switch( idx )
   {
      GNCOL_STD_CMD_CASE( interp, objc, objv, widget );
            break;

      case ConfigureIdx:
            {
               ScaleOptions opt;
               int ret = TCL_ERROR;

               memset( &opt, 0, sizeof( opt ) );
               if( gnoclParseAllWidgetOpts( interp, objc, objv, 1,
                     widgetOptions, (char *)&opt ) == TCL_OK )
                  ret = scaleConfigure( interp, para, &opt );

               freeOpts( &opt );
               if( ret != TCL_OK )
                  return TCL_ERROR;
               gtk_widget_show_all( widget );
            }
            break;
      case GetValueIdx:
            {
               GtkAdjustment *adjust = gtk_range_get_adjustment( 
                     GTK_RANGE( para->scale ) );
               if( objc != 2 )
               {
                  Tcl_WrongNumArgs( interp, 2, objv, NULL );
                  return TCL_ERROR;
               }
               Tcl_SetObjResult( interp, Tcl_NewDoubleObj( adjust->value ) );
            }
            break;
      case SetValueIdx:
            {
               if( objc != 3 )
               {
                  Tcl_WrongNumArgs( interp, 2, objv, "value" );
                  return TCL_ERROR;
               }
               if( setValue( para, objv[2] ) != TCL_OK )
                  return TCL_ERROR;
               return changedFuncRet( para );
            }
            break;
   }

   return TCL_OK;
}

int gnoclScaleCmd( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   ScaleOptions opt;
   ScaleParams *para;
   GtkOrientation orient = GTK_ORIENTATION_VERTICAL;
   GtkAdjustment *adjust;
   int          ret;
   
   memset( &opt, 0, sizeof( opt ) );
   if( gnoclParseAllWidgetOpts( interp, objc, objv, 0,
         widgetOptions, (char *)&opt ) != TCL_OK )
      return TCL_ERROR;

   para = g_new( ScaleParams, 1 );
   para->interp = interp;
   para->command = NULL;
   para->variable = NULL;
   para->name = gnoclGetAutoWidgetId();
   para->inSignal = 0;
   if( opt.orientation.changed )
   {
      if( gnoclGetOrientationType( interp, opt.orientation.val, &orient )
            != TCL_OK )
      {
         freeOpts( &opt );
         return TCL_ERROR;
      }
 
   }

   adjust = GTK_ADJUSTMENT( gtk_adjustment_new( 0, 0, 100, 1, 10, 0 ) );
   if( orient == GTK_ORIENTATION_HORIZONTAL )
      para->scale = GTK_SCALE( gtk_hscale_new( adjust ) );
   else
      para->scale = GTK_SCALE( gtk_vscale_new( adjust ) );

   ret = scaleConfigure( interp, para, &opt );
   freeOpts( &opt );
   if( ret != TCL_OK )
   {
      /* FIXME: g_free pp->name ... */
      gtk_widget_destroy( GTK_WIDGET( para->scale ) );
      return TCL_ERROR;
   }

   gtk_signal_connect( GTK_OBJECT( para->scale ), "destroy", 
         GTK_SIGNAL_FUNC( scaleDestroyFunc ), para );

   gnoclMemNameAndWidget( para->name, GTK_WIDGET( para->scale ) );
   gtk_widget_show( GTK_WIDGET( para->scale ) );

   Tcl_CreateObjCommand( interp, para->name, scaleFunc, para, NULL );
   Tcl_SetObjResult( interp, Tcl_NewStringObj( para->name, -1 ) );

   return TCL_OK;
}

