// Copyright (c) 2010 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 "ui/base/cocoa/hover_button.h"

#include <cmath>

namespace {

// Distance to start a drag when a dragDelegate is assigned.
constexpr CGFloat kDragDistance = 5;

}  // namespace

@implementation HoverButton

@synthesize hoverState = hoverState_;
@synthesize trackingEnabled = trackingEnabled_;
@synthesize dragDelegate = dragDelegate_;
@synthesize sendActionOnMouseDown = sendActionOnMouseDown_;

- (id)initWithFrame:(NSRect)frameRect {
  if ((self = [super initWithFrame:frameRect])) {
    [self commonInit];
  }
  return self;
}

- (void)awakeFromNib {
  [self commonInit];
}

- (void)commonInit {
  self.hoverState = kHoverStateNone;
  self.trackingEnabled = YES;
}

- (void)dealloc {
  self.trackingEnabled = NO;
  [super dealloc];
}

- (NSRect)hitbox {
  return NSZeroRect;
}

- (void)setTrackingEnabled:(BOOL)trackingEnabled {
  if (trackingEnabled == trackingEnabled_)
    return;
  trackingEnabled_ = trackingEnabled;
  [self updateTrackingAreas];
}

- (void)setEnabled:(BOOL)enabled {
  if (enabled == self.enabled)
    return;
  super.enabled = enabled;
  [self updateTrackingAreas];
}

- (void)mouseEntered:(NSEvent*)theEvent {
  if (trackingArea_.get())
    self.hoverState = kHoverStateMouseOver;
}

- (void)mouseMoved:(NSEvent*)theEvent {
  [self checkImageState];
}

- (void)mouseExited:(NSEvent*)theEvent {
  if (trackingArea_.get())
    self.hoverState = kHoverStateNone;
}

- (void)mouseDown:(NSEvent*)theEvent {
  if (!self.enabled)
    return;
  mouseDown_ = YES;
  self.hoverState = kHoverStateMouseDown;

  if (sendActionOnMouseDown_)
    [self sendAction:self.action to:self.target];

  // The hover button needs to hold onto itself here for a bit.  Otherwise,
  // it can be freed while in the tracking loop below.
  // http://crbug.com/28220
  base::scoped_nsobject<HoverButton> myself([self retain]);

  // Begin tracking the mouse.
  if ([theEvent type] == NSLeftMouseDown) {
    NSWindow* window = [self window];
    NSEvent* nextEvent = nil;

    // For the tracking loop ignore key events so that they don't pile up in
    // the queue and get processed after the user releases the mouse.
    const NSEventMask eventMask = (NSLeftMouseDraggedMask | NSLeftMouseUpMask |
                                   NSKeyDownMask | NSKeyUpMask);

    while ((nextEvent = [window nextEventMatchingMask:eventMask])) {
      if ([nextEvent type] == NSLeftMouseUp)
        break;
      // Update the image state, which will change if the user moves the mouse
      // into or out of the button.
      [self checkImageState];
      if (dragDelegate_ && [nextEvent type] == NSLeftMouseDragged) {
        const NSPoint startPos = [theEvent locationInWindow];
        const NSPoint pos = [nextEvent locationInWindow];
        if (std::abs(startPos.x - pos.x) > kDragDistance ||
            std::abs(startPos.y - pos.y) > kDragDistance) {
          [dragDelegate_ beginDragFromHoverButton:self event:nextEvent];
          mouseDown_ = NO;
          self.hoverState = kHoverStateNone;
          return;
        }
      }
    }
  }

  // If the mouse is still over the button, it means the user clicked the
  // button.
  if (!sendActionOnMouseDown_ && self.hoverState == kHoverStateMouseDown) {
    [self sendAction:self.action to:self.target];
  }

  // Clean up.
  mouseDown_ = NO;
  [self checkImageState];
}

- (void)setAccessibilityTitle:(NSString*)accessibilityTitle {
  NSCell* cell = [self cell];
  [cell accessibilitySetOverrideValue:accessibilityTitle
                         forAttribute:NSAccessibilityTitleAttribute];
}

- (void)updateTrackingAreas {
  if (trackingEnabled_ && self.enabled) {
    NSRect hitbox = self.hitbox;
    if (CrTrackingArea* trackingArea = trackingArea_.get()) {
      if (NSEqualRects(trackingArea.rect, hitbox))
        return;
      [self removeTrackingArea:trackingArea];
    }
    trackingArea_.reset([[CrTrackingArea alloc]
        initWithRect:hitbox
             options:NSTrackingMouseEnteredAndExited |
                     NSTrackingMouseMoved |
                     NSTrackingActiveAlways |
                     (NSIsEmptyRect(hitbox) ? NSTrackingInVisibleRect : 0)
               owner:self
            userInfo:nil]);
    [self addTrackingArea:trackingArea_.get()];

    // If you have a separate window that overlaps the close button, and you
    // move the mouse directly over the close button without entering another
    // part of the tab strip, we don't get any mouseEntered event since the
    // tracking area was disabled when we entered.
    // Done with a delay of 0 because sometimes an event appears to be missed
    // between the activation of the tracking area and the call to
    // checkImageState resulting in the button state being incorrect.
    [self performSelector:@selector(checkImageState)
               withObject:nil
               afterDelay:0];
  } else {
    if (trackingArea_.get()) {
      self.hoverState = kHoverStateNone;
      [self removeTrackingArea:trackingArea_.get()];
      trackingArea_.reset(nil);
    }
  }
  [super updateTrackingAreas];
  [self checkImageState];
}

- (void)checkImageState {
  if (!trackingArea_.get())
    return;

  NSEvent* currentEvent = [NSApp currentEvent];
  if (!currentEvent || currentEvent.window != self.window)
    return;

  // Update the button's state if the button has moved.
  const NSPoint mouseLoc =
      [self.superview convertPoint:currentEvent.locationInWindow fromView:nil];
  BOOL mouseInBounds = [self hitTest:mouseLoc] != nil;
  if (mouseDown_ && mouseInBounds) {
    self.hoverState = kHoverStateMouseDown;
  } else {
    self.hoverState = mouseInBounds ? kHoverStateMouseOver : kHoverStateNone;
  }
}

- (void)setHoverState:(HoverState)hoverState {
  if (hoverState == hoverState_)
    return;
  hoverState_ = hoverState;
  self.needsDisplay = YES;
}

- (NSView*)hitTest:(NSPoint)point {
  if (NSPointInRect([self.superview convertPoint:point toView:self],
                    self.hitbox)) {
    return self;
  }
  return [super hitTest:point];
}

@end
