/* distalign plug-in for PCB

   Functions to distribute (evenly spread out) and align PCB elements.

   Copyright (C) 2007 Ben Jackson <ben@ben.com>

   Licensed under the terms of the GNU General Public License, version
   2 or later.
*/

#include <stdio.h>
#include <math.h>

#include "global.h"
#include "data.h"
#include "hid.h"
#include "misc.h"
#include "create.h"
#include "rtree.h"
#include "undo.h"
#include "rats.h"

#define ARG(n) (argc > (n) ? argv[n] : 0)

static const char align_syntax[] = "Align(X/Y, [Lefts/Rights/Tops/Bottoms/Centers/Marks, [First/Last/Crosshair/Average[, Gridless]]])";

static const char distribute_syntax[] = "Distribute(Y, [Lefts/Rights/Tops/Bottoms/Centers/Marks/Gaps, [First/Last/Crosshair, First/Last/Crosshair[, Gridless]]])";

enum {
	K_X,
	K_Y,
	K_Lefts,
	K_Rights,
	K_Tops,
	K_Bottoms,
	K_Centers,
	K_Marks,
	K_Gaps,
	K_First,
	K_Last,
	K_Average,
	K_Crosshair,
	K_Gridless,
	K_none,
	K_align,
	K_distribute
};

static const char *keywords[] = {
	[K_X]		"X",
	[K_Y]		"Y",
	[K_Lefts]	"Lefts",
	[K_Rights]	"Rights",
	[K_Tops]	"Tops",
	[K_Bottoms]	"Bottoms",
	[K_Centers]	"Centers",
	[K_Marks]	"Marks",
	[K_Gaps]	"Gaps",
	[K_First]	"First",
	[K_Last]	"Last",
	[K_Average]	"Average",
	[K_Crosshair]	"Crosshair",
	[K_Gridless]	"Gridless",
};

static int
keyword(const char *s)
{
	int i;

	if (! s) {
		return K_none;
	}
	for (i = 0; i < ENTRIES(keywords); ++i) {
		if (keywords[i] && strcasecmp(s, keywords[i]) == 0)
			return i;
	}
	return -1;
}


/* this macro produces a function in X or Y that switches on 'point' */
#define COORD(DIR)						\
static inline LocationType					\
coord ## DIR(ElementTypePtr element, int point)			\
{								\
	switch (point) {					\
	case K_Marks:						\
		return element->Mark ## DIR;			\
	case K_Lefts:						\
	case K_Tops:						\
		return element->BoundingBox.DIR ## 1;		\
	case K_Rights:						\
	case K_Bottoms:						\
		return element->BoundingBox.DIR ## 2;		\
	case K_Centers:						\
	case K_Gaps:						\
		return (element->BoundingBox.DIR ## 1 +		\
		       element->BoundingBox.DIR ## 2) / 2;	\
	}							\
	return 0;						\
}

COORD(X)
COORD(Y)

/* return the element coordinate associated with the given internal point */
static LocationType
coord(ElementTypePtr element, int dir, int point)
{
	if (dir == K_X)
		return coordX(element, point);
	else
		return coordY(element, point);
}

static struct element_by_pos {
	ElementTypePtr	element;
	LocationType	pos;
	LocationType	width;
} *elements_by_pos;
static nelements_by_pos;

static int
cmp_ebp(const void *a, const void *b)
{
	const struct element_by_pos *ea = a;
	const struct element_by_pos *eb = b;

	return ea->pos - eb->pos;
}

/*
 * Find all selected objects, then order them in order by coordinate in
 * the 'dir' axis.  This is used to find the "First" and "Last" elements
 * and also to choose the distribution order.
 *
 * For alignment, first and last are in the orthogonal axis (imagine if
 * you were lining up letters in a sentence, aligning *vertically* to the
 * first letter means selecting the first letter *horizontally*).
 *
 * For distribution, first and last are in the distribution axis.
 */
static int
sort_elements_by_pos(int op, int dir, int point)
{
	int nsel = 0;
	struct element_pos *ep;

	if (nelements_by_pos)
		return nelements_by_pos;

	if (op == K_align)
		dir = dir == K_X ? K_Y : K_X;	/* see above */

	ELEMENT_LOOP(PCB->Data);
	{
		if (! TEST_FLAG (SELECTEDFLAG, element))
			continue;
		nsel++;
	}
	END_LOOP;

	if (! nsel)
		return 0;

	elements_by_pos = malloc(nsel * sizeof(*elements_by_pos));
	nelements_by_pos = nsel;

	nsel = 0;
	ELEMENT_LOOP(PCB->Data);
	{
		if (! TEST_FLAG (SELECTEDFLAG, element))
			continue;
		elements_by_pos[nsel].element = element;
		elements_by_pos[nsel++].pos = coord(element, dir, point);
	}
	END_LOOP;

	qsort(elements_by_pos, nelements_by_pos,
		sizeof(*elements_by_pos), cmp_ebp);
	return nelements_by_pos;
}

static void
free_elements_by_pos(void)
{
	if (nelements_by_pos) {
		free(elements_by_pos);
		elements_by_pos = NULL;
		nelements_by_pos = 0;
	}
}

/* find the reference coordinate from the specified points of all selected elements */
static LocationType
reference_coord(int op, int x, int y, int dir, int point, int reference)
{
	LocationType q;
	int nsel;

	switch (reference) {
	case K_Crosshair:
		if (dir == K_X)
			q = x;
		else
			q = y;
		break;

	case K_Average:		/* the average among selected elements */
		nsel = 0;
		q = 0;
		ELEMENT_LOOP(PCB->Data);
		{
			if (! TEST_FLAG (SELECTEDFLAG, element))
				continue;
			q += coord(element, dir, point);
			nsel++;
		}
		END_LOOP;
		if (nsel)
			q /= nsel;
		break;

	case K_First:		/* first or last in the orthogonal direction */
	case K_Last:
		if (! sort_elements_by_pos(op, dir, point)) {
			q = 0;
			break;
		}
		if (reference == K_First) {
			q = coord(elements_by_pos[0].element,
				dir, point);
		} else {
			q = coord(elements_by_pos[nelements_by_pos-1].element,
				dir, point);
		}
		break;
	}
	return q;
}

/*
 * Align(X, [Lefts/Rights/Centers/Marks, [First/Last/Crosshair/Average[, Gridless]]])
 * Align(Y, [Tops/Bottoms/Centers/Marks, [First/Last/Crosshair/Average[, Gridless]]])
 *
 *	X or Y		- Select which axis will move, other is untouched
 *	Lefts, Rights,
 *	Tops, Bottoms,
 *	Centers, Marks	- Pick alignment point within each element
 *	First, Last, 
 *	Crosshair, 
 *	Average		- Alignment reference, First=Topmost/Leftmost,
 *			  Last=Bottommost/Rightmost, Average or Crosshair point
 *	Gridless	- Do not force results to align to prevailing grid
 *
 * Defaults are Marks, First
 */
static int
align(int argc, char **argv, int x, int y)
{
	int dir;
	int point;
	int reference;
	int gridless;
	LocationType q;
	int changed = 0;

	if (argc < 1 || argc > 4) {
		AFAIL(align);
	}

	/* parse direction arg */
	switch ((dir = keyword(ARG(0)))) {
	case K_X:
	case K_Y:
		break;
	default:
		AFAIL(align);
	}

	/* parse point (within each element) which will be aligned */
	switch ((point = keyword(ARG(1)))) {
	case K_Centers:
	case K_Marks:
		break;
	case K_Lefts:
	case K_Rights:
		if (dir == K_Y) {
			AFAIL(align);
		}
		break;
	case K_Tops:
	case K_Bottoms:
		if (dir == K_X) {
			AFAIL(align);
		}
		break;
	case K_none:
		point = K_Marks;	/* default value */
		break;
	default:
		AFAIL(align);
	}

	/* parse reference which will determine alignment coordinates */
	switch ((reference = keyword(ARG(2)))) {
	case K_First:
	case K_Last:
	case K_Average:
	case K_Crosshair:
		break;
	case K_none:
		reference = K_First;	/* default value */
		break;
	default:
		AFAIL(align);
	}

	/* optionally work off the grid (solar cells!) */
	switch (keyword(ARG(3))) {
	case K_Gridless:
		gridless = 1;
		break;
	case K_none:
		gridless = 0;
		break;
	default:
		AFAIL(align);
	}

	/* find the final alignment coordinate using the above options */
	q = reference_coord(K_align, Crosshair.X, Crosshair.Y,
				dir, point, reference);

	/* move all selected elements to the new coordinate */
	ELEMENT_LOOP(PCB->Data);
	{
		LocationType p, dp, dx, dy;

		if (! TEST_FLAG (SELECTEDFLAG, element))
			continue;
		/* find delta from reference point to reference point */
		p = coord(element, dir, point);
		dp = q - p;
		/* ...but if we're gridful, keep the mark on the grid */
		if (! gridless) {
			dp -= (coord(element, dir, K_Marks) + dp)
					% (long) (PCB->Grid);
		}
		if (dp) {
			/* move from generic to X or Y */
			dx = dy = dp;
			if (dir == K_X)
				dy = 0;
			else
				dx = 0;
			MoveElementLowLevel(PCB->Data, element, dx, dy);
			AddObjectToMoveUndoList(ELEMENT_TYPE, NULL, NULL,
						element, dx, dy);
			changed = 1;
		}
	}
	END_LOOP;

	if (changed) {
		IncrementUndoSerialNumber();
		ClearAndRedrawOutput();
		SetChangedFlag(1);
	}

	free_elements_by_pos();

	return 0;
}

/*
 * Distribute(X, [Lefts/Rights/Centers/Marks/Gaps, [First/Last/Crosshair, First/Last/Crosshair[, Gridless]]])
 * Distribute(Y, [Tops/Bottoms/Centers/Marks/Gaps, [First/Last/Crosshair, First/Last/Crosshair[, Gridless]]])
 *
 * As with align, plus:
 *
 *	Gaps		- Make gaps even rather than spreading points evenly
 *	First, Last,
 *	Crosshair	- Two arguments specifying both ends of the
 *			  distribution, they can't both be the same.
 *
 * Defaults are Marks, First, Last
 *
 * Distributed elements always retain the same relative order they had
 * before they were distributed.
 */
static int
distribute(int argc, char **argv, int x, int y)
{
	int dir;
	int point;
	int refa, refb;
	int gridless;
	LocationType s, e, slack;
	int divisor;
	int changed = 0;
	int i;

	if (argc < 1 || argc == 3 || argc > 4) {
		AFAIL(distribute);
	}

	/* parse direction arg */
	switch ((dir = keyword(ARG(0)))) {
	case K_X:
	case K_Y:
		break;
	default:
		AFAIL(distribute);
	}

	/* parse point (within each element) which will be distributed */
	switch ((point = keyword(ARG(1)))) {
	case K_Centers:
	case K_Marks:
	case K_Gaps:
		break;
	case K_Lefts:
	case K_Rights:
		if (dir == K_Y) {
			AFAIL(distribute);
		}
		break;
	case K_Tops:
	case K_Bottoms:
		if (dir == K_X) {
			AFAIL(distribute);
		}
		break;
	case K_none:
		point = K_Marks;	/* default value */
		break;
	default:
		AFAIL(distribute);
	}

	/* parse reference which will determine first distribution coordinate */
	switch ((refa = keyword(ARG(2)))) {
	case K_First:
	case K_Last:
	case K_Average:
	case K_Crosshair:
		break;
	case K_none:
		refa = K_First;	/* default value */
		break;
	default:
		AFAIL(distribute);
	}

	/* parse reference which will determine final distribution coordinate */
	switch ((refb = keyword(ARG(3)))) {
	case K_First:
	case K_Last:
	case K_Average:
	case K_Crosshair:
		break;
	case K_none:
		refb = K_Last;	/* default value */
		break;
	default:
		AFAIL(distribute);
	}
	if (refa == refb) {
		AFAIL(distribute);
	}

	/* optionally work off the grid (solar cells!) */
	switch (keyword(ARG(4))) {
	case K_Gridless:
		gridless = 1;
		break;
	case K_none:
		gridless = 0;
		break;
	default:
		AFAIL(distribute);
	}

	/* build list of elements in orthogonal axis order */
	sort_elements_by_pos(K_distribute, dir, point);

	/* find the endpoints given the above options */
	s = reference_coord(K_distribute, x, y, dir, point, refa);
	e = reference_coord(K_distribute, x, y, dir, point, refb);
	slack = e - s;

	/* use this divisor to calculate spacing (for 1 elt, avoid 1/0) */
	divisor = (nelements_by_pos > 1) ? (nelements_by_pos - 1) : 1;

	/* even the gaps instead of the edges or whatnot */
	/* find the "slack" in the row */
	if (point == K_Gaps) {
		LocationType w;

		/* subtract all the "widths" from the slack */
		for (i = 0; i < nelements_by_pos; ++i) {
			ElementTypePtr element = elements_by_pos[i].element;

			/* coord doesn't care if I mix Lefts/Tops */
			w = elements_by_pos[i].width =
				coord(element, dir, K_Rights) -
				coord(element, dir, K_Lefts);
			/* Gaps distribution is on centers, so half of
			 * first and last element don't count */
			if (i == 0 || i == nelements_by_pos - 1) {
				w /= 2;
			}
			slack -= w;
		}
		/* slack could be negative */
	}

	/* move all selected elements to the new coordinate */
	for (i = 0; i < nelements_by_pos; ++i) {
		ElementTypePtr element = elements_by_pos[i].element;
		LocationType p, q, dp, dx, dy;

		/* find reference point for this element */
		q = s + slack * i / divisor;
		/* find delta from reference point to reference point */
		p = coord(element, dir, point);
		dp = q - p;
		/* ...but if we're gridful, keep the mark on the grid */
		if (! gridless) {
			dp -= (coord(element, dir, K_Marks) + dp)
					% (long) (PCB->Grid);
		}
		if (dp) {
			/* move from generic to X or Y */
			dx = dy = dp;
			if (dir == K_X)
				dy = 0;
			else
				dx = 0;
			MoveElementLowLevel(PCB->Data, element, dx, dy);
			AddObjectToMoveUndoList(ELEMENT_TYPE, NULL, NULL,
						element, dx, dy);
			changed = 1;
		}
		/* in gaps mode, accumulate part widths */
		if (point == K_Gaps) {
			/* move remaining half of our element */
			s += elements_by_pos[i].width / 2;
			/* move half of next element */
			if (i < nelements_by_pos - 1)
				s += elements_by_pos[i + 1].width / 2;
		}
	}

	if (changed) {
		IncrementUndoSerialNumber();
		ClearAndRedrawOutput();
		SetChangedFlag(1);
	}

    out:
	free_elements_by_pos();

	return 0;
}

static HID_Action distalign_action_list[] = {
	{"distribute", NULL, distribute, "Distribute Elements", distribute_syntax},
	{"align", NULL, align, "Align Elements", align_syntax}
};

REGISTER_ACTIONS (distalign_action_list)

void
hid_distalign_init()
{
	register_distalign_action_list();
}

