/* Dia -- a diagram creation/manipulation program -*- c -*-
 * Copyright (C) 2002 Alexander Larsson
 *
 * autoroute.c -- Automatic layout of connections.
 * Copyright (C) 2003 Lars Clausen
 *
 * 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.
 */

#include "config.h"

#include "object.h"
#include "connectionpoint.h"
#include "orth_conn.h"
#include "autoroute.h"

#define MAX_BADNESS 10000.0
/** Add badness if a line is shorter than this distance. */
#define MIN_DIST 1.0
/** The maximum badness that can be given for a line being too short. */
#define MAX_SMALL_BADNESS 10.0
/** The badness given for having extra segments. */
#define EXTRA_SEGMENT_BADNESS 10.0

static real calculate_badness(Point *ps, guint num_points);

static real autoroute_layout_parallel(Point *to, 
					guint *num_points, Point **points);
static real autoroute_layout_orthogonal(Point *to,
					  int enddir, 
					  guint *num_points, Point **points);
static real autoroute_layout_opposite(Point *to, 
					guint *num_points, Point **points);
static Point autolayout_adjust_for_gap(Point *pos, int dir, ConnectionPoint *cp);
static guint autolayout_normalize_points(guint startdir, guint enddir,
					 Point start, Point end,
					 Point *newend);
static Point *autolayout_unnormalize_points(guint dir,
					    Point start,
					    Point *points,
					    guint num_points);

/** Calculate a 'pleasing' route between two connection points.
 * If a good route is found, updates the given OrthConn with the values
 *   and returns TRUE.
 * Otherwise, the OrthConn is untouched, and the function returns FALSE.
 * Handles are not updated by this operation.
 * @param conn The orthconn object to autoroute for.
 * @param startconn The connectionpoint at the start of the orthconn,
 *                  or null if it is not connected there at the moment.
 * @param endconn The connectionpoint at the end (target) of the orthconn,
 *                or null if it is not connected there at the moment.
 * @returns TRUE if the orthconn could be laid out reasonably, FALSE otherwise.
 */
gboolean
autoroute_layout_orthconn(OrthConn *conn, 
			  ConnectionPoint *startconn, ConnectionPoint *endconn)
{
  real min_badness = MAX_BADNESS;
  Point *best_layout = NULL;
  guint best_num_points = 0;
  int startdir, enddir;

  int fromdir, todir;
  Point frompos, topos;

  frompos = conn->points[0];
  topos = conn->points[conn->numpoints-1];
  if (startconn != NULL) {
    fromdir = startconn->directions;
    frompos = startconn->pos;
  }
  else fromdir = DIR_NORTH|DIR_EAST|DIR_SOUTH|DIR_WEST;
  if (endconn != NULL) {
    todir = endconn->directions;
    topos = endconn->pos;
  }
  else todir = DIR_NORTH|DIR_EAST|DIR_SOUTH|DIR_WEST;

  for (startdir = DIR_NORTH; startdir <= DIR_WEST; startdir *= 2) {
    for (enddir = DIR_NORTH; enddir <= DIR_WEST; enddir *= 2) {
      if ((fromdir & startdir) &&
	  (todir & enddir)) {
	real this_badness;
	Point *this_layout = NULL;
	guint this_num_points;
	guint normal_enddir;
	Point startpoint, endpoint;
	Point otherpoint;
	startpoint = autolayout_adjust_for_gap(&frompos, startdir, startconn);
	endpoint = autolayout_adjust_for_gap(&topos, enddir, endconn);
	/*
	printf("Startdir %d enddir %d orgstart %.2f, %.2f orgend %.2f, %.2f start %.2f, %.2f end %.2f, %.2f\n",
	       startdir, enddir,
	       frompos.x, frompos.y,
	       topos.x, topos.y,
	       startpoint.x, startpoint.y,
	       endpoint.x, endpoint.y);
	*/
	normal_enddir = autolayout_normalize_points(startdir, enddir,
						    startpoint, endpoint,
						    &otherpoint);
	if (normal_enddir == DIR_NORTH ) {
	  this_badness = autoroute_layout_parallel(&otherpoint,
						   &this_num_points,
						   &this_layout);
	} else if (normal_enddir == DIR_SOUTH) {
	  this_badness = autoroute_layout_opposite(&otherpoint, 
						   &this_num_points,
						   &this_layout);
	} else {
	  this_badness = autoroute_layout_orthogonal(&otherpoint,
						     normal_enddir,
						     &this_num_points,
						     &this_layout);
	}
	if (this_layout != NULL) {
	  if (this_badness-min_badness < -0.00001) {
	    /*
	    printf("Dir %d to %d badness %f < %f\n", startdir, enddir,
		   this_badness, min_badness);
	    */
	    min_badness = this_badness;
	    if (best_layout != NULL) g_free(best_layout);
	    best_layout = autolayout_unnormalize_points(startdir, startpoint,
							this_layout, 
							this_num_points);
	    best_num_points = this_num_points;
	  } else {
	      g_free(this_layout);
	  }
	}
      }
    }    
  }
  
  if (min_badness < MAX_BADNESS) {
    orthconn_set_points(conn, best_num_points, best_layout);
    return TRUE;
  } else {
    g_free(best_layout);
    return FALSE;
  }
}

/** Returns the basic badness of a length.  The best length is MIN_DIST,
 * anything shorter quickly becomes messy, longer segments are linearly worse.
 * @param len The length of an orthconn segment.
 * @returns How bad this segment would be to have in the autorouting.
 */
static real
length_badness(real len)
{
  if (len < MIN_DIST) {
    /* This should be zero at MIN_DIST and MAX_SMALL_BADNESS at 0 */
    return 2*MAX_SMALL_BADNESS/(1.0+len/MIN_DIST) - MAX_SMALL_BADNESS;
  } else {
    return len-MIN_DIST;
  }
}

/** Returns the accumulated badness of a layout.  At the moment, this is
 * calculated as the sum of the badnesses of the segments plus a badness for
 * each bend in the line.
 * @param ps An array of points.
 * @param num_points How many points in the array.
 * @returns How bad the points would look as an orthconn layout.
 */
static real
calculate_badness(Point *ps, guint num_points)
{
  real badness;
  guint i;
  badness = (num_points-1)*EXTRA_SEGMENT_BADNESS;
  for (i = 0; i < num_points-1; i++) {
    real this_badness;
    real len = distance_point_point_manhattan(&ps[i], &ps[i+1]);
    this_badness = length_badness(len);
    badness += this_badness;
  }
  return badness;
}

/** Adjust one end of an orthconn for gaps, if autogap is on for the connpoint.
 * @param pos Point of the end of the line.
 * @param dir Which of the four cardinal directions the line goes from pos.
 * @param cp The connectionpoint the line is connected to.
 * @returns Where the line should end to be on the correct edge of the 
 *          object, if cp has autogap on.
 */
static Point
autolayout_adjust_for_gap(Point *pos, int dir, ConnectionPoint *cp)
{
  DiaObject *object;
  Point dir_other;
  /* Do absolute gaps here, once it's defined */
  
  if (!connpoint_is_autogap(cp)) {
    return *pos;
  }

  object  = cp->object;
  
  dir_other.x = pos->x;
  dir_other.y = pos->y;
  switch (dir) {
  case DIR_NORTH: 
    dir_other.y += 2 * (object->bounding_box.top - pos->y);
    break;
  case DIR_SOUTH:
    dir_other.y += 2 * (object->bounding_box.bottom - pos->y);
    break;
  case DIR_EAST:
    dir_other.x += 2 * (object->bounding_box.right - pos->x);
    break;
  case DIR_WEST:
    dir_other.x += 2 * (object->bounding_box.left - pos->x);
    break;
  default: 
    g_warning("Impossible direction %d\n", dir);
  }
  return calculate_object_edge(pos, &dir_other, object);
}

/** Lay out autorouting where start and end lines are parallel pointing the
 * same direction.  This can either a simple up-right-down layout, or if the
 * to point is too close to origo, it will go up-right-down-left-down.
 * @param to Where to lay out to, coming from origo.
 * @param num_points Return value of how many points in the points array.
 * @param points The points in the layout.  Free the array after use.  The
 *               passed in is ignored and overwritten, so should be NULL.
 * @returns The badness of this layout.
 */
static real
autoroute_layout_parallel(Point *to, guint *num_points, Point **points)
{
  Point *ps = NULL;
  if (fabs(to->x) > MIN_DIST) {
    real top = MIN(-MIN_DIST, to->y-MIN_DIST);
    /*
    printf("Doing parallel layout: Wide\n");
    */
    *num_points = 4;
    ps = g_new0(Point, *num_points);
    /* points[0] is 0,0 */
    ps[1].y = top;
    ps[2].x = to->x;
    ps[2].y = top;
    ps[3] = *to;
  } else if (to->y > 0) { /* Close together, end below */
    real top = -MIN_DIST;
    real off = to->x+MIN_DIST*(to->x>0?1.0:-1.0);
    real bottom = to->y-MIN_DIST;
    /*
    printf("Doing parallel layout: Narrow\n");
    */
    *num_points = 6;
    ps = g_new0(Point, *num_points);
    /* points[0] is 0,0 */
    ps[1].y = top;
    ps[2].x = off;
    ps[2].y = top;
    ps[3].x = off;
    ps[3].y = bottom;
    ps[4].x = to->x;
    ps[4].y = bottom;
    ps[5] = *to;
  } else {
    real top = to->y-MIN_DIST;
    real off = MIN_DIST*(to->x>0?-1.0:1.0);
    real bottom = -MIN_DIST;
    /*
    printf("Doing parallel layout: Narrow\n");
    */
    *num_points = 6;
    ps = g_new0(Point, *num_points);
    /* points[0] is 0,0 */
    ps[1].y = bottom;
    ps[2].x = off;
    ps[2].y = bottom;
    ps[3].x = off;
    ps[3].y = top;
    ps[4].x = to->x;
    ps[4].y = top;
    ps[5] = *to;
  }
  *points = ps;
  return calculate_badness(ps, *num_points);
}

/** Do layout for the case where the directions are orthogonal to each other.
 * If both x and y of to are far enough from origo, this will be a simple
 * bend, otherwise it will be a question-mark style line.
 * @param to Where to lay out to, coming from origo.
 * @param enddir What direction the endpoint goes, either east or west.
 * @param num_points Return value of how many points in the points array.
 * @param points The points in the layout.  Free the array after use.  The
 *               passed in is ignored and overwritten, so should be NULL.
 * @returns The badness of this layout.
 */
static real
autoroute_layout_orthogonal(Point *to, int enddir, 
			    guint *num_points, Point **points)
{
  /* This one doesn't consider enddir yet, not more complex layouts. */
  Point *ps = NULL;
  real dirmult = (enddir==DIR_WEST?1.0:-1.0);
  if (to->y < -MIN_DIST) {
    if (dirmult*to->x > MIN_DIST) {
      /*
      printf("Doing orthogonal layout: Three-way\n");
      */
      *num_points = 3;
      ps = g_new0(Point, *num_points);
      /* points[0] is 0,0 */
      ps[1].y = to->y;
      ps[2] = *to;
    } else {
      real off;
      if (dirmult*to->x > 0) off = -dirmult*MIN_DIST;
      else off = -dirmult*(MIN_DIST+fabs(to->x));
      *num_points = 5;
      ps = g_new0(Point, *num_points);
      ps[1].y = -MIN_DIST;
      ps[2].x = off;
      ps[2].y = -MIN_DIST;
      ps[3].x = off;
      ps[3].y = to->y;
      ps[4] = *to;
    }
  } else {
    if (dirmult*to->x > 2*MIN_DIST) {
      real mid = to->x/2;
      *num_points = 5;
      ps = g_new0(Point, *num_points);
      ps[1].y = -MIN_DIST;
      ps[2].x = mid;
      ps[2].y = -MIN_DIST;
      ps[3].x = mid;
      ps[3].y = to->y;
      ps[4] = *to;
    } else {
      real off;
      if (dirmult*to->x > 0) off = -dirmult*MIN_DIST;
      else off = -dirmult*(MIN_DIST+fabs(to->x));
      *num_points = 5;
      ps = g_new0(Point, *num_points);
      ps[1].y = -MIN_DIST;
      ps[2].x = off;
      ps[2].y = -MIN_DIST;
      ps[3].x = off;
      ps[3].y = to->y;
      ps[4] = *to;
    }
  }
  /*
  printf("Doing orthogonal layout\n");
  */
  *points = ps;
  return calculate_badness(ps, *num_points);
}

/** Do layout for the case where the end directions are opposite.
 * This can be either a straight line, a zig-zag, a rotated s-shape or
 * a spiral.
 * @param to Where to lay out to, coming from origo.
 * @param num_points Return value of how many points in the points array.
 * @param points The points in the layout.  Free the array after use.  The
 *               passed in is ignored and overwritten, so should be NULL.
 * @returns The badness of this layout.
 */
static real
autoroute_layout_opposite(Point *to, guint *num_points, Point **points)
{
  Point *ps = NULL;
  if (to->y < -MIN_DIST) {
    *num_points = 4;
    ps = g_new0(Point, *num_points);
    if (fabs(to->x) < 0.00000001) {
      ps[2] = ps[3] = *to;
      *points = ps;
      return length_badness(fabs(to->y))+2*EXTRA_SEGMENT_BADNESS;
    } else {
      real mid = to->y/2;
      /*
	printf("Doing opposite layout: Three-way\n");
      */
      /* points[0] is 0,0 */
      ps[1].y = mid;
      ps[2].x = to->x;
      ps[2].y = mid;
      ps[3] = *to;
      *points = ps;
      return 2*length_badness(fabs(mid))+2*EXTRA_SEGMENT_BADNESS;
    }
  } else if (fabs(to->x) > 2*MIN_DIST) {
    real mid = to->x/2;
    /*
    printf("Doing opposite layout: Doorhanger\n");
    */
    *num_points = 6;
    ps = g_new0(Point, *num_points);
    /* points[0] is 0,0 */
    ps[1].y = -MIN_DIST;
    ps[2].x = mid;
    ps[2].y = -MIN_DIST;
    ps[3].x = mid;
    ps[3].y = to->y+MIN_DIST;
    ps[4].x = to->x;
    ps[4].y = to->y+MIN_DIST;
    ps[5] = *to;
  } else {
    real off = MIN_DIST*(to->x>0?-1.0:1.0);
    /*
    printf("Doing opposite layout: Overlap\n");
    */
    *num_points = 6;
    ps = g_new0(Point, *num_points);
    ps[1].y = -MIN_DIST;
    ps[2].x = off;
    ps[2].y = -MIN_DIST;
    ps[3].x = off;
    ps[3].y = to->y+MIN_DIST;
    ps[4].x = to->x;
    ps[4].y = to->y+MIN_DIST;
    ps[5] = *to;
  }
  *points = ps;
  return calculate_badness(ps, *num_points);
}

/** Rotate a point clockwise.
 * @param p The point to rotate.
 */
static void
point_rotate_cw(Point *p)
{
  real tmp = p->x;
  p->x = -p->y;
  p->y = tmp;
}

/** Rotate a point counterclockwise.
 * @param p The point to rotate.
 */
static void
point_rotate_ccw(Point *p)
{
  real tmp = p->x;
  p->x = p->y;
  p->y = -tmp;
}

/** Rotate a point 180 degrees.
 * @param p The point to rotate.
 */
static void
point_rotate_180(Point *p)
{
  p->x = -p->x;
  p->y = -p->y;
}

/** Normalizes the directions and points to make startdir be north and
 * the starting point be 0,0.
 * @param startdir The original startdir.
 * @param enddir The original enddir.
 * @param start The original start point.
 * @param end The original end point.
 * @param newend Return address for the normalized end point.
 * @returns The normalized end direction.
 */
static guint
autolayout_normalize_points(guint startdir, guint enddir,
			    Point start, Point end, Point *newend)
{
  newend->x = end.x-start.x;
  newend->y = end.y-start.y;
  if (startdir == DIR_NORTH) {
    return enddir;
  } else if (startdir == DIR_EAST) {
    point_rotate_ccw(newend);
    if (enddir == DIR_NORTH) return DIR_WEST;
    return enddir/2;
  } else if (startdir == DIR_WEST) {
    point_rotate_cw(newend);
    if (enddir == DIR_WEST) return DIR_NORTH;
    return enddir*2;
  } else { /* startdir == DIR_SOUTH */
    point_rotate_180(newend);
    if (enddir < DIR_SOUTH) return enddir*4;
    else return enddir/4;
  }
  /* Insert handling of other stuff here */
  return enddir;
}

/** Reverses the normalizing process of autolayout_normalize_points by 
 * moving and rotating the points to start at `start' with the start direction
 * `startdir', instead of from origo going north.
 * Returns the new array of points, freeing the old one if necessary.
 * @param startdir The direction to use as a starting direction.
 * @param start The point to start at.
 * @param points A set of points laid out from origo northbound.  This array
 *               will be freed by calling this function.
 * @param num_points The number of points in the `points' array.
 * @returns A newly allocated array of points starting at `start'.
 */
static Point *
autolayout_unnormalize_points(guint startdir,
			      Point start,
			      Point *points,
			      guint num_points)
{
  Point *newpoints = g_new(Point, num_points);
  guint i;
  if (startdir == DIR_NORTH) {
    for (i = 0; i < num_points; i++) {
      newpoints[i] = points[i];
      point_add(&newpoints[i], &start);
    }
  } else if (startdir == DIR_WEST) {
    for (i = 0; i < num_points; i++) {
      newpoints[i] = points[i];
      point_rotate_ccw(&newpoints[i]);
      point_add(&newpoints[i], &start);
    }
  } else if (startdir == DIR_SOUTH) {
    for (i = 0; i < num_points; i++) {
      newpoints[i] = points[i];
      point_rotate_180(&newpoints[i]);
      point_add(&newpoints[i], &start);
    }
  } else if (startdir == DIR_EAST) {
    for (i = 0; i < num_points; i++) {
      newpoints[i] = points[i];
      point_rotate_cw(&newpoints[i]);
      point_add(&newpoints[i], &start);
    }
  }
  g_free(points);
  return newpoints;
}
