// Copyright (c) 2009 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/bookmark_tree_controller.h"

#include "app/l10n_util_mac.h"
#include "base/logging.h"
#import "chrome/browser/cocoa/bookmark_item.h"
#import "chrome/browser/cocoa/bookmark_manager_controller.h"
#include "grit/generated_resources.h"

// Outline-view column identifiers.
static NSString* const kTitleColIdent = @"title";
static NSString* const kURLColIdent = @"url";
static NSString* const kFolderColIdent = @"folder";


@implementation BookmarkTreeController

@synthesize showsLeaves = showsLeaves_;

// Initialization after the nib is loaded.
- (void)awakeFromNib {
  [outline_ setTarget:self];
  [outline_ setDoubleAction:@selector(openItems:)];
  [outline_ setAutoresizesOutlineColumn:NO];  // Prevents issue 35031
  [self registerDragTypes];
}

- (BookmarkItem*)group {
  return group_;
}

- (void)setGroup:(BookmarkItem*)group {
  if (group != group_) {
    group_ = group;

    [outline_ deselectAll:self];
    [outline_ reloadData];
    [outline_ noteNumberOfRowsChanged];
  }
}

- (NSOutlineView*)outline {
  return outline_;
}

- (BOOL)showsFolderColumn {
  NSTableColumn* col = [outline_ tableColumnWithIdentifier:kFolderColIdent];
  return ![col isHidden];
}

- (void)setShowsFolderColumn:(BOOL)show {
  NSTableColumn* col = [outline_ tableColumnWithIdentifier:kFolderColIdent];
  DCHECK(col);
  if (show != ![col isHidden]) {
    [col setHidden:!show];
    [outline_ sizeToFit];
  }
}

- (BOOL)flat {
  return flat_;
}

- (void)setFlat:(BOOL)flat {
  flat_ = flat;
  [outline_ setIndentationPerLevel:flat ? 0.0 : 16.0];
}


#pragma mark -
#pragma mark SELECTION:

// Getter for the |selectedItems| property.
- (NSArray*)selectedItems {
  NSMutableArray* items = [NSMutableArray array];
  NSIndexSet* selectedRows = [outline_ selectedRowIndexes];
  if (selectedRows != nil) {
    for (NSInteger row = [selectedRows firstIndex]; row != NSNotFound;
         row = [selectedRows indexGreaterThanIndex:row]) {
      [items addObject:[outline_ itemAtRow:row]];
    }
  }
  return items;
}

// Setter for the |selectedItems| property.
- (void)setSelectedItems:(NSArray*)items {
  NSMutableIndexSet* newSelection = [NSMutableIndexSet indexSet];

  for (NSUInteger i = 0; i < [items count]; i++) {
    NSInteger row = [outline_ rowForItem:[items objectAtIndex:i]];
    if (row >= 0) {
      [newSelection addIndex:row];
    }
  }

  [outline_ selectRowIndexes:newSelection byExtendingSelection:NO];
}

- (BookmarkItem*)selectedItem {
  BookmarkItem* selected = nil;
  if ([outline_ numberOfSelectedRows] == 1)
    selected = [outline_ itemAtRow:[outline_ selectedRow]];
  return selected;
}

- (void)setSelectedItem:(BookmarkItem*)item {
  [self setSelectedItems:item ? [NSArray arrayWithObject:item] : nil];
}

+ (NSArray*)keyPathsForValuesAffectingSelectedItem {
  return [NSArray arrayWithObject:@"selectedItems"];
}

- (BOOL)selectionShouldChangeInOutlineView:(NSOutlineView*)outlineView {
  [self willChangeValueForKey:@"selectedItems"];
  return YES;
}

- (void)outlineViewSelectionDidChange:(NSNotification*)notification {
  [self didChangeValueForKey:@"selectedItems"];
}

// Returns the selected/right-clicked item(s) for a command to act on.
- (NSArray*)actionItems {
  int row = [outline_ clickedRow];
  if (row >= 0 && ![outline_ isRowSelected:row]) {
    BookmarkItem* item = [outline_ itemAtRow:row];
    if (!item)
      return nil;   // clickedRow can occasionally return a nonexistent row
    return [NSArray arrayWithObject:item];
  }
  return [self selectedItems];
}

- (BOOL)expandItem:(BookmarkItem*)item {
  if (!item)
    return NO;
  if (item == group_)
    return YES;
  if ([outline_ rowForItem:item] < 0) {
    // If the item's not visible, expand its ancestors.
    if (![self expandItem:[item parent]])
      return NO;
  }
  if (![outline_ isExpandable:item])
    return NO;
  [outline_ expandItem:item];
  DCHECK([outline_ isItemExpanded:item]);
  return YES;
}

- (BOOL)revealItem:(BookmarkItem*)item {
  if ([item parent] && ![self expandItem:[item parent]])
    return NO;
  [outline_ scrollRowToVisible:[outline_ rowForItem:item]];
  [self setSelectedItems:[NSArray arrayWithObject:item]];
  return YES;
}

- (BOOL)getInsertionParent:(BookmarkItem**)outParent
                     index:(NSUInteger*)outIndex {
  NSArray* selItems = [self actionItems];
  NSUInteger numSelected = [selItems count];
  if (numSelected > 1) {
    return NO;
  } else if (numSelected == 1) {
    // Insert at selected/clicked row.
    BookmarkItem* selected = [selItems objectAtIndex:0];
    *outParent = [selected parent];
    if (*outParent && ![*outParent isFake])
      *outIndex = [*outParent indexOfChild:selected];
    else if ([selected isFolder]) {
      // If root item like Bookmarks Bar selected, insert _into_ it.
      *outParent = selected;
      *outIndex = 0;
    }
  } else {
    // Insert at very end if there's no selection:
    *outParent = group_;
    *outIndex = [group_ numberOfChildren];
  }
  // Can't add to a fake group.
  return *outParent && ![*outParent isFake];
}

- (BOOL)canInsert {
  BookmarkItem* parent;
  NSUInteger index;
  return [self getInsertionParent:&parent index:&index];
}


#pragma mark -
#pragma mark COMMANDS:


// Responds to a double-click by opening the selected URL(s).
- (IBAction)openItems:(id)sender {
  for (BookmarkItem* item in [self actionItems]) {
    if (![item isFolder]) {
      [item open];
    } else if (flat_) {
      [manager_ showGroup:item];
    } else {
      if ([outline_ isItemExpanded:item])
        [outline_ collapseItem:item];
      else
        [outline_ expandItem:item];
    }
  }
}

// The Delete command (also bound to the delete key.)
- (IBAction)delete:(id)sender {
  NSArray* items = [self actionItems];
  // Iterate backwards so that any selected children are deleted before
  // selected parents (opposite order could cause double-free!)
  bool any = false;
  if ([items count]) {
    for (NSInteger i = [items count] - 1; i >= 0; i--) {
      BookmarkItem* item = [items objectAtIndex:i];
      if ([[item parent] removeChild:item])
        any = true;
    }
  }
  if (any) {
    [outline_ reloadData];
    [outline_ deselectAll:self];
  } else {
    NSBeep();
  }
}

- (void)editTitleOfItem:(BookmarkItem*)item {
  int row = [outline_ rowForItem:item];
  DCHECK(row >= 0);
  [self setSelectedItem:item];
  [outline_ editColumn:[outline_ columnWithIdentifier:@"title"]
                   row:row
             withEvent:[NSApp currentEvent]
                select:YES];
}

- (IBAction)editTitle:(id)sender {
  NSArray* items = [self actionItems];
  if ([items count] == 1)
    [self editTitleOfItem:[items objectAtIndex:0]];
  else
    NSBeep();
}

- (BookmarkItem*)newFolderWithTitle:(NSString*)title {
  BookmarkItem* parent;
  NSUInteger index;
  if (![self getInsertionParent:&parent index:&index]) {
    NSBeep();
    return nil;
  }
  // Create the folder, then select it and make the title editable:
  BookmarkItem* folder = [parent addFolderWithTitle:title atIndex:index];
  [self revealItem:folder];
  return folder;
}

- (IBAction)newFolder:(id)sender {
  BookmarkItem* folder = [self newFolderWithTitle:@""];
  if (folder)
    [self editTitleOfItem:folder];
}

- (IBAction)revealSelectedItem:(id)sender {
  NSArray* selItems = [self actionItems];
  if ([selItems count] != 1 ||
      ![manager_ revealItem:[selItems objectAtIndex:0]])
    NSBeep();
  [[outline_ window] makeFirstResponder:outline_];
}

static void addItem(NSMenu* menu, int command, SEL action) {
  [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(command)
                  action:action
           keyEquivalent:@""];
}

// Generates a context menu for the outline view.
- (NSMenu*)menu {
  NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
  addItem(menu, IDS_BOOMARK_BAR_OPEN_IN_NEW_TAB, @selector(openItems:));
  if (showsLeaves_) {
    addItem(menu, IDS_BOOKMARK_MANAGER_SHOW_IN_FOLDER,
        @selector(revealSelectedItem:));
  }
  [menu addItem:[NSMenuItem separatorItem]];
  addItem(menu, IDS_BOOKMARK_BAR_EDIT, @selector(editTitle:));
  addItem(menu, IDS_BOOKMARK_BAR_REMOVE, @selector(delete:));
  [menu addItem:[NSMenuItem separatorItem]];
  addItem(menu, IDS_BOOMARK_BAR_NEW_FOLDER, @selector(newFolder:));
  for (NSMenuItem* item in [menu itemArray])
    [item setTarget:self];
  return menu;
}

// Selectively enables/disables menu commands.
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return [self validateAction:[item action]];
}

- (BOOL)validateAction:(SEL)action {
  NSArray* selected = [self actionItems];
  NSUInteger selCount = [selected count];
  if (action == @selector(copy:)) {
    return selCount > 0;

  } else if (action == @selector(cut:) || action == @selector(delete:)) {
    if (selCount == 0)
      return NO;
    for(BookmarkItem* item in selected) {
      if ([item isFixed])
        return NO;
    }
    return YES;

  } else if (action == @selector(paste:)) {
    return [self canInsert] &&
        [[NSPasteboard generalPasteboard] availableTypeFromArray:
            [outline_ registeredDraggedTypes]] != nil;

  } else if (action == @selector(openItems:)) {
    if (selCount == 0)
      return NO;
    // Disable if folder selected (if only because title says "Open In Tab".)
    for (BookmarkItem* item in selected) {
      if (![item URLString])
        return NO;
    }
    return YES;

  } else if (action == @selector(editTitle:)) {
    return selCount == 1 && ![[selected lastObject] isFixed];

  } else if (action == @selector(newFolder:)) {
    return [self canInsert];

  } else if (action == @selector(revealSelectedItem:)) {
    // Enable Show In Folder only in the flat list when
    // showing Recents or Search, and a URL is selected.
    return flat_ && [group_ isFake] &&
        selCount == 1 && [[selected lastObject] URLString];

  } else {
    return YES;
  }
}


#pragma mark -
#pragma mark DATA SOURCE:


// Returns the number of children of an item (NSOutlineView data source)
- (NSInteger)  outlineView:(NSOutlineView*)outlineView
    numberOfChildrenOfItem:(id)item {
  item = (item ? item : group_);
  return showsLeaves_ ? [item numberOfChildren] : [item numberOfChildFolders];
}

// Returns a child of an item (NSOutlineView data source)
- (id)outlineView:(NSOutlineView*)outlineView
            child:(NSInteger)index
           ofItem:(id)item {
  item = (item ? item : group_);
  if (showsLeaves_)
    return [item childAtIndex:index];
  else
    return [item childFolderAtIndex:index];
}

// Returns whether an item is expandable (NSOutlineView data source)
- (BOOL)outlineView:(NSOutlineView*)outlineView isItemExpandable:(id)item {
  if (flat_ || ![(item ? item : group_) isFolder])
    return NO;
  // In leafless mode, a folder with no subfolders isn't expandable.
  if (!showsLeaves_ && [item numberOfChildFolders] == 0)
    return NO;
  return YES;
}

- (id)          outlineView:(NSOutlineView*)outlineView
    itemForPersistentObject:(id)persistentID {
  return [group_ itemWithPersistentID:persistentID];
}

- (id)          outlineView:(NSOutlineView*)outlineView
    persistentObjectForItem:(id)item {
  item = (item ? item : group_);
  return [item persistentID];
}


// Returns the value to display in a cell (NSOutlineView data source)
- (id)            outlineView:(NSOutlineView*)outlineView
    objectValueForTableColumn:(NSTableColumn*)tableColumn
                       byItem:(id)item {
  item = (item ? item : group_);
  NSString* ident = [tableColumn identifier];
  if ([ident isEqualToString:kTitleColIdent]) {
    return [item title];
  } else if ([ident isEqualToString:kURLColIdent]) {
    return [item URLString];
  } else if ([ident isEqualToString:kFolderColIdent]) {
    return [item folderPath];
  } else {
    NOTREACHED();
    return nil;
  }
}

// Stores the edited value of a cell (NSOutlineView data source)
- (void)outlineView:(NSOutlineView*)outlineView
     setObjectValue:(id)value
     forTableColumn:(NSTableColumn*)tableColumn
             byItem:(id)item
{
  item = (item ? item : group_);
  NSString* ident = [tableColumn identifier];
  if ([ident isEqualToString:kTitleColIdent]) {
    [item setTitle:value];
  } else if ([ident isEqualToString:kURLColIdent]) {
    if ([value length])
      [item setURLString:value];
  }
}

// Returns whether a cell is editable (NSOutlineView data source)
- (BOOL)      outlineView:(NSOutlineView*)outlineView
    shouldEditTableColumn:(NSTableColumn*)tableColumn
                     item:(id)item {
  NSString* ident = [tableColumn identifier];
  if ([item isFixed])
    return NO;
  if ([ident isEqualToString:kURLColIdent] && [item isFolder])
    return NO;
  return YES;
}

// Sets a cell's icon before it's drawn (NSOutlineView data source)
- (void)outlineView:(NSOutlineView*)outlineView
    willDisplayCell:(id)cell
     forTableColumn:(NSTableColumn*)tableColumn
               item:(id)item
{
  // Use the bookmark/folder's icon.
  if ([[tableColumn identifier] isEqualToString:kTitleColIdent]) {
    item = (item ? item : group_);
    [cell setImage:[item icon]];
  }
}

// Updates the tree after the data model has changed.
- (void)itemChanged:(BookmarkItem*)nodeItem
    childrenChanged:(BOOL)childrenChanged {
  if (nodeItem == group_)
    nodeItem = nil;
  NSArray* sel = [self selectedItems];
  [outline_ reloadItem:nodeItem reloadChildren:childrenChanged];
  [self setSelectedItems:sel];
}

@end


@implementation BookmarksOutlineView

- (BookmarkTreeController*)bookmarkController {
  return (BookmarkTreeController*)[self delegate];
}

- (NSRect)frameOfOutlineCellAtRow:(NSInteger)row {
  // If the controller is in flat view, don't reserve space for the triangles.
  BookmarkTreeController* controller = [self bookmarkController];
  if ([controller flat])
    return NSZeroRect;
  return [super frameOfOutlineCellAtRow:row];
}

- (IBAction)cut:(id)sender {
  [[self bookmarkController] cut:sender];
}

- (IBAction)copy:(id)sender {
  [[self bookmarkController] copy:sender];
}

- (IBAction)paste:(id)sender {
  [[self bookmarkController] paste:sender];
}

- (IBAction)delete:(id)sender {
  [[self bookmarkController] delete:sender];
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return [[self bookmarkController] validateUserInterfaceItem:item];
}

- (void)keyDown:(NSEvent*)event {
  NSString* chars = [event charactersIgnoringModifiers];
  if ([chars length] == 1) {
    switch ([chars characterAtIndex:0]) {
      case NSDeleteCharacter:
      case NSDeleteFunctionKey:
        [self delete:self];
        return;
      case NSCarriageReturnCharacter:
      case NSEnterCharacter:
        [[self bookmarkController] editTitle:self];
        return;
      case NSTabCharacter:
        // For some reason NSTableView responds to the tab key by editing
        // the selected row. Override this with the normal behavior.
        [[self window] selectNextKeyView:self];
        return;
    }
  }
  [super keyDown:event];
}

- (NSMenu*)menu {
  return [[self bookmarkController] menu];
}

@end