// 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/info_bubble_window.h"

#include "base/basictypes.h"
#include "base/logging.h"
#include "base/memory/scoped_nsobject.h"
#include "content/common/notification_observer.h"
#include "content/common/notification_registrar.h"
#include "content/common/notification_service.h"
#include "content/common/notification_type.h"
#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"

namespace {
const CGFloat kOrderInSlideOffset = 10;
const NSTimeInterval kOrderInAnimationDuration = 0.2;
const NSTimeInterval kOrderOutAnimationDuration = 0.15;
// The minimum representable time interval.  This can be used as the value
// passed to +[NSAnimationContext setDuration:] to stop an in-progress
// animation as quickly as possible.
const NSTimeInterval kMinimumTimeInterval =
    std::numeric_limits<NSTimeInterval>::min();
}

@interface InfoBubbleWindow(Private)
- (void)appIsTerminating;
- (void)finishCloseAfterAnimation;
@end

// A helper class to proxy app notifications to the window.
class AppNotificationBridge : public NotificationObserver {
 public:
  explicit AppNotificationBridge(InfoBubbleWindow* owner) : owner_(owner) {
    registrar_.Add(this, NotificationType::APP_TERMINATING,
                   NotificationService::AllSources());
  }

  // Overridden from NotificationObserver.
  void Observe(NotificationType type,
               const NotificationSource& source,
               const NotificationDetails& details) {
    switch (type.value) {
      case NotificationType::APP_TERMINATING:
        [owner_ appIsTerminating];
        break;
      default:
        NOTREACHED() << L"Unexpected notification";
    }
  }

 private:
  // The object we need to inform when we get a notification. Weak. Owns us.
  InfoBubbleWindow* owner_;

  // Used for registering to receive notifications and automatic clean up.
  NotificationRegistrar registrar_;

  DISALLOW_COPY_AND_ASSIGN(AppNotificationBridge);
};

// A delegate object for watching the alphaValue animation on InfoBubbleWindows.
// An InfoBubbleWindow instance cannot be the delegate for its own animation
// because CAAnimations retain their delegates, and since the InfoBubbleWindow
// retains its animations a retain loop would be formed.
@interface InfoBubbleWindowCloser : NSObject {
 @private
  InfoBubbleWindow* window_;  // Weak. Window to close.
}
- (id)initWithWindow:(InfoBubbleWindow*)window;
@end

@implementation InfoBubbleWindowCloser

- (id)initWithWindow:(InfoBubbleWindow*)window {
  if ((self = [super init])) {
    window_ = window;
  }
  return self;
}

// Callback for the alpha animation. Closes window_ if appropriate.
- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
  // When alpha reaches zero, close window_.
  if ([window_ alphaValue] == 0.0) {
    [window_ finishCloseAfterAnimation];
  }
}

@end


@implementation InfoBubbleWindow

@synthesize delayOnClose = delayOnClose_;

- (id)initWithContentRect:(NSRect)contentRect
                styleMask:(NSUInteger)aStyle
                  backing:(NSBackingStoreType)bufferingType
                    defer:(BOOL)flag {
  if ((self = [super initWithContentRect:contentRect
                               styleMask:NSBorderlessWindowMask
                                 backing:bufferingType
                                   defer:flag])) {
    [self setBackgroundColor:[NSColor clearColor]];
    [self setExcludedFromWindowsMenu:YES];
    [self setOpaque:NO];
    [self setHasShadow:YES];
    delayOnClose_ = YES;
    notificationBridge_.reset(new AppNotificationBridge(self));

    // Start invisible. Will be made visible when ordered front.
    [self setAlphaValue:0.0];

    // Set up alphaValue animation so that self is delegate for the animation.
    // Setting up the delegate is required so that the
    // animationDidStop:finished: callback can be handled.
    // Notice that only the alphaValue Animation is replaced in case
    // superclasses set up animations.
    CAAnimation* alphaAnimation = [CABasicAnimation animation];
    scoped_nsobject<InfoBubbleWindowCloser> delegate(
        [[InfoBubbleWindowCloser alloc] initWithWindow:self]);
    [alphaAnimation setDelegate:delegate];
    NSMutableDictionary* animations =
        [NSMutableDictionary dictionaryWithDictionary:[self animations]];
    [animations setObject:alphaAnimation forKey:@"alphaValue"];
    [self setAnimations:animations];
  }
  return self;
}

// According to
// http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953,
// NSBorderlessWindowMask windows cannot become key or main. In this
// case, this is not a desired behavior. As an example, the bubble could have
// buttons.
- (BOOL)canBecomeKeyWindow {
  return YES;
}

- (void)close {
  // Block the window from receiving events while it fades out.
  closing_ = YES;

  if (!delayOnClose_) {
    [self finishCloseAfterAnimation];
  } else {
    // Apply animations to hide self.
    [NSAnimationContext beginGrouping];
    [[NSAnimationContext currentContext]
        gtm_setDuration:kOrderOutAnimationDuration
              eventMask:NSLeftMouseUpMask];
    [[self animator] setAlphaValue:0.0];
    [NSAnimationContext endGrouping];
  }
}

// If the app is terminating but the window is still fading out, cancel the
// animation and close the window to prevent it from leaking.
// See http://crbug.com/37717
- (void)appIsTerminating {
  if (!delayOnClose_)
    return;  // The close has already happened with no Core Animation.

  // Cancel the current animation so that it closes immediately, triggering
  // |finishCloseAfterAnimation|.
  [NSAnimationContext beginGrouping];
  [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
  [[self animator] setAlphaValue:0.0];
  [NSAnimationContext endGrouping];
}

// Called by InfoBubbleWindowCloser when the window is to be really closed
// after the fading animation is complete.
- (void)finishCloseAfterAnimation {
  if (closing_)
    [super close];
}

// Adds animation for info bubbles being ordered to the front.
- (void)orderWindow:(NSWindowOrderingMode)orderingMode
         relativeTo:(NSInteger)otherWindowNumber {
  // According to the documentation '0' is the otherWindowNumber when the window
  // is ordered front.
  if (orderingMode == NSWindowAbove && otherWindowNumber == 0) {
    // Order self appropriately assuming that its alpha is zero as set up
    // in the designated initializer.
    [super orderWindow:orderingMode relativeTo:otherWindowNumber];

    // Set up frame so it can be adjust down by a few pixels.
    NSRect frame = [self frame];
    NSPoint newOrigin = frame.origin;
    newOrigin.y += kOrderInSlideOffset;
    [self setFrameOrigin:newOrigin];

    // Apply animations to show and move self.
    [NSAnimationContext beginGrouping];
    // The star currently triggers on mouse down, not mouse up.
    [[NSAnimationContext currentContext]
        gtm_setDuration:kOrderInAnimationDuration
              eventMask:NSLeftMouseUpMask|NSLeftMouseDownMask];
    [[self animator] setAlphaValue:1.0];
    [[self animator] setFrame:frame display:YES];
    [NSAnimationContext endGrouping];
  } else {
    [super orderWindow:orderingMode relativeTo:otherWindowNumber];
  }
}

// If the window is currently animating a close, block all UI events to the
// window.
- (void)sendEvent:(NSEvent*)theEvent {
  if (!closing_)
    [super sendEvent:theEvent];
}

- (BOOL)isClosing {
  return closing_;
}

@end