/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998, 1999 Alexander Larsson
 *
 * Objects for drawing i* diagrams
 * This class supports all i* links as decoraterd bezier curves
 * Copyright (C) 2002-2003 Christophe Ponsard
 *
 * 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 <stdio.h>

#include <glib.h>

#include "intl.h"
#include "object.h"
#include "connection.h"
#include "diarenderer.h"
#include "handle.h"
#include "arrows.h"
#include "properties.h"
#include "dia_image.h"

#include "pixmaps/link.xpm"  /* generic "unspecified" link */

typedef struct _Link Link;

typedef enum {
   UNSPECIFIED,
   POS_CONTRIB,
   NEG_CONTRIB,
   DEPENDENCY,
   DECOMPOSITION,
   MEANS_ENDS
} LinkType;

struct _Link {
  Connection connection;
  ConnectionPoint connector;

  LinkType type;

  Point pm;
  BezPoint line[3];
  Handle pm_handle;

  int init;
};

#define HANDLE_MOVE_MID_POINT (HANDLE_CUSTOM1)

#define LINK_WIDTH 0.12
#define LINK_DASHLEN 0.5
#define LINK_FONTHEIGHT 0.7
#define LINK_ARROWLEN 0.8
#define LINK_ARROWWIDTH 0.5

#define LINK_FG_COLOR color_black
#define LINK_BG_COLOR color_white
#define LINK_DEP_WIDTH 0.8
#define LINK_DEP_HEIGHT 0.6
#define LINK_REF_WIDTH 1.0
#define LINK_REF_HEIGHT 1.0

static DiaFont *link_font = NULL;

static ObjectChange* link_move_handle(Link *link, Handle *handle,
				   Point *to, ConnectionPoint *cp,
				   HandleMoveReason reason, ModifierKeys modifiers);
static ObjectChange* link_move(Link *link, Point *to);
static void link_select(Link *link, Point *clicked_point,
			      DiaRenderer *interactive_renderer);
static void link_draw(Link *link, DiaRenderer *renderer);
static DiaObject *link_create(Point *startpoint,
				 void *user_data,
				 Handle **handle1,
				 Handle **handle2);
static real link_distance_from(Link *link, Point *point);
static void link_update_data(Link *link);
static void link_destroy(Link *link);
static DiaObject *link_load(ObjectNode obj_node, int version,
			    const char *filename);

static PropDescription *link_describe_props(Link *mes);
static void link_get_props(Link * link, GPtrArray *props);
static void link_set_props(Link *link, GPtrArray *props);


static void bernstein_develop(const real p[4],real *A,real *B,real *C,real *D);
static real bezier_eval(const real p[4],real u);
static real bezier_eval_tangent(const real p[4],real u);
static Point bezier_line_eval(BezPoint *line,int p,real u);
static Point compute_annot(Point* p1, Point* p2, Point* pm, double f, double d);
static void compute_dependency(BezPoint *line, BezPoint *bpl);
static void compute_line(Point* p1, Point* p2, Point *pm, BezPoint* line);


static ObjectTypeOps link_type_ops =
{
  (CreateFunc) link_create,
  (LoadFunc)   link_load,/*using_properties*/     /* load */
  (SaveFunc)   object_save_using_properties,      /* save */
  (GetDefaultsFunc)   NULL,
  (ApplyDefaultsFunc) NULL
};

DiaObjectType istar_link_type =
{
  "Istar - link",            /* name */
  0,                         /* version */
  (char **) istar_link_xpm,  /* pixmap */
  &link_type_ops             /* ops */
};


static ObjectOps link_ops = {
  (DestroyFunc)         link_destroy,
  (DrawFunc)            link_draw,
  (DistanceFunc)        link_distance_from,
  (SelectFunc)          link_select,
  (CopyFunc)            object_copy_using_properties,
  (MoveFunc)            link_move,
  (MoveHandleFunc)      link_move_handle,
  (GetPropertiesFunc)   object_create_props_dialog,
  (ApplyPropertiesFunc) object_apply_props_from_dialog,
  (ObjectMenuFunc)      NULL,
  (DescribePropsFunc)   link_describe_props,
  (GetPropsFunc)        link_get_props,
  (SetPropsFunc)        link_set_props
};

static PropEnumData prop_link_type_data[] = {
  { N_("Unspecified"),      UNSPECIFIED },
  { N_("Positive Contrib"), POS_CONTRIB },
  { N_("Negative contrib"), NEG_CONTRIB },
  { N_("Dependency"),       DEPENDENCY },
  { N_("Decomposition"),    DECOMPOSITION },
  { N_("Means-Ends"),       MEANS_ENDS },
  { NULL, 0}
};

static PropDescription link_props[] = {
  CONNECTION_COMMON_PROPERTIES,
  { "type", PROP_TYPE_ENUM, PROP_FLAG_VISIBLE,
    N_("Type:"), NULL, prop_link_type_data },
  { "pm", PROP_TYPE_POINT, 0, "pm", NULL, NULL},
  PROP_DESC_END
};

static PropDescription *
link_describe_props(Link *link)
{
  if (link_props[0].quark == 0)
    prop_desc_list_calculate_quarks(link_props);
  return link_props;

}

static PropOffset link_offsets[] = {
  CONNECTION_COMMON_PROPERTIES_OFFSETS,
  { "type", PROP_TYPE_ENUM, offsetof(Link,type)},
  { "pm", PROP_TYPE_POINT, offsetof(Link,pm) },
  { NULL, 0, 0 }
};

static void
link_get_props(Link * link, GPtrArray *props)
{
  object_get_props_from_offsets(&link->connection.object,
                                link_offsets, props);
}

static void
link_set_props(Link *link, GPtrArray *props)
{
  if (link->init==-1) { link->init++; return; }

  object_set_props_from_offsets(&link->connection.object,
                                link_offsets, props);
  link_update_data(link);
}


static real
link_distance_from(Link *link, Point *point)
{
  return distance_bez_line_point(link->line, 3, LINK_WIDTH, point);
}

static void
link_select(Link *link, Point *clicked_point,
	    DiaRenderer *interactive_renderer)
{
  connection_update_handles(&link->connection);
}

static ObjectChange*
link_move_handle(Link *link, Handle *handle,
		 Point *to, ConnectionPoint *cp, 
		 HandleMoveReason reason, ModifierKeys modifiers)
{
  Point p1, p2;
  Point *endpoints;

  assert(link!=NULL);
  assert(handle!=NULL);
  assert(to!=NULL);

  if (handle->id == HANDLE_MOVE_MID_POINT) {
    link->pm = *to;
  } else  {
    endpoints = &link->connection.endpoints[0];
    p1.x = 0.5*(endpoints[0].x + endpoints[1].x);
    p1.y = 0.5*(endpoints[0].y + endpoints[1].y);
    connection_move_handle(&link->connection, handle->id, to, cp, reason, modifiers);
    p2.x = 0.5*(endpoints[0].x + endpoints[1].x);
    p2.y = 0.5*(endpoints[0].y + endpoints[1].y);
    point_sub(&p2, &p1);
    point_add(&link->pm, &p2);
  }

  link_update_data(link);
  return NULL;
}

static ObjectChange*
link_move(Link *link, Point *to)
{
  Point start_to_end;
  Point *endpoints = &link->connection.endpoints[0];
  Point delta;

  delta = *to;
  point_sub(&delta, &endpoints[0]);

  start_to_end = endpoints[1];
  point_sub(&start_to_end, &endpoints[0]);

  endpoints[1] = endpoints[0] = *to;
  point_add(&endpoints[1], &start_to_end);

  point_add(&link->pm, &delta);

  link_update_data(link);
  return NULL;
}

static void
bernstein_develop(const real p[4],real *A,real *B,real *C,real *D)
{
  *A = -p[0]+3*p[1]-3*p[2]+p[3];
  *B = 3*p[0]-6*p[1]+3*p[2];
  *C = 3*p[1]-3*p[0];
  *D = p[0];
  /* if Q(u)=Sum(i=0..3)piBi(u) (Bi(u) being the Bernstein stuff),
     then Q(u)=Au^3+Bu^2+Cu+p[0]. */
}

static real
bezier_eval(const real p[4],real u) {
  real A,B,C,D;
  bernstein_develop(p,&A,&B,&C,&D);
  return A*u*u*u+B*u*u+C*u+D;
}

static real
bezier_eval_tangent(const real p[4],real u) {
  real A,B,C,D;
  bernstein_develop(p,&A,&B,&C,&D);
  return 3*A*u*u+2*B*u+C;
}

static Point bezier_line_eval(BezPoint *line,int p,real u) {
  real bx[4],by[4];
  Point res;

  bx[0]=line[p-1].p3.x;
  bx[1]=line[p].p1.x;
  bx[2]=line[p].p2.x;
  bx[3]=line[p].p3.x;

  by[0]=line[p-1].p3.y;
  by[1]=line[p].p1.y;
  by[2]=line[p].p2.y;
  by[3]=line[p].p3.y;

  res.x=bezier_eval(bx,u);
  res.y=bezier_eval(by,u);

  return res;
}

/*
  p1, p2, pm are start, end medium point
  f is fraction <0.5 for between p1 and pm, >05. for between pm and p2
  d is lateral offset
  cx,cy are text width/height
*/
static Point compute_annot(Point* p1, Point* p2, Point* pm, double f, double d) {
  Point res;
  double dx,dy,k;

  /* computing position */
  if (f<0.5) {
    f=f*2;
    dx=pm->x - p1->x;
    dy=pm->y - p1->y;
    res.x=p1->x+dx*f;
    res.y=p1->y+dy*f;
  } else {
    f=(f-0.5)*2;
    dx=p2->x - pm->x;
    dy=p2->y - pm->y;
    res.x=pm->x+dx*f;
    res.y=pm->y+dy*f;
  }

  /* some orthogonal shift */
  k=sqrt(dx*dx+dy*dy);
  if (k!=0) {
    res.x+=dy/k*d;
    res.y-=dx/k*d;
  }

  /* centering text */
  res.y+=0.25;

  return res;
}

/* compute bezier for Dependency */
static void compute_dependency(BezPoint *line, BezPoint *bpl) {
  Point ref;
  double dx,dy,dxp,dyp,k;
  real bx[4],by[4];

  /* computing anchor point and tangent */
  bx[0]=line[1].p3.x;
  bx[1]=line[2].p1.x;
  bx[2]=line[2].p2.x;
  bx[3]=line[2].p3.x;

  by[0]=line[1].p3.y;
  by[1]=line[2].p1.y;
  by[2]=line[2].p2.y;
  by[3]=line[2].p3.y;

  ref.x=bezier_eval(bx,0.25);
  ref.y=bezier_eval(by,0.25);
  dx=bezier_eval_tangent(bx,0.25);
  dy=bezier_eval_tangent(by,0.25);
  k=sqrt(dx*dx+dy*dy);

  /* normalizing */
  if (k!=0) {
    dx=dx/k;
    dy=dy/k;
    dxp=dy;
    dyp=-dx;
  } else {
    dx=0;
    dy=1;
    dxp=1;
    dyp=0;
  }

  /*  more offset for origin */
  ref.x -= LINK_DEP_HEIGHT * dx;
  ref.y -= LINK_DEP_HEIGHT * dy;

  /* bezier */
  bpl[0].type=BEZ_MOVE_TO;
  bpl[0].p1.x = ref.x + LINK_DEP_WIDTH/2.0 * dxp;
  bpl[0].p1.y = ref.y + LINK_DEP_WIDTH/2.0 * dyp;

  bpl[1].type=BEZ_CURVE_TO;
  bpl[1].p3.x = ref.x + LINK_DEP_HEIGHT * dx;
  bpl[1].p3.y = ref.y + LINK_DEP_HEIGHT * dy;
  bpl[1].p1.x = ref.x + LINK_DEP_WIDTH/2.0 * dxp + LINK_DEP_HEIGHT * dx;
  bpl[1].p1.y = ref.y + LINK_DEP_WIDTH/2.0 * dyp + LINK_DEP_HEIGHT * dy;
  bpl[1].p2.x = bpl[1].p1.x;
  bpl[1].p2.y = bpl[1].p1.y;

  bpl[2].type=BEZ_CURVE_TO;
  bpl[2].p3.x = ref.x - LINK_DEP_WIDTH/2.0 * dxp;
  bpl[2].p3.y = ref.y - LINK_DEP_WIDTH/2.0 * dyp;
  bpl[2].p1.x = ref.x - LINK_DEP_WIDTH/2.0 * dxp + LINK_DEP_HEIGHT * dx;
  bpl[2].p1.y = ref.y - LINK_DEP_WIDTH/2.0 * dyp + LINK_DEP_HEIGHT * dy;
  bpl[2].p2.x = bpl[2].p1.x;
  bpl[2].p2.y = bpl[2].p1.y;

  bpl[3].type=BEZ_LINE_TO;
  bpl[3].p1.x=bpl[0].p1.x;
  bpl[3].p1.y=bpl[0].p1.y;
}

static void
compute_line(Point* p1, Point* p2, Point *pm, BezPoint* line) {
  double dx,dy,k;
  double dx1,dy1,k1;
  double dx2,dy2,k2;
  double OFF=1.0;

  dx=p2->x-p1->x;
  dy=p2->y-p1->y;
  k=sqrt(dx*dx+dy*dy);
  if (k!=0) {
    dx=dx/k;
    dy=dy/k;
  } else {
    dx=0;
    dy=1;
  }

  dx1=pm->x-p1->x;
  dy1=pm->y-p1->y;
  k1=sqrt(dx*dx+dy*dy);
  if (k1!=0) {
    dx1=dx1/k;
    dy1=dy1/k;
  } else {
    dx1=0;
    dy1=1;
  }

  dx2=p2->x-pm->x;
  dy2=p2->y-pm->y;
  k2=sqrt(dx*dx+dy*dy);
  if (k2!=0) {
    dx2=dx2/k;
    dy2=dy2/k;
  } else {
    dx2=0;
    dy2=1;
  }

  line[0].type=BEZ_MOVE_TO;
  line[0].p1.x=p1->x;
  line[0].p1.y=p1->y;

  line[1].type=BEZ_CURVE_TO;
  line[1].p3.x=pm->x;
  line[1].p3.y=pm->y;
  line[1].p1.x=p1->x+dx1*OFF;
  line[1].p1.y=p1->y+dy1*OFF;
  line[1].p2.x=pm->x-dx*OFF;
  line[1].p2.y=pm->y-dy*OFF;

  line[2].type=BEZ_CURVE_TO;
  line[2].p3.x=p2->x;
  line[2].p3.y=p2->y;
  line[2].p1.x=pm->x+dx*OFF;
  line[2].p1.y=pm->y+dy*OFF;
  line[2].p2.x=p2->x-dx2*OFF;
  line[2].p2.y=p2->y-dy2*OFF;
}


/* drawing here -- TBD inverse flow ??  */
static void
link_draw(Link *link, DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Point *endpoints, p1, p2, pa;
  Arrow arrow;
  gchar *annot;
  double w;
  BezPoint bpl[4];

  /* some asserts */
  assert(link != NULL);
  assert(renderer != NULL);

  /* some point computations */
  endpoints = &link->connection.endpoints[0];
  p1 = endpoints[0];     /* could reverse direction here */
  p2 = endpoints[1];
  pa = compute_annot(&p1,&p2,&link->pm,0.75,0.75);

  /** computing properties **/
  w=LINK_WIDTH;
  annot=NULL;
  arrow.type = ARROW_FILLED_TRIANGLE;
  arrow.length = LINK_ARROWLEN;
  arrow.width = LINK_ARROWWIDTH;

  switch (link->type) {
    case POS_CONTRIB:
      w=1.5*LINK_WIDTH;
      annot = g_strdup("+");
      break;
    case NEG_CONTRIB:
      w=1.5*LINK_WIDTH;
      annot = g_strdup("-");
      break;
    case DEPENDENCY:
      annot = g_strdup("");
      break;
    case DECOMPOSITION:
      arrow.type = ARROW_CROSS;
      annot = g_strdup("");
      break;
    case MEANS_ENDS:
      arrow.type = ARROW_LINES;
      annot = g_strdup("");
      break;
    case UNSPECIFIED: /* use above defaults */
      annot = g_strdup("");
      break;
  }

  /** drawing line **/
  renderer_ops->set_linecaps(renderer, LINECAPS_BUTT);
  renderer_ops->set_linestyle(renderer, LINESTYLE_SOLID);
  renderer_ops->set_linewidth(renderer, w);
  renderer_ops->draw_bezier_with_arrows(renderer, link->line, 3, w, &LINK_FG_COLOR, NULL, &arrow);

  /** drawing decoration **/
  renderer_ops->set_font(renderer, link_font, LINK_FONTHEIGHT);
  if ((annot!=NULL)&& strlen(annot) != 0)
    renderer_ops->draw_string(renderer, annot, &pa, ALIGN_CENTER, &color_black);
  if (annot!=NULL) g_free(annot);

  /** special stuff for dependency **/
  if (link->type==DEPENDENCY) {
    compute_dependency(link->line,bpl);
    renderer_ops->draw_bezier(renderer, bpl, 4, &LINK_FG_COLOR);
  }
}

/* creation here */
static DiaObject *
link_create(Point *startpoint,
		  void *user_data,
		  Handle **handle1,
		  Handle **handle2)
{
  Link *link;
  Connection *conn;
  LineBBExtras *extra;
  DiaObject *obj;

  if (link_font == NULL) {
    link_font = dia_font_new_from_style(DIA_FONT_SANS, LINK_FONTHEIGHT);
  }

  link = g_malloc0(sizeof(Link));

  conn = &link->connection;
  conn->endpoints[0] = *startpoint;
  conn->endpoints[1] = *startpoint;
  conn->endpoints[1].y -= 2;

  obj = &conn->object;
  extra = &conn->extra_spacing;

  switch (GPOINTER_TO_INT(user_data)) {
    case 1: link->type=UNSPECIFIED; break;
    case 2: link->type=POS_CONTRIB; break;
    case 3: link->type=NEG_CONTRIB; break;
    case 4: link->type=DEPENDENCY;  break;
    case 5: link->type=DECOMPOSITION; break;
    case 6: link->type=MEANS_ENDS;  break;
    default: link->type=UNSPECIFIED; break;
  }

  obj->type = &istar_link_type;
  obj->ops = &link_ops;

  /* connectionpoint init */
  connection_init(conn, 3, 0);   /* with mid-control */

  link->pm.x=0.5*(conn->endpoints[0].x + conn->endpoints[1].x);
  link->pm.y=0.5*(conn->endpoints[0].y + conn->endpoints[1].y);
  link->pm_handle.id = HANDLE_MOVE_MID_POINT;
  link->pm_handle.type = HANDLE_MINOR_CONTROL;
  link->pm_handle.connect_type = HANDLE_NONCONNECTABLE;
  link->pm_handle.connected_to = NULL;
  obj->handles[2] = &link->pm_handle;

  compute_line(&conn->endpoints[0],&conn->endpoints[1],&link->pm,link->line);

  extra->start_long =
  extra->start_trans =
  extra->end_long = LINK_WIDTH/2.0;
  extra->end_trans = MAX(LINK_WIDTH,LINK_ARROWLEN)/2.0;

  link_update_data(link);

  *handle1 = obj->handles[0];
  *handle2 = obj->handles[1];

  /* bug workaround */
  if (GPOINTER_TO_INT(user_data)!=0) link->init=-1; else link->init=0;

  return &link->connection.object;
}

static void
link_destroy(Link *link)
{
  connection_destroy(&link->connection);
}

static void
link_update_data(Link *link)
{
  Connection *conn = &link->connection;
  DiaObject *obj = &conn->object;
  Rectangle rect;
  Point p1,p2,p3,p4,pa;

/* Too complex to easily decide */
/*
  if (connpoint_is_autogap(conn->endpoint_handles[0].connected_to) ||
      connpoint_is_autogap(conn->endpoint_handles[1].connected_to)) {
    connection_adjust_for_autogap(conn);
  }
*/
  obj->position = conn->endpoints[0];

  link->pm_handle.pos = link->pm;

  connection_update_handles(conn);
  connection_update_boundingbox(conn);

  /* endpoint */
  p1 = conn->endpoints[0];
  p2 = conn->endpoints[1];

  /* bezier */
  compute_line(&p1,&p2,&link->pm,link->line);

  /* connection point */
  link->connector.pos.x=p1.x;
  link->connector.pos.y=p1.y;

  /* Update boundingbox for mid-point (TBD is this necessary ?) */
  rectangle_add_point(&obj->bounding_box, &link->pm);

  /* Add boundingbox for annotation text (over-estimated) : */
  pa=compute_annot(&p1,&p2,&link->pm,0.75,0.75);
  rect.left = pa.x-0.3;
  rect.right = rect.left+0.6;
  rect.top = pa.y - LINK_FONTHEIGHT;
  rect.bottom = rect.top + 2*LINK_FONTHEIGHT;
  rectangle_union(&obj->bounding_box, &rect);

  /* Add boundingbox for dependency decoration (with some overestimation toi be safe) */
  pa=bezier_line_eval(link->line,2,0.25);
  p3.x=pa.x-LINK_DEP_WIDTH*1.5;
  p3.y=pa.y-LINK_DEP_HEIGHT*1.5;
  p4.x=p3.x+LINK_DEP_WIDTH*3;
  p4.y=p3.y+LINK_DEP_HEIGHT*3;
  rect.left=p3.x;
  rect.right=p4.x;
  rect.top=p3.y;
  rect.bottom=p4.y;
  rectangle_union(&obj->bounding_box, &rect);
}

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