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

#include "app/l10n_util_mac.h"
#include "app/table_model_observer.h"
#import "base/mac_util.h"
#import "base/scoped_nsobject.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/content_exceptions_table_model.h"
#include "chrome/common/notification_registrar.h"
#include "chrome/common/notification_service.h"
#include "grit/generated_resources.h"
#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"

@interface ContentExceptionsWindowController (Private)
- (id)initWithType:(ContentSettingsType)settingsType
       settingsMap:(HostContentSettingsMap*)settingsMap;
- (void)updateRow:(NSInteger)row
        withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry;
- (void)adjustEditingButtons;
- (void)modelDidChange;
- (size_t)menuItemCount;
- (NSString*)titleForIndex:(size_t)index;
- (ContentSetting)settingForIndex:(size_t)index;
- (size_t)indexForSetting:(ContentSetting)setting;
@end

////////////////////////////////////////////////////////////////////////////////
// PatternFormatter

// A simple formatter that accepts text that vaguely looks like a pattern.
@interface PatternFormatter : NSFormatter
@end

@implementation PatternFormatter
- (NSString*)stringForObjectValue:(id)object {
  if (![object isKindOfClass:[NSString class]])
    return nil;
  return object;
}

- (BOOL)getObjectValue:(id*)object
             forString:(NSString*)string
      errorDescription:(NSString**)error {
  if ([string length]) {
      if (HostContentSettingsMap::Pattern(
          base::SysNSStringToUTF8(string)).IsValid()) {
      *object = string;
      return YES;
    }
  }
  if (error)
    *error = @"Invalid pattern";
  return NO;
}

- (NSAttributedString*)attributedStringForObjectValue:(id)object
                                withDefaultAttributes:(NSDictionary*)attribs {
  return nil;
}
@end

////////////////////////////////////////////////////////////////////////////////
// UpdatingContentSettingsObserver

// UpdatingContentSettingsObserver is a notification observer that tells a
// window controller to update its data on every notification.
class UpdatingContentSettingsObserver : public NotificationObserver {
 public:
  UpdatingContentSettingsObserver(ContentExceptionsWindowController* controller)
      : controller_(controller) {
    // One would think one could register a TableModelObserver to be notified of
    // changes to ContentExceptionsTableModel. One would be wrong: The table
    // model only sends out changes that are made through the model, not for
    // changes made directly to its backing HostContentSettings object (that
    // happens e.g. if the user uses the cookie confirmation dialog). Hence,
    // observe the CONTENT_SETTINGS_CHANGED notification directly.
    registrar_.Add(this, NotificationType::CONTENT_SETTINGS_CHANGED,
                   NotificationService::AllSources());
  }
  virtual void Observe(NotificationType type,
                       const NotificationSource& source,
                       const NotificationDetails& details);
 private:
  NotificationRegistrar registrar_;
  ContentExceptionsWindowController* controller_;
};

void UpdatingContentSettingsObserver::Observe(
    NotificationType type,
    const NotificationSource& source,
    const NotificationDetails& details) {
  [controller_ modelDidChange];
}

////////////////////////////////////////////////////////////////////////////////
// Static functions

namespace  {

NSString* GetWindowTitle(ContentSettingsType settingsType) {
  switch (settingsType) {
    case CONTENT_SETTINGS_TYPE_COOKIES:
      return l10n_util::GetNSStringWithFixup(IDS_COOKIE_EXCEPTION_TITLE);
    case CONTENT_SETTINGS_TYPE_IMAGES:
      return l10n_util::GetNSStringWithFixup(IDS_IMAGES_EXCEPTION_TITLE);
    case CONTENT_SETTINGS_TYPE_JAVASCRIPT:
      return l10n_util::GetNSStringWithFixup(IDS_JS_EXCEPTION_TITLE);
    case CONTENT_SETTINGS_TYPE_PLUGINS:
      return l10n_util::GetNSStringWithFixup(IDS_PLUGINS_EXCEPTION_TITLE);
    case CONTENT_SETTINGS_TYPE_POPUPS:
      return l10n_util::GetNSStringWithFixup(IDS_POPUP_EXCEPTION_TITLE);
    default:
      NOTREACHED();
  }
  return @"";
}

const CGFloat kButtonBarHeight = 35.0;

// The settings shown in the combobox if showAsk_ is false;
const ContentSetting kNoAskSettings[] = { CONTENT_SETTING_ALLOW,
                                          CONTENT_SETTING_BLOCK };

// The settings shown in the combobox if showAsk_ is true;
const ContentSetting kAskSettings[] = { CONTENT_SETTING_ALLOW,
                                        CONTENT_SETTING_ASK,
                                        CONTENT_SETTING_BLOCK };

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ContentExceptionsWindowController implementation

static ContentExceptionsWindowController*
    g_exceptionWindows[CONTENT_SETTINGS_NUM_TYPES] = { nil };

@implementation ContentExceptionsWindowController

+ (id)controllerForType:(ContentSettingsType)settingsType
            settingsMap:(HostContentSettingsMap*)settingsMap {
  if (!g_exceptionWindows[settingsType]) {
    g_exceptionWindows[settingsType] =
        [[ContentExceptionsWindowController alloc] initWithType:settingsType
                                                    settingsMap:settingsMap];
  }
  return g_exceptionWindows[settingsType];
}

- (id)initWithType:(ContentSettingsType)settingsType
       settingsMap:(HostContentSettingsMap*)settingsMap {
  NSString* nibpath =
      [mac_util::MainAppBundle() pathForResource:@"ContentExceptionsWindow"
                                          ofType:@"nib"];
  if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
    settingsType_ = settingsType;
    settingsMap_ = settingsMap;
    model_.reset(new ContentExceptionsTableModel(settingsMap_, settingsType_));
    showAsk_ = settingsType_ == CONTENT_SETTINGS_TYPE_COOKIES;
    tableObserver_.reset(new UpdatingContentSettingsObserver(self));
    updatesEnabled_ = YES;

    // TODO(thakis): autoremember window rect.
    // TODO(thakis): sorting support.
  }
  return self;
}

- (void)awakeFromNib {
  DCHECK([self window]);
  DCHECK_EQ(self, [[self window] delegate]);
  DCHECK(tableView_);
  DCHECK_EQ(self, [tableView_ dataSource]);
  DCHECK_EQ(self, [tableView_ delegate]);

  [[self window] setTitle:GetWindowTitle(settingsType_)];

  CGFloat minWidth = [[addButton_ superview] bounds].size.width +
                     [[doneButton_ superview] bounds].size.width;
  [self setMinWidth:minWidth];

  [self adjustEditingButtons];

  // Initialize menu for the data cell in the "action" column.
  scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"exceptionMenu"]);
  for (size_t i = 0; i < [self menuItemCount]; ++i) {
    scoped_nsobject<NSMenuItem> allowItem([[NSMenuItem alloc]
        initWithTitle:[self titleForIndex:i] action:NULL keyEquivalent:@""]);
    [allowItem.get() setTag:[self settingForIndex:i]];
    [menu.get() addItem:allowItem.get()];
  }
  NSCell* menuCell =
      [[tableView_ tableColumnWithIdentifier:@"action"] dataCell];
  [menuCell setMenu:menu.get()];

  NSCell* patternCell =
      [[tableView_ tableColumnWithIdentifier:@"pattern"] dataCell];
  [patternCell setFormatter:[[[PatternFormatter alloc] init] autorelease]];
}

- (void)setMinWidth:(CGFloat)minWidth {
  NSWindow* window = [self window];
  [window setMinSize:NSMakeSize(minWidth, [window minSize].height)];
  if ([window frame].size.width < minWidth) {
    NSRect frame = [window frame];
    frame.size.width = minWidth;
    [window setFrame:frame display:NO];
  }
}

- (void)windowWillClose:(NSNotification*)notification {
  // Without this, some of the unit tests fail on 10.6:
  [tableView_ setDataSource:nil];

  g_exceptionWindows[settingsType_] = nil;
  [self autorelease];
}

- (BOOL)editingNewException {
  return newException_.get() != NULL;
}

// Let esc cancel editing if the user is currently editing a pattern. Else, let
// esc close the window.
- (void)cancel:(id)sender {
  if ([tableView_ currentEditor] != nil) {
    [tableView_ abortEditing];
    [[self window] makeFirstResponder:tableView_];  // Re-gain focus.

    if ([tableView_ selectedRow] == model_->RowCount()) {
      // Cancel addition of new row.
      [self removeException:self];
    }
  } else {
    [self closeSheet:self];
  }
}

- (void)keyDown:(NSEvent*)event {
  NSString* chars = [event charactersIgnoringModifiers];
  if ([chars length] == 1) {
    switch ([chars characterAtIndex:0]) {
      case NSDeleteCharacter:
      case NSDeleteFunctionKey:
        // Delete deletes.
        if ([[tableView_ selectedRowIndexes] count] > 0)
          [self removeException:self];
        return;
      case NSCarriageReturnCharacter:
      case NSEnterCharacter:
        // Return enters rename mode.
        if ([[tableView_ selectedRowIndexes] count] == 1) {
          [tableView_ editColumn:0
                             row:[[tableView_ selectedRowIndexes] lastIndex]
                       withEvent:nil
                          select:YES];
        }
        return;
    }
  }
  [super keyDown:event];
}

- (void)attachSheetTo:(NSWindow*)window {
  [NSApp beginSheet:[self window]
     modalForWindow:window
      modalDelegate:self
     didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
        contextInfo:nil];
}

- (void)sheetDidEnd:(NSWindow*)sheet
         returnCode:(NSInteger)returnCode
        contextInfo:(void*)context {
  [sheet close];
  [sheet orderOut:self];
}

- (IBAction)addException:(id)sender {
  if (newException_.get()) {
    // The invariant is that |newException_| is non-NULL exactly if the pattern
    // of a new exception is currently being edited - so there's nothing to do
    // in that case.
    return;
  }
  newException_.reset(new HostContentSettingsMap::PatternSettingPair);
  newException_->first = HostContentSettingsMap::Pattern(
      l10n_util::GetStringUTF8(IDS_EXCEPTIONS_SAMPLE_PATTERN));
  newException_->second = CONTENT_SETTING_BLOCK;
  [tableView_ reloadData];

  [self adjustEditingButtons];
  int index = model_->RowCount();
  NSIndexSet* selectedSet = [NSIndexSet indexSetWithIndex:index];
  [tableView_ selectRowIndexes:selectedSet byExtendingSelection:NO];
  [tableView_ editColumn:0 row:index withEvent:nil select:YES];
}

- (IBAction)removeException:(id)sender {
  updatesEnabled_ = NO;
  NSIndexSet* selection = [tableView_ selectedRowIndexes];
  [tableView_ deselectAll:self];  // Else we'll get a -setObjectValue: later.
  DCHECK_GT([selection count], 0U);
  NSUInteger index = [selection lastIndex];
  while (index != NSNotFound) {
    if (index == static_cast<NSUInteger>(model_->RowCount()))
      newException_.reset();
    else
      model_->RemoveException(index);
    index = [selection indexLessThanIndex:index];
  }
  updatesEnabled_ = YES;
  [self modelDidChange];
}

- (IBAction)removeAllExceptions:(id)sender {
  updatesEnabled_ = NO;
  [tableView_ deselectAll:self];  // Else we'll get a -setObjectValue: later.
  newException_.reset();
  model_->RemoveAll();
  updatesEnabled_ = YES;
  [self modelDidChange];
}

- (IBAction)closeSheet:(id)sender {
  [NSApp endSheet:[self window]];
}

// Table View Data Source -----------------------------------------------------

- (NSInteger)numberOfRowsInTableView:(NSTableView*)table {
  return model_->RowCount() + (newException_.get() ? 1 : 0);
}

- (id)tableView:(NSTableView*)tv
    objectValueForTableColumn:(NSTableColumn*)tableColumn
                          row:(NSInteger)row {
  const HostContentSettingsMap::PatternSettingPair* entry;
  if (newException_.get() && row >= model_->RowCount())
    entry = newException_.get();
  else
    entry = &model_->entry_at(row);

  NSObject* result = nil;
  NSString* identifier = [tableColumn identifier];
  if ([identifier isEqualToString:@"pattern"]) {
    result = base::SysUTF8ToNSString(entry->first.AsString());
  } else if ([identifier isEqualToString:@"action"]) {
    result = [NSNumber numberWithInt:[self indexForSetting:entry->second]];
  } else {
    NOTREACHED();
  }
  return result;
}

// Updates exception at |row| to contain the data in |entry|.
- (void)updateRow:(NSInteger)row
        withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry {
  // TODO(thakis): This apparently moves an edited row to the back of the list.
  // It's what windows and linux do, but it's kinda sucky. Fix.
  // http://crbug.com/36904
  updatesEnabled_ = NO;
  if (row < model_->RowCount())
    model_->RemoveException(row);
  model_->AddException(entry.first, entry.second);
  updatesEnabled_ = YES;
  [self modelDidChange];

  // For now, at least re-select the edited element.
  int newIndex = model_->IndexOfExceptionByPattern(entry.first);
  DCHECK(newIndex != -1);
  [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex]
          byExtendingSelection:NO];
}

- (void) tableView:(NSTableView*)tv
    setObjectValue:(id)object
    forTableColumn:(NSTableColumn*)tableColumn
               row:(NSInteger)row {
  // -remove: and -removeAll: both call |tableView_|'s -deselectAll:, which
  // calls this method if a cell is currently being edited. Do not commit edits
  // of rows that are about to be deleted.
  if (!updatesEnabled_) {
    // If this method gets called, the pattern filed of the new exception can no
    // longer be being edited. Reset |newException_| to keep the invariant true.
    newException_.reset();
    return;
  }

  // Get model object.
  bool isNewRow = newException_.get() && row >= model_->RowCount();
  HostContentSettingsMap::PatternSettingPair originalEntry =
      isNewRow ? *newException_ : model_->entry_at(row);
  HostContentSettingsMap::PatternSettingPair entry = originalEntry;

  // Modify it.
  NSString* identifier = [tableColumn identifier];
  if ([identifier isEqualToString:@"pattern"]) {
    entry.first = HostContentSettingsMap::Pattern(
                      base::SysNSStringToUTF8(object));
  }
  if ([identifier isEqualToString:@"action"]) {
    int index = [object intValue];
    entry.second = [self settingForIndex:index];
  }

  // Commit modification, if any.
  if (isNewRow) {
    newException_.reset();
    if (![identifier isEqualToString:@"pattern"]) {
      [tableView_ reloadData];
      [self adjustEditingButtons];
      return;  // Commit new rows only when the pattern has been set.
    }
    int newIndex = model_->IndexOfExceptionByPattern(entry.first);
    if (newIndex != -1) {
      // The new pattern was already in the table. Focus existing row instead of
      // overwriting it with a new one.
      [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex]
              byExtendingSelection:NO];
      [tableView_ reloadData];
      [self adjustEditingButtons];
      return;
    }
  }
  if (entry != originalEntry || isNewRow)
    [self updateRow:row withEntry:entry];
}


// Table View Delegate --------------------------------------------------------

// When the selection in the table view changes, we need to adjust buttons.
- (void)tableViewSelectionDidChange:(NSNotification*)notification {
  [self adjustEditingButtons];
}

// Private --------------------------------------------------------------------

// This method appropriately sets the enabled states on the table's editing
// buttons.
- (void)adjustEditingButtons {
  NSIndexSet* selection = [tableView_ selectedRowIndexes];
  [removeButton_ setEnabled:([selection count] > 0)];
  [removeAllButton_ setEnabled:([tableView_ numberOfRows] > 0)];
}

- (void)modelDidChange {
  // Some calls on |model_|, e.g. RemoveException(), change something on the
  // backing content settings map object (which sends a notification) and then
  // change more stuff in |model_|. If |model_| is deleted when the notification
  // is sent, this second access causes a segmentation violation. Hence, disable
  // resetting |model_| while updates can be in progress.
  if (!updatesEnabled_)
    return;

  // The model caches its data, meaning we need to recreate it on every change.
  model_.reset(new ContentExceptionsTableModel(settingsMap_, settingsType_));

  [tableView_ reloadData];
  [self adjustEditingButtons];
}

- (size_t)menuItemCount {
  return showAsk_ ? arraysize(kAskSettings) : arraysize(kNoAskSettings);
}

- (NSString*)titleForIndex:(size_t)index {
  switch ([self settingForIndex:index]) {
    case CONTENT_SETTING_ALLOW:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_ALLOW_BUTTON);
    case CONTENT_SETTING_BLOCK:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_BLOCK_BUTTON);
    case CONTENT_SETTING_ASK:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_ASK_BUTTON);
    default:
      NOTREACHED();
  }
  return @"";
}

- (ContentSetting)settingForIndex:(size_t)index {
  return showAsk_ ? kAskSettings[index] : kNoAskSettings[index];
}

- (size_t)indexForSetting:(ContentSetting)setting {
  for (size_t i = 0; i < [self menuItemCount]; ++i) {
    if ([self settingForIndex:i] == setting)
      return i;
  }
  NOTREACHED();
  return 0;
}

@end