/*
 * Copyright (c) 2010 David McIntosh <dmcintosh@df12.net>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#define _XOPEN_SOURCE 500

#import "GanttView.h"

#define DEBUG_MOUSE         NO
#define DEBUG_DRAW          NO

@interface NSObject (GanttViewDelegate)
- (GanttTask *)taskAtIndex:(NSInteger)index;
- (NSInteger)rootTasks;
- (NSEnumerator *)rootTaskEnumerator;
- (void)setGanttView:(id)aGanttView;
- (void)addNewTask:(id)sender;
- (void)addChildToSelectedTask:(id)sender;
- (void)zoomIn:(id)sender;
- (void)zoomOut:(id)sender;
- (void)selectionDidChange:(id)sender;
- (NSDate *)earliestStartDate;
- (NSDate *)latestEndDate;
@end


@interface GanttView (Private)
    
- (void)_updateGeometry;
- (NSTimeInterval)_gridInterval;
- (NSTimeInterval)_subGridInterval;

@end

//
// class variables
//
static id ganttCellClass = nil;

@implementation GanttView

+ (void)initialize
{
    if (self == [GanttView class])
    {
        [self setCellClass:[GanttCell class]];
    }
}

- (id)initWithFrame:(NSRect)aFrame
{
    if ( (self=[super initWithFrame:(NSRect)aFrame]) )
    {
        [self setZoomFactor:GanttZoomMedium];
        selection = [[NSMutableSet alloc] init];
        expandedTags = [[NSMutableSet alloc] init];
        [self setOriginDate:
            [[NSDate dateWithTimeIntervalSinceNow:-60*60*24*7] 
                dateByRoundingToNearest:GanttDay]];
        sendSelectionDelegateMessages=YES;
        [self _updateGeometry];
    }
    return self;
}

+ (Class) cellClass
{
    return ganttCellClass;
}

+ (void) setCellClass: (Class)classId
{
    ganttCellClass = classId;
}

/**
 * Overriden to support zooming with ctrl-scroll
 */
- (void)scrollWheel:(NSEvent *)theEvent
{
    CGFloat deltaY = [theEvent deltaY];
    if ([theEvent modifierFlags] & NSControlKeyMask)
    {
        if (deltaY > 0.0)
            [delegate zoomIn:self];
        else
            [delegate zoomOut:self];
    } else {
        [super scrollWheel:theEvent];
    }
}

/**
 * If the requested cell does not exist, create it
 * Configure the requested cell to reflect the represented task
 * before returning
 */
- (GanttCell *)cellAtIndex:(NSInteger)index
{
    int i;
    GanttCell *aCell;
    GanttTask *task;

    if (nil == cells)
        cells = [[NSMutableArray alloc] init];

    if ([cells count] <= index)
    {
        for (i=[cells count]; i <= index; i++)
        {
            aCell = [[GanttCell alloc] initTextCell:@""];
            [aCell setTarget:delegate];
            [aCell setControlView:self];
            [aCell setBounds:[self bounds]];
            [cells addObject:aCell];
            RELEASE(aCell);
        }
    }
    aCell = [cells objectAtIndex:index];
    task = [self taskAtIndex:index];
    [aCell setTag:[task tag]];
    [aCell setGridInterval:[self _subGridInterval]];
    [aCell setOriginDate:[self originDate]];
    [aCell setStartDate:[task startDate]];
    [aCell setEndDate:[task endDate]];
    [aCell setCompletion:[task completion]];
    return aCell;
}

- (BOOL)taskIsExpanded:(GanttTask *)task
{
    return [expandedTags containsObject:[NSNumber numberWithInt:[task tag]]];
}

- (GanttTask *)taskAtIndex:(NSInteger)index withRoot:(GanttTask *)root
{
    NSInteger remainder = index+1;
    NSInteger i = 0;
    GanttTask *child = nil;

    if ((nil == root && [delegate rootTasks] == 0) || 
        (nil != root && [root childCount] == 0))
    {
        return nil;
    }

    // UKLog(@"Called for index %d with root %@", index, root);

    while (remainder > 0)
    {
        if (nil == root)
            child = [delegate taskAtIndex:i];
        else
            child = [root childAtIndex:i];

        remainder--;
        i++;
        if (remainder > 0 && [child childCount] > 0 && 
            [self taskIsExpanded:child])
        {
            if ([self visibleTasksWithRoot:child] >= remainder)
            {
                return [self taskAtIndex:remainder-1 withRoot:child];
            } else {
                remainder -= [self visibleTasksWithRoot:child];
            }
        }
    }
    return child;
}

- (GanttTask *)taskAtIndex:(NSInteger)index
{
    return [self taskAtIndex:index withRoot:nil];
}

- (NSInteger)visibleTasksWithRoot:(GanttTask *)root
{
    NSInteger count = 0;
    id child;
    NSEnumerator *e;

    if (nil == root)
        e = [delegate rootTaskEnumerator];
    else
        e = [[root children] objectEnumerator];

    while ((child = [e nextObject]))
    {
        count++;
        if ([self taskIsExpanded:child])
            count += [self visibleTasksWithRoot:child];
    }
    return count;
}

- (NSInteger)visibleTasks
{
    return [self visibleTasksWithRoot:nil];
}

/**
 * Ensure that the cell bounds are updated to reflect the view bounds
 */
- (void)setBounds:(NSRect)bounds
{
    NSEnumerator *e = [cells objectEnumerator];
    GanttCell *aCell;
    while (( aCell = [e nextObject] ))
        [aCell setBounds:bounds];
    [super setBounds:bounds];
}

/**
 * Draws the grid lines representing the major hash marks
 * at the current zoom level
 */
- (void)drawGridInRect:(NSRect)rect 
{
    NSRect bounds, frame;
    NSTimeInterval notchIntv;
    int notches, i;
    NSPoint notch;
    NSBezierPath *p;
    float notchHours, notchWidth;

    bounds = [self bounds];
    frame = [self frame];

    // determine time interval represented by each notch
    notchIntv = [self _gridInterval];

    notch.y = rect.origin.y;
    notch.x = rect.origin.x;

    notchHours = ( notchIntv / (60.0 * 60.0) );
    notchWidth = notchHours * GanttPointsPerHour;
    notches = rect.size.width / notchWidth;

    // if the rectangle being drawn does not start on a grid line
    if ( fmod(notch.x, notchWidth) > 0.0 )
    {
        notch.x -= fmod(notch.x, notchWidth);
    }

    [[NSColor grayColor] set];

    // For each visible notch
    for (i = 0; i <= notches; i++)
    {
        notch.x += notchWidth;
        p = [NSBezierPath bezierPath];

        // Adjust grid line thickness to compensate for zoom
        switch ([self zoomFactor]) {
            case GanttZoomClose:
                [p setLineWidth:0.7]; break;
            case GanttZoomMedium:
                [p setLineWidth:1.0]; break;
            case GanttZoomWide:
                [p setLineWidth:2.0]; break;
        }

        [p moveToPoint:notch];
        [p lineToPoint:NSMakePoint(notch.x, notch.y+rect.size.height)];
        [p stroke];
    }
}

- (void)drawRect:(NSRect)rect
{
    NSPoint origin = rect.origin;
    float rowHeight = GanttRowHeight;;
    NSRect cellFrame;
    int rows = [self visibleTasks];
    int row;
    GanttCell *aCell;
    NSArray *rowBGColors = [NSArray arrayWithObjects:
        [NSColor colorWithDeviceRed:0.89 green:0.92 blue:0.96 alpha:1], 
        [NSColor whiteColor], 
        nil];
    NSColor *color;
    GanttTask *task;
    NSInteger tag;

    row = (int)( rect.origin.y / rowHeight );
    origin.y = row * rowHeight;
    while (origin.y < rect.origin.y+rect.size.height)
    {
        // draw the row
        cellFrame = NSMakeRect(origin.x, origin.y, 
            rect.size.width, rowHeight);
        
        color = [rowBGColors objectAtIndex:(row % [rowBGColors count])];
        if (row < rows && (task = [self taskAtIndex:row]))
        {
            tag = [task tag];
            if ([selection containsObject:[NSNumber numberWithInt:tag]])
                color = [color shadowWithLevel:0.4];
        }
        [color set];
        [NSBezierPath fillRect:cellFrame];
        row++;
        origin.y += rowHeight;
    }

    [self drawGridInRect:rect];

    row = (int)( rect.origin.y / rowHeight );
    origin.y = row * rowHeight;
    while (origin.y < rect.origin.y+rect.size.height)
    {
        // draw the row
        cellFrame = NSMakeRect(origin.x, origin.y, 
            rect.size.width, rowHeight);
        
        if (row < rows)
        {
            aCell = [self cellAtIndex:row];
            [aCell drawWithFrame:cellFrame inView:self];
        }
        row++;
        origin.y += rowHeight;
    }
}

- (void)clearSelection
{
    [selection removeAllObjects];
    if (sendSelectionDelegateMessages)
    {
        [delegate selectionDidChange:self];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"GanttViewSelectionDidChange" object:self userInfo:nil];
    }
}

/**
 * Adds a tag to the selected tags list
 */
- (void)selectTag:(NSInteger)tag
{
    [selection addObject:[NSNumber numberWithInt:tag]];
    if (sendSelectionDelegateMessages)
    {
        [delegate selectionDidChange:self];
        [[NSNotificationCenter defaultCenter] 
            postNotificationName:@"GanttViewSelectionDidChange" 
            object:self userInfo:nil];
    }
}

/**
 * Removes a tag from the selected tags list
 */
- (void)deselectTag:(NSInteger)tag
{
    [selection removeObject:[NSNumber numberWithInt:tag]];
    if (sendSelectionDelegateMessages)
    {
        [delegate selectionDidChange:self];
        [[NSNotificationCenter defaultCenter] 
            postNotificationName:@"GanttViewSelectionDidChange" 
            object:self userInfo:nil];
    }
}

- (NSInteger)selectedTag
{
    if ([selection count] != 1)
        return -1;
    return [[selection anyObject] intValue];
}

- (NSSet *)selectedTags
{
    return [NSSet setWithSet:selection];
}

- (void)selectRowIndexes:(NSIndexSet *)indexes byExtendingSelection:(BOOL)extend;
{
    int i;

    if ([indexes isEqualToIndexSet:[self selectedRowIndexes]])
        return;

    sendSelectionDelegateMessages=NO;

    if (!extend)
        [self clearSelection];

    for (i=0; i<[self visibleTasks]; i++)
    {
        if ([indexes containsIndex:i])
            [self selectTag:[[self taskAtIndex:i] tag]];
    }
    sendSelectionDelegateMessages=YES;
    [[NSNotificationCenter defaultCenter] 
        postNotificationName:@"GanttViewSelectionDidChange" 
        object:self userInfo:nil];
    [self setNeedsDisplay:YES];
}

- (NSIndexSet *)selectedRowIndexes
{
    NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
    int i;

    for (i=0; i<[self visibleTasks]; i++)
    {
        if ([selection 
                containsObject:[NSNumber numberWithInt:[[self taskAtIndex:i] tag]]])
        {
            [indexes addIndex:i];
        }
    }
    return indexes;
}

- (void)mouseDown:(NSEvent *)theEvent
{
    GanttCell *theCell;
    NSPoint thePoint = [self 
        convertPoint:[theEvent locationInWindow] 
            fromView:nil];
    float rowHeight = GanttRowHeight;;
    int rows = [self visibleTasks];
    int row = (int)( thePoint.y / rowHeight );
    NSDate *pointDate = 
        [originDate dateByAddingTimeInterval:(thePoint.x*60.0*60.0)];

    if (DEBUG_MOUSE)
        UKLog(@"MouseDown %@ Date %@", NSStringFromPoint(thePoint), pointDate);

    // check we are not exceeding the last row
    if (row+1 > rows)
    {
        // double click here is synonymn for "add task"
        if ([theEvent clickCount] == 2)
            [delegate addNewTask:self];
        else {
            [self clearSelection];
            [self setCell:nil];
            [self setNeedsDisplay:YES];
        }
        return;
    }

    theCell = [self cellAtIndex:row];

    if (([theEvent modifierFlags] & NSControlKeyMask) &&
        [selection count] > 0)
    {
        NSInteger tag = [theCell tag];
        if ( [selection containsObject:[NSNumber numberWithInt:tag]] )
        {
            if (DEBUG_MOUSE)
                UKLog(@"Shrinking selection");
            [self deselectTag:tag];
        } else {
            if (DEBUG_MOUSE)
                UKLog(@"Expanding selection");
            [self selectTag:tag];
        }
        [self setNeedsDisplay:YES];

    } else if ([theEvent clickCount] == 2) {

        [self clearSelection];
        [self selectTag:[theCell tag]];
        [delegate addChildToSelectedTask:self];

    } else {

        [self clearSelection];
        [self selectTag:[theCell tag]];
        [self setCell:theCell]; // only used for tracking
        [self setNeedsDisplay:YES];
        [theCell trackMouse:theEvent 
            inRect:NSMakeRect(INT_MIN, INT_MIN, MAXFLOAT, MAXFLOAT)
            ofView:self 
            untilMouseUp:YES];
    }
}

- (void)mouseUp:(NSEvent *)theEvent
{
    UKLog(@"Mouse Up");
    [self _updateGeometry];
}

- (void)resetCursorRects
{
    int rows = [self visibleTasks];
    int row;
    float rowHeight = GanttRowHeight;
    NSRect visibleRect = [self visibleRect];
    NSRect cellFrame;
    GanttCell *theCell;

    // check if visible
    if (NSEqualRects(visibleRect, NSZeroRect))
        return;

    row = (int)(visibleRect.origin.y / rowHeight);
    cellFrame.origin.x = visibleRect.origin.x;
    cellFrame.origin.y = (float)row * rowHeight;
    cellFrame.size.width = visibleRect.size.width;
    cellFrame.size.height = rowHeight;

    while (row < rows && cellFrame.origin.y < NSMaxY(visibleRect))
    {
        theCell = [self cellAtIndex:row];
        if (NSMaxY(cellFrame) > NSMaxY(visibleRect))
            cellFrame = NSIntersectionRect(visibleRect, cellFrame);
        [theCell resetCursorRectsForFrame:cellFrame
                   inView:self];

        row++;
        cellFrame.origin.y += rowHeight;
    }

    if (rows == 0)
        [self addCursorRect:[self frame]
                         cursor:[NSCursor arrowCursor]];
}

- (BOOL)isFlipped
{
    return YES;
}

- (void)windowDidResize:(NSNotification *)note
{
    if ([self isDescendantOf:[note object]])
    {
        [self _updateGeometry];
    }
}

- (void)taskDidExpand:(NSNotification *)note
{
    NSDictionary *userInfo = [note userInfo];
    GanttTask *task = [userInfo objectForKey:@"NSObject"];
    [expandedTags addObject:[NSNumber numberWithInt:[task tag]]];
    [self setNeedsDisplay:YES];
}

- (void)taskDidCollapse:(NSNotification *)note
{
    NSDictionary *userInfo = [note userInfo];
    GanttTask *task = [userInfo objectForKey:@"NSObject"];
    [expandedTags removeObject:[NSNumber numberWithInt:[task tag]]];
    [self setNeedsDisplay:YES];
}

- (void)awakeFromGSMarkup
{
    NSScrollView *scrollView;
    GanttRulerView *rulerView;
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

    [nc addObserver:self 
        selector:@selector(windowDidResize:) 
        name:NSWindowDidResizeNotification 
        object:[self window]];
    [nc addObserver:self 
        selector:@selector(windowDidResize:) 
        name:NSSplitViewDidResizeSubviewsNotification
        object:nil];
    scrollView = [self enclosingScrollView];
    rulerView = [[GanttRulerView alloc] 
        initWithScrollView:scrollView orientation:NSHorizontalRuler];
    [scrollView setRulersVisible:YES];
    [scrollView setHorizontalRulerView:rulerView];
    [scrollView setHasHorizontalRuler:YES];
    [self _updateGeometry];
    if (_tableView)
    {
        [nc addObserver:self 
            selector:@selector(taskDidCollapse:) 
            name:NSOutlineViewItemDidCollapseNotification
            object:_tableView];
        [nc addObserver:self 
            selector:@selector(taskDidExpand:) 
            name:NSOutlineViewItemDidExpandNotification
            object:_tableView];
        float headerHeight = [[_tableView headerView] frame].size.height;
        [rulerView setRuleThickness:headerHeight];
        [_tableView setRowHeight:GanttRowHeight];
    }
    [rulerView setReservedThicknessForMarkers:0];
    AUTORELEASE(rulerView);
    [self setCell:nil];
}

- (void)setDelegate:(id)obj
{
    delegate=obj;
}

- (void)setTableView:(NSTableView *)tableView
{
    ASSIGN(_tableView, tableView);
}

- (void)setOriginDate:(NSDate *)aDate
{
    ASSIGN(originDate, [aDate dateByRoundingToNearest:GanttHalfDayHours*GanttHour]);
}

- (NSDate *)originDate
{
    return originDate;
}

- (void)collapseAll:(id)sender
{
    [expandedTags removeAllObjects];
}

- (void)setZoomFactor:(GanttZoomFactor)zoomFactor
{
    _zoomFactor = zoomFactor;
    [self _updateGeometry];
}

- (void)_updateGeometry
{
    NSScrollView *scrollView = [self enclosingScrollView];
    NSRect documentRect = [scrollView documentVisibleRect];
    NSSize contentSize = [scrollView contentSize];
    float taskHeight = [self visibleTasks] * GanttRowHeight;
    NSRect frame=[self frame];
    NSDate *startDate;
    NSDate *endDate;
    NSDate *newOriginDate;
    NSString *zoomFactorName;
    NSTimeInterval padding;
    float multiplier;
    NSPoint scrollOrigin;
    NSTimeInterval scrollOriginTimeIntv;


    frame.size.width=contentSize.width;

    if (contentSize.height < taskHeight)
        frame.size.height=taskHeight;
    else
        frame.size.height=contentSize.height;

    startDate = [delegate earliestStartDate];
    endDate = [delegate latestEndDate];

    switch (_zoomFactor)
    {
        case GanttZoomWide:
            multiplier = GanttZoomWideMult;
            zoomFactorName=@"Wide";
            break;
        case GanttZoomMedium:
            multiplier = GanttZoomMediumMult;
            zoomFactorName=@"Medium";
            break;
        case GanttZoomClose:
            multiplier = GanttZoomCloseMult;
            zoomFactorName=@"Close";
            break;
        default:
            padding = 0;
            multiplier = 0;
            break;
    }

    padding = GanttDayHours * multiplier * GanttHour;

    if (nil != startDate)
    {
        startDate = [startDate dateByAddingTimeInterval:-padding];
        endDate = [endDate dateByAddingTimeInterval:padding];

        // clipViewOriginDate = originDate;
        newOriginDate = [originDate min:startDate];
        endDate = [endDate max:
            [originDate dateByAddingTimeInterval:
                (frame.size.width * multiplier * GanttHour)]];
        scrollOriginTimeIntv = 
            [originDate timeIntervalSinceDate:newOriginDate];
        scrollOrigin.x = (scrollOriginTimeIntv / GanttHour) / multiplier;
        scrollOrigin.y = documentRect.origin.x;
            
        UKLog(@"Start date: %@", startDate);
        UKLog(@"End date: %@", endDate);

        frame.size.width = ([endDate timeIntervalSinceDate:newOriginDate] / 
            GanttHour) / multiplier;

        [self setOriginDate: newOriginDate];
        [self setFrame:frame];
        [self scrollPoint:scrollOrigin];
    } else {
        UKLog(@"Setting size: %@", NSStringFromSize(contentSize));
        [self setFrameSize:contentSize];
    }

    if (DEBUG_DRAW)
        UKLog(@"Frame width: %f", frame.size.width);
    [self setBoundsSize:
        NSMakeSize(frame.size.width*multiplier, frame.size.height)];
    
    if (scrollView)
    {
        id rulerView = [scrollView horizontalRulerView];
        if (rulerView )
        {
            if ([rulerView respondsToSelector:@selector(setZoomFactor:)])
                [rulerView setZoomFactor:[self zoomFactor]];
            if ( [rulerView respondsToSelector:@selector(setOriginDate:)])
                [rulerView setOriginDate:originDate];
            [rulerView setNeedsDisplay:YES];
        }
    }
    [[self window] invalidateCursorRectsForView:self];
    [self setNeedsDisplay:YES];
}

- (GanttZoomFactor)zoomFactor
{
    return _zoomFactor;
}

- (NSTimeInterval)_gridInterval
{
    GanttZoomFactor zoomFactor = [self zoomFactor];
    switch (zoomFactor)
    {
        case GanttZoomWide:
            return GanttWeek;
        case GanttZoomMedium:
            return GanttDay;
        case GanttZoomClose:
            return GanttHalfDay;
    }
    return 0.0;
}

- (NSTimeInterval)_subGridInterval
{
    GanttZoomFactor zoomFactor = [self zoomFactor];
    switch (zoomFactor)
    {
        case GanttZoomWide:
            return GanttDay;
        case GanttZoomMedium:
            return GanttHalfDay;
        case GanttZoomClose:
            return GanttHalfDay;
    }
    return 0.0;
}

// The bounds interval in seconds
- (NSTimeInterval)extentInterval
{
    float extentHours = ([self bounds].size.width / GanttPointsPerHour);
    return extentHours / (60.0 * 60.0);
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] 
            removeObserver:self ];
    DESTROY(emptyCell);
    DESTROY(cells);
    DESTROY(originDate);
    DESTROY(_tableView);
    DESTROY(selection);
    DESTROY(expandedTags);
    [super dealloc];
}

@end

