/* Gridlock
Copyright (c) 2002-2003 by Brian Nenninger. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#import "BoardView.h"
#import "Preferences.h"
#import "CocoaAdditions.h"
#import "ImageStore.h"

static inline int max(int a, int b) {
  return (a>b) ? a : b;
}

@implementation BoardView

-(id)init {
  if (self=[super init]) {
    animationFrames = 6;
    inAnimation = NO;
    pieceImages = [[NSMutableDictionary alloc] init];
  }
  return self;
}

-(void)awakeFromNib {
  [self updateColors:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(updateColors:)
                                               name:@"PlayerColorsChangedNotification"
                                             object:nil];
  // we need to know when our window closes so we can stop the timers, otherwise nasty stuff happens
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(windowClosed:)
                                               name:NSWindowWillCloseNotification
                                             object:[self window]];

  [self addTrackingRect:[self bounds] owner:self userData:nil assumeInside:NO];
  [[self window] setAcceptsMouseMovedEvents:YES];
}

-(void)dealloc {
  //NSLog(@"BoardView dealloc");
  [flashingPositions release];
  [pieceImages release];
  
  [animationTimer invalidate];
  [flashTimer invalidate];
  
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

// timers retain their targets, so when our window is closed we need to invalidate them
-(void)windowClosed:(NSNotification *)note {
  if ([self window]==[note object]) {
    [animationTimer invalidate];
    animationTimer = nil;
    [flashTimer invalidate];
    flashTimer = nil;
  }
}

idAccessor(gameHistory, setGameHistory)
idAccessor(lastMouseOverPosition, setLastMouseOverPosition)
idAccessor(animatingPositions, setAnimatingPositions)

-(Game *)game {
  return [[self gameHistory] currentGame];
}

-(NSRect)updateRectForPositions:(NSArray *)positions {
  NSEnumerator *pe = [positions objectEnumerator];
  id pos;
  NSRect updateRect = NSMakeRect(0,0,0,0);
  while (pos=[pe nextObject]) {
    updateRect = NSUnionRect(updateRect, [self rectForGridPosition:pos]);
  }
  if (!NSIsEmptyRect(updateRect)) {
    float yoff = [self cellHeight]/2;
    float xoff = [self cellWidth]/2;
    updateRect.origin.x -= xoff;
    updateRect.origin.y -= yoff;
    updateRect.size.width  += 2*xoff;
    updateRect.size.height += 2*yoff;
  }
  return updateRect;
}


-(NSArray *)highlightedPositions {
  return highlightedPositions;
}
-(void)setHighlightedPositions:(NSArray *)positions {
  if (![[self highlightedPositions] isEqual:positions]) {
    // compute intersection of old and new highlighted positions to minimize drawing
    NSMutableArray *updatePositions = [NSMutableArray array];
    if (highlightedPositions) [updatePositions addObjectsFromArray:highlightedPositions];
    if (positions) [updatePositions addObjectsFromArray:positions];

    [highlightedPositions autorelease];
    highlightedPositions = [positions retain];

    [self setNeedsDisplayInRect:[self updateRectForPositions:updatePositions]];
  }
}

// assigned via nib connection, target is not retained
-(id <BoardViewTarget>)eventTarget {
  return eventTarget;
}

-(void)setEventTarget:(id <BoardViewTarget>)obj {
  eventTarget = obj;
}

// delegate *is* retained
-(id <BoardViewDelegate>)delegate {
  return delegate;
}
-(void)setDelegate:(id)value {
  [delegate autorelease];
  delegate = [value retain];
}

-(void)drawHighlightedPositions:(NSArray *)positions {
  NSEnumerator *pe = [positions objectEnumerator];
  id pos;
  while (pos=[pe nextObject]) {
    NSRect cellRect = [self rectForGridPosition:pos];
    NSBezierPath *path = [NSBezierPath bezierPathWithRect:cellRect];
    [path setLineWidth:3.0];
    [[[Preferences sharedInstance] highlightColorForPlayerNumber:[[self game] currentPlayerNumber]] set];
    [path stroke];
  }
}

- (void)drawRect:(NSRect)aRect {
  if ([self game]) {
    int r, c;
    int maxr = [[self game] numberOfRows];
    int maxc = [[self game] numberOfColumns];
    for(r=0; r<maxr; r++) {
      for(c=0; c<maxc; c++) {
        id position = [DCHypergridPosition positionWithRow:r column:c];
        if (NSIntersectsRect(aRect, [self rectForGridPosition:position])) {
          [self drawCellAtPosition:position];
        }
      }
    }
    [self drawHighlightedPositions:[self highlightedPositions]];
  }
}

-(float)cellWidth {
  NSRect bounds = [self bounds];
  int ncells = max([[self game] numberOfRows], [[self game] numberOfColumns]);
  return (bounds.size.width-1)/ncells;
}

-(float)cellHeight {
  NSRect bounds = [self bounds];
  int ncells = max([[self game] numberOfRows], [[self game] numberOfColumns]);
  return (bounds.size.height-1)/ncells;
}

-(void)updateColors:(id)sender {
  [self setNeedsDisplay:YES];
}

-(NSRect)rectForGridPosition:(DCHypergridPosition *)pos {
  int width  = [self cellWidth]; 
  int height = [self cellHeight]; 
  // in Quartz, integer coordinates are on the boundary between two pixels, so add 0.5
  // to get the coordinates for the center of a pixel.
  return NSMakeRect([pos column]*width+0.5, [pos row]*height+0.5, width, height);
}

-(BOOL)shouldDrawPreviousMoveIndicatorForPosition:(id)pos {
  // should allow individual games to customize this, for now draw indicator if pos
  // is the last position of the last move, and not flashing
  return ([[[[self gameHistory] previousMove] lastObject] isEqual:pos] &&
          [self isPositionFlashing:pos]==NO);
}

-(void)drawCellWithValue:(int)value atPosition:(id)pos inRect:(NSRect)rect {
  BOOL didDraw = NO;
  [[NSColor whiteColor] set];
  NSRectFill(rect);
  if ([[self delegate] respondsToSelector:@selector(drawCellWithValue:atRow:column:inRect:forGame:)]) {
    didDraw = [[self delegate] drawCellWithValue:value
                                           atRow:[pos row]
                                          column:[pos column]
                                          inRect:rect
                                         forGame:[self game]];
  }
  if (!didDraw) {
    if (value>0) {
      NSImage *image = [[ImageStore defaultStore] pieceImageForPlayer:value withSize:rect.size];
      [image compositeToPoint:rect.origin operation:NSCompositeSourceOver];
    }
  }
  // indicate if value changed
  if ([self shouldDrawPreviousMoveIndicatorForPosition:pos]) {
    NSRect changeRect = NSInsetRect(rect, 4*rect.size.width/9, 4*rect.size.height/9);
    [[NSColor blackColor] set];
    [[NSBezierPath bezierPathWithOvalInRect:changeRect] fill];
  }
}

-(NSImage *)cellImageForValue:(int)value atPosition:(id)pos withSize:(NSSize)size {
  NSImage *image = [[[NSImage alloc] initWithSize:size] autorelease];
  NSRect imageRect = NSMakeRect(0,0,size.width,size.height);
  [image lockFocus];
  [self drawCellWithValue:value atPosition:pos inRect:imageRect];
  [image unlockFocus];
  return image;
}

-(BOOL)isPositionFlashing:(id)pos {
  return (flashTimer!=nil && flashState==YES && [[self flashingPositions] containsObject:pos]);
}

-(void)drawCellAtPosition:(DCHypergridPosition *)pos {
  NSRect cellFrameRect = [self rectForGridPosition:pos];
  NSRect cellRect = cellFrameRect;
  int cellvalue = 0;
  int nextvalue = 0;

  // inset cellRect so it fits inside cellFrameRect
  ++cellRect.origin.x; ++cellRect.origin.y;
  cellRect.size.width -= 2; cellRect.size.height -= 2;
  
  if ([self isPositionFlashing:pos]) {
    [self drawCellWithValue:0 atPosition:pos inRect:cellRect];
  }
  else {
    cellvalue = nextvalue = [[self game] valueAtPosition:pos];
    if (inAnimation) {
      if ([[Preferences sharedInstance] animateCapturedPieces]==NO ||
          [immediateChangePositions containsObject:pos]) {
        // draw future value immediately
        cellvalue = nextvalue = [[self game] futureValueAtPosition:pos];
      }
      else if ([[self animatingPositions] containsObject:pos]) {
        nextvalue = [[self game] futureValueAtPosition:pos];
      }
    }

    if (inAnimation && cellvalue!=nextvalue) {
      // draw cell in transition
      float frac = ((float)animationFrameCounter)/animationFrames;
      NSImage *fromImage = [self cellImageForValue:cellvalue atPosition:pos withSize:cellRect.size];
      NSImage *toImage   = [self cellImageForValue:nextvalue atPosition:pos withSize:cellRect.size];
      [fromImage compositeToPoint:cellRect.origin operation:NSCompositeSourceOver];
      [toImage   dissolveToPoint:cellRect.origin  fraction:frac];
    }
    else {
      // draw static cell
      [self drawCellWithValue:cellvalue atPosition:pos inRect:cellRect];
    }
  }

  // draw the frame last so it always goes on top
  [[NSColor blackColor] set];
  [NSBezierPath strokeRect:cellFrameRect];
}

-(DCHypergridPosition *)positionForEvent:(NSEvent *)event {
  NSRect bounds = [self bounds];
  NSPoint pointInWindow = [event locationInWindow];
  NSPoint pointInView = [self convertPoint:pointInWindow fromView:nil];
  int c = (pointInView.x-bounds.origin.x)/([self cellWidth]);
  int r = (pointInView.y-bounds.origin.y)/([self cellHeight]);
  return [DCHypergridPosition positionWithRow:r column:c];
}

-(void)mouseDown:(NSEvent *)event {
  DCHypergridPosition *position = [self positionForEvent:event];
  if ([[self game] isPositionValid:position]) {
    [[self eventTarget] boardView:self clickedAtPosition:position];
    [self setLastMouseOverPosition:nil];
  }
}

-(void)mouseEntered:(NSEvent *)event {
  //NSLog(@"mouseEntered");
  [[self window] makeFirstResponder:self];
}


-(void)mouseExited:(NSEvent *)event {
  //NSLog(@"mouseExited");
  [[self eventTarget] boardView:self mouseMovedOverPosition:nil];
  [self setLastMouseOverPosition:nil];
}

-(void)mouseMoved:(NSEvent *)event {
  DCHypergridPosition *position = [self positionForEvent:event];
  if ([[self game] isPositionValid:position] && ![position isEqual:[self lastMouseOverPosition]]) {
    [[self eventTarget] boardView:self mouseMovedOverPosition:position];
    [self setLastMouseOverPosition:position];
  }
}

- (BOOL)acceptsFirstResponder {
  return YES;
}

- (BOOL)becomeFirstResponder {
  [[self window] setAcceptsMouseMovedEvents: YES];
  return YES;
}

- (BOOL)resignFirstResponder {
  [[self window] setAcceptsMouseMovedEvents: NO];
  return YES;
}


// animation support

-(id)animationCallbackObject {
  return animationCallbackObject;
}

-(void)setAnimationCallbackObject:(id)obj {
  [animationCallbackObject release];
  animationCallbackObject = [obj retain];
}

-(void)animationTimerFired:(NSTimer *)timer {
  NSRect updateRect = [self updateRectForPositions:[self animatingPositions]];
  if (timer!=animationTimer) return;
  ++animationFrameCounter;
  //NSLog(@"animationTimerFired:%d %d",animationFrameCounter, animationFrames);
  if (animationFrameCounter>=animationFrames || ![[self animatingPositions] count]) {
    //NSLog(@"Animation done");
    inAnimation = NO;
    [animationTimer invalidate];
    animationTimer = nil;
    [self finishMove];
  }
  // compute minimal rectangle to redraw
  [self setNeedsDisplayInRect:updateRect];
}

-(void)finishMove {
  [[self eventTarget] boardView:self finishedMoveAnimation:animationCallbackObject];
  [self setAnimationCallbackObject:nil];
  [self setAnimatingPositions:nil];
  [immediateChangePositions release];
  immediateChangePositions = nil;
  [self setLastMouseOverPosition:nil];
}

-(void)beginMoveAnimationForPositions:(NSArray *)animPositions immediatelyChangingPositions:(NSArray *)immdtPositions callbackObject:(id)obj {
  BOOL disableAnimation = NO;
  
  [self setAnimationCallbackObject:obj];
  [self setAnimatingPositions:animPositions];

  [immediateChangePositions release];
  immediateChangePositions = [[immdtPositions arrayByIntersectingArray_:animPositions] retain];
  
  inAnimation = YES;
  animationFrameCounter = 0;
  animationFrames = 6;

  // abort if game doesn't support animation
  disableAnimation = ([[self delegate] respondsToSelector:@selector(disableAnimation)] &&
                      [[self delegate] disableAnimation]);

  if (!disableAnimation && [[self animatingPositions] count]>[immediateChangePositions count]) {
    animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                      target:self
                                                    selector:@selector(animationTimerFired:)
                                                    userInfo:nil
                                                     repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:animationTimer forMode:NSEventTrackingRunLoopMode];
  }
  else {
    inAnimation = NO;
    [self finishMove];
  }
  [self setNeedsDisplay:YES];
}

-(NSArray *)flashingPositions {
  return flashingPositions;
}

-(void)setFlashingPositions:(NSArray *)positions {
  if ([positions count]) {
    [flashingPositions release];
    flashingPositions = [positions retain];
    if (!flashTimer) {
      flashState = NO;
      flashTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                    target:self
                                                  selector:@selector(flashTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];
      [[NSRunLoop currentRunLoop] addTimer:flashTimer forMode:NSEventTrackingRunLoopMode];
    }
  }
  else {
    [flashingPositions release];
    flashingPositions = nil;
    [flashTimer invalidate];
    flashTimer = nil;
  }
}

-(void)flashTimerFired:(NSTimer *)timer {
  if (timer!=flashTimer) return;
  flashState = !flashState;
  [self setNeedsDisplayInRect:[self updateRectForPositions:[self flashingPositions]]];
}

@end
