/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998 Alexander Larsson
 * Association updates Copyright(c) 2004 David Klotzbach
 *
 * 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.
 *
 *
 */

/*--------------------------------------------------------------------------**
** In the fall of 2004, I started to use the UML portion of this dia        **
** program in earnest. I had been using several commercial programs in my   **
** work and wanted something I could use for my "home" projects. Even       **
** though Dia was advertised has having complete support for the UML static **
** structures, I found that I was used to having a design tool that was     **
** much closer to the language as described by the OMG and the Three        **
** Amigos.                                                                  **
** Always willing to give back to the culture that has supported me for the **
** last 30+ years, I have endeavored to make the UML portion of Dia more    **
** compliant with the standard as is is currently described.                **
** My changes do not include any of the enhancements as described in        **
** UML 2.0.                                                                 **
**                                                                          **
** Association change Oct 31, 2004 - Dave Klotzbach                         **
** dklotzbach@foxvalley.net                                                 **
**                                                                          **
** Now for a description of what I have done here in Association. To begin  **
** with, the implementation of Association is darn near complete. However,  **
** as described in "The Unified Modeling Language Users Guide", the roles   **
** that an association may have do have "visibility" attributes, and these  **
** attributes apply independently to each of the roles.                     **
**                                                                          **
** What is still missing from this version are the concepts of              **
** "Qualification", "Interface Specifier" and "Association Classes"         **
**--------------------------------------------------------------------------*/

/* DO NOT USE THIS OBJECT AS A BASIS FOR A NEW OBJECT. */

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

#include <assert.h>
#include <gtk/gtk.h>
#include <math.h>
#include <string.h>

#include "intl.h"
#include "object.h"
#include "orth_conn.h"
#include "diarenderer.h"
#include "attributes.h"
#include "arrows.h"
#include "uml.h"

#include "properties.h"

#include "pixmaps/association.xpm"

extern char visible_char[];	/* The definitions are in UML.c. Used here to avoid getting out of sync */

typedef struct _Association Association;
typedef struct _AssociationState AssociationState;
typedef struct _AssociationPropertiesDialog AssociationPropertiesDialog;

typedef enum {
  ASSOC_NODIR,
  ASSOC_RIGHT,
  ASSOC_LEFT
} AssociationDirection;

typedef enum {
  AGGREGATE_NONE,
  AGGREGATE_NORMAL,
  AGGREGATE_COMPOSITION
} AggregateType;

typedef struct _AssociationEnd {
  gchar *role; /* Can be NULL */
  gchar *multiplicity; /* Can be NULL */
  Point text_pos;
  real text_width;
  real role_ascent;
  real role_descent;
  real multi_ascent;
  real multi_descent;
  Alignment text_align;
  UMLVisibility visibility;	/* This value is only relevant if role is not null */
  
  int arrow;
  AggregateType aggregate; /* Note: Can only be != NONE on ONE side! */
} AssociationEnd;

struct _AssociationState {
  ObjectState obj_state;

  gchar *name;
  AssociationDirection direction;

  struct {
    gchar *role;
    gchar *multiplicity;
    UMLVisibility visibility;	/* This value is only relevant if role is not null */

    int arrow;
    AggregateType aggregate;
  } end[2];
};


struct _Association {
  OrthConn orth;

  Point text_pos;
  Alignment text_align;
  real text_width;
  real ascent;
  real descent;
    
  gchar *name;
  AssociationDirection direction;

  AssociationEnd end[2];
  
  AssociationPropertiesDialog* properties_dialog;
};

struct _AssociationPropertiesDialog {
  GtkWidget *dialog;
  
  GtkEntry *name;
  GtkMenu *dir_menu;
  GtkOptionMenu *dir_omenu;
  
  struct {
    GtkEntry *role;
    GtkEntry *multiplicity;
    GtkMenu  *attr_visible;
    GtkOptionMenu   *attr_visible_button;
    GtkToggleButton *draw_arrow;
    GtkToggleButton *aggregate;
    GtkToggleButton *composition;
  } end[2];
};

#define ASSOCIATION_WIDTH 0.1
#define ASSOCIATION_TRIANGLESIZE 0.8
#define ASSOCIATION_DIAMONDLEN 1.4
#define ASSOCIATION_DIAMONDWIDTH 0.7
#define ASSOCIATION_FONTHEIGHT 0.8
#define ASSOCIATION_END_SPACE 0.2
static DiaFont *assoc_font = NULL;

static real association_distance_from(Association *assoc, Point *point);
static void association_select(Association *assoc, Point *clicked_point,
			       DiaRenderer *interactive_renderer);
static ObjectChange* association_move_handle(Association *assoc, Handle *handle,
					     Point *to, ConnectionPoint *cp,
					     HandleMoveReason reason, ModifierKeys modifiers);
static ObjectChange* association_move(Association *assoc, Point *to);
static void association_draw(Association *assoc, DiaRenderer *renderer);
static DiaObject *association_create(Point *startpoint,
				  void *user_data,
				  Handle **handle1,
				  Handle **handle2);
static void association_destroy(Association *assoc);
static DiaObject *association_copy(Association *assoc);
static GtkWidget *association_get_properties(Association *assoc, gboolean is_default);
static ObjectChange *association_apply_properties(Association *assoc);
static DiaMenu *association_get_object_menu(Association *assoc,
					    Point *clickedpoint);
static PropDescription *association_describe_props(Association *assoc);
static void association_get_props(Association *assoc, GPtrArray *props);
static void association_set_props(Association *assoc, GPtrArray *props);

static AssociationState *association_get_state(Association *assoc);
static void association_set_state(Association *assoc,
				  AssociationState *state);

static void association_save(Association *assoc, ObjectNode obj_node,
			     const char *filename);
static DiaObject *association_load(ObjectNode obj_node, int version,
				const char *filename);

static void association_update_data(Association *assoc);
static coord get_aggregate_pos_diff(AssociationEnd *end);

static ObjectTypeOps association_type_ops =
{
  (CreateFunc) association_create,
  (LoadFunc)   association_load,
  (SaveFunc)   association_save
};

DiaObjectType association_type =
{
  "UML - Association",   /* name */
  /* Version 0 had no autorouting and so shouldn't have it set by default. */
  1,                      /* version */
  (char **) association_xpm,  /* pixmap */
  
  &association_type_ops,      /* ops */
  NULL,                 /* pixmap_file */
  0                     /* default_user_data */
};

static ObjectOps association_ops = {
  (DestroyFunc)         association_destroy,
  (DrawFunc)            association_draw,
  (DistanceFunc)        association_distance_from,
  (SelectFunc)          association_select,
  (CopyFunc)            association_copy,
  (MoveFunc)            association_move,
  (MoveHandleFunc)      association_move_handle,
  (GetPropertiesFunc)   association_get_properties,
  (ApplyPropertiesFunc) association_apply_properties,
  (ObjectMenuFunc)      association_get_object_menu,
  (DescribePropsFunc)   association_describe_props,
  (GetPropsFunc)        association_get_props,
  (SetPropsFunc)        association_set_props
};

static PropDescription association_props[] = {
  ORTHCONN_COMMON_PROPERTIES,
  { "name", PROP_TYPE_STRING, PROP_FLAG_VISIBLE,
    N_("Name:"), NULL, NULL },
  PROP_DESC_END
};

static PropDescription *
association_describe_props(Association *assoc)
{
  if (association_props[0].quark == 0) {
    prop_desc_list_calculate_quarks(association_props);
  }
  return association_props;
}

static PropOffset association_offsets[] = {
  ORTHCONN_COMMON_PROPERTIES_OFFSETS,
  { "name", PROP_TYPE_STRING, offsetof(Association, name) },
  { NULL, 0, 0 }
};

static void
association_get_props(Association *assoc, GPtrArray *props)
{
  object_get_props_from_offsets(&assoc->orth.object,
                                association_offsets,props);
}

static void
association_set_props(Association *assoc, GPtrArray *props)
{
  object_set_props_from_offsets(&assoc->orth.object, 
                                association_offsets, props);
  association_update_data(assoc);
}

static real
association_distance_from(Association *assoc, Point *point)
{
  OrthConn *orth = &assoc->orth;
  return orthconn_distance_from(orth, point, ASSOCIATION_WIDTH);
}

static void
association_select(Association *assoc, Point *clicked_point,
		  DiaRenderer *interactive_renderer)
{
  orthconn_update_data(&assoc->orth);
}

static ObjectChange*
association_move_handle(Association *assoc, Handle *handle,
			Point *to, ConnectionPoint *cp,
			HandleMoveReason reason, ModifierKeys modifiers)
{
  ObjectChange *change;
  assert(assoc!=NULL);
  assert(handle!=NULL);
  assert(to!=NULL);
  
  change = orthconn_move_handle(&assoc->orth, handle, to, cp, reason, modifiers);
  association_update_data(assoc);

  return change;
}

static ObjectChange*
association_move(Association *assoc, Point *to)
{
  ObjectChange *change;

  change = orthconn_move(&assoc->orth, to);
  association_update_data(assoc);

  return change;
}

static void
association_draw(Association *assoc, DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  OrthConn *orth = &assoc->orth;
  Point *points;
  Point poly[3];
  int n,i;
  Point pos;
  Arrow startarrow, endarrow;
  
  points = &orth->points[0];
  n = orth->numpoints;
  
  renderer_ops->set_linewidth(renderer, ASSOCIATION_WIDTH);
  renderer_ops->set_linestyle(renderer, LINESTYLE_SOLID);
  renderer_ops->set_linejoin(renderer, LINEJOIN_MITER);
  renderer_ops->set_linecaps(renderer, LINECAPS_BUTT);
  
  startarrow.length = ASSOCIATION_TRIANGLESIZE;
  startarrow.width = ASSOCIATION_TRIANGLESIZE;
  if (assoc->end[0].arrow) {
    startarrow.type = ARROW_LINES;
  } else if (assoc->end[0].aggregate != AGGREGATE_NONE) {
    startarrow.length = ASSOCIATION_DIAMONDLEN;
    startarrow.width = ASSOCIATION_TRIANGLESIZE*0.6;
    startarrow.type = assoc->end[0].aggregate == AGGREGATE_NORMAL ?
      ARROW_HOLLOW_DIAMOND : ARROW_FILLED_DIAMOND;
  } else {
    startarrow.type = ARROW_NONE;
  }
  endarrow.length = ASSOCIATION_TRIANGLESIZE;
  endarrow.width = ASSOCIATION_TRIANGLESIZE;
  if (assoc->end[1].arrow) {
    endarrow.type = ARROW_LINES;
  } else if (assoc->end[1].aggregate != AGGREGATE_NONE) {
    endarrow.length = ASSOCIATION_DIAMONDLEN;
    endarrow.width = ASSOCIATION_TRIANGLESIZE*0.6;
    endarrow.type = assoc->end[1].aggregate == AGGREGATE_NORMAL ?
      ARROW_HOLLOW_DIAMOND : ARROW_FILLED_DIAMOND;
  } else {
    endarrow.type = ARROW_NONE;
  }
  renderer_ops->draw_polyline_with_arrows(renderer, points, n,
					   ASSOCIATION_WIDTH,
					   &color_black,
					   &startarrow, &endarrow);

  /* Name: */
  renderer_ops->set_font(renderer, assoc_font, ASSOCIATION_FONTHEIGHT);
 
  if (assoc->name != NULL) {
    pos = assoc->text_pos;
    renderer_ops->draw_string(renderer, assoc->name,
			       &pos, assoc->text_align,
			       &color_black);
  }

  /* Direction: */
  renderer_ops->set_fillstyle(renderer, FILLSTYLE_SOLID);

  switch (assoc->direction) {
  case ASSOC_NODIR:
    break;
  case ASSOC_RIGHT:
    poly[0].x = assoc->text_pos.x + assoc->text_width + 0.1;
    if (assoc->text_align == ALIGN_CENTER)
      poly[0].x -= assoc->text_width/2.0;
    poly[0].y = assoc->text_pos.y;
    poly[1].x = poly[0].x;
    poly[1].y = poly[0].y - ASSOCIATION_FONTHEIGHT*0.5;
    poly[2].x = poly[0].x + ASSOCIATION_FONTHEIGHT*0.5;
    poly[2].y = poly[0].y - ASSOCIATION_FONTHEIGHT*0.5*0.5;
    renderer_ops->fill_polygon(renderer, poly, 3, &color_black);
    break;
  case ASSOC_LEFT:
    poly[0].x = assoc->text_pos.x - 0.2;
    if (assoc->text_align == ALIGN_CENTER)
      poly[0].x -= assoc->text_width/2.0;
    poly[0].y = assoc->text_pos.y;
    poly[1].x = poly[0].x;
    poly[1].y = poly[0].y - ASSOCIATION_FONTHEIGHT*0.5;
    poly[2].x = poly[0].x - ASSOCIATION_FONTHEIGHT*0.5;
    poly[2].y = poly[0].y - ASSOCIATION_FONTHEIGHT*0.5*0.5;
    renderer_ops->fill_polygon(renderer, poly, 3, &color_black);
    break;
  }


  for (i=0;i<2;i++) {
    AssociationEnd *end = &assoc->end[i];
    pos = end->text_pos;

    if (end->role != NULL) {
      gchar *role_name = g_strdup_printf ("%c%s", visible_char[(int) end->visibility], end->role);
      renderer_ops->draw_string(renderer, 
                                role_name,
				&pos, 
				end->text_align,
				&color_black);
      g_free (role_name);
      pos.y += ASSOCIATION_FONTHEIGHT;
    }
    if (end->multiplicity != NULL) {
      renderer_ops->draw_string(renderer, end->multiplicity,
				 &pos, end->text_align,
				 &color_black);
    }
  }
}

static void
association_state_free(ObjectState *ostate)
{
  AssociationState *state = (AssociationState *)ostate;
  int i;
  g_free(state->name);

  for (i=0;i<2;i++) {
    g_free(state->end[i].role);
    g_free(state->end[i].multiplicity);
  }
}

static AssociationState *
association_get_state(Association *assoc)
{
  int i;
  AssociationEnd *end;

  AssociationState *state = g_new0(AssociationState, 1);

  state->obj_state.free = association_state_free;

  state->name = g_strdup(assoc->name);
  state->direction = assoc->direction;
  
  for (i=0;i<2;i++) {
    end = &assoc->end[i];
    state->end[i].role = g_strdup(end->role);
    state->end[i].multiplicity = g_strdup(end->multiplicity);
    state->end[i].arrow = end->arrow;
    state->end[i].aggregate = end->aggregate;
	state->end[i].visibility = end->visibility;
  }

  return state;
}

static void
association_set_state(Association *assoc, AssociationState *state)
{
  int i;
  AssociationEnd *end;
  
  g_free(assoc->name);
  assoc->name = state->name;
  assoc->text_width = 0.0;
  assoc->ascent = 0.0;
  assoc->descent = 0.0;
  if (assoc->name != NULL) {
    assoc->text_width =
      dia_font_string_width(assoc->name, assoc_font, ASSOCIATION_FONTHEIGHT);
    assoc->ascent = 
      dia_font_ascent(assoc->name, assoc_font, ASSOCIATION_FONTHEIGHT);
    assoc->descent =     
      dia_font_descent(assoc->name, assoc_font, ASSOCIATION_FONTHEIGHT);
  } 
  
  assoc->direction = state->direction;
  
  for (i=0;i<2;i++) {
    end = &assoc->end[i];
    g_free(end->role);
    g_free(end->multiplicity);
    end->role = state->end[i].role;
    end->multiplicity = state->end[i].multiplicity;
    end->arrow = state->end[i].arrow;
    end->aggregate = state->end[i].aggregate;
	end->visibility = state->end[i].visibility;

    end->text_width = 0.0;
    end->role_ascent = 0.0;
    end->role_descent = 0.0;
    end->multi_ascent = 0.0;
    end->multi_descent = 0.0;
    if (end->role != NULL) {
      end->text_width = 
          dia_font_string_width(end->role, assoc_font, ASSOCIATION_FONTHEIGHT);
      end->role_ascent =
          dia_font_ascent(end->role, assoc_font, ASSOCIATION_FONTHEIGHT);
      end->role_descent =
          dia_font_ascent(end->role, assoc_font, ASSOCIATION_FONTHEIGHT);          
    }
    if (end->multiplicity != NULL) {
      end->text_width = MAX(end->text_width,
                            dia_font_string_width(end->multiplicity,
                                                  assoc_font,
                                                  ASSOCIATION_FONTHEIGHT) );
      end->role_ascent = dia_font_ascent(end->multiplicity,
                                         assoc_font,ASSOCIATION_FONTHEIGHT);
      end->role_descent = dia_font_descent(end->multiplicity,
                                           assoc_font,ASSOCIATION_FONTHEIGHT);
    }
  }

  g_free(state);
  
  association_update_data(assoc);
}

static void
association_update_data_end(Association *assoc, int endnum)
{
  OrthConn *orth = &assoc->orth;
  DiaObject *obj = &orth->object;
  Point *points  = orth->points;
  Rectangle rect;
  AssociationEnd *end;
  Orientation dir;
  int n = orth->numpoints - 1, fp, sp;

  /* Find the first and second points depending on which end: */
  if (endnum) {
      fp = n;
      sp = n-1;
      dir = assoc->orth.orientation[n-1];
  } else {
      fp = 0;
      sp = 1;
      dir = assoc->orth.orientation[0];
  }

  /* If the points are the same, find a better candidate: */
  if (points[fp].x == points[sp].x && points[fp].y == points[sp].y) {
      sp += (endnum ? -1 : 1);
      if (sp < 0)
	  sp = 0;
      if (sp > n)
	  sp = n;
      if (points[fp].y != points[sp].y)
	  dir = VERTICAL;
      else      
	  dir = HORIZONTAL;
  }

  /* Update the text-points of the ends: */
  end = &assoc->end[endnum];
  end->text_pos = points[fp];
  switch (dir) {
  case HORIZONTAL:
    end->text_pos.y -= end->role_descent;
    if (points[fp].x < points[sp].x) {
      end->text_align = ALIGN_LEFT;
      end->text_pos.x += (get_aggregate_pos_diff(end) + ASSOCIATION_END_SPACE);
    } else {
      end->text_align = ALIGN_RIGHT;    
      end->text_pos.x -= (get_aggregate_pos_diff(end) + ASSOCIATION_END_SPACE);
    }
    break;
  case VERTICAL:
    if (end->arrow || end->aggregate != AGGREGATE_NONE)
	end->text_pos.x += ASSOCIATION_DIAMONDWIDTH / 2;
    end->text_pos.x += ASSOCIATION_END_SPACE;

    end->text_pos.y += end->role_ascent;
    if (points[fp].y > points[sp].y) {
      if (end->role!=NULL)
          end->text_pos.y -= ASSOCIATION_FONTHEIGHT;
      if (end->multiplicity!=NULL)
          end->text_pos.y -= ASSOCIATION_FONTHEIGHT;
    }

    end->text_align = ALIGN_LEFT;
    break;
  }
  /* Add the text recangle to the bounding box: */
  rect.left = end->text_pos.x
      - (end->text_align == ALIGN_LEFT ? 0 : end->text_width);
  rect.right = rect.left + end->text_width;
  rect.top = end->text_pos.y - end->role_ascent;
  rect.bottom = rect.top + 2*ASSOCIATION_FONTHEIGHT;
  
  rectangle_union(&obj->bounding_box, &rect);
}

static void
association_update_data(Association *assoc)
{
        /* FIXME: The ascent and descent computation logic here is
           fundamentally slow. */

  OrthConn *orth = &assoc->orth;
  DiaObject *obj = &orth->object;
  PolyBBExtras *extra = &orth->extra_spacing;
  int num_segm, i;
  Point *points;
  Rectangle rect;
  Orientation dir;
  
  orthconn_update_data(orth);  
  
  extra->start_trans = 
    extra->start_long = (assoc->end[0].aggregate == AGGREGATE_NONE?
                         ASSOCIATION_WIDTH/2.0:
                         (ASSOCIATION_WIDTH + ASSOCIATION_DIAMONDLEN)/2.0);
  extra->middle_trans = ASSOCIATION_WIDTH/2.0;
  extra->end_trans = 
    extra->end_long = (assoc->end[1].aggregate == AGGREGATE_NONE?
                         ASSOCIATION_WIDTH/2.0:
                         (ASSOCIATION_WIDTH + ASSOCIATION_DIAMONDLEN)/2.0);

  if (assoc->end[0].arrow)
    extra->start_trans = MAX(extra->start_trans, ASSOCIATION_TRIANGLESIZE);
  if (assoc->end[1].arrow)
    extra->end_trans = MAX(extra->end_trans, ASSOCIATION_TRIANGLESIZE);

  orthconn_update_boundingbox(orth);
  
  /* Calc text pos: */
  num_segm = assoc->orth.numpoints - 1;
  points = assoc->orth.points;
  i = num_segm / 2;
  
  if ((num_segm % 2) == 0) { /* If no middle segment, use horizontal */
    if (assoc->orth.orientation[i]==VERTICAL)
      i--;
  }
  dir = assoc->orth.orientation[i];
  /* also adapt for degenerated segement */
  if (VERTICAL == dir && points[i].y == points[i+1].y)
    dir = HORIZONTAL;
  else if (HORIZONTAL == dir && points[i].x == points[i+1].x)
    dir = VERTICAL;

  switch (dir) {
  case HORIZONTAL:
    assoc->text_align = ALIGN_CENTER;
    assoc->text_pos.x = 0.5*(points[i].x+points[i+1].x);
    assoc->text_pos.y = points[i].y - assoc->descent;
    break;
  case VERTICAL:
    assoc->text_align = ALIGN_LEFT;
    assoc->text_pos.x = points[i].x + 0.1;
    assoc->text_pos.y = 0.5*(points[i].y+points[i+1].y) - assoc->descent;
    break;
  }

  /* Add the text recangle to the bounding box: */
  rect.left = assoc->text_pos.x;
  if (assoc->text_align == ALIGN_CENTER)
    rect.left -= assoc->text_width/2.0;
  rect.right = rect.left + assoc->text_width;
  rect.top = assoc->text_pos.y - assoc->ascent;
  rect.bottom = rect.top + ASSOCIATION_FONTHEIGHT;

  rectangle_union(&obj->bounding_box, &rect);

  association_update_data_end(assoc, 0);
  association_update_data_end(assoc, 1);
}

static coord get_aggregate_pos_diff(AssociationEnd *end)
{
  coord width=0;
  if(end->arrow){
    width = ASSOCIATION_TRIANGLESIZE;
  }
  switch(end->aggregate){
  case AGGREGATE_COMPOSITION:
  case AGGREGATE_NORMAL:
    if(width!=0) width = MAX(ASSOCIATION_TRIANGLESIZE, ASSOCIATION_DIAMONDLEN);
    else width = ASSOCIATION_DIAMONDLEN;
  case AGGREGATE_NONE:
    break;
  }
  return width;
}

static DiaObject *
association_create(Point *startpoint,
	       void *user_data,
  	       Handle **handle1,
	       Handle **handle2)
{
  Association *assoc;
  OrthConn *orth;
  DiaObject *obj;
  int i;
  int user_d;

  if (assoc_font == NULL) {
    assoc_font = dia_font_new_from_style(DIA_FONT_MONOSPACE, ASSOCIATION_FONTHEIGHT);
  }
  
  assoc = g_malloc0(sizeof(Association));
  orth = &assoc->orth;
  obj = &orth->object;

  obj->type = &association_type;

  obj->ops = &association_ops;

  orthconn_init(orth, startpoint);
  
  assoc->name = NULL;
  assoc->direction = ASSOC_NODIR;
  for (i=0;i<2;i++) {
    assoc->end[i].role = NULL;
    assoc->end[i].multiplicity = NULL;
    assoc->end[i].arrow = FALSE;
    assoc->end[i].aggregate = AGGREGATE_NONE;
    assoc->end[i].text_width = 0.0;
  }
  
  assoc->text_width = 0.0;
  assoc->properties_dialog = NULL;

  user_d = GPOINTER_TO_INT(user_data);
  switch (user_d) {
  case 0:
    /* Ok already. */
    break;
  case 1:
    assoc->end[1].aggregate = AGGREGATE_NORMAL;
    break;
  }

  association_update_data(assoc);
  
  *handle1 = orth->handles[0];
  *handle2 = orth->handles[orth->numpoints-2];

  return &assoc->orth.object;
}

static ObjectChange *
association_add_segment_callback(DiaObject *obj, Point *clicked, gpointer data)
{
  ObjectChange *change;
  change = orthconn_add_segment((OrthConn *)obj, clicked);
  association_update_data((Association *)obj);
  return change;
}

static ObjectChange *
association_delete_segment_callback(DiaObject *obj, Point *clicked, gpointer data)
{
  ObjectChange *change;
  change = orthconn_delete_segment((OrthConn *)obj, clicked);
  association_update_data((Association *)obj);
  return change;
}


static DiaMenuItem object_menu_items[] = {
  { N_("Add segment"), association_add_segment_callback, NULL, 1 },
  { N_("Delete segment"), association_delete_segment_callback, NULL, 1 },
  ORTHCONN_COMMON_MENUS,
};

static DiaMenu object_menu = {
  "Association",
  sizeof(object_menu_items)/sizeof(DiaMenuItem),
  object_menu_items,
  NULL
};

static DiaMenu *
association_get_object_menu(Association *assoc, Point *clickedpoint)
{
  OrthConn *orth;

  orth = &assoc->orth;
  /* Set entries sensitive/selected etc here */
  object_menu_items[0].active = orthconn_can_add_segment(orth, clickedpoint);
  object_menu_items[1].active = orthconn_can_delete_segment(orth, clickedpoint);
  orthconn_update_object_menu(orth, clickedpoint, &object_menu_items[2]);
  return &object_menu;
}

static void
association_destroy(Association *assoc)
{
  int i;
  
  orthconn_destroy(&assoc->orth);

  g_free(assoc->name);

  for (i=0;i<2;i++) {
    g_free(assoc->end[i].role);
    g_free(assoc->end[i].multiplicity);
  }

  if (assoc->properties_dialog != NULL) {
    gtk_widget_destroy(assoc->properties_dialog->dialog);
    g_free(assoc->properties_dialog);
  }
}

static DiaObject *
association_copy(Association *assoc)
{
  Association *newassoc;
  OrthConn *orth, *neworth;
  DiaObject *newobj;
  int i;
  
  orth = &assoc->orth;
  
  newassoc = g_malloc0(sizeof(Association));
  neworth = &newassoc->orth;
  newobj = &neworth->object;

  orthconn_copy(orth, neworth);

  newassoc->name = g_strdup(assoc->name);
  newassoc->direction = assoc->direction;
  for (i=0;i<2;i++) {
    newassoc->end[i] = assoc->end[i];
    newassoc->end[i].role =
      (assoc->end[i].role != NULL)?g_strdup(assoc->end[i].role):NULL;
    newassoc->end[i].multiplicity =
      (assoc->end[i].multiplicity != NULL)?g_strdup(assoc->end[i].multiplicity):NULL;
  }

  newassoc->text_width = assoc->text_width;
  newassoc->properties_dialog = NULL;
  
  association_update_data(newassoc);
  
  return &newassoc->orth.object;
}


static void
association_save(Association *assoc, ObjectNode obj_node,
		 const char *filename)
{
  int i;
  AttributeNode attr;
  DataNode composite;
  
  orthconn_save(&assoc->orth, obj_node);

  data_add_string(new_attribute(obj_node, "name"),
		  assoc->name);
  data_add_enum(new_attribute(obj_node, "direction"),
		assoc->direction);

  attr = new_attribute(obj_node, "ends");
  for (i=0;i<2;i++) {
    composite = data_add_composite(attr, NULL);

    data_add_string(composite_add_attribute(composite, "role"),
		    assoc->end[i].role);
    data_add_string(composite_add_attribute(composite, "multiplicity"),
		    assoc->end[i].multiplicity);
    data_add_boolean(composite_add_attribute(composite, "arrow"),
		  assoc->end[i].arrow);
    data_add_enum(composite_add_attribute(composite, "aggregate"),
		  assoc->end[i].aggregate);
  	data_add_enum(composite_add_attribute(composite, "visibility"),
		assoc->end[i].visibility);
  }
}

static DiaObject *
association_load(ObjectNode obj_node, int version, const char *filename)
{
  Association *assoc;
  AttributeNode attr;
  DataNode composite;
  OrthConn *orth;
  DiaObject *obj;
  int i;
  
  if (assoc_font == NULL) {
	  assoc_font = dia_font_new_from_style(DIA_FONT_MONOSPACE,
                                         ASSOCIATION_FONTHEIGHT);
  }

  assoc = g_new0(Association, 1);

  orth = &assoc->orth;
  obj = &orth->object;

  obj->type = &association_type;
  obj->ops = &association_ops;

  orthconn_load(orth, obj_node);

  assoc->name = NULL;
  attr = object_find_attribute(obj_node, "name");
  if (attr != NULL)
    assoc->name = data_string(attribute_first_data(attr));
  
  assoc->text_width = 0.0;
  if (assoc->name != NULL) {
    assoc->text_width =
      dia_font_string_width(assoc->name, assoc_font, ASSOCIATION_FONTHEIGHT);
  }

  assoc->direction = ASSOC_NODIR;
  attr = object_find_attribute(obj_node, "direction");
  if (attr != NULL)
    assoc->direction = data_enum(attribute_first_data(attr));

  attr = object_find_attribute(obj_node, "ends");
  composite = attribute_first_data(attr);
  for (i=0;i<2;i++) {

    assoc->end[i].role = NULL;
    attr = composite_find_attribute(composite, "role");
    if (attr != NULL) {
      assoc->end[i].role = data_string(attribute_first_data(attr));
    }
    if (   assoc->end[i].role != NULL 
        && 0 == strcmp(assoc->end[i].role, "")) {
      g_free(assoc->end[i].role);
      assoc->end[i].role = NULL;
    }
    
    assoc->end[i].multiplicity = NULL;
    attr = composite_find_attribute(composite, "multiplicity");
    if (attr != NULL) {
      assoc->end[i].multiplicity = data_string(attribute_first_data(attr));
    }
    if (   assoc->end[i].multiplicity != NULL
	&& 0 == strcmp(assoc->end[i].multiplicity, "")) {
      g_free(assoc->end[i].multiplicity);
      assoc->end[i].multiplicity = NULL;
    }
    
    assoc->end[i].arrow = FALSE;
    attr = composite_find_attribute(composite, "arrow");
    if (attr != NULL)
      assoc->end[i].arrow = data_boolean(attribute_first_data(attr));

    assoc->end[i].aggregate = AGGREGATE_NONE;
    attr = composite_find_attribute(composite, "aggregate");
    if (attr != NULL)
      assoc->end[i].aggregate = data_enum(attribute_first_data(attr));
  
    assoc->end[i].visibility = FALSE;
    attr = composite_find_attribute(composite, "visibility");
    if (attr != NULL)
      assoc->end[i].visibility =  data_enum( attribute_first_data(attr) );

    assoc->end[i].text_width = 0.0;
    if (assoc->end[i].role != NULL) {
      assoc->end[i].text_width = 
          dia_font_string_width(assoc->end[i].role, assoc_font,
                                ASSOCIATION_FONTHEIGHT);
    }
    if (assoc->end[i].multiplicity != NULL) {
      assoc->end[i].text_width =
          MAX(assoc->end[i].text_width,
              dia_font_string_width(assoc->end[i].multiplicity,
                                    assoc_font, ASSOCIATION_FONTHEIGHT) );
    }
    composite = data_next(composite);
  }

  assoc->properties_dialog = NULL;
  
  association_set_state(assoc, association_get_state(assoc));

  return &assoc->orth.object;
}

static ObjectChange *
association_apply_properties(Association *assoc)
{
  AssociationPropertiesDialog *prop_dialog;
  const char *str;
  GtkWidget *menuitem;
  int i;
  ObjectState *old_state;
  
  prop_dialog = assoc->properties_dialog;

  old_state = (ObjectState *)association_get_state(assoc);
  
  /* Read from dialog and put in object: */
  g_free(assoc->name);
  str = gtk_entry_get_text(prop_dialog->name);
  if (str && strlen (str) != 0)
    assoc->name = g_strdup (str);
  else
    assoc->name = NULL;

  assoc->text_width = 0.0;

  if (assoc->name != NULL) {
    assoc->text_width =
      dia_font_string_width(assoc->name, assoc_font, ASSOCIATION_FONTHEIGHT);
  }

  menuitem = gtk_menu_get_active(prop_dialog->dir_menu);
  assoc->direction =
    GPOINTER_TO_INT(gtk_object_get_user_data(GTK_OBJECT(menuitem)));

  for (i=0;i<2;i++) {
    AssociationEnd *end = &assoc->end[i];

    end->visibility = (UMLVisibility)
	 GPOINTER_TO_INT (gtk_object_get_user_data (
			   GTK_OBJECT (gtk_menu_get_active (prop_dialog->end[i].attr_visible))));

    /* Role: */
    g_free(end->role);
    str = gtk_entry_get_text(prop_dialog->end[i].role);
    if (str && strlen (str) != 0)
      end->role = g_strdup (str);
    else
      end->role = NULL;

    /* Multiplicity: */
    g_free(end->multiplicity);
    str = gtk_entry_get_text(prop_dialog->end[i].multiplicity);
    if (strlen (str) != 0)
      end->multiplicity = g_strdup(str);
    else
      end->multiplicity = NULL;

    end->text_width = 0.0;

    if (end->role != NULL) {
      end->text_width = 
          dia_font_string_width(end->role, assoc_font, ASSOCIATION_FONTHEIGHT);
    }
    if (end->multiplicity != NULL) {
      end->text_width =
          MAX(end->text_width,
              dia_font_string_width(end->multiplicity,
                                    assoc_font, ASSOCIATION_FONTHEIGHT) );
    }

    end->arrow = prop_dialog->end[i].draw_arrow->active;
    
    end->aggregate = AGGREGATE_NONE;
    if (prop_dialog->end[i].aggregate->active)
      end->aggregate = AGGREGATE_NORMAL;
    if (prop_dialog->end[i].composition->active)
      end->aggregate = AGGREGATE_COMPOSITION;

  }

  association_set_state(assoc, association_get_state(assoc));
  return new_object_state_change(&assoc->orth.object, old_state, 
				 (GetStateFunc)association_get_state,
				 (SetStateFunc)association_set_state);
}

static void
fill_in_dialog(Association *assoc)
{
  AssociationPropertiesDialog *prop_dialog;
  int i;
 
  prop_dialog = assoc->properties_dialog;

  if (assoc->name != NULL)
    gtk_entry_set_text(prop_dialog->name, assoc->name);
  else
    gtk_entry_set_text(prop_dialog->name, "");
  
  gtk_option_menu_set_history(prop_dialog->dir_omenu, assoc->direction);

  for (i=0;i<2;i++) {
    if (assoc->end[i].role != NULL)
      gtk_entry_set_text(prop_dialog->end[i].role, assoc->end[i].role);
    else
      gtk_entry_set_text(prop_dialog->end[i].role, "");
    
    if (assoc->end[i].multiplicity != NULL)
      gtk_entry_set_text(prop_dialog->end[i].multiplicity, 
                         assoc->end[i].multiplicity);
    else
      gtk_entry_set_text(prop_dialog->end[i].multiplicity, "");

    gtk_option_menu_set_history(prop_dialog->end[i].attr_visible_button,
			      (gint)assoc->end[i].visibility);
    gtk_toggle_button_set_active(prop_dialog->end[i].draw_arrow,
				assoc->end[i].arrow);
    gtk_toggle_button_set_active(prop_dialog->end[i].aggregate,
				assoc->end[i].aggregate == AGGREGATE_NORMAL);
    gtk_toggle_button_set_active(prop_dialog->end[i].composition,
				assoc->end[i].aggregate == AGGREGATE_COMPOSITION);
  }
}

static void
mutex_aggregate_callback(GtkWidget *widget,
			 AssociationPropertiesDialog *prop_dialog)
{
  int i;
  GtkToggleButton *button;

  button = GTK_TOGGLE_BUTTON(widget);

  if (!button->active)
    return;

  for (i=0;i<2;i++) {
    if (prop_dialog->end[i].aggregate != button) {
      gtk_toggle_button_set_active(prop_dialog->end[i].aggregate, 0);
    }
    if (prop_dialog->end[i].composition != button) {
      gtk_toggle_button_set_active(prop_dialog->end[i].composition, 0);
    }
  }
}

static GtkWidget *
association_get_properties(Association *assoc, gboolean is_default)
{
  AssociationPropertiesDialog *prop_dialog;
  GtkWidget *dialog;
  GtkWidget *frame;
  GtkWidget *entry;
  GtkWidget *hbox;
  GtkWidget *split_hbox;
  GtkWidget *vbox;
  GtkWidget *label;
  GtkWidget *omenu;
  GtkWidget *menu;
  GtkWidget *submenu;
  GtkWidget *menuitem;
  GtkWidget *checkbox;
  GSList *group;
  int i;
  
  if (assoc->properties_dialog == NULL) {

    prop_dialog = g_new(AssociationPropertiesDialog, 1);
    assoc->properties_dialog = prop_dialog;

    dialog = gtk_vbox_new(FALSE, 0);
    gtk_object_ref(GTK_OBJECT(dialog));
    gtk_object_sink(GTK_OBJECT(dialog));
    prop_dialog->dialog = dialog;
    
    /* Name entry: */
    hbox = gtk_hbox_new(FALSE, 5);
    label = gtk_label_new(_("Name:"));
    gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
    entry = gtk_entry_new();
    prop_dialog->name = GTK_ENTRY(entry);
    gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
    gtk_widget_grab_focus(entry);
    gtk_widget_show (label);
    gtk_widget_show (entry);
    gtk_box_pack_start (GTK_BOX (dialog), hbox, TRUE, TRUE, 0);
    gtk_widget_show(hbox);

    /* Direction entry: */
    hbox = gtk_hbox_new(FALSE, 5);
    label = gtk_label_new(_("Direction:"));
    gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
    
    omenu = gtk_option_menu_new ();
    menu = gtk_menu_new ();
    prop_dialog->dir_menu = GTK_MENU(menu);
    prop_dialog->dir_omenu = GTK_OPTION_MENU(omenu);
    submenu = NULL;
    group = NULL;
    
    menuitem = gtk_radio_menu_item_new_with_label (group, _("None"));
    gtk_object_set_user_data(GTK_OBJECT(menuitem),
			     GINT_TO_POINTER(ASSOC_NODIR));
    group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
    gtk_menu_append (GTK_MENU (menu), menuitem);
    gtk_widget_show (menuitem);
    
    menuitem = gtk_radio_menu_item_new_with_label (group, _("From A to B"));
    gtk_object_set_user_data(GTK_OBJECT(menuitem),
			     GINT_TO_POINTER(ASSOC_RIGHT));
    group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
    gtk_menu_append (GTK_MENU (menu), menuitem);
    gtk_widget_show (menuitem);
    
    menuitem = gtk_radio_menu_item_new_with_label (group, _("From B to A"));
    gtk_object_set_user_data(GTK_OBJECT(menuitem),
			     GINT_TO_POINTER(ASSOC_LEFT));
    group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
    gtk_menu_append (GTK_MENU (menu), menuitem);
    gtk_widget_show (menuitem);
    
    gtk_option_menu_set_menu (GTK_OPTION_MENU (omenu), menu);
    gtk_box_pack_start (GTK_BOX (hbox), omenu, FALSE, TRUE, 0);
    
    gtk_widget_show (label);
    gtk_widget_show (omenu);
    gtk_box_pack_start (GTK_BOX (dialog), hbox, TRUE, TRUE, 0);
    gtk_widget_show(hbox);
    
    split_hbox = gtk_hbox_new(TRUE, 5);
    gtk_box_pack_start (GTK_BOX (dialog), split_hbox, TRUE, TRUE, 0);
    gtk_widget_show(split_hbox);

    group = NULL; /* For the radio-buttons */
    
    for (i=0;i<2;i++) {
      char *str;
      if (i==0)
	str = _("Side A");
      else
	str = _("Side B");
      frame = gtk_frame_new(str);
      
      vbox = gtk_vbox_new(FALSE, 5);
      /* End 'i' into vbox: */
      if (i==0)
	label = gtk_label_new(_("Side A"));
      else
	label = gtk_label_new(_("Side B"));
      
      gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0);
      
      /* Role entry: */
      hbox = gtk_hbox_new(FALSE, 5);
      label = gtk_label_new(_("Role:"));
      gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
      entry = gtk_entry_new();
      prop_dialog->end[i].role = GTK_ENTRY(entry);
      gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
      gtk_widget_show (label);
      gtk_widget_show (entry);
      gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
      gtk_widget_show(hbox);

      /* Multiplicity entry: */
      hbox = gtk_hbox_new(FALSE, 5);
      label = gtk_label_new(_("Multiplicity:"));
      gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
      entry = gtk_entry_new();
      prop_dialog->end[i].multiplicity = GTK_ENTRY(entry);
      gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
      gtk_widget_show (label);
      gtk_widget_show (entry);
      gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
      gtk_widget_show(hbox);

      hbox = gtk_hbox_new(FALSE, 5);
      label = gtk_label_new(_("Visibility:"));
      gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);

      omenu = gtk_option_menu_new ();
      menu = gtk_menu_new ();
      prop_dialog->end[i].attr_visible = GTK_MENU(menu);
      prop_dialog->end[i].attr_visible_button = GTK_OPTION_MENU(omenu);
      submenu = NULL;
      group = NULL;
      menuitem = gtk_radio_menu_item_new_with_label (group, _("Public"));
/*
      gtk_signal_connect (GTK_OBJECT (menuitem), "activate",
    		      GTK_SIGNAL_FUNC (attributes_update), umlclass);
*/
      gtk_object_set_user_data(GTK_OBJECT(menuitem),
    			   GINT_TO_POINTER(UML_PUBLIC) );
      group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
      gtk_menu_append (GTK_MENU (menu), menuitem);
      gtk_widget_show (menuitem);
      menuitem = gtk_radio_menu_item_new_with_label (group, _("Private"));
/*
      gtk_signal_connect (GTK_OBJECT (menuitem), "activate",
    		      GTK_SIGNAL_FUNC (attributes_update), umlclass);
*/
      gtk_object_set_user_data(GTK_OBJECT(menuitem),
    			   GINT_TO_POINTER(UML_PRIVATE) );
      group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
      gtk_menu_append (GTK_MENU (menu), menuitem);
      gtk_widget_show (menuitem);
      menuitem = gtk_radio_menu_item_new_with_label (group, _("Protected"));
/*
      gtk_signal_connect (GTK_OBJECT (menuitem), "activate",
    		      GTK_SIGNAL_FUNC (attributes_update), umlclass);
*/
      gtk_object_set_user_data(GTK_OBJECT(menuitem),
    			   GINT_TO_POINTER(UML_PROTECTED) );
      group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
      gtk_menu_append (GTK_MENU (menu), menuitem);
      gtk_widget_show (menuitem);
      menuitem = gtk_radio_menu_item_new_with_label (group, _("Implementation"));
/*
      gtk_signal_connect (GTK_OBJECT (menuitem), "activate",
    		      GTK_SIGNAL_FUNC (attributes_update), umlclass);
*/
      gtk_object_set_user_data(GTK_OBJECT(menuitem),
    			   GINT_TO_POINTER(UML_IMPLEMENTATION) );
      group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem));
      gtk_menu_append (GTK_MENU (menu), menuitem);
      gtk_widget_show (menuitem);
      
      gtk_option_menu_set_menu (GTK_OPTION_MENU (omenu), menu);
      gtk_box_pack_start (GTK_BOX (hbox), omenu, FALSE, TRUE, 0);
      gtk_widget_show(label);
      gtk_widget_show(omenu);
      gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
      gtk_widget_show(hbox);

      /* Show arrow: */
      checkbox = gtk_check_button_new_with_label(_("Show arrow"));
      prop_dialog->end[i].draw_arrow = GTK_TOGGLE_BUTTON( checkbox );
      gtk_widget_show(checkbox);
      gtk_box_pack_start (GTK_BOX (vbox), checkbox, TRUE, TRUE, 0);

      /* Aggregate */
      checkbox = gtk_check_button_new_with_label(_("Aggregate"));
      prop_dialog->end[i].aggregate = GTK_TOGGLE_BUTTON( checkbox );
      gtk_signal_connect(GTK_OBJECT(checkbox), "toggled",
			 (GtkSignalFunc) mutex_aggregate_callback, prop_dialog);
      gtk_widget_show(checkbox);
      gtk_box_pack_start (GTK_BOX (vbox), checkbox, TRUE, TRUE, 0);

      /* Composition */
      checkbox = gtk_check_button_new_with_label(_("Composition"));
      prop_dialog->end[i].composition = GTK_TOGGLE_BUTTON( checkbox );
      gtk_signal_connect(GTK_OBJECT(checkbox), "toggled",
			 (GtkSignalFunc) mutex_aggregate_callback, prop_dialog);
      gtk_widget_show(checkbox);
      gtk_box_pack_start (GTK_BOX (vbox), checkbox, TRUE, TRUE, 0);
	
      gtk_container_set_border_width(GTK_CONTAINER(vbox), 5);
      gtk_container_add(GTK_CONTAINER(frame), vbox);
      gtk_box_pack_start (GTK_BOX (split_hbox), frame, TRUE, TRUE, 0);
      gtk_widget_show(vbox);
      gtk_widget_show(frame);
    }

  }
  fill_in_dialog(assoc);
  gtk_widget_show (assoc->properties_dialog->dialog);

  return assoc->properties_dialog->dialog;
}




