// 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 <cmath>

#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 = 0.45;

}

// Private /////////////////////////////////////////////////////////////////////

@interface DraggableButtonImpl (Private)

- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
                                   yDelta:(float)yDelta
                              xHysteresis:(float)xHysteresis
                              yHysteresis:(float)yHysteresis;
- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
                                           yDelta:(float)yDelta
                                      xHysteresis:(float)xHysteresis
                                      yHysteresis:(float)yHysteresis;
- (void)performMouseDownAction:(NSEvent*)theEvent;
- (void)endDrag;
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
                      withExpiration:(NSDate*)expiration;
- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
                      withExpiration:(NSDate*)expiration
                         xHysteresis:(float)xHysteresis
                         yHysteresis:(float)yHysteresis;

@end

// Implementation //////////////////////////////////////////////////////////////

@implementation DraggableButtonImpl

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

- (id)initWithButton:(NSButton<DraggableButtonMixin>*)button {
  if ((self = [super init])) {
    button_ = button;
    draggable_ = YES;
    actsOnMouseDown_ = NO;
    actionHasFired_ = NO;
  }
  return self;
}

// NSButton/NSResponder Implementations ////////////////////////////////////////

- (DraggableButtonResult)mouseUpImpl:(NSEvent*)theEvent {
  durationMouseWasDown_ = [theEvent timestamp] - whenMouseDown_;

  if (actionHasFired_)
    return kDraggableButtonImplDidWork;

  if (!draggable_)
    return kDraggableButtonMixinCallSuper;

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

  return kDraggableButtonImplDidWork;
}

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

  whenMouseDown_ = [theEvent timestamp];
  actionHasFired_ = NO;

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

  return kDraggableButtonImplDidWork;
}

// Idempotent Helpers //////////////////////////////////////////////////////////

- (BOOL)deltaIndicatesDragStartWithXDelta:(float)xDelta
                                   yDelta:(float)yDelta
                              xHysteresis:(float)xHysteresis
                              yHysteresis:(float)yHysteresis {
  if ([button_ respondsToSelector:@selector(deltaIndicatesDragStartWithXDelta:
                                            yDelta:
                                            xHysteresis:
                                            yHysteresis:
                                            indicates:)]) {
    BOOL indicates = NO;
    DraggableButtonResult result = [button_
        deltaIndicatesDragStartWithXDelta:xDelta
        yDelta:yDelta
        xHysteresis:xHysteresis
        yHysteresis:yHysteresis
        indicates:&indicates];
    if (result != kDraggableButtonImplUseBase)
      return indicates;
  }
  return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
}

- (BOOL)deltaIndicatesConclusionReachedWithXDelta:(float)xDelta
                                           yDelta:(float)yDelta
                                      xHysteresis:(float)xHysteresis
                                      yHysteresis:(float)yHysteresis {
  if ([button_ respondsToSelector:
          @selector(deltaIndicatesConclusionReachedWithXDelta:
                    yDelta:
                    xHysteresis:
                    yHysteresis:
                    indicates:)]) {
    BOOL indicates = NO;
    DraggableButtonResult result = [button_
        deltaIndicatesConclusionReachedWithXDelta:xDelta
        yDelta:yDelta
        xHysteresis:xHysteresis
        yHysteresis:yHysteresis
        indicates:&indicates];
    if (result != kDraggableButtonImplUseBase)
      return indicates;
  }
  return (std::abs(xDelta) >= xHysteresis) || (std::abs(yDelta) >= yHysteresis);
}

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

// Implementation Details //////////////////////////////////////////////////////

// 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 = [[button_ 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;
}

- (void)secondaryMouseUpAction:(BOOL)wasInside {
  if ([button_ respondsToSelector:_cmd])
    [button_ secondaryMouseUpAction:wasInside];

  // No actual implementation yet.
}

- (void)performMouseDownAction:(NSEvent*)event {
  if ([button_ respondsToSelector:_cmd] &&
      [button_ performMouseDownAction:event] != kDraggableButtonImplUseBase) {
      return;
  }

  int eventMask = NSLeftMouseUpMask;

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

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

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

  [button_ highlight:NO];
}

- (void)endDrag {
  if ([button_ respondsToSelector:_cmd] &&
      [button_ endDrag] != kDraggableButtonImplUseBase) {
    return;
  }
  [button_ highlight:NO];
}

@end