
/*
 * gnoclCanvas.c --
 *
 * This file implements the canvas 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:
        11: added scroll window
        05: added rectangle, line, text
   2001-03: Begin of developement
            first item is ellipse
 */

#include "canvas.h"
#include <string.h>
#include <math.h>
#include <assert.h>

GnoclItemCreateFunc gnoclCanvasCreateLine;
GnoclItemCreateFunc gnoclCanvasCreateEllipse;
GnoclItemCreateFunc gnoclCanvasCreateRectangle;
GnoclItemCreateFunc gnoclCanvasCreateWidget;
GnoclItemCreateFunc gnoclCanvasCreateText;

enum {Affine, Move, Scale, Rotate};

typedef Gnocl_CanvasItemInfo *Gnocl_PCanvasItemInfo;

typedef struct
{
   Gnocl_IntBool  width;
   Gnocl_IntBool  height;
   Gnocl_BoolBool antialiased;
   Gnocl_ObjBool  scrollbar;
   Gnocl_ObjBool  scrollRegion;
} CanvasOptions;


static const GnoclWidgetOptions widgetOptions[] =
{
   { "-width", GNOCL_INT, GNOCL_OFFSET( CanvasOptions, width ) },
   { "-height", GNOCL_INT, GNOCL_OFFSET( CanvasOptions, height ) },
   { "-antialiased", GNOCL_BOOL, GNOCL_OFFSET( CanvasOptions, antialiased ) },
   { "-scrollbar", GNOCL_OBJ, GNOCL_OFFSET( CanvasOptions, scrollbar ) },
   { "-scrollRegion", GNOCL_OBJ, GNOCL_OFFSET( CanvasOptions, scrollRegion ) },
   { NULL, 0, 0 }
};

typedef struct
{
   GnomeCanvas       *canvas;
   GtkScrolledWindow *scrollWin;
   GArray            *items;
} CanvasParams;


static void freeOpts( CanvasOptions *p )
{
   /* GNOCL_STD_OPTIONS_FREE( p ); */

   GNOCL_FREE_TCL_OPT( p->scrollbar );
}

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

/* TODO: return (GArray *) of Gnocl_CanvasItemInfo */
/* only tag "all" is yet implemented for delete */
static Gnocl_CanvasItemInfo *getCanvasItemsFromTagOrId( CanvasParams *pp, 
      Tcl_Obj *obj, int deleteIndex )
{
   int id;
   if( Tcl_GetIntFromObj( NULL, obj, &id ) == TCL_OK )
   {
      int n;
      for( n = 0; n < pp->items->len; ++n )
      {
         Gnocl_CanvasItemInfo *info = g_array_index( pp->items, 
               Gnocl_PCanvasItemInfo, n );
         if( info->id == id )
         {
            if( deleteIndex )
               pp->items = g_array_remove_index_fast( pp->items, n );
            return info;
         }
      }
   }
   /* TODO: tag */
   return NULL;
}

static Gnocl_CanvasItemInfo *getInfoFromCanvasItem( CanvasParams *pp, 
      GnomeCanvasItem *item )
{
   if( item != NULL )
   {
      int n;
      for( n = 0; n < pp->items->len; ++n )
      {
         Gnocl_CanvasItemInfo *info = g_array_index( pp->items, 
               Gnocl_PCanvasItemInfo, n );
         if( info->item == item )
            return info;
      }
   }
   return NULL;
}

static int canvasConfigure( Tcl_Interp *interp, CanvasParams *para,
      CanvasOptions *popt )
{
   GtkStyle    *style;
   GdkColormap *colormap;

   if( popt->height.changed )
      gtk_widget_set_usize( GTK_WIDGET( para->canvas ), -2, popt->height.val );

   if( popt->width.changed )
      gtk_widget_set_usize( GTK_WIDGET( para->canvas ), popt->width.val, -2 );

   if( popt->scrollRegion.changed )
   {
      int     k, no;
      Tcl_Obj *tp;
      double  val[4];   /* x, y, w, h */

      if( Tcl_ListObjLength( interp, popt->scrollRegion.val, &no ) != TCL_OK 
         || no != 4 )
      {
         Tcl_SetResult( interp, 
               "scrollRegion must be proper list with four members", 
               TCL_STATIC );
         return TCL_ERROR;
      }
      for( k = 0; k < no; ++k )
      {
         if( Tcl_ListObjIndex( interp, popt->scrollRegion.val, 
               k, &tp ) != TCL_OK )
            return TCL_ERROR;
         if( Tcl_GetDoubleFromObj( interp, tp, &val[k] ) )
            return TCL_ERROR;
      }
      gnome_canvas_set_scroll_region( para->canvas, 
            val[0], val[1], val[0] + val[2], val[1] + val[3] ); 
   }
  
   if( popt->scrollbar.changed )
   {
      GtkPolicyType hor, vert;
      if( gnoclGetScrollbarPolicy( interp, popt->scrollbar.val, 
            &hor, &vert ) != TCL_OK )
         return TCL_ERROR;

      gtk_scrolled_window_set_policy( para->scrollWin, hor, vert );
   }



   colormap = gtk_widget_get_colormap( GTK_WIDGET( para->canvas ) );
   style = gtk_widget_get_style( GTK_WIDGET( para->canvas ) );
   style = gtk_style_copy( style );
   /*
   style->bg[0].pixel = 0;
   style->bg[0].red = 65535;
   style->bg[0].green = 65535;
   style->bg[0].blue = 65535;
   */
   /* TODO: gdk_color_parse () */
   gdk_color_white( colormap, &style->bg[0] );
   gtk_style_set_background( style, (GdkWindow *)para->canvas, 
         GTK_STATE_NORMAL ); 
   /* gtk_style_unref( style ); FIXME: no unref? */
   gtk_widget_set_style( GTK_WIDGET( para->canvas ), style );

   return TCL_OK;
}

static int windowToWorld( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *params, int reverse )
{
   Tcl_Obj *resList;
   int     noCoords, n;
   if( objc != 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, 
            /* canvas windowToWorld */
            "list-of-coordinates ?option val ...?" );
      return TCL_ERROR;
   }
   /* TODO  
         -only [xy]:          only x, y coordinates
         -pairs [true|false]: list of coordinate pairs (lists)
   */
   if( Tcl_ListObjLength( interp, objv[2], &noCoords ) != TCL_OK
         || ( noCoords % 2 ) )
   {
      Tcl_SetResult( interp, 
            "size of list-of-coordinates must be even", 
            TCL_STATIC );
      return TCL_ERROR;
   }
   resList = Tcl_NewListObj( 0, NULL );
   for( n = 0; n < noCoords; n += 2 )
   {
      Tcl_Obj *tp;
      double xw, yw, x, y;
      int ret = Tcl_ListObjIndex( interp, objv[2], n, &tp );
      if( ret == TCL_OK )
         ret = Tcl_GetDoubleFromObj( interp, tp, &xw );
      if( ret == TCL_OK )
         ret = Tcl_ListObjIndex( interp, objv[2], n + 1, &tp );
      if( ret == TCL_OK )
         ret = Tcl_GetDoubleFromObj( interp, tp, &yw );

      if( ret != TCL_OK )
      {
         Tcl_DecrRefCount( resList );  /* FIXME: is this correct? */
         return TCL_ERROR;
      }
      if( reverse )
         gnome_canvas_world_to_window( params->canvas, xw, yw, &x, &y );
      else
         gnome_canvas_window_to_world( params->canvas, xw, yw, &x, &y );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( x ) );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( y ) );
   }
   Tcl_SetObjResult( interp, resList );

   return TCL_OK;
}

static int findItemAt( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *params )
{
   double   x, y;
   GnomeCanvasItem *item;
   Gnocl_CanvasItemInfo *info;

   if( objc != 4 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, 
            /* canvas findItemAt */
            "x y ?option val ...?" );
      return TCL_ERROR;
   }
   if( Tcl_GetDoubleFromObj( interp, objv[2], &x ) != TCL_OK )
      return TCL_ERROR;
   if( Tcl_GetDoubleFromObj( interp, objv[3], &y ) != TCL_OK )
      return TCL_ERROR;
   
   item = gnome_canvas_get_item_at( params->canvas, x, y );
   /* printf( "item: %p %f %f\n", item, x, y ); */
   info = getInfoFromCanvasItem( params, item );
   if( info )
      Tcl_SetObjResult( interp, Tcl_NewIntObj( info->id ) );

   return TCL_OK;
}

static int getBounds( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param )
{
   Gnocl_CanvasItemInfo *info;

   if( objc != 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, 
            /* canvas getBounds */
            "item ?option val ...?" );
      return TCL_ERROR;
   }

   info = getCanvasItemsFromTagOrId( param, objv[2], 0 );
   if( info )
   {
      double x1, y1, x2, y2;
      Tcl_Obj *resList;

      gnome_canvas_item_get_bounds( info->item, &x1, &y1, &x2, &y2 );

      resList = Tcl_NewListObj( 0, NULL );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( x1 ) );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( y1 ) );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( x2 ) );
      Tcl_ListObjAppendElement( interp, resList, Tcl_NewDoubleObj( y2 ) );
      Tcl_SetObjResult( interp, resList );
   }

   return TCL_OK;
}


static int canvasItemConfigure( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param )
{
   /* canvas itemConfigure tag-or-id ?option val ...? */
   Gnocl_CanvasItemInfo *info;
   if( objc < 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, "tag-or-id ?option val ...?" );
      return TCL_ERROR;
   }
   
   info = getCanvasItemsFromTagOrId( param, objv[2], 0 );
   if( info )
      return (*info->configFunc)( interp, objc, objv, info );

   return TCL_OK;
}

static int canvasShowItem( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param, int show )
{
   Gnocl_CanvasItemInfo *info;
   if( objc != 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, "tag-or-id" );
      return TCL_ERROR;
   }
   
   info = getCanvasItemsFromTagOrId( param, objv[2], 0 );
   if( info )
   {
      if( show )
         gnome_canvas_item_show( info->item );
      else
         gnome_canvas_item_hide( info->item );
   }
   return TCL_OK;
}

static int canvasDeleteItem( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param )
{
   int noTags;
   int k;

   if( objc != 3 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, 
            /* canvas delete */ "list-of-items-or-tags" );
      return TCL_ERROR;
   }

   if( Tcl_ListObjLength( interp, objv[2], &noTags ) != TCL_OK )
      return TCL_ERROR;

   for( k = 0; k < noTags; ++k )
   {
      Tcl_Obj *tp;
      int item;

      if( Tcl_ListObjIndex( interp, objv[2], k, &tp ) != TCL_OK )
         return TCL_ERROR;
      /* test, if item or tag */
      if( Tcl_GetIntFromObj( NULL, tp, &item ) == TCL_OK )
      {
         Gnocl_CanvasItemInfo *info = 
               getCanvasItemsFromTagOrId( param, tp, 1 );
         if( info )
         {
            (*info->deleteFunc)( interp, info );
            /* deleteCanvasItemEvent( GTK_OBJECT( info->item ) ); */
            gtk_object_destroy( GTK_OBJECT( info->item ) );
            g_free( info );
         }
      }
      else
      {
         const char *tag = Tcl_GetString( tp );
         /* TODO:
            getCanvasItemFromTag()
         */
         if( strcmp( tag, "all" ) == 0 )
         {
            int n;
            for( n = 0; n < param->items->len; ++n )
            {
               Gnocl_CanvasItemInfo *info = g_array_index( param->items, 
                     Gnocl_PCanvasItemInfo, n );
               (*info->deleteFunc)( interp, info );
               /* deleteCanvasItemEvent( GTK_OBJECT( info->item ) ); */
               gtk_object_destroy( GTK_OBJECT( info->item ) );
               g_free( info );
            }
            param->items = g_array_set_size( param->items, 0 );  
         }
      }
   }
   return TCL_OK;
}

static int canvasCreateItem( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param )
{
   static char *items[] = { "line", "rectangle", "ellipse", 
         "text", "widget",
         NULL };
   enum itemIdx { LineIdx, RectangleIdx, EllipseIdx, TextIdx, WidgetIdx };

   static int noItems = 0;

   Gnocl_CanvasItemInfo *item;
   GnoclItemCreateFunc *func;
   int idx;

   if( objc < 4 )
   {
      Tcl_WrongNumArgs( interp, 2, objv, 
            /* canvas create */
            "type list-of-coordinates ?option val ...?" );
      return TCL_ERROR;
   }

   if( Tcl_GetIndexFromObj( interp, objv[2], items, "item type", 
         TCL_EXACT, &idx ) != TCL_OK )
      return TCL_ERROR;

   switch( idx )
   {
      case LineIdx:        func = gnoclCanvasCreateLine; break;
      case RectangleIdx:   func = gnoclCanvasCreateRectangle; break;
      case EllipseIdx:     func = gnoclCanvasCreateEllipse; break;
      case TextIdx:        func = gnoclCanvasCreateText; break;
      case WidgetIdx:      func = gnoclCanvasCreateWidget; break;
      default:             assert( 0 );
                           return TCL_ERROR; /* should never happen */
   }

   item = (*func)( interp, objc, objv, param->canvas );
   if( item == NULL )
      return TCL_ERROR;

   item->id = ++noItems;
   param->items = g_array_append_val( param->items, item );

   gnoclMemNameAndCanvasInfo( noItems, item );

   /* gnome_canvas_update_now( param->canvas ); */
   Tcl_SetObjResult( interp, Tcl_NewIntObj( noItems ) );

   return TCL_OK;
}

static int affine( Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[], CanvasParams *param, int type )
{
   Gnocl_CanvasItemInfo *info;
   int noCoords;
   int absolute = 0; /* TODO: parameter -absolute boolean */

   if( objc != 4  )
   {
      /* canvas (affine|move|scale|rotate) */
      Tcl_WrongNumArgs( interp, 2, objv, "tag-or-id list-of-coordinates" );
      return TCL_ERROR;
   }
   
   info = getCanvasItemsFromTagOrId( param, objv[2], 0 );

   if( Tcl_ListObjLength( interp, objv[3], &noCoords ) != TCL_OK )
      return TCL_ERROR;

   if( info != NULL )
   {
      int k;
      double af[6];
      Tcl_Obj *tp;

      for( k = 0; k < 6 && k < noCoords; ++k )
      {
         if( Tcl_ListObjIndex( interp, objv[3], k, &tp ) != TCL_OK )
            return TCL_ERROR;
         if( Tcl_GetDoubleFromObj( interp, tp, &af[k] ) != TCL_OK )
            return TCL_ERROR;
      }

      /*
         x' = af[0] * x + af[2] * y + af[4];
         y' = af[1] * x + af[3] * y + af[5];
      */
      switch( type )
      {
         case Affine:
               if( noCoords != 6 )
               {
                  Tcl_SetResult( interp, 
                        "size of list-of-coordinates must be 6", TCL_STATIC );
                  return TCL_ERROR;
               }
               break;
         case Move:
               if( noCoords != 2 )
               {
                  Tcl_SetResult( interp, "size of list-of-coordinates must "
                        "be 2 (delta-x and delta-y)", TCL_STATIC );
                  return TCL_ERROR;
               }
               af[4] = af[0];
               af[5] = af[1];

               af[0] = 1.; af[2] = 0.;
               af[1] = 0.; af[3] = 1.;
               break;
         case Scale:
               if( noCoords != 3 && noCoords != 4 )
               {
                  Tcl_SetResult( interp, "size of list-of-coordinates must "
                        "be 3 or 4 (center, x-scale and y-scale)", 
                        TCL_STATIC );
                  return TCL_ERROR;
               }
               else
               {
                  double x = af[0];
                  double y = af[1];
                  double scalex = af[2];
                  double scaley = (noCoords == 4) ? af[3] : scalex;
                  af[0] = scalex; af[2] = .0;     af[4] = x * (1. - scalex);
                  af[1] = .0;     af[3] = scaley; af[5] = y * (1. - scaley);
               }
               break;
         case Rotate:
               /* there seems to be some problems with the 
                  canvas without antialiasing and rotating 
                  (bizarre drawings)
               */
               if( noCoords != 3 )
               {
                  Tcl_SetResult( interp, "size of list-of-coordinates must "
                        "be 3 (center and angle)", TCL_STATIC );
                  return TCL_ERROR;
               }
               else
               {
                  double x = af[0];
                  double y = af[1];
                  double w = af[2];
                  const double cw = cos( w );
                  const double sw = sin( w );

                  af[0] = cw; af[2] = -sw; af[4] = x - x * cw + y * sw;
                  af[1] = sw; af[3] =  cw; af[5] = y - x * sw - y * cw;
               }
               break;
      }

      /*
      for( k = 0; k < 6; k += 2 )
         printf( "%d: %f ", k, af[k] );
      printf( "\n" );
      for( k = 1; k < 6; k += 2 )
         printf( "%d: %f ", k, af[k] );
      printf( "\n" );
      */

      if( absolute )
         gnome_canvas_item_affine_absolute( info->item, af );
      else
         gnome_canvas_item_affine_relative( info->item, af );
   }

   return TCL_OK;
}

static int canvasFunc( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   static char *cmds[] = { GNOCL_STD_CMD_NAMES,
         "configure", "create", "itemDelete", "itemHide", "itemShow",
         "itemConfigure", "coords",
         "affine", "scale", "move", "rotate",
         "windowToWorld", "worldToWindow",
         "findItemAt", "getBounds",
         NULL };
   enum cmdIdx { GNOCL_STD_CMD_ENUMS,
         ConfigureIdx, CreateIdx, ItemDeleteIdx, ItemHideIdx, ItemShowIdx,
         ItemConfigureIdx, CoordsIdx,
         AffineIdx, ScaleIdx, MoveIdx, RotateIdx,
         WindowToWorldIdx, WorldToWindowIdx,
         FindItemAtIdx, GetBoundsIdx };
   CanvasParams *pp = (CanvasParams *)data;
   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, GTK_WIDGET( pp->canvas ) );
            break;

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

               memset( &opt, 0, sizeof( opt ) );
               if( gnoclParseAllWidgetOpts( interp, objc, objv, 1,
                     widgetOptions, (char *)&opt ) == TCL_OK )
                  ret = canvasConfigure( interp, pp, &opt );
               freeOpts( &opt );
               if( ret != TCL_OK )
                  return TCL_ERROR;
            }
            break;
      case ItemDeleteIdx:
            return canvasDeleteItem( interp, objc, objv, pp );
      case ItemHideIdx:
            return canvasShowItem( interp, objc, objv, pp, 0 );
      case ItemShowIdx:
            return canvasShowItem( interp, objc, objv, pp, 1 );
      case CreateIdx:
            return canvasCreateItem( interp, objc, objv, pp );
      case ItemConfigureIdx:
            return canvasItemConfigure( interp, objc, objv, pp );
      case AffineIdx:
            return affine( interp, objc, objv, pp, Affine );
      case ScaleIdx:
            return affine( interp, objc, objv, pp, Scale );
      case MoveIdx:
            return affine( interp, objc, objv, pp, Move );
      case RotateIdx:
            return affine( interp, objc, objv, pp, Rotate );
      case WindowToWorldIdx:
            return windowToWorld( interp, objc, objv, pp, 0 );
      case WorldToWindowIdx:
            return windowToWorld( interp, objc, objv, pp, 1 );
      case FindItemAtIdx:
            return findItemAt( interp, objc, objv, pp );
      case GetBoundsIdx:
            return getBounds( interp, objc, objv, pp );
   }

   return TCL_OK;
}

int gnoclCanvasCmd( ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj * const objv[] )
{
   CanvasOptions opt;
   CanvasParams *para;
   char *name = gnoclGetAutoWidgetId();
   
   memset( &opt, 0, sizeof( opt ) );
   if( gnoclParseAllWidgetOpts( interp, objc, objv, 0,
         widgetOptions, (char *)&opt ) != TCL_OK )
      return TCL_ERROR;

   para = g_new( CanvasParams, 1 );

   /* antialiased is default */
   if( opt.antialiased.changed && !opt.antialiased.val )
      para->canvas = GNOME_CANVAS( gnome_canvas_new( ) );
   else
      para->canvas = GNOME_CANVAS( gnome_canvas_new_aa( ) );
   para->items = g_array_new( 0, 0, sizeof( Gnocl_PCanvasItemInfo ) );

   para->scrollWin = GTK_SCROLLED_WINDOW( 
         gtk_scrolled_window_new( NULL, NULL ) );
   gtk_scrolled_window_set_policy( para->scrollWin, 
         GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC );
   gtk_container_add( GTK_CONTAINER( para->scrollWin ), 
         GTK_WIDGET( para->canvas ) );
   
   if( canvasConfigure( interp, para, &opt ) != TCL_OK )
   {
      /* FIXME: g_free para->name ... */
      freeOpts( &opt );
      gtk_widget_destroy( GTK_WIDGET( para->canvas ) );
      gtk_widget_destroy( GTK_WIDGET( para->scrollWin ) );
      return TCL_ERROR;
   }
   freeOpts( &opt );

   /*
   gtk_signal_connect( GTK_OBJECT( para->canvas ), "clicked", 
         GTK_SIGNAL_FUNC( canvasCmdFunc ), para );
   */

   gnoclMemNameAndWidget( name, GTK_WIDGET( para->scrollWin ) );
   gtk_widget_show_all( GTK_WIDGET( para->scrollWin ) );

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

   return TCL_OK;
}

