// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/ui/cocoa/draggable_button.h"

#include "base/logging.h"
#import "base/memory/scoped_nsobject.h"

namespace {

// Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>.
// TODO(viettrungluu): Do we want common, standard code for drag hysteresis?
const CGFloat kWebDragStartHysteresisX = 5.0;
const CGFloat kWebDragStartHysteresisY = 5.0;
const CGFloat kDragExpirationTimeout = 1.0;

}

@implementation DraggableButton

@synthesize draggable = draggable_;
@synthesize actsOnMouseDown = actsOnMouseDown_;
@synthesize durationMouseWasDown = durationMouseWasDown_;
@synthesize actionHasFired = actionHasFired_;
@synthesize whenMouseDown = whenMouseDown_;


- (id)initWithFrame:(NSRect)frame {
  if ((self = [super initWithFrame:frame])) {
    draggable_ = YES;
    actsOnMouseDown_ = NO;
    actionHasFired_ = NO;
  }
  return self;
}

- (id)initWithCoder:(NSCoder*)coder {
  if ((self = [super initWithCoder:coder])) {
    draggable_ = YES;
    actsOnMouseDown_ = NO;
    actionHasFired_ = NO;
  }
  return self;
}

- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
                                   yDelta:(float)yDelta
                              xHysteresis:(float)xHysteresis
                              yHysteresis:(float)yHysteresis {
  return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis);
}

- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
                                           yDelta:(float)yDelta
                                      xHysteresis:(float)xHysteresis
                                      yHysteresis:(float)yHysteresis {
  return (ABS(xDelta) >= xHysteresis) || (ABS(yDelta) >= yHysteresis);
}


// Determine whether a mouse down should turn into a drag; started as copy of
// NSTableView code.
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
                      withExpiration:(NSDate*)expiration
                         xHysteresis:(float)xHysteresis
                         yHysteresis:(float)yHysteresis {
  if ([mouseDownEvent type] != NSLeftMouseDown) {
    return NO;
  }

  NSEvent* nextEvent = nil;
  NSEvent* firstEvent = nil;
  NSEvent* dragEvent = nil;
  NSEvent* mouseUp = nil;
  BOOL dragIt = NO;

  while ((nextEvent = [[self window]
      nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask)
                  untilDate:expiration
                     inMode:NSEventTrackingRunLoopMode
                    dequeue:YES]) != nil) {
    if (firstEvent == nil) {
      firstEvent = nextEvent;
    }
    if ([nextEvent type] == NSLeftMouseDragged) {
      float deltax = [nextEvent locationInWindow].x -
          [mouseDownEvent locationInWindow].x;
      float deltay = [nextEvent locationInWindow].y -
          [mouseDownEvent locationInWindow].y;
      dragEvent = nextEvent;
      if ([self deltaIndicatesConclusionReachedWithXDelta:deltax
                                                   yDelta:deltay
                                              xHysteresis:xHysteresis
                                              yHysteresis:yHysteresis]) {
        dragIt = [self deltaIndicatesDragStartWithXDelta:deltax
                                                  yDelta:deltay
                                             xHysteresis:xHysteresis
                                             yHysteresis:yHysteresis];
        break;
      }
    } else if ([nextEvent type] == NSLeftMouseUp) {
      mouseUp = nextEvent;
      break;
    }
  }

  // Since we've been dequeuing the events (If we don't, we'll never see
  // the mouse up...), we need to push some of the events back on.
  // It makes sense to put the first and last drag events and the mouse
  // up if there was one.
  if (mouseUp != nil) {
    [NSApp postEvent:mouseUp atStart:YES];
  }
  if (dragEvent != nil) {
    [NSApp postEvent:dragEvent atStart:YES];
  }
  if (firstEvent != mouseUp && firstEvent != dragEvent) {
    [NSApp postEvent:firstEvent atStart:YES];
  }

  return dragIt;
}

- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
                      withExpiration:(NSDate*)expiration {
  return [self dragShouldBeginFromMouseDown:mouseDownEvent
                             withExpiration:expiration
                                xHysteresis:kWebDragStartHysteresisX
                                yHysteresis:kWebDragStartHysteresisY];
}

- (void)mouseUp:(NSEvent*)theEvent {
  durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;

  if (actionHasFired_)
    return;

  if (!draggable_) {
    [super mouseUp:theEvent];
    return;
  }

  // There are non-drag cases where a mouseUp: may happen
  // (e.g. mouse-down, cmd-tab to another application, move mouse,
  // mouse-up).  So we check.
  NSPoint viewLocal = [self convertPoint:[theEvent locationInWindow]
                                fromView:[[self window] contentView]];
  if (NSPointInRect(viewLocal, [self bounds])) {
    [self performClick:self];
  }
}

- (void)secondaryMouseUpAction:(BOOL)wasInside {
  // Override if you want to do any extra work on mouseUp, after a mouseDown
  // action has already fired.
}

- (void)performMouseDownAction:(NSEvent*)theEvent {
  int eventMask = NSLeftMouseUpMask;

  [[self target] performSelector:[self action] withObject:self];
  actionHasFired_ = YES;

  while (1) {
    theEvent = [[self window] nextEventMatchingMask:eventMask];
    if (!theEvent)
      continue;
    NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow]
                                 fromView:nil];
    BOOL isInside = [self mouse:mouseLoc inRect:[self bounds]];
    [self highlight:isInside];

    switch ([theEvent type]) {
      case NSLeftMouseUp:
        durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;
        [self secondaryMouseUpAction:isInside];
        break;
      default:
        /* Ignore any other kind of event. */
        break;
    }
  }

  [self highlight:NO];
}

// Mimic "begin a click" operation visually.  Do NOT follow through
// with normal button event handling.
- (void)mouseDown:(NSEvent*)theEvent {
  [[NSCursor arrowCursor] set];

  whenMouseDown_ = [theEvent timestamp];
  actionHasFired_ = NO;

  if (draggable_) {
    NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
    if ([self dragShouldBeginFromMouseDown:theEvent
                            withExpiration:date]) {
      [self beginDrag:theEvent];
      [self endDrag];
    } else {
      if (actsOnMouseDown_) {
        [self performMouseDownAction:theEvent];
      } else {
        [super mouseDown:theEvent];
      }

    }
  } else {
    if (actsOnMouseDown_) {
      [self performMouseDownAction:theEvent];
    } else {
      [super mouseDown:theEvent];
    }
  }
}

- (void)beginDrag:(NSEvent*)dragEvent {
  // Must be overridden by subclasses.
  NOTREACHED();
}

- (void)endDrag {
  [self highlight:NO];
}

@end  // @interface DraggableButton