// Copyright (c) 2013 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/message_center/cocoa/notification_controller.h"

#include <stddef.h>

#include <algorithm>

#include "base/mac/foundation_util.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/url_formatter/elide_url.h"
#include "skia/ext/skia_utils_mac.h"
#import "ui/base/cocoa/hover_image_button.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/notification.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "url/gurl.h"

@interface MCNotificationProgressBar : NSProgressIndicator
@end

@implementation MCNotificationProgressBar
- (void)drawRect:(NSRect)dirtyRect {
  NSRect sliceRect, remainderRect;
  double progressFraction = ([self doubleValue] - [self minValue]) /
      ([self maxValue] - [self minValue]);
  NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
               NSWidth(dirtyRect) * progressFraction, NSMinXEdge);

  NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
      xRadius:message_center::kProgressBarCornerRadius
      yRadius:message_center::kProgressBarCornerRadius];
  [skia::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
      set];
  [path fill];

  if (progressFraction == 0.0)
    return;

  path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
      xRadius:message_center::kProgressBarCornerRadius
      yRadius:message_center::kProgressBarCornerRadius];
  [skia::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor)
      set];
  [path fill];
}

- (id)accessibilityAttributeValue:(NSString*)attribute {
  double progressValue = 0.0;
  if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
    progressValue = [self doubleValue];
  } else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
    progressValue = [self minValue];
  } else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) {
    progressValue = [self maxValue];
  } else {
    return [super accessibilityAttributeValue:attribute];
  }

  return [NSString stringWithFormat:@"%lf", progressValue];
}
@end

////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationButton : NSButton
@end

@implementation MCNotificationButton
// drawRect: needs to fill the button with a background, otherwise we don't get
// subpixel antialiasing.
- (void)drawRect:(NSRect)dirtyRect {
  NSColor* color = skia::SkColorToCalibratedNSColor(
      message_center::kNotificationBackgroundColor);
  [color set];
  NSRectFill(dirtyRect);
  [super drawRect:dirtyRect];
}
@end

@interface MCNotificationButtonCell : NSButtonCell {
  BOOL hovered_;
}
@end

////////////////////////////////////////////////////////////////////////////////
@implementation MCNotificationButtonCell
- (BOOL)isOpaque {
  return YES;
}

- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
  // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
  // valid.
  DCHECK([self showsBorderOnlyWhileMouseInside]);

  if (!hovered_)
    return;
  [skia::SkColorToCalibratedNSColor(
      message_center::kHoveredButtonBackgroundColor) set];
  NSRectFill(frame);
}

- (void)drawImage:(NSImage*)image
        withFrame:(NSRect)frame
           inView:(NSView*)controlView {
  if (!image)
    return;
  NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
                           message_center::kButtonIconTopPadding,
                           message_center::kNotificationButtonIconSize,
                           message_center::kNotificationButtonIconSize);
  [image drawInRect:rect
            fromRect:NSZeroRect
           operation:NSCompositeSourceOver
            fraction:1.0
      respectFlipped:YES
               hints:nil];
}

- (NSRect)drawTitle:(NSAttributedString*)title
          withFrame:(NSRect)frame
             inView:(NSView*)controlView {
  CGFloat offsetX = message_center::kButtonHorizontalPadding;
  if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
    offsetX += message_center::kNotificationButtonIconSize +
               message_center::kButtonIconToTitlePadding;
  }
  frame.origin.x = offsetX;
  frame.size.width -= offsetX;

  NSDictionary* attributes = @{
    NSFontAttributeName :
        [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
    NSForegroundColorAttributeName :
        skia::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
  };
  [[title string] drawWithRect:frame
                       options:(NSStringDrawingUsesLineFragmentOrigin |
                                NSStringDrawingTruncatesLastVisibleLine)
                    attributes:attributes];
  return frame;
}

- (void)mouseEntered:(NSEvent*)event {
  hovered_ = YES;

  // Else the cell won't be repainted on hover.
  [super mouseEntered:event];
}

- (void)mouseExited:(NSEvent*)event {
  hovered_ = NO;
  [super mouseExited:event];
}
@end

////////////////////////////////////////////////////////////////////////////////

@interface MCNotificationView : NSBox {
 @private
  MCNotificationController* controller_;
}

- (id)initWithController:(MCNotificationController*)controller
                   frame:(NSRect)frame;
@end

@implementation MCNotificationView
- (id)initWithController:(MCNotificationController*)controller
                   frame:(NSRect)frame {
  if ((self = [super initWithFrame:frame]))
    controller_ = controller;
  return self;
}

- (void)mouseUp:(NSEvent*)event {
  if (event.type != NSLeftMouseUp) {
    [super mouseUp:event];
    return;
  }
  if (NSPointInRect([self convertPoint:event.locationInWindow fromView:nil],
                    self.bounds)) {
    [controller_ notificationClicked];
  }
}

- (NSView*)hitTest:(NSPoint)point {
  // Route the mouse click events on NSTextView to the container view.
  NSView* hitView = [super hitTest:point];
  if (hitView)
    return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
  return nil;
}

- (BOOL)accessibilityIsIgnored {
  return NO;
}

- (NSArray*)accessibilityActionNames {
  return @[ NSAccessibilityPressAction ];
}

- (void)accessibilityPerformAction:(NSString*)action {
  if ([action isEqualToString:NSAccessibilityPressAction]) {
    [controller_ notificationClicked];
    return;
  }
  [super accessibilityPerformAction:action];
}
@end

////////////////////////////////////////////////////////////////////////////////

@interface AccessibilityIgnoredBox : NSBox
@end

// Ignore this element, but expose its children to accessibility.
@implementation AccessibilityIgnoredBox
- (BOOL)accessibilityIsIgnored {
  return YES;
}

// Pretend this element has no children.
// TODO(petewil): Until we have alt text available, we will hide the children of
//  the box also.  Remove this override once alt text is set (by using
// NSAccessibilityDescriptionAttribute).
- (id)accessibilityAttributeValue:(NSString*)attribute {
  // If we get a request for NSAccessibilityChildrenAttribute, return an empty
  // array to pretend we have no children.
  if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
    return @[];
  else
    return [super accessibilityAttributeValue:attribute];
}
@end

////////////////////////////////////////////////////////////////////////////////

@interface MCNotificationController (Private)
// Configures a NSBox to be borderless, titleless, and otherwise appearance-
// free.
- (void)configureCustomBox:(NSBox*)box;

// Initializes the icon_ ivar and returns the view to insert into the hierarchy.
- (NSView*)createIconView;

// Creates a box that shows a border when the icon is not big enough to fill the
// space.
- (NSBox*)createImageBox:(const gfx::Image&)notificationImage;

// Initializes the closeButton_ ivar with the configured button.
- (void)configureCloseButtonInFrame:(NSRect)rootFrame;

// Initializes the settingsButton_ ivar with the configured button.
- (void)configureSettingsButtonInFrame:(NSRect)rootFrame;

// Creates the smallImage_ ivar with the appropriate frame.
- (NSView*)createSmallImageInFrame:(NSRect)rootFrame;

// Initializes title_ in the given frame.
- (void)configureTitleInFrame:(NSRect)rootFrame;

// Initializes message_ in the given frame.
- (void)configureBodyInFrame:(NSRect)rootFrame;

// Initializes contextMessage_ in the given frame.
- (void)configureContextMessageInFrame:(NSRect)rootFrame;

// Creates a NSTextView that the caller owns configured as a label in a
// notification.
- (NSTextView*)newLabelWithFrame:(NSRect)frame;

// Gets the rectangle in which notification content should be placed. This
// rectangle is to the right of the icon and left of the control buttons.
// This depends on the icon_ and closeButton_ being initialized.
- (NSRect)currentContentRect;

// Returns the wrapped text that could fit within the content rect with not
// more than the given number of lines. The wrapped text would be painted using
// the given font. The Ellipsis could be added at the end of the last line if
// it is too long. Outputs the number of lines computed in the actualLines
// parameter.
- (base::string16)wrapText:(const base::string16&)text
                   forFont:(NSFont*)font
          maxNumberOfLines:(size_t)lines
               actualLines:(size_t*)actualLines;

// Same as above without outputting the lines formatted.
- (base::string16)wrapText:(const base::string16&)text
                   forFont:(NSFont*)font
          maxNumberOfLines:(size_t)lines;

@end

////////////////////////////////////////////////////////////////////////////////

@implementation MCNotificationController

- (id)initWithNotification:(const message_center::Notification*)notification
    messageCenter:(message_center::MessageCenter*)messageCenter {
  if ((self = [super initWithNibName:nil bundle:nil])) {
    notification_ = notification;
    notificationID_ = notification_->id();
    messageCenter_ = messageCenter;
  }
  return self;
}

- (void)loadView {
  // Create the root view of the notification.
  NSRect rootFrame = NSMakeRect(0, 0,
      message_center::kNotificationPreferredImageWidth,
      message_center::kNotificationIconSize);
  base::scoped_nsobject<MCNotificationView> rootView(
      [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
  [self configureCustomBox:rootView];
  [rootView setFillColor:skia::SkColorToCalibratedNSColor(
      message_center::kNotificationBackgroundColor)];
  [self setView:rootView];

  [rootView addSubview:[self createIconView]];

  // Create the close button.
  [self configureCloseButtonInFrame:rootFrame];
  [rootView addSubview:closeButton_];

  // Create the small image.
  [rootView addSubview:[self createSmallImageInFrame:rootFrame]];

  // Create the settings button.
  if (notification_->delegate() &&
      notification_->delegate()->ShouldDisplaySettingsButton()) {
    [self configureSettingsButtonInFrame:rootFrame];
    [rootView addSubview:settingsButton_];
  }

  NSRect contentFrame = [self currentContentRect];

  // Create the title.
  [self configureTitleInFrame:contentFrame];
  [rootView addSubview:title_];

  // Create the message body.
  [self configureBodyInFrame:contentFrame];
  [rootView addSubview:message_];

  // Create the context message body.
  [self configureContextMessageInFrame:contentFrame];
  [rootView addSubview:contextMessage_];

  // Populate the data.
  [self updateNotification:notification_];
}

- (NSRect)updateNotification:(const message_center::Notification*)notification {
  DCHECK_EQ(notification->id(), notificationID_);
  notification_ = notification;

  message_center::NotificationLayoutParams layoutParams;
  layoutParams.rootFrame =
      NSMakeRect(0, 0, message_center::kNotificationPreferredImageWidth,
                 message_center::kNotificationIconSize);

  [smallImage_ setImage:notification_->small_image().AsNSImage()];

  // Update the icon.
  [icon_ setImage:notification_->icon().AsNSImage()];

  // The message_center:: constants are relative to capHeight at the top and
  // relative to the baseline at the bottom, but NSTextField uses the full line
  // height for its height.
  CGFloat titleTopGap =
      roundf([[title_ font] ascender] - [[title_ font] capHeight]);
  CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
  CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;

  CGFloat messageTopGap =
      roundf([[message_ font] ascender] - [[message_ font] capHeight]);
  CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
  CGFloat messagePadding =
      message_center::kTextTopPadding - titleBottomGap - messageTopGap;

  CGFloat contextMessageTopGap = roundf(
      [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
  CGFloat contextMessagePadding =
      message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;

  // Set the title and recalculate the frame.
  size_t actualTitleLines = 0;
  [title_ setString:base::SysUTF16ToNSString([self
                                wrapText:notification_->title()
                                 forFont:[title_ font]
                        maxNumberOfLines:message_center::kMaxTitleLines
                             actualLines:&actualTitleLines])];
  [title_ sizeToFit];
  layoutParams.titleFrame = [title_ frame];
  layoutParams.titleFrame.origin.y = NSMaxY(layoutParams.rootFrame) -
                                     titlePadding -
                                     NSHeight(layoutParams.titleFrame);

  // The number of message lines depends on the number of context message lines
  // and the lines within the title, and whether an image exists.
  int messageLineLimit = message_center::kMessageExpandedLineLimit;
  if (actualTitleLines > 1)
    messageLineLimit -= (actualTitleLines - 1) * 2;
  if (!notification_->image().IsEmpty()) {
    messageLineLimit /= 2;

    if (!notification_->context_message().empty() &&
        !notification_->UseOriginAsContextMessage())
      messageLineLimit -= message_center::kContextMessageLineLimit;
  }
  if (messageLineLimit < 0)
    messageLineLimit = 0;

  // Set the message and recalculate the frame.
  [message_ setString:base::SysUTF16ToNSString(
      [self wrapText:notification_->message()
             forFont:[message_ font]
      maxNumberOfLines:messageLineLimit])];
  [message_ sizeToFit];
  layoutParams.messageFrame = [message_ frame];

  // If there are list items, then the message_ view should not be displayed.
  const std::vector<message_center::NotificationItem>& items =
      notification->items();
  // If there are list items, don't show the main message.  Also if the message
  // is empty, mark it as hidden and set 0 height, so it doesn't take up any
  // space (size to fit leaves it 15 px tall.
  if (items.size() > 0 || notification_->message().empty()) {
    [message_ setHidden:YES];
    layoutParams.messageFrame.origin.y = layoutParams.titleFrame.origin.y;
    layoutParams.messageFrame.size.height = 0;
  } else {
    [message_ setHidden:NO];
    layoutParams.messageFrame.origin.y = NSMinY(layoutParams.titleFrame) -
                                         messagePadding -
                                         NSHeight(layoutParams.messageFrame);
    layoutParams.messageFrame.size.height = NSHeight([message_ frame]);
  }

  // Set the context message and recalculate the frame.
  base::string16 message;
  if (notification->UseOriginAsContextMessage()) {
    gfx::FontList font_list((gfx::Font([message_ font])));
    message =
        url_formatter::ElideHost(notification->origin_url(), font_list,
                                 message_center::kContextMessageViewWidth);
  } else {
    message = notification->context_message();
  }

  base::string16 elided =
      [self wrapText:message
             forFont:[contextMessage_ font]
          maxNumberOfLines:message_center::kContextMessageLineLimit];
  [contextMessage_ setString:base::SysUTF16ToNSString(elided)];
  [contextMessage_ sizeToFit];

  layoutParams.contextMessageFrame = [contextMessage_ frame];

  if (notification->context_message().empty() &&
      !notification->UseOriginAsContextMessage()) {
    [contextMessage_ setHidden:YES];
    layoutParams.contextMessageFrame.origin.y =
        layoutParams.messageFrame.origin.y;
    layoutParams.contextMessageFrame.size.height = 0;
  } else {
    [contextMessage_ setHidden:NO];

    // If the context message is used as a domain make sure it's placed at the
    // bottom of the top section.
    CGFloat contextMessageY = NSMinY(layoutParams.messageFrame) -
                              contextMessagePadding -
                              NSHeight(layoutParams.contextMessageFrame);
    layoutParams.contextMessageFrame.origin.y =
        notification->UseOriginAsContextMessage()
            ? std::min(NSMinY([icon_ frame]) + contextMessagePadding,
                       contextMessageY)
            : contextMessageY;
    layoutParams.contextMessageFrame.size.height =
        NSHeight([contextMessage_ frame]);
  }

  // Calculate the settings button position. It is dependent on whether the
  // context message aligns or not with the icon.
  layoutParams.settingsButtonFrame = [settingsButton_ frame];
  layoutParams.settingsButtonFrame.origin.y =
      MIN(NSMinY([icon_ frame]) + message_center::kSmallImagePadding,
          NSMinY(layoutParams.contextMessageFrame));

  // Create the list item views (up to a maximum).
  [listView_ removeFromSuperview];
  layoutParams.listFrame = NSZeroRect;
  if (items.size() > 0) {
    layoutParams.listFrame = [self currentContentRect];
    layoutParams.listFrame.origin.y = 0;
    layoutParams.listFrame.size.height = 0;
    listView_.reset([[NSView alloc] initWithFrame:layoutParams.listFrame]);
    [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
                                    forAttribute:NSAccessibilityRoleAttribute];
    [listView_
        accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
                         forAttribute:NSAccessibilitySubroleAttribute];
    CGFloat y = 0;

    NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
    CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));

    const int kNumNotifications =
        std::min(items.size(), message_center::kNotificationMaximumItems);
    for (int i = kNumNotifications - 1; i >= 0; --i) {
      NSTextView* itemView = [self
          newLabelWithFrame:NSMakeRect(0, y, NSWidth(layoutParams.listFrame),
                                       lineHeight)];
      [itemView setFont:font];

      // Disable the word-wrap in order to show the text in single line.
      [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
      [[itemView textContainer] setWidthTracksTextView:NO];

      // Construct the text from the title and message.
      base::string16 text =
          items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
      base::string16 ellidedText =
          [self wrapText:text forFont:font maxNumberOfLines:1];
      [itemView setString:base::SysUTF16ToNSString(ellidedText)];

      // Use dim color for the title part.
      NSColor* titleColor =
          skia::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
      NSRange titleRange = NSMakeRange(
          0,
          std::min(ellidedText.size(), items[i].title.size()));
      [itemView setTextColor:titleColor range:titleRange];

      // Use dim color for the message part if it has not been truncated.
      if (ellidedText.size() > items[i].title.size() + 1) {
        NSColor* messageColor =
            skia::SkColorToCalibratedNSColor(message_center::kDimTextColor);
        NSRange messageRange = NSMakeRange(
            items[i].title.size() + 1,
            ellidedText.size() - items[i].title.size() - 1);
        [itemView setTextColor:messageColor range:messageRange];
      }

      [listView_ addSubview:itemView];
      y += lineHeight;
    }
    // TODO(thakis): The spacing is not completely right.
    CGFloat listTopPadding =
        message_center::kTextTopPadding - contextMessageTopGap;
    layoutParams.listFrame.size.height = y;
    layoutParams.listFrame.origin.y = NSMinY(layoutParams.contextMessageFrame) -
                                      listTopPadding -
                                      NSHeight(layoutParams.listFrame);
    [listView_ setFrame:layoutParams.listFrame];
    [[self view] addSubview:listView_];
  }

  // Create the progress bar view if needed.
  [progressBarView_ removeFromSuperview];
  layoutParams.progressBarFrame = NSZeroRect;
  if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
    layoutParams.progressBarFrame = [self currentContentRect];
    layoutParams.progressBarFrame.origin.y =
        NSMinY(layoutParams.contextMessageFrame) -
        message_center::kProgressBarTopPadding -
        message_center::kProgressBarThickness;
    layoutParams.progressBarFrame.size.height =
        message_center::kProgressBarThickness;
    progressBarView_.reset([[MCNotificationProgressBar alloc]
        initWithFrame:layoutParams.progressBarFrame]);
    // Setting indeterminate to NO does not work with custom drawRect.
    [progressBarView_ setIndeterminate:YES];
    [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
    [progressBarView_ setDoubleValue:notification->progress()];
    [[self view] addSubview:progressBarView_];
  }

  // If the bottom-most element so far is out of the rootView's bounds, resize
  // the view.
  CGFloat minY = NSMinY(layoutParams.contextMessageFrame);
  if (listView_ && NSMinY(layoutParams.listFrame) < minY)
    minY = NSMinY(layoutParams.listFrame);
  if (progressBarView_ && NSMinY(layoutParams.progressBarFrame) < minY)
    minY = NSMinY(layoutParams.progressBarFrame);
  if (minY < messagePadding) {
    CGFloat delta = messagePadding - minY;
    [self adjustFrameHeight:&layoutParams delta:delta];
  }

  // Add the bottom container view.
  NSRect frame = layoutParams.rootFrame;
  frame.size.height = 0;
  [bottomView_ removeFromSuperview];
  bottomView_.reset([[NSView alloc] initWithFrame:frame]);
  CGFloat y = 0;

  // Create action buttons if appropriate, bottom-up.
  std::vector<message_center::ButtonInfo> buttons = notification->buttons();
  for (int i = buttons.size() - 1; i >= 0; --i) {
    message_center::ButtonInfo buttonInfo = buttons[i];
    NSRect buttonFrame = frame;
    buttonFrame.origin = NSMakePoint(0, y);
    buttonFrame.size.height = message_center::kButtonHeight;
    base::scoped_nsobject<MCNotificationButton> button(
        [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
    base::scoped_nsobject<MCNotificationButtonCell> cell(
        [[MCNotificationButtonCell alloc]
            initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
    [cell setShowsBorderOnlyWhileMouseInside:YES];
    [button setCell:cell];
    [button setImage:buttonInfo.icon.AsNSImage()];
    [button setBezelStyle:NSSmallSquareBezelStyle];
    [button setImagePosition:NSImageLeft];
    [button setTag:i];
    [button setTarget:self];
    [button setAction:@selector(buttonClicked:)];
    y += NSHeight(buttonFrame);
    frame.size.height += NSHeight(buttonFrame);
    [bottomView_ addSubview:button];

    NSRect separatorFrame = frame;
    separatorFrame.origin = NSMakePoint(0, y);
    separatorFrame.size.height = 1;
    base::scoped_nsobject<NSBox> separator(
        [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
    [self configureCustomBox:separator];
    [separator setFillColor:skia::SkColorToCalibratedNSColor(
        message_center::kButtonSeparatorColor)];
    y += NSHeight(separatorFrame);
    frame.size.height += NSHeight(separatorFrame);
    [bottomView_ addSubview:separator];
  }

  // Create the image view if appropriate.
  gfx::Image notificationImage = notification->image();
  if (!notificationImage.IsEmpty()) {
    NSBox* imageBox = [self createImageBox:notificationImage];
    NSRect outerFrame = frame;
    outerFrame.origin = NSMakePoint(0, y);
    outerFrame.size = [imageBox frame].size;
    [imageBox setFrame:outerFrame];

    y += NSHeight(outerFrame);
    frame.size.height += NSHeight(outerFrame);

    [bottomView_ addSubview:imageBox];
  }

  [bottomView_ setFrame:frame];
  [[self view] addSubview:bottomView_];
  [self adjustFrameHeight:&layoutParams delta:NSHeight(frame)];

  // Make sure that there is a minimum amount of spacing below the icon and
  // the edge of the frame.
  CGFloat bottomDelta =
      NSHeight(layoutParams.rootFrame) - NSHeight([icon_ frame]);
  if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
    CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
    [self adjustFrameHeight:&layoutParams delta:bottomAdjust];
  }

  [[self view] setFrame:layoutParams.rootFrame];
  [title_ setFrame:layoutParams.titleFrame];
  [message_ setFrame:layoutParams.messageFrame];
  [contextMessage_ setFrame:layoutParams.contextMessageFrame];
  [settingsButton_ setFrame:layoutParams.settingsButtonFrame];
  [listView_ setFrame:layoutParams.listFrame];
  [progressBarView_ setFrame:layoutParams.progressBarFrame];

  return layoutParams.rootFrame;
}

- (void)close:(id)sender {
  [closeButton_ setTarget:nil];
  messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
}

- (void)settingsClicked:(id)sender {
  [NSApp activateIgnoringOtherApps:YES];
  messageCenter_->ClickOnSettingsButton([self notificationID]);
}

- (void)buttonClicked:(id)button {
  messageCenter_->ClickOnNotificationButton([self notificationID],
                                            [button tag]);
}

- (const message_center::Notification*)notification {
  return notification_;
}

- (const std::string&)notificationID {
  return notificationID_;
}

- (void)notificationClicked {
  messageCenter_->ClickOnNotification([self notificationID]);
}

- (void)adjustFrameHeight:(message_center::NotificationLayoutParams*)frames
                    delta:(CGFloat)delta {
  frames->rootFrame.size.height += delta;
  frames->titleFrame.origin.y += delta;
  frames->messageFrame.origin.y += delta;
  frames->contextMessageFrame.origin.y += delta;
  frames->settingsButtonFrame.origin.y += delta;
  frames->listFrame.origin.y += delta;
  frames->progressBarFrame.origin.y += delta;
}

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

- (void)configureCustomBox:(NSBox*)box {
  [box setBoxType:NSBoxCustom];
  [box setBorderType:NSNoBorder];
  [box setTitlePosition:NSNoTitle];
  [box setContentViewMargins:NSZeroSize];
}

- (NSView*)createIconView {
  // Create another box that shows a background color when the icon is not
  // big enough to fill the space.
  NSRect imageFrame = NSMakeRect(0, 0,
       message_center::kNotificationIconSize,
       message_center::kNotificationIconSize);
  base::scoped_nsobject<NSBox> imageBox(
      [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
  [self configureCustomBox:imageBox];
  [imageBox setAutoresizingMask:NSViewMinYMargin];

  // Inside the image box put the actual icon view.
  icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
  [imageBox setContentView:icon_];

  return imageBox.autorelease();
}

- (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
  using message_center::kNotificationImageBorderSize;
  using message_center::kNotificationPreferredImageWidth;
  using message_center::kNotificationPreferredImageHeight;

  NSRect imageFrame = NSMakeRect(0, 0,
       kNotificationPreferredImageWidth,
       kNotificationPreferredImageHeight);
  base::scoped_nsobject<NSBox> imageBox(
      [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
  [self configureCustomBox:imageBox];
  [imageBox setFillColor:skia::SkColorToCalibratedNSColor(
      message_center::kImageBackgroundColor)];

  // Images with non-preferred aspect ratios get a border on all sides.
  gfx::Size idealSize = gfx::Size(
      kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
  gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
      idealSize, notificationImage.Size());
  if (scaledSize != idealSize) {
    NSSize borderSize =
        NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
    [imageBox setContentViewMargins:borderSize];
  }

  NSImage* image = notificationImage.AsNSImage();
  base::scoped_nsobject<NSImageView> imageView(
      [[NSImageView alloc] initWithFrame:imageFrame]);
  [imageView setImage:image];
  [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
  [imageBox setContentView:imageView];

  return imageBox.autorelease();
}

- (void)configureCloseButtonInFrame:(NSRect)rootFrame {
  // The close button is configured to be the same size as the small image.
  int closeButtonOriginOffset =
      message_center::kSmallImageSize + message_center::kSmallImagePadding;
  NSRect closeButtonFrame =
      NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
                 NSMaxY(rootFrame) - closeButtonOriginOffset,
                 message_center::kSmallImageSize,
                 message_center::kSmallImageSize);
  closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  [closeButton_ setDefaultImage:
      rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
  [closeButton_ setHoverImage:
      rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
  [closeButton_ setPressedImage:
      rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
  [[closeButton_ cell] setHighlightsBy:NSOnState];
  [closeButton_ setTrackingEnabled:YES];
  [closeButton_ setBordered:NO];
  [closeButton_ setAutoresizingMask:NSViewMinYMargin];
  [closeButton_ setTarget:self];
  [closeButton_ setAction:@selector(close:)];
  [closeButton_ setDisableActivationOnClick:YES];
  [[closeButton_ cell]
      accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
                       forAttribute:NSAccessibilitySubroleAttribute];
  [[closeButton_ cell]
      accessibilitySetOverrideValue:
          l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
                       forAttribute:NSAccessibilityTitleAttribute];
}

- (void)configureSettingsButtonInFrame:(NSRect)rootFrame {
  // The settings button is configured to be the same size as the small image.
  int settingsButtonOriginOffset =
      message_center::kSmallImageSize + message_center::kSmallImagePadding;
  NSRect settingsButtonFrame = NSMakeRect(
      NSMaxX(rootFrame) - settingsButtonOriginOffset,
      message_center::kSmallImagePadding, message_center::kSmallImageSize,
      message_center::kSmallImageSize);

  settingsButton_.reset(
      [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  [settingsButton_ setDefaultImage:rb.GetNativeImageNamed(
                                         IDR_NOTIFICATION_SETTINGS_BUTTON_ICON)
                                       .ToNSImage()];
  [settingsButton_
      setHoverImage:rb.GetNativeImageNamed(
                          IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_HOVER)
                        .ToNSImage()];
  [settingsButton_
      setPressedImage:rb.GetNativeImageNamed(
                            IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_PRESSED)
                          .ToNSImage()];
  [[settingsButton_ cell] setHighlightsBy:NSOnState];
  [settingsButton_ setTrackingEnabled:YES];
  [settingsButton_ setBordered:NO];
  [settingsButton_ setAutoresizingMask:NSViewMinYMargin];
  [settingsButton_ setTarget:self];
  [settingsButton_ setAction:@selector(settingsClicked:)];
  [[settingsButton_ cell]
      accessibilitySetOverrideValue:
          l10n_util::GetNSString(
              IDS_MESSAGE_NOTIFICATION_SETTINGS_BUTTON_ACCESSIBLE_NAME)
                       forAttribute:NSAccessibilityTitleAttribute];
}

- (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
  int smallImageXOffset =
      message_center::kSmallImagePadding + message_center::kSmallImageSize;
  NSRect boxFrame =
      NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
                 NSMinY(rootFrame) + message_center::kSmallImagePadding,
                 message_center::kSmallImageSize,
                 message_center::kSmallImageSize);

  // Put the smallImage inside another box which can hide it from accessibility
  // until we have some alt text to go with it.  Once we have alt text, remove
  // the box, and set NSAccessibilityDescriptionAttribute with it.
  base::scoped_nsobject<NSBox> imageBox(
      [[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]);
  [self configureCustomBox:imageBox];
  [imageBox setAutoresizingMask:NSViewMinYMargin];

  NSRect smallImageFrame =
      NSMakeRect(0,0,
                 message_center::kSmallImageSize,
                 message_center::kSmallImageSize);

  smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
  [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
  [imageBox setContentView:smallImage_];

  return imageBox.autorelease();
}

- (void)configureTitleInFrame:(NSRect)contentFrame {
  contentFrame.size.height = 0;
  title_.reset([self newLabelWithFrame:contentFrame]);
  [title_ setAutoresizingMask:NSViewMinYMargin];
  [title_ setTextColor:skia::SkColorToCalibratedNSColor(
      message_center::kRegularTextColor)];
  [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
}

- (void)configureBodyInFrame:(NSRect)contentFrame {
  contentFrame.size.height = 0;
  message_.reset([self newLabelWithFrame:contentFrame]);
  [message_ setAutoresizingMask:NSViewMinYMargin];
  [message_ setTextColor:skia::SkColorToCalibratedNSColor(
      message_center::kRegularTextColor)];
  [message_ setFont:
      [NSFont messageFontOfSize:message_center::kMessageFontSize]];
}

- (void)configureContextMessageInFrame:(NSRect)contentFrame {
  contentFrame.size.height = 0;
  contextMessage_.reset([self newLabelWithFrame:contentFrame]);
  [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
  [contextMessage_ setTextColor:skia::SkColorToCalibratedNSColor(
      message_center::kDimTextColor)];
  [contextMessage_ setFont:
      [NSFont messageFontOfSize:message_center::kMessageFontSize]];
}

- (NSTextView*)newLabelWithFrame:(NSRect)frame {
  NSTextView* label = [[NSTextView alloc] initWithFrame:frame];

  // The labels MUST draw their background so that subpixel antialiasing can
  // happen on the text.
  [label setDrawsBackground:YES];
  [label setBackgroundColor:skia::SkColorToCalibratedNSColor(
      message_center::kNotificationBackgroundColor)];

  [label setEditable:NO];
  [label setSelectable:NO];
  [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
  [[label textContainer] setLineFragmentPadding:0.0f];
  return label;
}

- (NSRect)currentContentRect {
  DCHECK(icon_);
  DCHECK(closeButton_);
  DCHECK(smallImage_);

  NSRect iconFrame, contentFrame;
  NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
      NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
      NSMinXEdge);
  // The content area is between the icon on the left and the control area
  // on the right.
  int controlAreaWidth =
      std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
  contentFrame.size.width -=
      2 * message_center::kSmallImagePadding + controlAreaWidth;
  return contentFrame;
}

- (base::string16)wrapText:(const base::string16&)text
                   forFont:(NSFont*)nsfont
          maxNumberOfLines:(size_t)lines
               actualLines:(size_t*)actualLines {
  *actualLines = 0;
  if (text.empty() || lines == 0)
    return base::string16();
  gfx::FontList font_list((gfx::Font(nsfont)));
  int width = NSWidth([self currentContentRect]);
  int height = (lines + 1) * font_list.GetHeight();

  std::vector<base::string16> wrapped;
  gfx::ElideRectangleText(text, font_list, width, height,
                          gfx::WRAP_LONG_WORDS, &wrapped);

  // This could be possible when the input text contains only spaces.
  if (wrapped.empty())
    return base::string16();

  if (wrapped.size() > lines) {
    // Add an ellipsis to the last line. If this ellipsis makes the last line
    // too wide, that line will be further elided by the gfx::ElideText below.
    base::string16 last =
        wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
    if (gfx::GetStringWidth(last, font_list) > width)
      last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL);
    wrapped.resize(lines - 1);
    wrapped.push_back(last);
  }

  *actualLines = wrapped.size();
  return lines == 1 ? wrapped[0]
                    : base::JoinString(wrapped, base::ASCIIToUTF16("\n"));
}

- (base::string16)wrapText:(const base::string16&)text
                   forFont:(NSFont*)nsfont
          maxNumberOfLines:(size_t)lines {
  size_t unused;
  return [self wrapText:text
                forFont:nsfont
       maxNumberOfLines:lines
            actualLines:&unused];
}

@end
