/*
 * gnoclOptionMenu.c --
 *
 * This file implements the optionMenu 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: renamed "invoke" to "eval"
   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 menu;
   Gnocl_StringBool variable;
} OptionMenuOptions;

static const GnoclWidgetOptions widgetOptions[] =
{
   GNOCL_STD_OPTIONS_NAMES( OptionMenuOptions ),
   { "-command", GNOCL_STRING, GNOCL_OFFSET( OptionMenuOptions, command ) },
   { "-menu", GNOCL_STRING, GNOCL_OFFSET( OptionMenuOptions, menu ) },
   { "-variable", GNOCL_STRING, GNOCL_OFFSET( OptionMenuOptions, variable ) },
   { 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 } StatusType;
typedef struct
{
   char          *name;
   Tcl_Interp    *interp;
   GtkOptionMenu *optionMenu;
   char          *command;
   char          *variable;
   GSList        *items;
   StatusType    status;
   int           minWidth;
} OptionMenuParams;

typedef struct
{
   GtkMenuItem      *item;
   Tcl_Obj          *val;
   OptionMenuParams *omp;
}MenuItemParam;

static void freeOpts( OptionMenuOptions *p )
{
   GNOCL_STD_OPTIONS_FREE( p );
   GNOCL_FREE_MEM_OPT( p->command );
   GNOCL_FREE_MEM_OPT( p->menu );
   GNOCL_FREE_MEM_OPT( p->variable );
}

static MenuItemParam *findActiveItem( OptionMenuParams *para )
{
   GSList    *p = para->items;
   GtkMenu   *menu = GTK_MENU( gtk_option_menu_get_menu( para->optionMenu ) );
   GtkWidget *active = gtk_menu_get_active( menu );

   int n = 0;
   for( ; p != NULL; ++n, p = p->next )
   {
      MenuItemParam *mip = (MenuItemParam *)p->data;
      if( GTK_WIDGET( mip->item ) == active )
         return mip;
   }

   return NULL;
}


static int findItemFromValue( OptionMenuParams *para, Tcl_Obj *obj,
      MenuItemParam **item )
{
   if( obj != NULL )
   {
      GSList *p = para->items;
      /* TODO: compare int, if possible */
      char *txt = Tcl_GetString( obj );
      int n = 0;
      for( ; p != NULL; ++n, p = p->next )
      {
         MenuItemParam *mip = (MenuItemParam *)p->data;
         if( strcmp( txt, Tcl_GetString( mip->val ) ) == 0 )
         {
            if( item )
               *item = mip;
            return n;
         }
      }
   }

   return -1;
}

static int menuItemCmdFuncRet( GtkWidget *widget, MenuItemParam *mPara )
{
   OptionMenuParams *para = mPara->omp;
   int ret = TCL_OK;

   /* see checkButton.c */
   if( (para->status & DONT_SET) == 0 && para->variable )
   {
      Tcl_Obj *obj;
      para->status |= DONT_GET;       /* don't execute traceFunc */
      Tcl_IncrRefCount( mPara->val );
      obj = Tcl_SetVar2Ex( para->interp, para->variable, NULL, mPara->val, 
            TCL_GLOBAL_ONLY );
      /* DEBUG: 
      if( obj != mPara->val ) printf( "ERROR: %p != %p\n", obj, mPara->val );
      */
         
      para->status &= ~DONT_GET;
      if( obj == NULL )
         ret = TCL_ERROR;
   }

   if( (para->status & DONT_COMMAND) == 0 && 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 = mPara->val;
      /* don't overwrite an error from variable */
      if( gnoclPercentSubstAndEval( para->interp, ps, para->command ) 
            != TCL_OK ) 
         ret = TCL_ERROR;
   }
   return ret;
}


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

   if( (para->status & DONT_GET) == 0 )
   {
      Tcl_Obj *val = Tcl_GetVar2Ex( interp, name1, name2, flags );
      MenuItemParam *mip;
      int n = findItemFromValue( para, val, &mip );
      if( n >= 0 )
      {
         gtk_option_menu_set_history( para->optionMenu, n );
         para->status |= DONT_SET;    /* don't set variable */
         menuItemCmdFuncRet( GTK_WIDGET ( para->optionMenu ), mip );
         para->status &= ~DONT_SET;
      }
      /* TODO: what to do here: val not found */
   }

   return NULL;
}


static void menuItemCmdFunc( GtkWidget *widget, gpointer data )
{
   menuItemCmdFuncRet( widget, (MenuItemParam *)data );
}

int matchVarAndWidget( OptionMenuParams *para )
{
   if( para->variable != NULL && para->items )
   {
      Tcl_Obj *val = Tcl_GetVar2Ex( para->interp, para->variable, 
            NULL, TCL_GLOBAL_ONLY );
      if( val == NULL )    
      {
         /* variable does not exist -> set variable according to widget */
         MenuItemParam *mip = findActiveItem( para );
         if( mip != NULL )
         {
            para->status = DONT_COMMAND;
            menuItemCmdFuncRet( GTK_WIDGET( para->optionMenu ), mip );
            para->status = 0;
         }
      }
      else                 
      {
         /* variable does exist -> set widget according to variable */
         int n = findItemFromValue( para, val, NULL );
         if( n >= 0 )
            gtk_option_menu_set_history( para->optionMenu, n );
         /*
         traceFunc( para, para->interp, para->variable, NULL, 
               TCL_GLOBAL_ONLY );
         */
      }
   }
   return 0;
}


static void optionMenuCmdFunc( GtkWidget *widget, gpointer data )
{
   /*
   OptionMenuParams *p = (OptionMenuParams *)data;
   if( p->command )
      Tcl_Eval( p->interp, p->command );
   */
}

static void optionMenuDestroyFunc( GtkWidget *widget, gpointer data )
{
   OptionMenuParams *p = (OptionMenuParams *)data;
   gnoclForgetWidgetFromName( p->name );
   Tcl_DeleteCommand( p->interp, p->name );
   
   if( p->variable )
      Tcl_UntraceVar( p->interp, p->variable, TCL_TRACE_WRITES, 
            traceFunc, p );

   g_free( p->variable );
   g_free( p->command );
   g_free( p->name );
   g_free( p );
}

static int optionMenuConfigure( Tcl_Interp *interp, OptionMenuParams *para, 
      OptionMenuOptions *popt )
{
   GNOCL_STD_OPTIONS_HANDLE( popt, GTK_WIDGET( para->optionMenu ) );

   if( popt->command.changed )
   {
      g_free( para->command );
      para->command = popt->command.val; /* transfer ownership */
      popt->command.val = NULL;
   }

   if( popt->menu.changed )
   {
      GtkWidget *menu = gnoclGetWidgetFromName( popt->menu.val, interp );
      if( menu == NULL )
         return TCL_ERROR;

      if( !GTK_CHECK_TYPE( menu, GTK_TYPE_MENU ) )
      {
         Tcl_AppendResult( interp, "child window \"", 
               popt->menu.val, "\" is not a menu.", (char *)NULL );
         return TCL_ERROR;
      }

      /*
      if( gnoclAssertNotPacked( menu, interp, popt->menu.val ) )
         return TCL_ERROR;
      */

      gtk_option_menu_set_menu( para->optionMenu, menu );
   }

   if( popt->variable.changed )
   {
      if( para->variable )
      {
         Tcl_UntraceVar( interp, para->variable, 
               TCL_TRACE_WRITES | TCL_GLOBAL_ONLY, traceFunc, para );
         g_free( para->variable );
      }
      if( popt->variable.val == NULL || popt->variable.val[0] == 0 )
         para->variable = NULL;
      else
      {
         para->variable = popt->variable.val;
         popt->variable.val = NULL;
         Tcl_TraceVar( interp, para->variable, 
               TCL_TRACE_WRITES | TCL_GLOBAL_ONLY, traceFunc, para );
      }
      matchVarAndWidget( para );
   }

   return TCL_OK;
}

static int addItem( OptionMenuParams *para, Tcl_Interp *interp, 
      int objc, Tcl_Obj * const objv[] )
{
   typedef struct
   {
      Gnocl_ObjBool value;
      Gnocl_ObjBool icon;
   } AddOptions;
   AddOptions params;

   GnoclWidgetOptions options[] = 
   {
      { "-value", GNOCL_OBJ, GNOCL_OFFSET( AddOptions, value ) },
      { "-icon", GNOCL_OBJ, GNOCL_OFFSET( AddOptions, icon ) },
      { NULL, 0, 0 }
   };

   GtkMenu        *menu;
   GtkWidget      *widget;
   int            isNew = 0;
   MenuItemParam  *mPara;
   GtkRequisition menuReq, req;

   /* id add text ? opt ...? */
   if( objc < 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, "text ?option val ...?" );
      return TCL_ERROR;
   }

   memset( &params, 0, sizeof( params ) );
   if( gnoclParseAllWidgetOpts( interp, objc, objv,  2,
         options, (char *)&params ) != TCL_OK )
      return TCL_ERROR;

   widget = gtk_option_menu_get_menu( para->optionMenu );
   if( widget == NULL )
   {
      widget = gtk_menu_new( );
      gtk_option_menu_set_menu( para->optionMenu, widget );
      gtk_widget_show( widget );
      isNew = 1;
   }
   menu = GTK_MENU( widget );

   mPara = g_new( MenuItemParam, 1 );
   /* TODO: allow empty "" icon? */
   /* TODO: free options */
   if( params.icon.changed )
   {
      GtkWidget *icon;
      GtkWidget *label;
      int ret = gnoclGetPixmap( interp, NULL, params.icon.val, &icon );
      if( ret != TCL_OK )
         return TCL_ERROR;

      label = gtk_label_new( gnoclGetString( objv[2] ) );
      gtk_misc_set_alignment( GTK_MISC( label ), 0, 0.5 );

      gtk_widget_show_all( label );
      mPara->item = GTK_MENU_ITEM( gtk_pixmap_menu_item_new() );
      gtk_container_add( GTK_CONTAINER( mPara->item ), label );
      if( icon != NULL )
      {
         gtk_pixmap_menu_item_set_pixmap( 
               GTK_PIXMAP_MENU_ITEM( mPara->item ), icon );
      }
   }
   else
      mPara->item = GTK_MENU_ITEM( gtk_menu_item_new_with_label(
            gnoclGetString( objv[2] ) ) ); 
   if( params.value.changed )
   {
      mPara->val = params.value.val;
      params.value.val = NULL;
   }
   else
   {
      int n = g_list_length( gtk_container_children( GTK_CONTAINER( menu ) ) );
      mPara->val = Tcl_NewIntObj( n );
      Tcl_IncrRefCount( mPara->val );
   }
   mPara->omp = para;
   gtk_signal_connect( GTK_OBJECT( mPara->item ), "activate", 
         menuItemCmdFunc, mPara );
   /* 
      gtk_signal_disconnect_by_func( obj, menuItemCmdFunc, data );
      is automatically called on destroy
   */

   gtk_menu_append( menu, GTK_WIDGET( mPara->item ) );
   gtk_widget_show_all( GTK_WIDGET( mPara->item ) );

   /* size_request must be before set_history, else the width of the
      first item is calculated wrong. Why? */
   gtk_widget_size_request( GTK_WIDGET( mPara->item ), &menuReq );

   if( isNew )
      gtk_option_menu_set_history( para->optionMenu, 0 );

   para->items = g_slist_append( para->items, mPara );


   /* resize optionMenu, so that max of items fit in it 
      TODO: same for -menu
      TODO?: don't adapt, if user set width via parameter? 
   */
   gtk_widget_size_request( GTK_WIDGET( para->optionMenu ), &req );
   /*
   printf( "DEBUG: optionMenu old size: %d menu size: %d min size: %d\n", 
         req.width, menuReq.width, para->minWidth );
   */
   if( menuReq.width + para->minWidth > req.width )
   {
      gtk_widget_set_usize( GTK_WIDGET( para->optionMenu ), 
            menuReq.width + para->minWidth, -2 );
   }
   matchVarAndWidget( para );

   return TCL_OK;
}

static int optionMenuFunc( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   static char *cmds[] = { GNOCL_STD_CMD_NAMES,
         "configure", "add", "getValue", "setValue", "eval",
         /* TODO? addList? */
         NULL };
   enum cmdIdx { GNOCL_STD_CMD_ENUMS, 
         ConfigureIdx, AddIdx, GetValueIdx, SetValueIdx, EvalIdx };
   OptionMenuParams *para = (OptionMenuParams *)data;
   GtkWidget *widget = GTK_WIDGET( para->optionMenu );
   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:
            {
               OptionMenuOptions opt;
               int ret = TCL_ERROR;

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

               freeOpts( &opt );
               if( ret != TCL_OK )
                  return TCL_ERROR;
            }
            break;
      case AddIdx:
            return addItem( para, interp, objc, objv );
      case GetValueIdx:
            {
               MenuItemParam *mip;

               if( objc != 2 )
               {
                  Tcl_WrongNumArgs( interp, 2, objv, NULL );
                  return TCL_ERROR;
               }
               mip = findActiveItem( para );
               assert( mip != NULL );
               Tcl_SetObjResult( interp, mip->val );
               return TCL_OK;
            }
      case SetValueIdx:
            {
               int n;
               MenuItemParam *mip;

               if( objc != 3 )
               {
                  Tcl_WrongNumArgs( interp, 2, objv, "value" );
                  return TCL_ERROR;
               }
 
               n = findItemFromValue( para, objv[2], &mip );
               if( n < 0 )
               {
                  Tcl_AppendResult( interp, "invalid value \"", 
                        Tcl_GetString( objv[2] ), "\"", NULL );
                  return TCL_ERROR;
               }
               gtk_option_menu_set_history( para->optionMenu, n );
               /* call command and set variable */
               if( menuItemCmdFuncRet( widget, mip ) != TCL_OK )
                  return TCL_ERROR;
               break;
            }
      case EvalIdx:
            {
               MenuItemParam *mip = findActiveItem( para );
               if( mip != NULL )
               {
                  int ret;
                  para->status |= DONT_GET;
                  ret = menuItemCmdFuncRet( widget, mip );
                  para->status &= ~DONT_GET;
                  return ret;
               }
            }
            break;

   }

   return TCL_OK;
}


int gnoclOptionMenuCmd( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   OptionMenuOptions opt;
   OptionMenuParams *para;
   int          ret;
   GtkRequisition req;
   
   memset( &opt, 0, sizeof( opt ) );
   if( gnoclParseAllWidgetOpts( interp, objc, objv, 0,
         widgetOptions, (char *)&opt ) != TCL_OK )
      return TCL_ERROR;

   para = g_new( OptionMenuParams, 1 );
   para->interp = interp;
   para->command = NULL;
   para->variable = NULL;
   para->name = gnoclGetAutoWidgetId();
   para->optionMenu = GTK_OPTION_MENU( gtk_option_menu_new() ); 
   para->items = NULL;
   para->status = 0;

   gtk_widget_size_request( GTK_WIDGET( para->optionMenu ), &req );
   para->minWidth = req.width;

   ret = optionMenuConfigure( interp, para, &opt );
   freeOpts( &opt );
   if( ret != TCL_OK )
   {
      /* FIXME: g_free pp->name ... */
      gtk_widget_destroy( GTK_WIDGET( para->optionMenu ) );
      return TCL_ERROR;
   }
   gtk_signal_connect( GTK_OBJECT( para->optionMenu ), "clicked", 
         GTK_SIGNAL_FUNC( optionMenuCmdFunc ), para );
   gtk_signal_connect( GTK_OBJECT( para->optionMenu ), "destroy", 
         GTK_SIGNAL_FUNC( optionMenuDestroyFunc ), para );

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

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

   return TCL_OK;
}

