//
//  TimeChartView.m
//  Created by Toshi Nagata on Sat Jan 25 2003.
//
/*
    Copyright (c) 2003-2012 Toshi Nagata. All rights reserved.

 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 version 2 of the License.
 
 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.
 */

#import "TimeChartView.h"
#import "GraphicWindowController.h"
#import "MyDocument.h"
#import "MyMIDISequence.h"
#import "MDObjects.h"
#import "NSStringAdditions.h"
#import <math.h>
#import "MDHeaders.h"
#import "NSCursorAdditions.h"
#import "PlayingViewController.h"

typedef struct TimeScalingRecord {
	MDTickType startTick;  /*  Start tick of the time region to be scaled   */
	MDTickType endTick;    /*  End tick of the time region to be scaled  */
	MDTickType newEndTick; /*  End tick after scaling the time region  */
	int ntracks;           /*  Number of editable tracks  */
	int *trackNums;        /*  Track numbers  */
	long *startPos;        /*  Start positions for each track to modify ticks  */
	MDTickType **originalTicks;  /*  Arrays of original ticks for each track  */
} TimeScalingRecord;

@implementation TimeChartView

//- (BOOL)willChangeSelectionOnMouseDown
//{
//    return NO;
//}

+ (float)minHeight
{
	return 36.0;
}

- (int)clientViewType
{
	return kGraphicTimeChartViewType;
}

//- (id)initWithFrame: (NSRect)rect
//{
//    self = [super initWithFrame: rect];
//    if (self) {
//		selectMode = kGraphicClientIbeamMode;
//    }
//    return self;
//}

- (void)drawRect: (NSRect)aRect
{
	float ppt;
	MDTickType beginTick, endTick;
	float originx;
	float limitx;
    float basey, maxLabelWidth;
	NSPoint pt1, pt2;
    NSRect visibleRect = [self visibleRect];
    float editingRangeStartX, editingRangeEndX;

    basey = visibleRect.origin.y + 0.5;
	ppt = [dataSource pixelsPerTick];
	limitx = [dataSource sequenceDurationInQuarter] * [dataSource pixelsPerQuarter];

	[self paintEditingRange: aRect startX: &editingRangeStartX endX: &editingRangeEndX];
	
    /*  Draw horizontal axis  */
    [NSBezierPath strokeLineFromPoint: NSMakePoint(aRect.origin.x, basey) toPoint: NSMakePoint(aRect.origin.x + aRect.size.width, basey)];
    
    /*  Draw ticks, labels, and time signatures  */
    maxLabelWidth = [@"0000:00:0000" sizeWithAttributes: nil].width;
    originx = aRect.origin.x - maxLabelWidth;
    if (originx < 0.0)
        originx = 0.0;
	beginTick = originx / ppt;
	endTick = (aRect.origin.x + aRect.size.width) / ppt + 1;
	while (beginTick < endTick) {
		int mediumCount, majorCount, i, numLines;
        int sigNumerator, sigDenominator;
		MDEvent *sig1, *sig2;
		MDTickType sigTick, nextSigTick;
		float interval, startx;
        float widthPerBeat, widthPerMeasure;
		[dataSource verticalLinesFromTick: beginTick timeSignature: &sig1 nextTimeSignature: &sig2 lineIntervalInPixels: &interval mediumCount: &mediumCount majorCount: &majorCount];
		sigTick = (sig1 == NULL ? 0 : MDGetTick(sig1));
		nextSigTick = (sig2 == NULL ? kMDMaxTick : MDGetTick(sig2));
		if (nextSigTick > endTick)
			nextSigTick = endTick;
		startx = sigTick * ppt;
        sigDenominator = (sig1 == NULL ? 4 : (1 << (int)(MDGetMetaDataPtr(sig1)[1])));
        if (sigNumerator == 0)
            sigNumerator = 4;
        sigNumerator = (sig1 == NULL ? 4 : MDGetMetaDataPtr(sig1)[0]);
        [[NSString stringWithFormat: @"%d/%d", sigNumerator, sigDenominator] drawAtPoint: NSMakePoint(startx, basey + 22.0) withAttributes: nil clippingRect: aRect];
		numLines = floor((nextSigTick - sigTick) * ppt / interval) + 1;
		i = (startx >= originx ? 0 : floor((originx - startx) / interval));
		[[NSColor blackColor] set];
		for ( ; i < numLines; i++) {
            pt1 = NSMakePoint(floor(startx + i * interval) + 0.5, basey);
            pt2.x = pt1.x;
			if (pt1.x > limitx)
				[[NSColor grayColor] set];
            if (i % majorCount == 0) {
                /*  Draw label  */
                NSString *label;
                long n1, n2, n3;
                [dataSource convertTick: (MDTickType)floor((startx + i * interval) / ppt + 0.5) toMeasure: &n1 beat: &n2 andTick: &n3];
                widthPerBeat = [(MyDocument *)[dataSource document] timebase] * ppt * 4 / sigDenominator;
                widthPerMeasure = widthPerBeat * sigNumerator;
                if (interval * majorCount >= widthPerMeasure) {
                    label = [NSString stringWithFormat: @"%d", n1];
                } else if (interval * majorCount >= widthPerBeat) {
                    //  The major tick interval >= beat
                    label = [NSString stringWithFormat: @"%d:%d", n1, n2];
                } else {
                    //  The major tick interval < beat
                    label = [NSString stringWithFormat: @"%d:%d:%d", n1, n2, n3];
                }
                [label drawAtPoint: NSMakePoint(floor(pt1.x), floor(pt1.y + 10.0)) withAttributes: nil clippingRect: aRect];
            }
			if (pt1.x >= aRect.origin.x && pt1.x <= aRect.origin.x + aRect.size.width) {
                /*  Draw axis marks */
				if (i % majorCount == 0) {
                    pt2.y = pt1.y + 9.0;
                } else if (i % mediumCount == 0)
                    pt2.y = pt1.y + 6.0;
                else pt2.y = pt1.y + 3.0;
                [NSBezierPath strokeLineFromPoint: pt1 toPoint: pt2];
            }
		}
		beginTick = nextSigTick;
	}
	/*  Draw selection range symbols  */
	[[NSColor blackColor] set];
	if (editingRangeStartX >= 0) {
		static NSImage *sStartEditingImage = nil;
		static NSImage *sEndEditingImage = nil;
		if (sStartEditingImage == nil) {
			sStartEditingImage = [[NSImage allocWithZone: [self zone]] initWithContentsOfFile: [[NSBundle mainBundle] pathForResource: @"StartEditingRange.png" ofType: nil]];
		}
		if (sEndEditingImage == nil) {
			sEndEditingImage = [[NSImage allocWithZone: [self zone]] initWithContentsOfFile: [[NSBundle mainBundle] pathForResource: @"EndEditingRange.png" ofType: nil]];
		}
		pt1.x = editingRangeStartX - 5.0;
		pt1.y = visibleRect.origin.y + 1.0;
		if (pt1.x >= aRect.origin.x && pt1.x < aRect.origin.x + aRect.size.width) {
			[sStartEditingImage compositeToPoint: pt1 operation: NSCompositeSourceAtop fraction: 1.0];
		}
		pt1.x = editingRangeEndX;
		if (pt1.x >= aRect.origin.x && pt1.x < aRect.origin.x + aRect.size.width) {
			[sEndEditingImage compositeToPoint: pt1 operation: NSCompositeSourceAtop fraction: 1.0];
		}
	}
	
	/*  Draw end-of-track symbol  */
	if (aRect.origin.x + aRect.size.width > limitx) {
		float defaultLineWidth;
		pt1.x = pt2.x = limitx;
		pt1.y = basey;
		pt2.y = basey + 12.0;
		[NSBezierPath strokeLineFromPoint: pt1 toPoint: pt2];
		defaultLineWidth = [NSBezierPath defaultLineWidth];
		[NSBezierPath setDefaultLineWidth: 2.0];
		pt1.x = pt2.x += 3.0;
		[NSBezierPath strokeLineFromPoint: pt1 toPoint: pt2];
		[NSBezierPath setDefaultLineWidth: defaultLineWidth];
	}
	
//    if ([self isDragging])
		[self drawSelectRegion];

}

//  Examine whether the mouse pointer is on one of the editing range marks
//  Return value: -1, start pos, 0: none, 1: end pos
//  pt should be in view coordinates
- (int)isMouseOnEditStartPositions:(NSPoint)pt
{
    NSRect visibleRect = [self visibleRect];
	if (pt.y >= visibleRect.origin.y && pt.y <= visibleRect.origin.y + 5) {
		MDTickType startTick, endTick;
		float startx, endx;
		float ppt = [dataSource pixelsPerTick];
		[(MyDocument *)[dataSource document] getEditingRangeStart: &startTick end: &endTick];
		if (startTick >= 0 && startTick < kMDMaxTick && endTick >= startTick) {
			startx = floor(startTick * ppt);
			endx = floor(endTick * ppt);
			if (startx - 5 <= pt.x && pt.x <= startx)
				return -1;
			if (endx <= pt.x && pt.x <= endx + 5)
				return 1;
		}
	}
	return 0;
}

/*  See also: -[MyDocument scaleSelectedTime:]  */
- (void)scaleSelectedTimeWithEvent: (NSEvent *)theEvent undoEnabled:(BOOL)undoEnabled
{
	MDSequence *seq;
	int i, j;
	if (timeScaling == NULL)
		return;  /*  Do nothing  */
	if (theEvent == NULL)
		timeScaling->newEndTick = timeScaling->endTick;  /*  Return to the original  */
	else {
		NSPoint mousePt = [self convertPoint:[theEvent locationInWindow] fromView:nil];
		float ppt = [dataSource pixelsPerTick];
		timeScaling->newEndTick = mousePt.x / ppt;
		if (timeScaling->newEndTick < timeScaling->startTick)
			timeScaling->newEndTick = timeScaling->startTick;
	}
	seq = [[[dataSource document] myMIDISequence] mySequence];
	for (i = 0; i < timeScaling->ntracks; i++) {
		MDTrack *track = MDSequenceGetTrack(seq, timeScaling->trackNums[i]);
		int n = MDTrackGetNumberOfEvents(track) - timeScaling->startPos[i];  /*  Number of events  */
		MDPointer *pt = MDPointerNew(track);
		MDEvent *ep;
		MDPointSetObject *psobj = nil;
		NSMutableData *dt = nil;
		MDTickType *mutableBytes = NULL;
		MDPointerSetPosition(pt, timeScaling->startPos[i]);
		if (undoEnabled && n > 0) {
			psobj = [[MDPointSetObject allocWithZone:[self zone]] init];
			MDPointSetAdd([psobj pointSet], timeScaling->startPos[i], n);
			dt = [[NSMutableData allocWithZone:[self zone]] initWithLength:sizeof(MDTickType) * n];
			mutableBytes = (MDTickType *)[dt mutableBytes];
		}
		for (ep = MDPointerCurrent(pt), j = 0; ; ep = MDPointerForward(pt), j++) {
			MDTickType tick = timeScaling->originalTicks[i][j];
			if (timeScaling->newEndTick != timeScaling->endTick) {
				if (tick < timeScaling->endTick)
					tick = timeScaling->startTick + (double)(tick - timeScaling->startTick) * (timeScaling->newEndTick - timeScaling->startTick) / (timeScaling->endTick - timeScaling->startTick);
				else
					tick += (timeScaling->newEndTick - timeScaling->endTick);
			}
			if (ep == NULL) {
				if (undoEnabled) {
					if (n > 0) {
						[[dataSource document] modifyTick:dt ofMultipleEventsAt:psobj inTrack:timeScaling->trackNums[i] mode:MyDocumentModifySet destinationPositions:nil];
						[dt release];
						[psobj release];
					}
					[[dataSource document] changeTrackDuration:tick ofTrack:timeScaling->trackNums[i]];	
				} else {
					MDTrackSetDuration(track, tick);
				}
				break;
			} else {
				if (undoEnabled)
					mutableBytes[j] = tick;
				else
					MDSetTick(ep, tick);
			}
		}
	}
	[dataSource reloadClientViews];
}

- (void)doMouseDown: (NSEvent *)theEvent
{
	NSPoint pt = [self convertPoint:[theEvent locationInWindow] fromView:nil];
	int n = [self isMouseOnEditStartPositions:pt];
	if (n != 0) {
		/*  Mofity selectPoints so that the 'other' edit start/end position
		    looks like the dragging start points  */
		MDTickType startTick, endTick;
		float ppt = [dataSource pixelsPerTick];
		[(MyDocument *)[dataSource document] getEditingRangeStart: &startTick end: &endTick];
		if (n < 0)
			pt.x = endTick * ppt;
		else
			pt.x = startTick * ppt;
		[selectPoints replaceObjectAtIndex: 0 withObject: [NSValue valueWithPoint:pt]];
		if (n == 1 && ([theEvent modifierFlags] & (NSAlternateKeyMask | NSShiftKeyMask)) && startTick < endTick) {
			/*  Scale selected time: initialize the internal information  */
			int i, j;
			long trackNo;
			MDTrack *track;
			MDSequence *seq = [[[dataSource document] myMIDISequence] mySequence];
			timeScaling = (TimeScalingRecord *)calloc(sizeof(TimeScalingRecord), 1);
			timeScaling->startTick = startTick;
			timeScaling->endTick = endTick;
			j = MDSequenceGetNumberOfTracks(seq);
			timeScaling->trackNums = (int *)calloc(sizeof(int), j);
			for (i = 0; (trackNo = [dataSource sortedTrackNumberAtIndex: i]) >= 0; i++) {
				if (![dataSource isFocusTrack: trackNo])
					continue;
				timeScaling->trackNums[timeScaling->ntracks] = trackNo;
				timeScaling->ntracks++;
			}
			timeScaling->trackNums = (int *)realloc(timeScaling->trackNums, sizeof(int) * timeScaling->ntracks);
			timeScaling->startPos = (long *)calloc(sizeof(long), timeScaling->ntracks);
			timeScaling->originalTicks = (MDTickType **)calloc(sizeof(MDTickType *), timeScaling->ntracks);
			for (i = 0; i < timeScaling->ntracks; i++) {
				MDPointer *pt;
				MDEvent *ep;
				track = MDSequenceGetTrack(seq, timeScaling->trackNums[i]);
				pt = MDPointerNew(track);
				if (MDPointerJumpToTick(pt, startTick)) {
					timeScaling->startPos[i] = MDPointerGetPosition(pt);
				} else {
					timeScaling->startPos[i] = MDTrackGetNumberOfEvents(track);
				}
				timeScaling->originalTicks[i] = (MDTickType *)calloc(sizeof(MDTickType), MDTrackGetNumberOfEvents(track) - timeScaling->startPos[i] + 1);  /*  +1 for end-of-track  */
				for (ep = MDPointerCurrent(pt), j = 0; ep != NULL; ep = MDPointerForward(pt), j++) {
					timeScaling->originalTicks[i][j] = MDGetTick(ep);
				}
				MDPointerRelease(pt);
				timeScaling->originalTicks[i][j] = MDTrackGetDuration(track);
			}
		}
	} else if ((initialModifierFlags & NSCommandKeyMask)) {
		/*  Command + click/drag is similar to shift + click/drag, except that
		 all events in the new editing range are added to the selection  */
		/*  Do nothing; this avoids deselecting the current selection  */
	} else {
		[super doMouseDown: theEvent];
	}
	if (localGraphicTool == kGraphicRectangleSelectTool)
		localGraphicTool = kGraphicIbeamSelectTool;
}

- (void)doMouseDragged: (NSEvent *)theEvent
{
	if (timeScaling != NULL) {
		[self scaleSelectedTimeWithEvent:theEvent undoEnabled:NO];
		return;
	}
	
	[super doMouseDragged: theEvent];
	if (selectionPath != nil) {
		int i;
		GraphicClientView *view;
		NSRect rect;

		rect = [selectionPath bounds];

		/*  Command + drag: modify selection path with current editing range  */
		if (initialModifierFlags & NSCommandKeyMask) {
			MDTickType tick1, tick2;
			[(MyDocument *)[dataSource document] getEditingRangeStart: &tick1 end: &tick2];
			if (tick1 >= 0 && tick2 < kMDMaxTick && tick1 <= tick2) {
				float ppt = [dataSource pixelsPerTick];
				float x1 = tick1 * ppt;
				float x2 = tick2 * ppt;
				float x3 = rect.origin.x + rect.size.width;
				if (x2 > x3)
					x3 = x2;
				if (x1 < rect.origin.x)
					rect.origin.x = x1;
				rect.size.width = x3 - rect.origin.x;
				[self setSelectRegion: [NSBezierPath bezierPathWithRect: rect]];
			}
		}
		
		/*  Set the selection paths for other clientViews  */
		for (i = 1; (view = [dataSource clientViewAtIndex: i]) != nil; i++) {
			NSRect viewRect = [view bounds];
			rect.origin.y = viewRect.origin.y - 1.0;
			rect.size.height = viewRect.size.height + 2.0;
			[view setSelectRegion: [NSBezierPath bezierPathWithRect: rect]];
		}
	}
}

- (void)doMouseUp: (NSEvent *)theEvent
{
	NSPoint pt1, pt2;
	float ppt;
	MDTickType tick1, tick2;
	int i;
	long trackNo;
	GraphicClientView *view;
	BOOL shiftDown = (([theEvent modifierFlags] & NSShiftKeyMask) != 0);
	MyDocument *document = (MyDocument *)[dataSource document];

	/*  Clear the selection paths for other clientViews  */
	for (i = 1; (view = [dataSource clientViewAtIndex: i]) != nil; i++) {
		[view setSelectRegion: nil];
	}
	
	if (timeScaling != NULL) {
		/*  Time scaling  */
		/*  Register undo for selections and editing range  */
		[document getEditingRangeStart: &tick1 end: &tick2];
		[[[self undoManager] prepareWithInvocationTarget:document]
		 setEditingRangeStart:tick1 end:tick2];
		for (i = 0; (trackNo = [dataSource sortedTrackNumberAtIndex: i]) >= 0; i++) {
			MDSelectionObject *sel;
			if (![dataSource isFocusTrack: trackNo])
				continue;
			sel = [document selectionOfTrack:trackNo];
			[[[self undoManager] prepareWithInvocationTarget: document]
			 setSelection:sel inTrack:trackNo sender:self];
		}
		[self scaleSelectedTimeWithEvent:nil undoEnabled:NO];
		[self scaleSelectedTimeWithEvent:theEvent undoEnabled:YES];
		tick1 = timeScaling->startTick;
		tick2 = timeScaling->newEndTick;
		for (i = 0; i < timeScaling->ntracks; i++)
			free(timeScaling->originalTicks[i]);
		free(timeScaling->originalTicks);
		free(timeScaling->startPos);
		free(timeScaling->trackNums);
		free(timeScaling);
		timeScaling = NULL;
		shiftDown = NO;
		
	} else {
		
		if (isLoupeDragging) {
			[super doMouseUp: theEvent];
			return;
		}

		/*  Editing range  */
		pt1 = [[selectPoints objectAtIndex: 0] pointValue];
		if (isDragging) {
			pt2 = [[selectPoints objectAtIndex: 1] pointValue];
		} else pt2 = pt1;
		ppt = [dataSource pixelsPerTick];
		tick1 = floor(pt1.x / ppt);
		tick2 = floor(pt2.x / ppt);
		if (tick1 < 0)
			tick1 = 0;
		if (tick2 < 0)
			tick2 = 0;
		if (tick1 > tick2) {
			MDTickType tick3 = tick1;
			tick1 = tick2;
			tick2 = tick3;
		}
		if (initialModifierFlags & NSCommandKeyMask) {
			MDTickType rtick1, rtick2;
			[document getEditingRangeStart: &rtick1 end: &rtick2];
			if (rtick1 >= 0 && rtick2 < kMDMaxTick && rtick1 <= rtick2) {
				if (rtick1 < tick1)
					tick1 = rtick1;
				if (rtick2 > tick2)
					tick2 = rtick2;
			}
		}
	}
	
	if (tick1 < tick2) {
		/*  Select all events within this tick range  */
		for (i = 0; (trackNo = [dataSource sortedTrackNumberAtIndex: i]) >= 0; i++) {
			MDTrack *track;
			MDPointer *pt;
			MDPointSet *pset;
			MDSelectionObject *obj;
			long pos1, pos2;
			if (![dataSource isFocusTrack: trackNo])
				continue;
			track = [[document myMIDISequence] getTrackAtIndex: trackNo];
			if (track == NULL)
				continue;
			pt = MDPointerNew(track);
			if (pt == NULL)
				break;
			pset = MDPointSetNew();
			if (pset == NULL)
				break;
			MDPointerJumpToTick(pt, tick1);
			pos1 = MDPointerGetPosition(pt);
			MDPointerJumpToTick(pt, tick2);
			pos2 = MDPointerGetPosition(pt);
			if (pos1 < pos2) {
				if (MDPointSetAdd(pset, pos1, pos2 - pos1) != kMDNoError)
					break;
				obj = [[MDSelectionObject allocWithZone: [self zone]] initWithMDPointSet: pset];
				if (shiftDown) {
					[document toggleSelection: obj inTrack: trackNo sender: self];
				} else {
					[document setSelection: obj inTrack: trackNo sender: self];
				}
				[obj release];
			}
			MDPointSetRelease(pset);
			MDPointerRelease(pt);
		}
	}
	
	/*  Change editing range  */
	if (shiftDown) {
		MDTickType oldTick1, oldTick2;
		[document getEditingRangeStart: &oldTick1 end: &oldTick2];
		if (oldTick1 >= 0 && oldTick1 < tick1)
			tick1 = oldTick1;
		if (oldTick2 > tick2)
			tick2 = oldTick2;
	}
	[document setEditingRangeStart: tick1 end: tick2];
	if (tick1 == tick2 && [theEvent clickCount] >= 2)
		[[dataSource playingViewController] setCurrentTick: tick1];
}

- (void)doMouseMoved: (NSEvent *)theEvent
{
	NSPoint pt;
	int n;
	unsigned modifierFlags;
	if ([theEvent type] == NSFlagsChanged) {
		pt = [self convertPoint:[[self window] convertScreenToBase:[NSEvent mouseLocation]] fromView:nil];
	} else {		
		pt = [self convertPoint:[theEvent locationInWindow] fromView:nil];
	}
	modifierFlags = [theEvent modifierFlags];
	n = [self isMouseOnEditStartPositions:pt];
	if (n != 0) {
		if (n == 1 && (modifierFlags & (NSAlternateKeyMask | NSShiftKeyMask))) {
			[[NSCursor horizontalMoveZoomCursor] set];
		} else {
			[[NSCursor horizontalMoveCursor] set];
		}
	} else {
		if ([theEvent modifierFlags] & NSAlternateKeyMask)
			[[NSCursor loupeCursor] set];
		else [[NSCursor IBeamCursor] set];
	}
}

- (void)doFlagsChanged:(NSEvent *)theEvent
{
	[self doMouseMoved:theEvent];
}

@end
