// 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 "chrome/browser/ui/cocoa/extensions/extension_infobar_controller.h"

#include <cmath>

#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/extensions/extension_infobar_delegate.h"
#include "chrome/browser/extensions/image_loading_tracker.h"
#import "chrome/browser/ui/cocoa/animatable_view.h"
#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
#import "chrome/browser/ui/cocoa/menu_button.h"
#include "chrome/browser/ui/cocoa/infobars/infobar.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_icon_set.h"
#include "chrome/common/extensions/extension_resource.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "grit/theme_resources.h"
#include "skia/ext/skia_utils_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas_skia.h"

namespace {
const CGFloat kAnimationDuration = 0.12;
const CGFloat kBottomBorderHeightPx = 1.0;
const CGFloat kButtonHeightPx = 26.0;
const CGFloat kButtonLeftMarginPx = 2.0;
const CGFloat kButtonWidthPx = 34.0;
const CGFloat kDropArrowLeftMarginPx = 3.0;
const CGFloat kToolbarMinHeightPx = 36.0;
const CGFloat kToolbarMaxHeightPx = 72.0;
}  // namespace

@interface ExtensionInfoBarController(Private)
// Called when the extension's hosted NSView has been resized.
- (void)extensionViewFrameChanged;
// Returns the clamped height of the extension view to be within the min and max
// values defined above.
- (CGFloat)clampedExtensionViewHeight;
// Adjusts the width of the extension's hosted view to match the window's width
// and sets the proper height for it as well.
- (void)adjustExtensionViewSize;
// Sets the image to be used in the button on the left side of the infobar.
- (void)setButtonImage:(NSImage*)image;
@end

// A helper class to bridge the asynchronous Skia bitmap loading mechanism to
// the extension's button.
class InfobarBridge : public ExtensionInfoBarDelegate::DelegateObserver,
                      public ImageLoadingTracker::Observer {
 public:
  explicit InfobarBridge(ExtensionInfoBarController* owner)
      : owner_(owner),
        delegate_([owner delegate]->AsExtensionInfoBarDelegate()),
        tracker_(this) {
    delegate_->set_observer(this);
    LoadIcon();
  }

  virtual ~InfobarBridge() {
    if (delegate_)
      delegate_->set_observer(NULL);
  }

  // Load the Extension's icon image.
  void LoadIcon() {
    const Extension* extension = delegate_->extension_host()->extension();
    ExtensionResource icon_resource = extension->GetIconResource(
        Extension::EXTENSION_ICON_BITTY, ExtensionIconSet::MATCH_EXACTLY);
    if (!icon_resource.relative_path().empty()) {
      tracker_.LoadImage(extension, icon_resource,
                         gfx::Size(Extension::EXTENSION_ICON_BITTY,
                                   Extension::EXTENSION_ICON_BITTY),
                         ImageLoadingTracker::DONT_CACHE);
    } else {
      OnImageLoaded(NULL, icon_resource, 0);
    }
  }

  // ImageLoadingTracker::Observer implementation.
  // TODO(andybons): The infobar view implementations share a lot of the same
  // code. Come up with a strategy to share amongst them.
  virtual void OnImageLoaded(
      SkBitmap* image, const ExtensionResource& resource, int index) {
    if (!delegate_)
      return;  // The delegate can go away while the image asynchronously loads.

    ResourceBundle& rb = ResourceBundle::GetSharedInstance();

    // Fall back on the default extension icon on failure.
    SkBitmap* icon;
    if (!image || image->empty())
      icon = rb.GetBitmapNamed(IDR_EXTENSIONS_SECTION);
    else
      icon = image;

    SkBitmap* drop_image = rb.GetBitmapNamed(IDR_APP_DROPARROW);

    const int image_size = Extension::EXTENSION_ICON_BITTY;
    scoped_ptr<gfx::CanvasSkia> canvas(
        new gfx::CanvasSkia(
            image_size + kDropArrowLeftMarginPx + drop_image->width(),
            image_size, false));
    canvas->DrawBitmapInt(*icon,
                          0, 0, icon->width(), icon->height(),
                          0, 0, image_size, image_size,
                          false);
    canvas->DrawBitmapInt(*drop_image,
                          image_size + kDropArrowLeftMarginPx,
                          image_size / 2);
    [owner_ setButtonImage:gfx::SkBitmapToNSImage(canvas->ExtractBitmap())];
  }

  // Overridden from ExtensionInfoBarDelegate::DelegateObserver:
  virtual void OnDelegateDeleted() {
    delegate_ = NULL;
  }

 private:
  // Weak. Owns us.
  ExtensionInfoBarController* owner_;

  // Weak.
  ExtensionInfoBarDelegate* delegate_;

  // Loads the extensions's icon on the file thread.
  ImageLoadingTracker tracker_;

  DISALLOW_COPY_AND_ASSIGN(InfobarBridge);
};


@implementation ExtensionInfoBarController

- (id)initWithDelegate:(InfoBarDelegate*)delegate
                window:(NSWindow*)window {
  if ((self = [super initWithDelegate:delegate])) {
    window_ = window;
    dropdownButton_.reset([[MenuButton alloc] init]);
    [dropdownButton_ setOpenMenuOnClick:YES];

    ExtensionHost* extensionHost = delegate_->AsExtensionInfoBarDelegate()->
        extension_host();
    contextMenu_.reset([[ExtensionActionContextMenu alloc]
        initWithExtension:extensionHost->extension()
                  profile:extensionHost->profile()
          extensionAction:NULL]);
    // See menu_button.h for documentation on why this is needed.
    NSMenuItem* dummyItem =
        [[[NSMenuItem alloc] initWithTitle:@""
                                    action:nil
                             keyEquivalent:@""] autorelease];
    [contextMenu_ insertItem:dummyItem atIndex:0];
    [dropdownButton_ setAttachedMenu:contextMenu_.get()];

    bridge_.reset(new InfobarBridge(self));
  }
  return self;
}

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

- (void)addAdditionalControls {
  [self removeButtons];

  extensionView_ = delegate_->AsExtensionInfoBarDelegate()->extension_host()->
      view()->native_view();

  // Add the extension's RenderWidgetHostViewMac to the view hierarchy of the
  // InfoBar and make sure to place it below the Close button.
  [infoBarView_ addSubview:extensionView_
                positioned:NSWindowBelow
                relativeTo:(NSView*)closeButton_];

  // Add the context menu button to the hierarchy.
  [dropdownButton_ setShowsBorderOnlyWhileMouseInside:YES];
  CGFloat buttonY =
      std::floor(NSMidY([infoBarView_ frame]) - (kButtonHeightPx / 2.0)) +
          kBottomBorderHeightPx;
  NSRect buttonFrame = NSMakeRect(
      kButtonLeftMarginPx, buttonY, kButtonWidthPx, kButtonHeightPx);
  [dropdownButton_ setFrame:buttonFrame];
  [dropdownButton_ setAutoresizingMask:NSViewMinYMargin | NSViewMaxYMargin];
  [infoBarView_ addSubview:dropdownButton_];

  // Because the parent view has a bottom border, account for it during
  // positioning.
  NSRect extensionFrame = [extensionView_ frame];
  extensionFrame.origin.y = kBottomBorderHeightPx;

  [extensionView_ setFrame:extensionFrame];
  // The extension's native view will only have a height that is non-zero if it
  // already has been loaded and rendered, which is the case when you switch
  // back to a tab with an extension infobar within it. The reason this is
  // needed is because the extension view's frame will not have changed in the
  // above case, so the NSViewFrameDidChangeNotification registered below will
  // never fire.
  if (NSHeight(extensionFrame) > 0.0) {
    NSSize infoBarSize = [[self view] frame].size;
    infoBarSize.height = [self clampedExtensionViewHeight] +
        kBottomBorderHeightPx;
    [[self view] setFrameSize:infoBarSize];
    [infoBarView_ setFrameSize:infoBarSize];
  }

  [self adjustExtensionViewSize];

  // These two notification handlers are here to ensure the width of the
  // native extension view is the same as the browser window's width and that
  // the parent infobar view matches the height of the extension's native view.
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(extensionViewFrameChanged)
             name:NSViewFrameDidChangeNotification
           object:extensionView_];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(adjustWidthToFitWindow)
             name:NSWindowDidResizeNotification
           object:window_];
}

- (void)extensionViewFrameChanged {
  [self adjustExtensionViewSize];

  AnimatableView* view = [self animatableView];
  NSRect infoBarFrame = [view frame];
  CGFloat newHeight = [self clampedExtensionViewHeight] + kBottomBorderHeightPx;
  [infoBarView_ setPostsFrameChangedNotifications:NO];
  infoBarFrame.size.height = newHeight;
  [infoBarView_ setFrame:infoBarFrame];
  [infoBarView_ setPostsFrameChangedNotifications:YES];
  [view animateToNewHeight:newHeight duration:kAnimationDuration];
}

- (CGFloat)clampedExtensionViewHeight {
  return std::max(kToolbarMinHeightPx,
      std::min(NSHeight([extensionView_ frame]), kToolbarMaxHeightPx));
}

- (void)adjustExtensionViewSize {
  [extensionView_ setPostsFrameChangedNotifications:NO];
  NSSize extensionViewSize = [extensionView_ frame].size;
  extensionViewSize.width = NSWidth([window_ frame]);
  extensionViewSize.height = [self clampedExtensionViewHeight];
  [extensionView_ setFrameSize:extensionViewSize];
  [extensionView_ setPostsFrameChangedNotifications:YES];
}

- (void)setButtonImage:(NSImage*)image {
  [dropdownButton_ setImage:image];
}

@end

InfoBar* ExtensionInfoBarDelegate::CreateInfoBar() {
  NSWindow* window = [(NSView*)tab_contents_->GetContentNativeView() window];
  ExtensionInfoBarController* controller =
      [[ExtensionInfoBarController alloc] initWithDelegate:this
                                                    window:window];
  return new InfoBar(controller);
}