/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998, 1999 Alexander Larsson
 *
 * 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 <string.h>


#include "orth_conn.h"
#include "diarenderer.h"

#include "pixmaps/transition.xpm"

#include "uml.h"

typedef struct _Transition Transition;

struct _Transition {
  OrthConn orth;
  
  Handle trigger_text_handle;
  Point trigger_text_pos;
  gchar *trigger_text;
  gchar *action_text;
  Handle guard_text_handle;
  Point guard_text_pos;
  gchar *guard_text;
  gboolean direction_inverted;
};


#define TRANSITION_WIDTH 0.1
#define TRANSITION_FONTHEIGHT 0.8
#define TRANSITION_ARROWLEN 0.5
#define TRANSITION_ARROWWIDTH 0.5
#define HANDLE_MOVE_TRIGGER_TEXT (HANDLE_CUSTOM2)
#define HANDLE_MOVE_GUARD_TEXT (HANDLE_CUSTOM3)
#define TEXT_HANDLE_DISTANCE_FROM_STARTPOINT 0.5

static DiaFont *transition_font = NULL;

static DiaObject *transition_create(Point *startpoint,
                                    void *user_data,
                                    Handle **handle1,
                                    Handle **handle2);
static DiaObject *transition_load(ObjectNode obj_node, int version,
                                  const char *filename);

static void transition_destroy(Transition* transition);
static void transition_draw(Transition* transition, DiaRenderer* ddisp);
static real transition_distance(Transition* transition, Point* point);
static DiaMenu* transition_get_object_menu(Transition* transition,
                                           Point* clickedpoint);
static void transition_select(Transition* obj, 
                              Point* clicked_point,
                              DiaRenderer* interactive_renderer);
static ObjectChange* transition_move(Transition* transition, Point* pos);
static ObjectChange* transition_move_handle(Transition*      transition,
                                            Handle*          handle,
                                            Point*           pos,
                                            ConnectionPoint* cp,
                                            HandleMoveReason reason,
                                            ModifierKeys     modifiers);
static void transition_set_props(Transition *transition, GPtrArray *props);
static void transition_get_props(Transition *transition, GPtrArray *props);
static PropDescription *transition_describe_props(Transition *transition);
static void uml_transition_update_data(Transition *transition);
static ObjectChange* transition_add_segment_cb(DiaObject *obj,
                                               Point *clicked_point,
                                               gpointer data);
static ObjectChange* transition_del_segment_cb(DiaObject *obj,
                                               Point *clicked_point,
                                               gpointer data);          

static ObjectTypeOps uml_transition_type_ops = {
  (CreateFunc)transition_create,
  (LoadFunc)transition_load,
  (SaveFunc)object_save_using_properties,
  (GetDefaultsFunc)NULL, 
  (ApplyDefaultsFunc)NULL
};

/* The "uml_" prefix is used to avoid clashing with the 
   GRAFCET transition */
DiaObjectType uml_transition_type = {
  "UML - Transition",       /* Name */
  /* Version 0 had no autorouting and so shouldn't have it set by default. */
  /* version 0 and 1 expects the arrow to be drawn on the wrong end */ 
  2,                      /* version */
  (char **) transition_xpm, /* Pixmap */
  &uml_transition_type_ops
};


/* These methods are described in the object.h file */
static ObjectOps uml_transition_ops = {
  (DestroyFunc)         transition_destroy,                        
  (DrawFunc)            transition_draw,
  (DistanceFunc)        transition_distance,
  (SelectFunc)          transition_select,
  (CopyFunc)            object_copy_using_properties,
  (MoveFunc)            transition_move,
  (MoveHandleFunc)      transition_move_handle,
  (GetPropertiesFunc)   object_create_props_dialog,
  (ApplyPropertiesFunc) object_apply_props_from_dialog,
  (ObjectMenuFunc)      transition_get_object_menu,
  (DescribePropsFunc)   transition_describe_props,
  (GetPropsFunc)        transition_get_props,
  (SetPropsFunc)        transition_set_props
};

static PropDescription transition_props[] = {
  ORTHCONN_COMMON_PROPERTIES,
  { "trigger", PROP_TYPE_STRING, PROP_FLAG_VISIBLE, N_("Trigger"), 
    N_("The event that causes this transition to be taken"), NULL },
  { "action", PROP_TYPE_STRING, PROP_FLAG_VISIBLE, N_("Action"),
    N_("Action to perform when this transition is taken"), NULL },
  { "guard", PROP_TYPE_STRING, PROP_FLAG_VISIBLE, N_("Guard"), 
    N_("Condition for taking this transition when the event is fired"), NULL },
  { "trigger_text_pos", PROP_TYPE_POINT, 0, "trigger_text_pos:", NULL, NULL },
  { "guard_text_pos", PROP_TYPE_POINT, 0, "guard_text_pos:", NULL, NULL },
  { "direction_inverted", PROP_TYPE_BOOL, PROP_FLAG_OPTIONAL, "direction_inverted:", NULL, NULL },
  PROP_DESC_END
};


static PropOffset transition_offsets[] = {
  ORTHCONN_COMMON_PROPERTIES_OFFSETS,           
  { "trigger", PROP_TYPE_STRING, offsetof(Transition, trigger_text) },
  { "action", PROP_TYPE_STRING, offsetof(Transition, action_text) },
  { "guard", PROP_TYPE_STRING, offsetof(Transition, guard_text) },
  { "trigger_text_pos", PROP_TYPE_POINT, offsetof(Transition,trigger_text_pos)},
  { "guard_text_pos", PROP_TYPE_POINT,   offsetof(Transition,guard_text_pos)},
  { "direction_inverted", PROP_TYPE_BOOL, offsetof(Transition,direction_inverted)},
  { NULL, 0, 0 }
};


#define TRANSITION_MENU_ADD_SEGMENT_OFFSET 0 
#define TRANSITION_MENU_DEL_SEGMENT_OFFSET 1
#define TRANSITION_MENU_OFFSET_TO_ORTH_COMMON 2
static DiaMenuItem transition_menu_items[] = {
  { N_("Add segment"), transition_add_segment_cb, NULL, 0 } ,
  { N_("Delete segment"), transition_del_segment_cb, NULL, 0 } ,
  ORTHCONN_COMMON_MENUS
};


static DiaMenu transition_menu = {
  "Transition",
  sizeof(transition_menu_items)/sizeof(DiaMenuItem),
  transition_menu_items,
  NULL
};


static DiaObject *
transition_create(Point *startpoint,
                  void *user_data,
                  Handle **handle1,
                  Handle **handle2)
{
  Transition *transition;
  OrthConn *orth;
  DiaObject *obj;
  Point temp_point;
  
  if (transition_font == NULL) {
    transition_font = 
      dia_font_new_from_style (DIA_FONT_SANS, TRANSITION_FONTHEIGHT);
  }

  transition = g_malloc0(sizeof(Transition));
  
  orth = &transition->orth;
  obj = &orth->object;
  obj->type = &uml_transition_type;
  obj->ops = &uml_transition_ops;
  
  orthconn_init(orth, startpoint);
  
  /* Prepare the handles for trigger and guard text */
  transition->trigger_text_handle.id = HANDLE_MOVE_TRIGGER_TEXT;
  transition->trigger_text_handle.type = HANDLE_MINOR_CONTROL;
  transition->trigger_text_handle.connect_type = HANDLE_NONCONNECTABLE;
  transition->trigger_text_handle.connected_to = NULL;
  temp_point = *startpoint;
  temp_point.y -= TEXT_HANDLE_DISTANCE_FROM_STARTPOINT;
  transition->trigger_text_pos = temp_point;
  transition->trigger_text_handle.pos = temp_point;
  object_add_handle(obj, &transition->trigger_text_handle);  
  
  transition->guard_text_handle.id = HANDLE_MOVE_GUARD_TEXT;
  transition->guard_text_handle.type = HANDLE_MINOR_CONTROL;
  transition->guard_text_handle.connect_type = HANDLE_NONCONNECTABLE;
  transition->guard_text_handle.connected_to = NULL;
  temp_point = *startpoint;
  temp_point.y += TEXT_HANDLE_DISTANCE_FROM_STARTPOINT;
  transition->guard_text_pos = transition->guard_text_handle.pos = temp_point;
  object_add_handle(obj, &transition->guard_text_handle);
  
  transition->guard_text = NULL;
  transition->trigger_text = NULL;
  transition->action_text = NULL;
  
  uml_transition_update_data(transition);
   
  *handle1 = obj->handles[0];
  *handle2 = obj->handles[1];
  return obj;
}

static DiaObject *transition_load(ObjectNode obj_node, int version,
                                  const char *filename)
{
  DiaObject *obj = object_load_using_properties(&uml_transition_type,
                                                obj_node,version,filename);
  if (version == 0) {
    AttributeNode attr;
    /* In old objects with no autorouting, set it to false. */
    attr = object_find_attribute(obj_node, "autorouting");
    if (attr == NULL)
      ((OrthConn*)obj)->autorouting = FALSE;
  }
  if (version < 2) {
      /* Versions prior to 2 have the arrowheads inverted */
      ((Transition*)obj)->direction_inverted = TRUE;
  }
  return obj;
}

static void
transition_set_props(Transition *transition, GPtrArray *props)
{
  object_set_props_from_offsets(&transition->orth.object,
                                transition_offsets, props);
  uml_transition_update_data(transition);
}


static void
transition_get_props(Transition *transition, GPtrArray *props)
{
  object_get_props_from_offsets(&transition->orth.object, 
                                transition_offsets, props);
}

static PropDescription *
transition_describe_props(Transition *transition)
{
  if (transition_props[0].quark == 0)
    prop_desc_list_calculate_quarks(transition_props);
  return transition_props;

}

static void
transition_destroy(Transition* transition)
{
  orthconn_destroy(&transition->orth);
}

static DiaMenu*
transition_get_object_menu(Transition* transition, Point* clickedpoint)
{
  OrthConn *orth = &transition->orth;
  /* Set/clear the active flag of the add/remove segment according to the 
     placement of mouse pointer and placement of the transition and its handles */
  transition_menu_items[TRANSITION_MENU_ADD_SEGMENT_OFFSET].active =
      orthconn_can_add_segment(orth, clickedpoint);
  transition_menu_items[TRANSITION_MENU_DEL_SEGMENT_OFFSET].active =
      orthconn_can_delete_segment(orth, clickedpoint);
      
  orthconn_update_object_menu(orth, clickedpoint, 
             &transition_menu_items[TRANSITION_MENU_OFFSET_TO_ORTH_COMMON]);
  return &transition_menu;
}

static gchar* create_event_action_text(Transition* transition)
{
  gchar *temp_text;  
  if (transition->action_text && strlen(transition->action_text) != 0)
  {
    temp_text = g_strdup_printf("%s/%s", transition->trigger_text,
                                         transition->action_text);
  }
  else
  {
	  temp_text = g_strdup_printf("%s", transition->trigger_text ? transition->trigger_text : "");
  }
  return temp_text;
}

static gchar* create_guard_text(Transition* transition)
{
  gchar *temp_text;  
  temp_text = g_strdup_printf("[%s]", transition->guard_text); 
  return temp_text;
}

static void transition_draw(Transition* transition, DiaRenderer* renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS(renderer);
  Arrow arrow;
  Arrow *start_arrow;
  Arrow *end_arrow;
  Point* points;
  OrthConn *orth = &transition->orth;
  int num_points;

  assert(transition != NULL);
  assert(renderer != NULL);


  /* Draw the arrow / line */
  points = &orth->points[0];
  num_points = orth->numpoints;
  
  arrow.type = ARROW_LINES;
  arrow.length = TRANSITION_ARROWLEN;
  arrow.width = TRANSITION_ARROWWIDTH;
  
  
  /* Is it necessary to call set_linewidth? The draw_line_with_arrows() method
     got a linewidth parameter... */
  renderer_ops->set_linewidth(renderer, TRANSITION_WIDTH);
  /* TODO, find out about the meaning of this... */
  renderer_ops->set_linecaps(renderer, LINECAPS_BUTT); 

  if (transition->direction_inverted) {
      start_arrow = &arrow;
      end_arrow = NULL;
  }
  else {
      start_arrow = NULL;
      end_arrow = & arrow;
  }
  renderer_ops->draw_polyline_with_arrows(renderer, points, num_points,
                                          TRANSITION_WIDTH,
                                          &color_black, /* TODO, allow colors */
                                          start_arrow, end_arrow);


  renderer_ops->set_font(renderer, transition_font,
                         TRANSITION_FONTHEIGHT);
                                      
  /* Draw the guard text */
  if (transition->guard_text && strlen(transition->guard_text) != 0)
  {
    gchar *text = create_guard_text(transition);
    renderer_ops->draw_string(renderer,
                              text,
                              &transition->guard_text_pos,
                              ALIGN_CENTER,
                              &color_black); /* TODO, allow other colors */
    g_free(text);
  }
  
  /* Draw the trigger text */
  if (transition->trigger_text && strlen(transition->trigger_text) != 0)
  {
    gchar *text = create_event_action_text(transition);
    renderer_ops->draw_string(renderer,
                              text,
                              &transition->trigger_text_pos,
                              ALIGN_CENTER,
                              &color_black); /* TODO, allow other colors */
    g_free(text);
  }
  
}
                                                     
static real
transition_distance(Transition* transition, Point* point)
{
  return orthconn_distance_from(&transition->orth, point, TRANSITION_WIDTH);
}

static void                                                          
transition_select(Transition* transition,
                  Point* clicked_point,                      
                  DiaRenderer* interactive_renderer)
{                                                                             
  uml_transition_update_data(transition);
}
                                       
static ObjectChange*
transition_move(Transition* transition, Point* newpos)
{
  Point delta;
  ObjectChange *change;

  
  /* Find a delta in order to move the text handles along with the transition */
  delta = *newpos;
  point_sub(&delta, &transition->orth.points[0]);
  
  change = orthconn_move(&transition->orth, newpos);

  /* Move the text handles */ 
  point_add(&transition->trigger_text_pos, &delta);
  point_add(&transition->guard_text_pos, &delta);
  
  uml_transition_update_data(transition);

  return change;
}

static ObjectChange* 
transition_move_handle(Transition*      transition,
                       Handle*          handle,
                       Point*           newpos,
                       ConnectionPoint* cp,
                       HandleMoveReason reason,
                       ModifierKeys     modifiers)
{
  ObjectChange *change = NULL;
  
  assert(transition != NULL);
  assert(handle != NULL);
  assert(newpos != NULL);
  
  switch (handle->id)
  {
    case HANDLE_MOVE_TRIGGER_TEXT:
      /* The trigger text is being moved */
      transition->trigger_text_pos = *newpos;
      break;
    
    case HANDLE_MOVE_GUARD_TEXT:
      /* The guard text is being moved */
      transition->guard_text_pos = *newpos;
      break;
    
    default:
      /* Tell the connection that one of its handles is being moved */
      orthconn_move_handle(&transition->orth, handle, newpos, cp, reason,
                           modifiers);
      break;
  }
  
  /* Update ourselves to reflect the new handle position */
  uml_transition_update_data(transition);

  return change;
}

static ObjectChange* transition_add_segment_cb(DiaObject *obj,
                                               Point *clickedpoint,
                                               gpointer data)
{
  ObjectChange *change;
  change = orthconn_add_segment((OrthConn*)obj, clickedpoint);
  uml_transition_update_data((Transition*)obj);
  return change;
}
                                               
                                            
static ObjectChange* transition_del_segment_cb(DiaObject *obj,
                                               Point *clickedpoint,
                                               gpointer data)             
{
  ObjectChange *change;
  change = orthconn_delete_segment((OrthConn*)obj, clickedpoint);
  uml_transition_update_data((Transition*)obj);
  return change;
}



static void expand_bbox_for_text(Rectangle* bbox,
                                 Point* text_pos,
                                 gchar* text)
{
  Rectangle text_box;
  real text_width;

  text_width = dia_font_string_width(text, transition_font,
                                     TRANSITION_FONTHEIGHT);
  text_box.left = text_pos->x - text_width/2;
  text_box.right = text_box.left + text_width;
  text_box.top = text_pos->y - dia_font_ascent(text, transition_font, 
                                              TRANSITION_FONTHEIGHT);
  text_box.bottom = text_box.top + TRANSITION_FONTHEIGHT;
  rectangle_union(bbox, &text_box);
    
}

static void
uml_transition_update_data(Transition *transition)
{
  gchar *temp_text;  
  Point *points;
  
  /* Setup helpful pointers as shortcuts */
  OrthConn *orth = &transition->orth;
  DiaObject *obj = &orth->object;
  PolyBBExtras *extra = &orth->extra_spacing;
  points = &orth->points[0];
  
  /* Set the transitions position */
  obj->position = points[0];
  transition->trigger_text_handle.pos = transition->trigger_text_pos;
  transition->guard_text_handle.pos = transition->guard_text_pos;
  
  /* Update the orthogonal connection to match the new data */
  orthconn_update_data(orth);

  extra->start_long = extra->end_long 
                    = extra->middle_trans
                    = TRANSITION_WIDTH/2.0;
  extra->start_trans = extra->end_trans
                     = MAX(TRANSITION_ARROWLEN, TRANSITION_WIDTH/2.0);

  /* Update the bounding box to match the new connection data */
  orthconn_update_boundingbox(orth);
  /* Update the bounding box to match the new trigger text size and position */
  temp_text = create_event_action_text(transition); 
  expand_bbox_for_text(&obj->bounding_box, &transition->trigger_text_pos, 
                       temp_text);
  g_free(temp_text);
  /* Update the bounding box to match the new guard text size and position */
  temp_text = g_strdup_printf("[%s]", transition->guard_text ? transition->guard_text : ""); 
  expand_bbox_for_text(&obj->bounding_box, &transition->guard_text_pos, 
                       temp_text); 
  g_free(temp_text);
}

