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

#include "app/l10n_util.h"
#include "base/mac_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/browser.h"
#include "chrome/browser/browser_window.h"
#include "chrome/browser/cocoa/browser_window_cocoa.h"
#include "chrome/browser/cocoa/browser_window_controller.h"
#include "chrome/browser/cocoa/extensions/browser_actions_controller.h"
#include "chrome/browser/cocoa/hover_close_button.h"
#include "chrome/browser/cocoa/info_bubble_view.h"
#include "chrome/browser/cocoa/location_bar/location_bar_view_mac.h"
#include "chrome/browser/cocoa/toolbar_controller.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_action.h"
#include "chrome/common/notification_registrar.h"
#include "chrome/common/notification_service.h"
#include "grit/generated_resources.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"


// C++ class that receives EXTENSION_LOADED notifications and proxies them back
// to |controller|.
class ExtensionLoadedNotificationObserver : public NotificationObserver {
 public:
  ExtensionLoadedNotificationObserver(
      ExtensionInstalledBubbleController* controller)
          : controller_(controller) {
    registrar_.Add(this, NotificationType::EXTENSION_LOADED,
        NotificationService::AllSources());
  }

 private:
  // NotificationObserver implementation. Tells the controller to start showing
  // its window on the main thread when the extension has finished loading.
  void Observe(NotificationType type,
               const NotificationSource& source,
               const NotificationDetails& details) {
    if (type == NotificationType::EXTENSION_LOADED) {
      Extension* extension = Details<Extension>(details).ptr();
      if (extension == [controller_ extension]) {
        [controller_ performSelectorOnMainThread:@selector(showWindow:)
                                      withObject:controller_
                                   waitUntilDone:NO];
      }
    } else {
      NOTREACHED() << "Received unexpected notification.";
    }
  }

  NotificationRegistrar registrar_;
  ExtensionInstalledBubbleController* controller_;  // weak, owns us
};

@implementation ExtensionInstalledBubbleController

@synthesize extension = extension_;
@synthesize pageActionRemoved = pageActionRemoved_;  // Exposed for unit test.

- (id)initWithParentWindow:(NSWindow*)parentWindow
                 extension:(Extension*)extension
                   browser:(Browser*)browser
                      icon:(SkBitmap)icon {
  NSString* nibPath =
      [mac_util::MainAppBundle() pathForResource:@"ExtensionInstalledBubble"
                                          ofType:@"nib"];
  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
    DCHECK(parentWindow);
    parentWindow_ = parentWindow;
    DCHECK(extension);
    extension_ = extension;
    DCHECK(browser);
    browser_ = browser;
    icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
    pageActionRemoved_ = NO;

    if (extension->browser_action()) {
      type_ = extension_installed_bubble::kBrowserAction;
    } else if (extension->page_action() &&
               !extension->page_action()->default_icon_path().empty()) {
      type_ = extension_installed_bubble::kPageAction;
    } else {
      NOTREACHED();  // kGeneric installs handled in the extension_install_ui.
    }

    // Start showing window only after extension has fully loaded.
    extensionObserver_.reset(new ExtensionLoadedNotificationObserver(self));
  }
  return self;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

- (void)close {
  [parentWindow_ removeChildWindow:[self window]];
  [super close];
}

- (void)windowWillClose:(NSNotification*)notification {
  // Turn off page action icon preview when the window closes, unless we
  // already removed it when the window resigned key status.
  [self removePageActionPreviewIfNecessary];
  extension_ = NULL;
  browser_ = NULL;
  parentWindow_ = nil;
  // We caught a close so we don't need to watch for the parent closing.
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [self autorelease];
}

// The controller is the delegate of the window, so it receives "did resign
// key" notifications.  When key is resigned, close the window.
- (void)windowDidResignKey:(NSNotification*)notification {
  NSWindow* window = [self window];
  DCHECK_EQ([notification object], window);
  DCHECK([window isVisible]);

  // If the browser window is closing, we need to remove the page action
  // immediately, otherwise the closing animation may overlap with
  // browser destruction.
  [self removePageActionPreviewIfNecessary];
  [self close];
}

- (IBAction)closeWindow:(id)sender {
  DCHECK([[self window] isVisible]);
  [self close];
}

// Extracted to a function here so that it can be overwritten for unit
// testing.
- (void)removePageActionPreviewIfNecessary {
  DCHECK(extension_);
  if (!extension_->page_action() || pageActionRemoved_)
    return;
  pageActionRemoved_ = YES;
  BrowserWindowCocoa* window = static_cast<BrowserWindowCocoa*>(
      browser_->window());
  LocationBarViewMac* locationBarView = static_cast<LocationBarViewMac*>(
      [[window->cocoa_controller() toolbarController] locationBarBridge]);

  locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
                                               false);  // disables preview.
}

// The extension installed bubble points at the browser action icon or the
// page action icon (shown as a preview), depending on the extension type.
// We need to calculate the location of these icons and the size of the
// message itself (which varies with the title of the extension) in order
// to figure out the origin point for the extension installed bubble.
// TODO(mirandac): add framework to easily test extension UI components!
- (NSPoint)calculateArrowPoint {
  BrowserWindowCocoa* window =
      static_cast<BrowserWindowCocoa*>(browser_->window());
  NSPoint arrowPoint = NSZeroPoint;

  switch(type_) {
    case extension_installed_bubble::kBrowserAction: {
      BrowserActionsController* controller =
          [[window->cocoa_controller() toolbarController]
              browserActionsController];
      arrowPoint = [controller popupPointForBrowserAction:extension_];
      break;
    }
    case extension_installed_bubble::kPageAction: {
      LocationBarViewMac* locationBarView =
          static_cast<LocationBarViewMac*>(
              [[window->cocoa_controller() toolbarController]
               locationBarBridge]);
      // Tell the location bar to show a preview of the page action icon, which
      // would ordinarily only be displayed on a page of the appropriate type.
      // We remove this preview when the extension installed bubble closes.
      locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
                                                   true);

      // Find the center of the bottom of the page action icon.
      const NSRect frame =
          locationBarView->GetPageActionFrame(extension_->page_action());
      arrowPoint = NSMakePoint(NSMidX(frame), NSMinY(frame));
      break;
    }
    default: {
      NOTREACHED() << "Generic extension type not allowed in install bubble.";
    }
  }
  return arrowPoint;
}

// We want this to be a child of a browser window.  addChildWindow:
// (called from this function) will bring the window on-screen;
// unfortunately, [NSWindowController showWindow:] will also bring it
// on-screen (but will cause unexpected changes to the window's
// position).  We cannot have an addChildWindow: and a subsequent
// showWindow:. Thus, we have our own version.
- (void)showWindow:(id)sender {
  // Generic extensions get an infobar rather than a bubble.
  DCHECK(type_ != extension_installed_bubble::kGeneric);
  DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));

  // Load nib and calculate height based on messages to be shown.
  NSWindow* window = [self initializeWindow];
  int newWindowHeight = [self calculateWindowHeight];
  [infoBubbleView_ setFrameSize:NSMakeSize(
      NSWidth([[window contentView] bounds]), newWindowHeight)];
  NSSize windowDelta = NSMakeSize(
      0, newWindowHeight - NSHeight([[window contentView] bounds]));
  windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
  NSRect newFrame = [window frame];
  newFrame.size.height += windowDelta.height;
  [window setFrame:newFrame display:NO];

  // Now that we have resized the window, adjust y pos of the messages.
  [self setMessageFrames:newWindowHeight];

  // Find window origin, taking into account bubble size and arrow location.
  NSPoint origin =
      [parentWindow_ convertBaseToScreen:[self calculateArrowPoint]];
  NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
                              info_bubble::kBubbleArrowWidth / 2.0, 0);
  offsets = [[window contentView] convertSize:offsets toView:nil];
  origin.x -= NSWidth([window frame]) - offsets.width;
  origin.y -= NSHeight([window frame]);
  [window setFrameOrigin:origin];

  [parentWindow_ addChildWindow:window
                        ordered:NSWindowAbove];
  [window makeKeyAndOrderFront:self];
}

// Finish nib loading, set arrow location and load icon into window.  This
// function is exposed for unit testing.
- (NSWindow*)initializeWindow {
  NSWindow* window = [self window];  // completes nib load
  [infoBubbleView_ setArrowLocation:info_bubble::kTopRight];

  // Set appropriate icon, resizing if necessary.
  if ([icon_ size].width > extension_installed_bubble::kIconSize) {
    [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
                              extension_installed_bubble::kIconSize)];
  }
  [iconImage_ setImage:icon_];
  [iconImage_ setNeedsDisplay:YES];
  return window;
 }

// Calculate the height of each install message, resizing messages in their
// frames to fit window width.  Return the new window height, based on the
// total of all message heights.
- (int)calculateWindowHeight {
  // Adjust the window height to reflect the sum height of all messages
  // and vertical padding.
  int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;

  // First part of extension installed message.
  [extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF(
      IDS_EXTENSION_INSTALLED_HEADING, UTF8ToUTF16(extension_->name()))];
  [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:extensionInstalledMsg_];
  newWindowHeight += [extensionInstalledMsg_ frame].size.height +
      extension_installed_bubble::kInnerVerticalMargin;

  // If type is page action, include a special message about page actions.
  if (type_ == extension_installed_bubble::kPageAction) {
    [pageActionInfoMsg_ setHidden:NO];
    [[pageActionInfoMsg_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:pageActionInfoMsg_];
    newWindowHeight += [pageActionInfoMsg_ frame].size.height +
        extension_installed_bubble::kInnerVerticalMargin;
  }

  // Second part of extension installed message.
  [[extensionInstalledInfoMsg_ cell]
      setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
  [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_];
  newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height;

  return newWindowHeight;
}

// Adjust y-position of messages to sit properly in new window height.
- (void)setMessageFrames:(int)newWindowHeight {
  // The extension messages will always be shown.
  NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame];
  NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame];

  extensionMessageFrame1.origin.y = newWindowHeight - (
      extensionMessageFrame1.size.height +
      extension_installed_bubble::kOuterVerticalMargin);
  [extensionInstalledMsg_ setFrame:extensionMessageFrame1];
  if (type_ == extension_installed_bubble::kPageAction) {
    // The page action message is only shown when appropriate.
    NSRect pageActionMessageFrame = [pageActionInfoMsg_ frame];
    pageActionMessageFrame.origin.y = extensionMessageFrame1.origin.y - (
        pageActionMessageFrame.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
    [pageActionInfoMsg_ setFrame:pageActionMessageFrame];
    extensionMessageFrame2.origin.y = pageActionMessageFrame.origin.y - (
        extensionMessageFrame2.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
  } else {
    extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - (
        extensionMessageFrame2.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
  }
  [extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2];
}

// Exposed for unit testing.
- (NSRect)getExtensionInstalledMsgFrame {
  return [extensionInstalledMsg_ frame];
}

- (NSRect)getPageActionInfoMsgFrame {
  return [pageActionInfoMsg_ frame];
}

- (NSRect)getExtensionInstalledInfoMsgFrame {
  return [extensionInstalledInfoMsg_ frame];
}

@end