/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998 Alexander Larsson
 *
 * Chronogram objects support
 * Copyright (C) 2000, 2001 Cyrille Chepelov 
 * 
 * Ultimately forked from Flowchart toolbox -- objects for drawing flowcharts.
 * Copyright (C) 1999 James Henstridge.
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <assert.h>
#include <math.h>
#include <string.h>
#include <glib.h>

#include "intl.h"
#include "object.h"
#include "element.h"
#include "connectionpoint.h"
#include "diarenderer.h"
#include "attributes.h"
#include "text.h"
#include "widgets.h"
#include "message.h"
#include "connpoint_line.h"
#include "color.h"
#include "properties.h"

#include "chronogram.h"
#include "chronoline_event.h"

#include "pixmaps/chronoline.xpm"

#define DEFAULT_WIDTH 7.0
#define DEFAULT_HEIGHT 5.0

typedef struct _Chronoline {
  Element element;

  real main_lwidth;
  Color color;
  real start_time;
  real end_time;
  real data_lwidth;
  Color data_color;
  char *events;
  char *name;
  real rise_time;
  real fall_time;
  gboolean multibit;
  DiaFont *font;
  real font_size;
  Color font_color;
  
  /* computed values : */
  ConnPointLine *snap; /* not saved ; num_connections derived from
			  the event string. */
  CLEventList *evtlist;

  int checksum;
  real labelwidth;
  real y_down,y_up;
  Color gray, datagray;
} Chronoline;


static real chronoline_distance_from(Chronoline *chronoline, Point *point);
static void chronoline_select(Chronoline *chronoline, Point *clicked_point,
		       DiaRenderer *interactive_renderer);
static ObjectChange* chronoline_move_handle(Chronoline *chronoline, Handle *handle,
					    Point *to, ConnectionPoint *cp,
					    HandleMoveReason reason, 
			    ModifierKeys modifiers);
static ObjectChange* chronoline_move(Chronoline *chronoline, Point *to);
static void chronoline_draw(Chronoline *chronoline, DiaRenderer *renderer);
static void chronoline_update_data(Chronoline *chronoline);
static DiaObject *chronoline_create(Point *startpoint,
			  void *user_data,
			  Handle **handle1,
			  Handle **handle2);
static void chronoline_destroy(Chronoline *chronoline);
static DiaObject *chronoline_load(ObjectNode obj_node, int version, 
                               const char *filename);
static PropDescription *chronoline_describe_props(Chronoline *chronoline);
static void chronoline_get_props(Chronoline *chronoline, 
                                 GPtrArray *props);
static void chronoline_set_props(Chronoline *chronoline, 
                                 GPtrArray *props);

static ObjectTypeOps chronoline_type_ops =
{
  (CreateFunc) chronoline_create,
  (LoadFunc)   chronoline_load/*using properties*/,
  (SaveFunc)   object_save_using_properties,
  (GetDefaultsFunc)   NULL,
  (ApplyDefaultsFunc) NULL
};

DiaObjectType chronoline_type =
{
  "chronogram - line",  /* name */
  0,                 /* version */
  (char **) chronoline_xpm, /* pixmap */

  &chronoline_type_ops      /* ops */
};

static ObjectOps chronoline_ops = {
  (DestroyFunc)         chronoline_destroy,
  (DrawFunc)            chronoline_draw,
  (DistanceFunc)        chronoline_distance_from,
  (SelectFunc)          chronoline_select,
  (CopyFunc)            object_copy_using_properties,
  (MoveFunc)            chronoline_move,
  (MoveHandleFunc)      chronoline_move_handle,
  (GetPropertiesFunc)   object_create_props_dialog,
  (ApplyPropertiesFunc) object_apply_props_from_dialog,
  (ObjectMenuFunc)      NULL,
  (DescribePropsFunc)   chronoline_describe_props,
  (GetPropsFunc)        chronoline_get_props,
  (SetPropsFunc)        chronoline_set_props
};

static PropNumData time_range = { -32767.0, 32768.0, 0.1};
static PropNumData edge_range = { 0.0, 1000.0, 0.1};

static PropDescription chronoline_props[] = {
  ELEMENT_COMMON_PROPERTIES,
  PROP_STD_NOTEBOOK_BEGIN,
  PROP_NOTEBOOK_PAGE("data",PROP_FLAG_DONT_MERGE,N_("Data")),
  { "name", PROP_TYPE_STRING,PROP_FLAG_VISIBLE|PROP_FLAG_DONT_MERGE,
    N_("Data name"),NULL },
  { "events", PROP_TYPE_MULTISTRING,PROP_FLAG_VISIBLE|PROP_FLAG_DONT_MERGE,
    N_("Events"), NULL,GINT_TO_POINTER(5) },
  { "help", PROP_TYPE_STATIC,
    PROP_FLAG_VISIBLE|PROP_FLAG_DONT_SAVE|PROP_FLAG_DONT_MERGE,
    N_("Event specification"),N_(
   "@ time    set the pointer at an absolute time.\n"
   "( duration  sets the signal up, then wait 'duration'.\n"
   ") duration  sets the signal down, then wait 'duration'.\n" 
   "u duration  sets the signal to \"unknown\" state, then wait 'duration'.\n"
   "example : @ 1.0 (2.0)1.0(2.0)\n" )},

  PROP_NOTEBOOK_PAGE("parameters",0,N_("Parameters")),
  { "start_time",PROP_TYPE_REAL,PROP_FLAG_VISIBLE,
    N_("Start time"),NULL,&time_range},
  { "end_time",PROP_TYPE_REAL,PROP_FLAG_VISIBLE,
    N_("End time"),NULL,&time_range},
  { "rise_time",PROP_TYPE_REAL,PROP_FLAG_VISIBLE,
    N_("Rise time"),NULL,&edge_range},
  { "fall_time",PROP_TYPE_REAL,PROP_FLAG_VISIBLE,
    N_("Fall time"),NULL,&edge_range},
  { "multibit",PROP_TYPE_BOOL,PROP_FLAG_VISIBLE, N_("Multi-bit data"),NULL},

  PROP_NOTEBOOK_PAGE("aspect",0,N_("Aspect")),
  { "data_color", PROP_TYPE_COLOUR, PROP_FLAG_VISIBLE,
    N_("Data color"),NULL},
  { "data_lwidth", PROP_TYPE_REAL, PROP_FLAG_VISIBLE,
    N_("Data line width"),NULL, &prop_std_line_width_data},
  { "color", PROP_TYPE_COLOUR, PROP_FLAG_VISIBLE,
    N_("Line color"),NULL},
  { "main_lwidth", PROP_TYPE_REAL, PROP_FLAG_VISIBLE,
    N_("Line width"),NULL, &prop_std_line_width_data},

  { "font", PROP_TYPE_FONT, PROP_FLAG_VISIBLE, N_("Font"), NULL, NULL },
  { "font_size", PROP_TYPE_REAL, PROP_FLAG_VISIBLE,
    N_("Font size"), NULL, &prop_std_text_height_data },
  { "font_color", PROP_TYPE_COLOUR, PROP_FLAG_VISIBLE,
    N_("Text color"), NULL, NULL },

  PROP_STD_NOTEBOOK_END,
  {NULL}
};

static PropDescription *
chronoline_describe_props(Chronoline *chronoline) 
{
  if (chronoline_props[0].quark == 0) {
    prop_desc_list_calculate_quarks(chronoline_props);
  }
  return chronoline_props;
}    

static PropOffset chronoline_offsets[] = {
  ELEMENT_COMMON_PROPERTIES_OFFSETS,
  PROP_OFFSET_STD_NOTEBOOK_BEGIN,

  PROP_OFFSET_NOTEBOOK_PAGE("data"),
  { "name", PROP_TYPE_STRING, offsetof(Chronoline,name)},
  { "events", PROP_TYPE_MULTISTRING, offsetof(Chronoline,events)},
  { "help", PROP_TYPE_STATIC, 0},

  PROP_OFFSET_NOTEBOOK_PAGE("parameters"),
  { "start_time",PROP_TYPE_REAL, offsetof(Chronoline,start_time)},
  { "end_time",PROP_TYPE_REAL, offsetof(Chronoline,end_time)},
  { "rise_time",PROP_TYPE_REAL, offsetof(Chronoline,rise_time)},
  { "fall_time",PROP_TYPE_REAL, offsetof(Chronoline,fall_time)},
  { "multibit",PROP_TYPE_BOOL, offsetof(Chronoline,multibit)},

  PROP_OFFSET_NOTEBOOK_PAGE("aspect"),
  { "data_color", PROP_TYPE_COLOUR, offsetof(Chronoline,data_color)},
  { "data_lwidth", PROP_TYPE_REAL, offsetof(Chronoline,data_lwidth)},
  { "color", PROP_TYPE_COLOUR, offsetof(Chronoline,color)},
  { "main_lwidth", PROP_TYPE_REAL, offsetof(Chronoline,main_lwidth)},
  { "font", PROP_TYPE_FONT, offsetof(Chronoline,font)},
  { "font_size", PROP_TYPE_REAL, offsetof(Chronoline,font_size)},
  { "font_color", PROP_TYPE_COLOUR, offsetof(Chronoline,font_color)},

  PROP_OFFSET_STD_NOTEBOOK_END,
  {NULL}
};

static void
chronoline_get_props(Chronoline *chronoline, GPtrArray *props)
{  
  object_get_props_from_offsets(&chronoline->element.object,
                                chronoline_offsets,props);
}

static void
chronoline_set_props(Chronoline *chronoline, GPtrArray *props)
{
  object_set_props_from_offsets(&chronoline->element.object,
                                chronoline_offsets,props);
  chronoline_update_data(chronoline);
}

static real
chronoline_distance_from(Chronoline *chronoline, Point *point)
{
  DiaObject *obj = &chronoline->element.object;
  return distance_rectangle_point(&obj->bounding_box, point);
}

static void
chronoline_select(Chronoline *chronoline, Point *clicked_point,
	   DiaRenderer *interactive_renderer)
{
  element_update_handles(&chronoline->element);
}

static ObjectChange*
chronoline_move_handle(Chronoline *chronoline, Handle *handle,
		       Point *to, ConnectionPoint *cp,
		       HandleMoveReason reason, ModifierKeys modifiers)
{
  g_assert(chronoline!=NULL);
  g_assert(handle!=NULL);
  g_assert(to!=NULL);

  element_move_handle(&chronoline->element, handle->id, to, cp, 
		      reason, modifiers);
  chronoline_update_data(chronoline);

  return NULL;
}

static ObjectChange*
chronoline_move(Chronoline *chronoline, Point *to)
{
  chronoline->element.corner = *to;
  chronoline_update_data(chronoline);

  return NULL;
}

static void 
cld_onebit(Chronoline *chronoline,
	   DiaRenderer *renderer,
	   real x1,CLEventType s1,
	   real x2,CLEventType s2,
	   gboolean fill)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Point pts[4];
  real y_down = chronoline->y_down;
  real y_up = chronoline->y_up;
  
  pts[0].x = pts[1].x = x1;
  pts[2].x = pts[3].x = x2;

  pts[0].y = pts[3].y = chronoline->y_down;
  pts[1].y = (s1 == CLE_OFF)?y_down:y_up;
  pts[2].y = (s2 == CLE_OFF)?y_down:y_up;

  if (fill) {
    if ((s1 == CLE_UNKNOWN) || (s2 == CLE_UNKNOWN)) {
      renderer_ops->fill_polygon(renderer,pts,sizeof(pts)/sizeof(pts[0]),
				  &chronoline->datagray);
    } else {
      renderer_ops->fill_polygon(renderer,pts,sizeof(pts)/sizeof(pts[0]),
				  &color_white);
    }    
  } else {
    renderer_ops->draw_line(renderer,&pts[1],&pts[2],
			     &chronoline->data_color);
  }
}


static void 
cld_multibit(Chronoline *chronoline,
	     DiaRenderer *renderer,
	     real x1,CLEventType s1,
	     real x2,CLEventType s2,
	     gboolean fill)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Point pts[4];
  real ymid = .5 * (chronoline->y_down + chronoline->y_up);
  real y_down = chronoline->y_down;
  real y_up = chronoline->y_up;

  pts[0].x = pts[1].x = x1;
  pts[2].x = pts[3].x = x2;

  if (s1 == CLE_OFF) {
    pts[0].y = pts[1].y = ymid;
  } else {
    pts[0].y = y_down;
    pts[1].y = y_up;
  }
  if (s2 == CLE_OFF) {
    pts[2].y = pts[3].y = ymid;
  } else {
    pts[3].y = y_down;
    pts[2].y = y_up;
  }

  if (fill) { 
    if ((s1 == CLE_UNKNOWN) || (s2 == CLE_UNKNOWN)) {
      renderer_ops->fill_polygon(renderer,pts,sizeof(pts)/sizeof(pts[0]),
				  &chronoline->datagray);
    } else {
      renderer_ops->fill_polygon(renderer,pts,sizeof(pts)/sizeof(pts[0]),
				  &color_white);
    }    
  } else {
    renderer_ops->draw_line(renderer,&pts[1],&pts[2],
			     &chronoline->data_color);
    renderer_ops->draw_line(renderer,&pts[0],&pts[3],
			     &chronoline->data_color);
  }
}



inline static void
chronoline_draw_really(Chronoline *chronoline, DiaRenderer *renderer, 
		       gboolean fill)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Element *elem = &chronoline->element;
  real oldx,newx;

  gboolean finished = FALSE;
  CLEventType state = CLE_UNKNOWN;

  CLEventList *lst;
  CLEvent *evt;
  real start_time = chronoline->start_time;
  real end_time = chronoline->end_time;
  
  oldx = elem->corner.x;

  lst = chronoline->evtlist;
  renderer_ops->set_linejoin(renderer, LINEJOIN_MITER);
  renderer_ops->set_linestyle(renderer, LINESTYLE_SOLID);
  renderer_ops->set_linewidth(renderer,chronoline->data_lwidth);

  while (lst) {
    evt = (CLEvent *)lst->data;
    g_assert(evt);
    if (evt->time >= start_time) {
      if (evt->time <= end_time) {
	/* regular point */
	newx = evt->x;
	
	if (chronoline->multibit) 
	  cld_multibit(chronoline,renderer,oldx,state,newx,evt->type,fill);
	else
	  cld_onebit(chronoline,renderer,oldx,state,newx,evt->type,fill);
	oldx = newx;
      } else {
	newx = elem->corner.x + elem->width;
	if (!finished) {
	  if (chronoline->multibit) 
	    cld_multibit(chronoline,renderer,oldx,state,newx,evt->type,fill);
	  else
	    cld_onebit(chronoline,renderer,oldx,state,newx,evt->type,fill);
	  finished = TRUE;
	} 
      }	
    }
    state = evt->type;
    lst = g_slist_next(lst);
  }
  if (!finished) {
    newx = chronoline->element.corner.x+chronoline->element.width;
    if (chronoline->multibit) 
      cld_multibit(chronoline,renderer,oldx,state,newx,state,fill);
    else
      cld_onebit(chronoline,renderer,oldx,state,newx,state,fill);
  }    
}

static void
chronoline_draw(Chronoline *chronoline, DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Element *elem;
  Point lr_corner;
  Point p1,p2,p3;

  g_assert(chronoline != NULL);
  g_assert(renderer != NULL);

  elem = &chronoline->element;

  renderer_ops->set_linejoin(renderer, LINEJOIN_MITER);
  renderer_ops->set_linestyle(renderer, LINESTYLE_DOTTED);
  renderer_ops->set_linewidth(renderer, chronoline->main_lwidth);
  p1.x = elem->corner.x + elem->width;
  p1.y = elem->corner.y;
  renderer_ops->draw_line(renderer,&elem->corner,&p1,&chronoline->gray);

  chronoline_draw_really(chronoline,renderer,TRUE);
  chronoline_draw_really(chronoline,renderer,FALSE);

  renderer_ops->set_linestyle(renderer, LINESTYLE_SOLID);

  lr_corner.x = elem->corner.x + elem->width;
  lr_corner.y = elem->corner.y + elem->height;

  p1.x = elem->corner.x;
  p2.x = lr_corner.x;
  p1.y = p2.y = chronoline->y_down;

  renderer_ops->set_linewidth(renderer, chronoline->main_lwidth);
  renderer_ops->draw_line(renderer,&p1,&p2,&chronoline->color);
  p2.x = p1.x = elem->corner.x;
  p1.y = chronoline->y_down;
  p2.y = chronoline->y_up;
  renderer_ops->draw_line(renderer,&p1,&p2,&chronoline->color);

  renderer_ops->set_font(renderer, chronoline->font, 
                          chronoline->font_size);
  p3.y = lr_corner.y - chronoline->font_size 
      + dia_font_ascent(chronoline->name,
                        chronoline->font,chronoline->font_size);
  p3.x = p1.x - chronoline->main_lwidth;
  renderer_ops->draw_string(renderer,chronoline->name,&p3,ALIGN_RIGHT,
			     &chronoline->color);
}

inline static void grayify(Color *col,Color *src)
{
  col->red = .5 * (src->red + color_white.red);
  col->green = .5 * (src->green + color_white.green);
  col->blue = .5 * (src->blue + color_white.blue);
}

static void
chronoline_update_data(Chronoline *chronoline)
{
  Element *elem = &chronoline->element;
  DiaObject *obj = &elem->object;
  real time_span;
  Point ur_corner;
  int shouldbe,i;
  real realheight;
  CLEventList *lst;
  CLEvent *evt;  
  GSList *conn_elem;
  ElementBBExtras *extra = &elem->extra_spacing;

  grayify(&chronoline->datagray,&chronoline->data_color);
  grayify(&chronoline->gray,&chronoline->color);

  chronoline->labelwidth = dia_font_string_width(chronoline->name,
                                                 chronoline->font,
                                                 chronoline->font_size);

  chronoline->y_up = elem->corner.y;
  chronoline->y_down = elem->corner.y + elem->height;
  
  /* Now, update the drawing helper counters */
  time_span = chronoline->end_time - chronoline->start_time;
  if (time_span == 0) {
    chronoline->end_time = chronoline->start_time + .1;
    time_span = .1;
  } else if (time_span < 0) {
    chronoline->start_time = chronoline->end_time;
    time_span = -time_span;
    chronoline->end_time = chronoline->start_time + time_span;
  }

  extra->border_trans = chronoline->main_lwidth / 2;
  element_update_boundingbox(elem);

  /* fix boundingbox for special extras: */
  realheight = obj->bounding_box.bottom - obj->bounding_box.top;
  realheight = MAX(realheight,chronoline->font_size);
  
  obj->bounding_box.left -= chronoline->labelwidth;
  obj->bounding_box.bottom = obj->bounding_box.top + realheight + 
    chronoline->main_lwidth;
  
  obj->position = elem->corner;
  
  element_update_handles(elem);

  /* Update connections: */
  ur_corner.x = elem->corner.x + elem->width;
  ur_corner.y = elem->corner.y;

  /* Update the events : count those which fit in [start_time,end_time].
   */
  
  reparse_clevent(chronoline->events,
		  &chronoline->evtlist,
		  &chronoline->checksum,
		  chronoline->rise_time,
		  chronoline->fall_time,
		  chronoline->end_time);
  shouldbe = 0;
  lst = chronoline->evtlist;

  while (lst) {
    evt = (CLEvent *)lst->data;
    
    if ((evt->time >= chronoline->start_time) && 
	(evt->time <= chronoline->end_time))
      shouldbe++;
    lst = g_slist_next(lst);
  }

  connpointline_adjust_count(chronoline->snap,shouldbe,&ur_corner);
  connpointline_update(chronoline->snap);
  /* connpointline_putonaline(chronoline->snap,&elem->corner,&ur_corner); */

  /* Now fix the actual connection point positions : */
  lst = chronoline->evtlist;
  conn_elem = chronoline->snap->connections;
  i = 0;
  while (lst && lst->data && conn_elem && conn_elem->data) {
    ConnectionPoint *cp = (ConnectionPoint *)(conn_elem->data);
    evt = (CLEvent *)lst->data;
    
    if ((evt->time >= chronoline->start_time) && 
	(evt->time <= chronoline->end_time)) {
      evt->x = elem->corner.x + 
	elem->width*(evt->time-chronoline->start_time)/time_span;
      g_assert(cp);
      g_assert(i < chronoline->snap->num_connections);
      cp->pos.x = evt->x;
      if (chronoline->multibit) {
	cp->pos.y = .5 * (chronoline->y_down + chronoline->y_up);
	cp->directions = DIR_ALL;
      } else {
	cp->pos.y = (evt->type==CLE_OFF?
		     chronoline->y_down:chronoline->y_up);
	cp->directions = (evt->type==CLE_OFF?DIR_SOUTH:DIR_NORTH);
      }
      i++;
      conn_elem = g_slist_next(conn_elem);
    } else if (evt->time >= chronoline->start_time) {
      evt->x = elem->corner.x; 
    } else if (evt->time <= chronoline->end_time) {
      evt->x = elem->corner.x + elem->width;
    }
    lst = g_slist_next(lst);
  }
}

static DiaObject *
chronoline_create(Point *startpoint,
	   void *user_data,
	   Handle **handle1,
	   Handle **handle2)
{
  Chronoline *chronoline;
  Element *elem;
  DiaObject *obj;

  chronoline = g_new0(Chronoline,1);
  elem = &(chronoline->element);

  obj = &(chronoline->element.object);
  obj->type = &chronoline_type;
  obj->ops = &chronoline_ops;

  chronoline->snap = connpointline_create(obj,0); 

  elem->corner = *startpoint;
  elem->width = 20.0;
  elem->height = 3.0;

  element_init(elem, 8, 0);

  chronoline->name = g_strdup("");
  chronoline->events = g_strdup("");

  chronoline->font = dia_font_new_from_style (DIA_FONT_SANS,1.0);
  chronoline->font_size = 1.0;
  chronoline->font_color = color_black;
  chronoline->start_time = 0.0;
  chronoline->end_time = 20.0;
  chronoline->rise_time = .3;
  chronoline->fall_time = .3;
  chronoline->color = color_black;
  chronoline->main_lwidth = .1;
  chronoline->data_lwidth = .1;
  chronoline->data_color.red = 1.0;
  chronoline->data_color.green = 0.0;
  chronoline->data_color.blue = 0.0;
  chronoline->multibit = FALSE;
  
  chronoline->evtlist = NULL;
  chronoline_update_data(chronoline);

  *handle1 = NULL;
  *handle2 = obj->handles[7];  
  return &chronoline->element.object;
}

static void 
chronoline_destroy(Chronoline *chronoline)
{
  g_free(chronoline->name);
  g_free(chronoline->events);
  dia_font_unref(chronoline->font);
  connpointline_destroy(chronoline->snap);
  destroy_clevent_list(chronoline->evtlist);
  element_destroy(&chronoline->element);
}

static DiaObject *
chronoline_load(ObjectNode obj_node, int version, const char *filename)
{
  return object_load_using_properties(&chronoline_type,
                                      obj_node,version,filename);  
}
