// -*- objc -*-

#import "MiscColorMerge.h"

#define DRAG_OPERATION_NONE     0
#define DRAG_OPERATION_PURE     1
#define DRAG_OPERATION_BLENDED  2

#define LEFT_MARGIN           (bounds.size.width / 5.0)
#define RIGHT_MARGIN          (bounds.size.width * 4.0 / 5.0)

@interface MiscColorMerge ( DragAndDrop )

- (NXDragOperation) draggingEntered: (id <NXDraggingInfo>) sender;
- (NXDragOperation) draggingUpdated: (id <NXDraggingInfo>) sender;
- draggingExited: (id <NXDraggingInfo>) sender;
- (BOOL) prepareForDragOperation: (id <NXDraggingInfo>) sender;
- (BOOL) performDragOperation: (id <NXDraggingInfo>) sender;

@end

@interface MiscColorMerge (Internals)

- postInit;

- generateVertices;

- notifyDelegateOfChange;

@end


@implementation MiscColorMerge

/*"
MiscColorMerge objects allow the user to view and change color spreads. By
dragging colors into or out of the MiscColorMerge, it is possible to
adjust the spread to the needs of the task at hand.

The colors that are dragged into the spread (the pure colors) are blended
into each other by interpolating the colors between them. The interpolation
can either be a simple linear interpolation between the respective
RGB-values or a more sophisticated Catmull-Rom spline, which is laid through
the colors, represented as points in the RGB-cube, giving a more 'natural'
look.

Pure and blended colors can also be set and queried programatically. Pure
and blended colors have positions, ranging from 0.0 (the lower end of
the View) to 1.0 (the upper end). Pure colors do also have indexes,
starting with 0 for the lowermost pure color.
"*/

- initFrame: (const NXRect*) frameRect
/*"
Initializes the MiscColorMerge object and sets the interpolation method
to Catmull-Rom.

This is the designated initializer for MiscColorMerge.
"*/
{
    [super initFrame: frameRect];

    mergeMode = MergeModeCatmullRom;

    firstColor = NULL;
    numColors = 0;

    [self postInit];

    return self;
}

- free
/*"
Frees the MiscColorMerge instance.
"*/
{
    free(vertices);

    while (firstColor != NULL)
    {
	MiscColorAtPosition *next = firstColor->next;

	free(firstColor);
	firstColor = next;
    }

    return [super free];
}

- delegate
/*"
Returns the MiscColorMerge object's delegate.
"*/
{
    return delegate;
}

- setDelegate: newDelegate
/*"
Sets the MiscColorMerge object's delegate to newDelegate. Returns self.
"*/
{
    delegate = newDelegate;

    return self;
}

- (MiscColorMergeMode) mergeMode
/*"
Returns the merge mode currently in use.
"*/
{
    return mergeMode;
}

- setMergeMode: (MiscColorMergeMode) aMergeMode
/*"
Sets the merge mode to aMergeMode and updates itself. Returns self.
"*/
{
    mergeMode = aMergeMode;

    [self update];

    return self;
}

- (int) numPureColors
/*"
Returns the number of pure colors.
"*/
{
    return numColors;
}

- addColor: (NXColor) theColor atPosition: (double) thePosition
/*"
Adds the pure color theColor to the position thePosition. Returns self
if 0.0 <= thePosition <= 1.0, nil otherwise.
"*/
{
    MiscColorAtPosition *newColor;

    if (thePosition < 0.0 || thePosition > 1.0)
	return nil;

    newColor = (MiscColorAtPosition*)malloc(sizeof(MiscColorAtPosition));
    newColor->color = theColor;
    newColor->position = thePosition;

    if (firstColor == NULL || firstColor->position > thePosition)
    {
	newColor->next = firstColor;
	firstColor = newColor;
    }
    else
    {
	MiscColorAtPosition *colorPtr = firstColor;

	while (colorPtr != NULL)
	{
	    if (colorPtr->next == NULL ||
		(colorPtr->next != NULL &&
		 colorPtr->next->position > thePosition))
	    {
		newColor->next = colorPtr->next;
		colorPtr->next = newColor;

		break;
	    }

	    colorPtr = colorPtr->next;
	}
    }

    numColors++;

    [self generateVertices];
    [self update];

    return self;
}

- getColor: (NXColor*) theColor andPosition: (double*) thePosition
   atIndex: (int) theIndex
/*"
Gets the pure color and its position at index theIndex. Returns self
if theIndex is valid, nil otherwise.
"*/
{
    MiscColorAtPosition *aColor;
    int i;

    if (theIndex < 0 || theIndex >= numColors)
	return nil;

    aColor = firstColor;
    for (i = 0; i < theIndex; ++i)
	aColor = aColor->next;

    *theColor = aColor->color;
    *thePosition = aColor->position;

    return self;
}

- removeColorAtIndex: (int) index
/*"
Removes the pure color at index. Returns self if index is valid, nil
otherwise.
"*/
{
    MiscColorAtPosition *removedColor;
    int i;

    if (index < 0 || index >= numColors)
	return nil;

    if (index == 0)
    {
	removedColor = firstColor;
	firstColor = firstColor->next;
    }
    else
    {
	MiscColorAtPosition *aColor = firstColor;

	for (i = 0; i < index - 1; ++i)
	    aColor = aColor->next;

	removedColor = aColor->next;
	aColor->next = removedColor->next;
    }

    free(removedColor);

    --numColors;

    [self generateVertices];
    [self update];

    return self;
}

- removeAllColors
/*"
Removes all pure colors. Returns self.
"*/
{
    while (firstColor != NULL)
    {
	MiscColorAtPosition *next = firstColor->next;

	free(firstColor);
	firstColor = next;
    }

    numColors = 0;

    return self;
}

- calcColor: (NXColor*) theColor atPosition: (double) thePosition
/*"
Calculates the blended color at position thePosition. Returns self.
"*/
{
    if (numColors == 0)
	*theColor = NX_COLORBLACK;
    else if (numColors == 1)
	*theColor = firstColor->color;
    else if (thePosition <= firstColor->position)
	*theColor = firstColor->color;
    else
    {
	MiscColorAtPosition *colorPtr = firstColor;
	int i = 1;

	while (colorPtr->next != NULL)
	{
	    if (colorPtr->next->position >= thePosition)
	    {
		Vector3D vec;

		if (mergeMode == MergeModeLinear)
		{
		    double secondWeight = (thePosition - colorPtr->position) /
			(colorPtr->next->position - colorPtr->position),
			firstWeight = 1.0 - secondWeight;
		    Vector3D first,
			second;

		    MultScalar3D(&first, firstWeight, &(vertices[i]));
		    MultScalar3D(&second, secondWeight, &(vertices[i + 1]));

		    AddVectors3D(&vec, &first, &second);
		}
		else
		    CatmullRom(&vec,
			       &(vertices[i - 1]), &(vertices[i]),
			       &(vertices[i + 1]), &(vertices[i + 2]),
			       (thePosition - colorPtr->position) /
			       (colorPtr->next->position -
				colorPtr->position));

		*theColor = NXConvertRGBToColor(vec.x, vec.y, vec.z);

		break;
	    }

	    colorPtr = colorPtr->next;
	    i++;
	}
	if (colorPtr->next == NULL)
	    *theColor = colorPtr->color;
    }

    return self;
}

- calcArray: (NXColor*) colors ofNumColors: (int) arraySize
/*"
Calculates an array of arraySize NXColor's representing the whole color
spread. colors must point to an array of at least arraySize NXColor's.

This method is to be preferred over #{calcColor:atPosition:} if applicable,
since it may be optimized in the future (presently it is not faster than
the respective sequence of calls to #{calcColor:atPosition:}).

Returns self.
"*/
{
    int i;

    for (i = 0; i < arraySize; ++i)
    {
	double position = (double)i / (double)(arraySize - 1);

	[self calcColor: &(colors[i]) atPosition: position];
    }

    return self;
}

- drawSelf: (const NXRect*) rects : (int) numRects
/*"
Draws the MiscColorMerge and returns self.
"*/
{
    int maxi = rects->origin.y + rects->size.height,
	height = bounds.size.height,
	i;
    float xmin = bounds.size.width / 5.0,
	xmax = 4.0 * bounds.size.width / 5.0,
	arrowx;
    NXImage *arrowImage = [NXImage findImageNamed: "littleArrow"];
    NXSize arrowSize;
    MiscColorAtPosition *aColor;

    PSsetgray(NX_LTGRAY);
    NXRectFill(rects);

    for (i = rects->origin.y; i <= maxi; ++i)
    {
	NXColor color;
	float red,
	    green,
	    blue;

	[self calcColor: &color atPosition: (double)i / (double)height];
	NXConvertColorToRGB(color, &red, &green, &blue);

	PSsetrgbcolor(red, green, blue);
	PSmoveto(xmin, i);
	PSlineto(xmax, i);
	PSstroke();
    }

    [arrowImage getSize: &arrowSize];
    arrowx = (xmax + bounds.size.width - arrowSize.width) / 2;

    for (aColor = firstColor; aColor != 0; aColor = aColor->next)
    {
	int arrowy = aColor->position * bounds.size.height -
	    arrowSize.height / 2;
	NXPoint pnt = { arrowx, arrowy };

	[arrowImage composite: NX_SOVER toPoint: &pnt];
    }

    return self;
}

- (BOOL) acceptsFirstMouse
/*"
Returns YES.
"*/
{
    return YES;
}

- mouseDown: (NXEvent*) theEvent
/*"
Responds to mouse-down events. Returns self.
"*/
{
    NXImage *arrowImage = [NXImage findImageNamed: "littleArrow"];
    NXSize arrowSize;
    float xmin = 4.0 * bounds.size.width / 5.0,
	xmax;
    MiscColorAtPosition *aColor;
    int index;

    draggingStart = theEvent->location;
    [self convertPoint: &draggingStart fromView: nil];

    [arrowImage getSize: &arrowSize];

    xmin = (xmin + bounds.size.width - arrowSize.width) / 2.0;
    xmax = xmin + arrowSize.width;

    if (draggingStart.x < LEFT_MARGIN)
	return self;
    else if (draggingStart.x < RIGHT_MARGIN)
	dragOperation = DRAG_OPERATION_BLENDED;
    else
    {
	if (draggingStart.x < xmin || draggingStart.x >= xmax)
	    return self;

	for (aColor = firstColor, index = 0; aColor != 0;
	     aColor = aColor->next, ++index)
	{
	    float ymin = aColor->position * bounds.size.height -
		arrowSize.height / 2.0,
		ymax = ymin + arrowSize.height;

	    if (draggingStart.y >= ymin && draggingStart.y < ymax)
		break;
	}

	if (aColor == 0)
	    return self;

	dragOperation = DRAG_OPERATION_PURE;
	draggedColorIndex = index;
    }

    mouseDownEvent = *theEvent;

    oldEventMask = [window addToEventMask:
			       NX_MOUSEDRAGGEDMASK | NX_MOUSEUPMASK];

    return self;
}

- mouseDragged: (NXEvent*) theEvent
/*"
Responds to mouse-dragged events and initiates a drag-operation if
appropriate. Returns self.
"*/
{
    NXPoint pnt = theEvent->location;

    if (dragOperation == DRAG_OPERATION_NONE)
	return self;           /* this should not happen */

    [self convertPoint: &pnt fromView: nil];

    if (abs(pnt.x - draggingStart.x) > 2 ||
	abs(pnt.y - draggingStart.y) > 2)
    {
	NXColor color;

	if (dragOperation == DRAG_OPERATION_PURE)
	{
	    double dummy;

	    [self getColor: &color andPosition: &dummy
		  atIndex: draggedColorIndex];

	    [self removeColorAtIndex: draggedColorIndex];

	    [self notifyDelegateOfChange];
	}
	else if (dragOperation == DRAG_OPERATION_BLENDED)
	{
	    float position = draggingStart.y / bounds.size.height;

	    [self calcColor: &color atPosition: position];
	}

	dragOperation = DRAG_OPERATION_NONE;

	[window setEventMask: oldEventMask];

	[NXColorPanel dragColor: color withEvent: &mouseDownEvent
		      fromView: self];
    }

    return self;
}

- mouseUp: (NXEvent*) theEvent
/*"
Responds to mouse-up events. Returns self.
"*/
{
    if (dragOperation != DRAG_OPERATION_NONE)
    {
	dragOperation = DRAG_OPERATION_NONE;

	[window setEventMask: oldEventMask];
    }

    return self;
}

- read: (NXTypedStream*) theStream
/*"
Reads the MiscColorMerge object in from the typed stream theStream.
Returns self.
"*/
{
    int i;
    MiscColorAtPosition *lastColor = 0;

    [super read: theStream];

    NXReadTypes(theStream, "ii", &mergeMode, &numColors);

    for (i = 0; i < numColors; ++i)
    {
	MiscColorAtPosition *color =
	    (MiscColorAtPosition*)malloc(sizeof(MiscColorAtPosition));

	color->color = NXReadColor(theStream);
	NXReadType(theStream, "d", &(color->position));

	color->next = 0;

	if (lastColor == 0)
	    firstColor = lastColor = color;
	else
	{
	    lastColor->next = color;
	    lastColor = color;
	}
    }

    [self postInit];

    return self;
}

- write: (NXTypedStream*) theStream
/*"
Writes the MiscColorMerge object to the typed stream theStream. Returns self.
"*/
{
    MiscColorAtPosition *color;

    [super write: theStream];

    NXWriteTypes(theStream, "ii", &mergeMode, &numColors);

    for (color = firstColor; color != 0; color = color->next)
    {
	NXWriteColor(theStream, color->color);
	NXWriteType(theStream, "d", &(color->position));
    }

    return self;
}

@end

@implementation MiscColorMerge ( DragAndDrop )

// borrowed from NeXT's CompositeView example.

static BOOL
includesType (const NXAtom *types, NXAtom type)
{
    if (types)
	while (*types)
	    if (*types++ == type)
		return YES;
    return NO;
}

- (NXDragOperation) draggingEntered: (id <NXDraggingInfo>) sender
{
    if ([sender draggingSourceOperationMask] & NX_DragOperationGeneric)
    {
	Pasteboard *pboard = [sender draggingPasteboard];

	if (includesType([pboard types], NXColorPboardType))
	{
	    NXPoint pnt = [sender draggingLocation];
	    NXRect rect;

	    [self convertPoint: &pnt fromView: nil];

	    rect.origin.x = 0.0;
	    rect.origin.y = pnt.y - 1.0;
	    rect.size.width = bounds.size.width;
	    rect.size.height = 1.0;

	    [self lockFocus];

	    bufferedLine = [[NXBitmapImageRep alloc]
			       initData: NULL fromRect: &rect];
	    bufferedLinePos = pnt.y - 1.0;

	    PSsetgray(NX_BLACK);
	    PSmoveto(0.0, pnt.y);
	    PSlineto(bounds.size.width, pnt.y);
	    PSstroke();

	    [self unlockFocus];

	    [window flushWindow];

	    return NX_DragOperationGeneric;
	}
    }

    return NX_DragOperationNone;
}

- (NXDragOperation) draggingUpdated: (id <NXDraggingInfo>) sender
{
    if ([sender draggingSourceOperationMask] & NX_DragOperationGeneric)
    {
	Pasteboard *pboard = [sender draggingPasteboard];

	if (includesType([pboard types], NXColorPboardType))
	{
	    NXPoint pnt = [sender draggingLocation];

	    [self convertPoint: &pnt fromView: nil];

	    if (pnt.y != bufferedLinePos)
	    {
		NXPoint lineLocation = { 0.0, bufferedLinePos };
		NXRect rect;

		rect.origin.x = 0.0;
		rect.origin.y = pnt.y - 1.0;
		rect.size.width = bounds.size.width;
		rect.size.height = 1.0;

		[self lockFocus];

		[bufferedLine drawAt: &lineLocation];

		[bufferedLine free];

		bufferedLine = [[NXBitmapImageRep alloc]
				   initData: NULL fromRect: &rect];
		bufferedLinePos = pnt.y - 1.0;

		PSsetgray(NX_BLACK);
		PSmoveto(0.0, pnt.y);
		PSlineto(bounds.size.width, pnt.y);
		PSstroke();

		[self unlockFocus];

		[window flushWindow];
	    }

	    return NX_DragOperationGeneric;
	}
    }

    return NX_DragOperationNone;
}

- draggingExited: (id <NXDraggingInfo>) sender
{
    NXPoint lineLocation = { 0.0, bufferedLinePos };

    [self lockFocus];

    [bufferedLine drawAt: &lineLocation];

    [bufferedLine free];

    [self unlockFocus];

    [window flushWindow];

    return self;
}

- (BOOL) prepareForDragOperation: (id <NXDraggingInfo>) sender
{
    if ([sender draggingSourceOperationMask] & NX_DragOperationGeneric)
    {
	Pasteboard *pboard = [sender draggingPasteboard];

	if (includesType([pboard types], NXColorPboardType))
	    return YES;
    }

    return NO;
}

- (BOOL) performDragOperation: (id <NXDraggingInfo>) sender
{
    NXPoint pnt = [sender draggingLocation];
    NXColor color = NXReadColorFromPasteboard([sender draggingPasteboard]);
    double position;

    [self convertPoint: &pnt fromView: nil];

    position = pnt.y / bounds.size.height;

    [self addColor: color atPosition: position];

    [self notifyDelegateOfChange];

    return YES;
}

@end

@implementation MiscColorMerge ( Internals )

- postInit
{
    vertices = NULL;
    numAllocedVertices = 0;

    [self generateVertices];

    dragOperation = DRAG_OPERATION_NONE;

    [self registerForDraggedTypes: &NXColorPboardType count: 1];

    return self;
}

- generateVertices
{
    int i;
    MiscColorAtPosition *color;
    Vector3D dummy;

    if (numColors + 2 > numAllocedVertices)
    {
	free(vertices);
	vertices = (Vector3D*)malloc(sizeof(Vector3D) * (numColors + 2));
	numAllocedVertices = numColors + 2;
    }

    for (i = 1, color = firstColor; color != NULL; ++i, color = color->next)
    {
	float red,
	    green,
	    blue;

	NXConvertColorToRGB(color->color, &red, &green, &blue);

	vertices[i].x = red;
	vertices[i].y = green;
	vertices[i].z = blue;
    }

    SubVectors3D(&(vertices[0]), &(vertices[1]),
		 SubVectors3D(&dummy, &(vertices[2]), &(vertices[1])));

    AddVectors3D(&(vertices[numColors + 1]), &(vertices[numColors]),
		 SubVectors3D(&dummy, &(vertices[numColors]),
			      &(vertices[numColors - 1])));

    return self;
}

- notifyDelegateOfChange
{
    if (delegate != nil &&
	[delegate respondsTo: @selector(colorMergeDidChange:)])
	[delegate colorMergeDidChange: self];

    return self;
}

@end

@implementation MiscColorMerge ( InterfaceBuilder )

- (const char*) getInspectorClassName
{
    return "MiscColorMergeInspector";
}

@end
