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

#include "app/l10n_util.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/blocked_content_container.h"
#import "chrome/browser/cocoa/content_settings_dialog_controller.h"
#import "chrome/browser/cocoa/hyperlink_button_cell.h"
#import "chrome/browser/cocoa/info_bubble_view.h"
#import "chrome/browser/cocoa/l10n_util.h"
#include "chrome/browser/content_setting_bubble_model.h"
#include "chrome/browser/host_content_settings_map.h"
#include "chrome/browser/plugin_updater.h"
#include "chrome/common/chrome_switches.h"
#include "grit/generated_resources.h"
#include "skia/ext/skia_utils_mac.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#include "webkit/glue/plugins/plugin_list.h"

namespace {

// Must match the tag of the unblock radio button in the xib files.
const int kAllowTag = 1;

// Must match the tag of the block radio button in the xib files.
const int kBlockTag = 2;

// Height of one link in the popup list.
const int kLinkHeight = 16;

// Space between two popup links.
const int kLinkPadding = 4;

// Space taken in total by one popup link.
const int kLinkLineHeight = kLinkHeight + kLinkPadding;

// Space between popup list and surrounding UI elements.
const int kLinkOuterPadding = 8;

// Height of each of the labels in the geolocation bubble.
const int kGeoLabelHeight = 14;

// Height of the "Clear" button in the geolocation bubble.
const int kGeoClearButtonHeight = 17;

// Padding between radio buttons and "Load all plugins" button
// in the plugin bubble.
const int kLoadAllPluginsButtonVerticalPadding = 8;

// General padding between elements in the geolocation bubble.
const int kGeoPadding = 8;

// Padding between host names in the geolocation bubble.
const int kGeoHostPadding = 4;

// Minimal padding between "Manage" and "Done" buttons.
const int kManageDonePadding = 8;

void SetControlSize(NSControl* control, NSControlSize controlSize) {
  CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize];
  NSCell* cell = [control cell];
  NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize];
  [cell setFont:font];
  [cell setControlSize:controlSize];
}

// Returns an autoreleased NSTextField that is configured to look like a Label
// looks in Interface Builder.
NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) {
  NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
  [label setStringValue:text];
  [label setSelectable:NO];
  [label setBezeled:NO];
  return [label autorelease];
}

}  // namespace

@interface ContentSettingBubbleController(Private)
- (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel
       parentWindow:(NSWindow*)parentWindow
         anchoredAt:(NSPoint)anchoredAt;
- (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
                                title:(NSString*)title
                                 icon:(NSImage*)icon
                       referenceFrame:(NSRect)referenceFrame;
- (void)initializeBlockedPluginsList;
- (void)initializeTitle;
- (void)initializeRadioGroup;
- (void)initializePopupList;
- (void)initializeGeoLists;
- (void)sizeToFitLoadPluginsButton;
- (void)sizeToFitManageDoneButtons;
- (void)removeInfoButton;
- (void)popupLinkClicked:(id)sender;
- (void)clearGeolocationForCurrentHost:(id)sender;
@end

@implementation ContentSettingBubbleController

+ (ContentSettingBubbleController*)
    showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
    parentWindow:(NSWindow*)parentWindow
      anchoredAt:(NSPoint)anchor {
  // Autoreleases itself on bubble close.
  return [[ContentSettingBubbleController alloc]
             initWithModel:contentSettingBubbleModel
              parentWindow:parentWindow
                anchoredAt:anchor];
}

- (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
       parentWindow:(NSWindow*)parentWindow
         anchoredAt:(NSPoint)anchoredAt {
  // This method takes ownership of |contentSettingBubbleModel| in all cases.
  scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
  DCHECK(model.get());

  NSString* const nibPaths[] = {
    @"ContentBlockedCookies",
    @"ContentBlockedImages",
    @"ContentBlockedJavaScript",
    @"ContentBlockedPlugins",
    @"ContentBlockedPopups",
    @"ContentBubbleGeolocation",
    @"",  // Notifications do not have a bubble.
  };
  COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES,
                 nibPaths_requires_an_entry_for_every_setting_type);
  const int settingsType = model->content_type();
  // Nofifications do not have a bubble.
  CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
  DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES);
  if ((self = [super initWithWindowNibPath:nibPaths[settingsType]
                              parentWindow:parentWindow
                                anchoredAt:anchoredAt])) {
    contentSettingBubbleModel_.reset(model.release());
    [self showWindow:nil];
  }
  return self;
}

- (void)initializeTitle {
  if (!titleLabel_)
    return;

  NSString* label = base::SysUTF8ToNSString(
      contentSettingBubbleModel_->bubble_content().title);
  [titleLabel_ setStringValue:label];

  // Layout title post-localization.
  CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:titleLabel_];
  NSRect windowFrame = [[self window] frame];
  windowFrame.size.height += deltaY;
  [[self window] setFrame:windowFrame display:NO];
  NSRect titleFrame = [titleLabel_ frame];
  titleFrame.origin.y -= deltaY;
  [titleLabel_ setFrame:titleFrame];
}

- (void)initializeRadioGroup {
  // Configure the radio group. For now, only deal with the
  // strictly needed case of group containing 2 radio buttons.
  const ContentSettingBubbleModel::RadioGroup& radio_group =
      contentSettingBubbleModel_->bubble_content().radio_group;

  // Select appropriate radio button.
  [allowBlockRadioGroup_ selectCellWithTag:
      radio_group.default_item == 0 ? kAllowTag : kBlockTag];

  const ContentSettingBubbleModel::RadioItems& radio_items =
      radio_group.radio_items;
  DCHECK_EQ(2u, radio_items.size()) << "Only 2 radio items per group supported";
  // Set radio group labels from model.
  NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag];
  [radioCell setTitle:base::SysUTF8ToNSString(radio_items[0])];

  radioCell = [allowBlockRadioGroup_ cellWithTag:kBlockTag];
  [radioCell setTitle:base::SysUTF8ToNSString(radio_items[1])];

  // Layout radio group labels post-localization.
  [GTMUILocalizerAndLayoutTweaker
      wrapRadioGroupForWidth:allowBlockRadioGroup_];
  CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
      sizeToFitView:allowBlockRadioGroup_].height;
  NSRect windowFrame = [[self window] frame];
  windowFrame.size.height += radioDeltaY;
  [[self window] setFrame:windowFrame display:NO];
}

- (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
                                title:(NSString*)title
                                 icon:(NSImage*)icon
                       referenceFrame:(NSRect)referenceFrame {
  scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc]
      initTextCell:title]);
  [cell.get() setAlignment:NSNaturalTextAlignment];
  if (icon) {
    [cell.get() setImagePosition:NSImageLeft];
    [cell.get() setImage:icon];
  } else {
    [cell.get() setImagePosition:NSNoImage];
  }
  [cell.get() setControlSize:NSSmallControlSize];

  NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
  // Cell must be set immediately after construction.
  [button setCell:cell.get()];

  // If the link text is too long, clamp it.
  [button sizeToFit];
  int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
  NSRect buttonFrame = [button frame];
  if (NSWidth(buttonFrame) > maxWidth) {
    buttonFrame.size.width = maxWidth;
    [button setFrame:buttonFrame];
  }

  [button setTarget:self];
  [button setAction:@selector(popupLinkClicked:)];
  return button;
}

- (void)initializeBlockedPluginsList {
  NSMutableArray* pluginArray = [NSMutableArray array];
  const std::set<std::string>& plugins =
      contentSettingBubbleModel_->bubble_content().resource_identifiers;
  if (plugins.empty()) {
    int delta = NSMinY([titleLabel_ frame]) -
                NSMinY([blockedResourcesField_ frame]);
    [blockedResourcesField_ removeFromSuperview];
    NSRect frame = [[self window] frame];
    frame.size.height -= delta;
    [[self window] setFrame:frame display:NO];
  } else {
    for (std::set<std::string>::iterator it = plugins.begin();
         it != plugins.end(); ++it) {
      NSString* name;
      NPAPI::PluginList::PluginMap groups;
      NPAPI::PluginList::Singleton()->GetPluginGroups(false, &groups);
      if (groups.find(*it) != groups.end())
        name = base::SysUTF16ToNSString(groups[*it]->GetGroupName());
      else
        name = base::SysUTF8ToNSString(*it);
      [pluginArray addObject:name];
    }
    [blockedResourcesField_
        setStringValue:[pluginArray componentsJoinedByString:@"\n"]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:blockedResourcesField_];
  }
}

- (void)initializePopupList {
  // I didn't put the buttons into a NSMatrix because then they are only one
  // entity in the key view loop. This way, one can tab through all of them.
  const ContentSettingBubbleModel::PopupItems& popupItems =
      contentSettingBubbleModel_->bubble_content().popup_items;

  // Get the pre-resize frame of the radio group. Its origin is where the
  // popup list should go.
  NSRect radioFrame = [allowBlockRadioGroup_ frame];

  // Make room for the popup list. The bubble view and its subviews autosize
  // themselves when the window is enlarged.
  // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
  // so only 1 * kLinkOuterPadding more is needed.
  int delta = popupItems.size() * kLinkLineHeight - kLinkPadding +
              kLinkOuterPadding;
  NSSize deltaSize = NSMakeSize(0, delta);
  deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
  NSRect windowFrame = [[self window] frame];
  windowFrame.size.height += deltaSize.height;
  [[self window] setFrame:windowFrame display:NO];

  // Create popup list.
  int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
  int row = 0;
  for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
       it(popupItems.begin()); it != popupItems.end(); ++it, ++row) {
    const SkBitmap& icon = it->bitmap;
    NSImage* image = nil;
    if (!icon.empty())
      image = gfx::SkBitmapToNSImage(icon);

    std::string title(it->title);
    // The popup may not have committed a load yet, in which case it won't
    // have a URL or title.
    if (title.empty())
      title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE);

    NSRect linkFrame =
        NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row,
                   200, kLinkHeight);
    NSButton* button = [self
        hyperlinkButtonWithFrame:linkFrame
                           title:base::SysUTF8ToNSString(title)
                            icon:image
                  referenceFrame:radioFrame];
    [[self bubble] addSubview:button];
    popupLinks_[button] = row;
  }
}

- (void)initializeGeoLists {
  // Cocoa has its origin in the lower left corner. This means elements are
  // added from bottom to top, which explains why loops run backwards and the
  // order of operations is the other way than on Linux/Windows.
  const ContentSettingBubbleModel::BubbleContent& content =
      contentSettingBubbleModel_->bubble_content();
  NSRect containerFrame = [contentsContainer_ frame];
  NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);

  // "Clear" button.
  if (!content.clear_link.empty()) {
    NSRect buttonFrame = NSMakeRect(0, 0,
                                    NSWidth(containerFrame),
                                    kGeoClearButtonHeight);
    scoped_nsobject<NSButton> button([[NSButton alloc]
                                      initWithFrame:buttonFrame]);
    [button setTitle:base::SysUTF8ToNSString(content.clear_link)];
    [button setTarget:self];
    [button setAction:@selector(clearGeolocationForCurrentHost:)];
    [button setBezelStyle:NSRoundRectBezelStyle];
    SetControlSize(button, NSSmallControlSize);
    [button sizeToFit];

    // If the button is wider than the container, widen the window.
    CGFloat buttonWidth = NSWidth([button frame]);
    if (buttonWidth > NSWidth(containerFrame)) {
      NSRect windowFrame = [[self window] frame];
      windowFrame.size.width += buttonWidth - NSWidth(containerFrame);
      [[self window] setFrame:windowFrame display:NO];
      // Fetch the updated sizes.
      containerFrame = [contentsContainer_ frame];
      frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
    }

    // Add the button.
    [contentsContainer_ addSubview:button];

    frame.origin.y = NSMaxY([button frame]) + kGeoPadding;
  }

  typedef
      std::vector<ContentSettingBubbleModel::DomainList>::const_reverse_iterator
      GeolocationGroupIterator;
  for (GeolocationGroupIterator i = content.domain_lists.rbegin();
       i != content.domain_lists.rend(); ++i) {
    // Add all hosts in the current domain list.
    for (std::set<std::string>::const_reverse_iterator j = i->hosts.rbegin();
         j != i->hosts.rend(); ++j) {
      NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
      SetControlSize(title, NSSmallControlSize);
      [contentsContainer_ addSubview:title];

      frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
          [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
    }
    if (!i->hosts.empty())
      frame.origin.y += kGeoPadding - kGeoHostPadding;

    // Add the domain list's title.
    NSTextField* title =
        LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
    SetControlSize(title, NSSmallControlSize);
    [contentsContainer_ addSubview:title];

    frame.origin.y = NSMaxY(frame) + kGeoPadding +
        [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
  }

  CGFloat containerHeight = frame.origin.y;
  // Undo last padding.
  if (!content.domain_lists.empty())
    containerHeight -= kGeoPadding;

  // Resize container to fit its subviews, and window to fit the container.
  NSRect windowFrame = [[self window] frame];
  windowFrame.size.height += containerHeight - NSHeight(containerFrame);
  [[self window] setFrame:windowFrame display:NO];
  containerFrame.size.height = containerHeight;
  [contentsContainer_ setFrame:containerFrame];
}

- (void)sizeToFitLoadPluginsButton {
  if (!CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kDisableClickToPlay)) {
    const ContentSettingBubbleModel::BubbleContent& content =
        contentSettingBubbleModel_->bubble_content();
    [loadAllPluginsButton_ setEnabled:content.load_plugins_link_enabled];

    // Resize horizontally to fit button if necessary.
    NSRect windowFrame = [[self window] frame];
    int widthNeeded = NSWidth([loadAllPluginsButton_ frame]) +
        2 * NSMinX([loadAllPluginsButton_ frame]);
    if (NSWidth(windowFrame) < widthNeeded) {
      windowFrame.size.width = widthNeeded;
      [[self window] setFrame:windowFrame display:NO];
    }
  } else {
    // Remove button and resize vertically.
    int deltaY = kLoadAllPluginsButtonVerticalPadding +
        NSHeight([loadAllPluginsButton_ frame]);
    [loadAllPluginsButton_ removeFromSuperview];
    NSRect frame = [[self window] frame];
    frame.size.height -= deltaY;
    [[self window] setFrame:frame display:NO];
    NSPoint radioOrigin = [allowBlockRadioGroup_ frame].origin;
    radioOrigin.y -= deltaY;
    [allowBlockRadioGroup_ setFrameOrigin:radioOrigin];
    [allowBlockRadioGroup_ setNeedsDisplay];
  }
}

- (void)sizeToFitManageDoneButtons {
  CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
  CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
      NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
  if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
    return;

  // Resize window, autoresizing takes care of the rest.
  NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
  size = [[[self window] contentView] convertSize:size toView:nil];
  NSRect frame = [[self window] frame];
  frame.origin.x -= size.width;
  frame.size.width += size.width;
  [[self window] setFrame:frame display:NO];
}

- (void)awakeFromNib {
  [[self bubble] setBubbleType:info_bubble::kWhiteInfoBubble];
  [[self bubble] setArrowLocation:info_bubble::kTopRight];

  // Adapt window size to bottom buttons. Do this before all other layouting.
  [self sizeToFitManageDoneButtons];

  [self initializeTitle];

  ContentSettingsType type = contentSettingBubbleModel_->content_type();
  if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
    [self sizeToFitLoadPluginsButton];
    [self initializeBlockedPluginsList];
  }
  if (allowBlockRadioGroup_)  // not bound in cookie bubble xib
    [self initializeRadioGroup];

  if (type == CONTENT_SETTINGS_TYPE_POPUPS)
    [self initializePopupList];
  if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
    [self initializeGeoLists];
}

///////////////////////////////////////////////////////////////////////////////
// Actual application logic

- (IBAction)allowBlockToggled:(id)sender {
  NSButtonCell *selectedCell = [sender selectedCell];
  contentSettingBubbleModel_->OnRadioClicked(
      [selectedCell tag] == kAllowTag ? 0 : 1);
}

- (IBAction)closeBubble:(id)sender {
  [self close];
}

- (IBAction)manageBlocking:(id)sender {
  contentSettingBubbleModel_->OnManageLinkClicked();
}

- (IBAction)showMoreInfo:(id)sender {
  contentSettingBubbleModel_->OnInfoLinkClicked();
  [self close];
}

- (IBAction)loadAllPlugins:(id)sender {
  contentSettingBubbleModel_->OnLoadPluginsLinkClicked();
  [self close];
}

- (void)popupLinkClicked:(id)sender {
  content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
  DCHECK(i != popupLinks_.end());
  contentSettingBubbleModel_->OnPopupClicked(i->second);
}

- (void)clearGeolocationForCurrentHost:(id)sender {
  contentSettingBubbleModel_->OnClearLinkClicked();
  [self close];
}

@end  // ContentSettingBubbleController