/*
SRSearchEnginesController.m

Author: Makoto Kinoshita

Copyright 2004 The Shiira Project. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted 
provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions 
  and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice, this list of 
  conditions and the following disclaimer in the documentation and/or other materials provided 
  with the distribution.

THIS SOFTWARE IS PROVIDED BY THE SHIIRA PROJECT ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE SHIIRA PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/

#import "SRDefaultsKey.h"

#import "SRSearchEnginesController.h"
#import "SRBookmarkIconDatabase.h"
#import "SRPredictEncoding.h"
#import "SRPreferencesController.h"

#import "SRImageTextCell.h"

#import "FoundationEx.h"

#define kSearchLabelTrimingLength   40
#define kSearchEngineRecentsLimitDefault   16

NSString*   SRSearchEnginesFileName = @"Shiira/SearchEngines.plist";

// Notification names
NSString*   SRSearchEnginesListChanged = @"SRSearchEnginesListChanged";

// Pboard type names
NSString*   SRSearchEnginePboardType = @"SRSearchEnginePboardType";

#pragma mark -

//--------------------------------------------------------------//
// SRSearchEnginesManager
//--------------------------------------------------------------//

@implementation SRSearchEnginesManager

//--------------------------------------------------------------//
#pragma mark -- Initialize --
//--------------------------------------------------------------//

+ (SRSearchEnginesManager*)sharedInstance
{
    static SRSearchEnginesManager*  _sharedInstance = nil;
    if (!_sharedInstance) {
        _sharedInstance = [[SRSearchEnginesManager alloc] init];
    }
    
    return _sharedInstance;
}

- (id)init
{
    self = [super init];
    if (!self) {
        return self;
    }
    
    // Initialize member variables
    _searchEngines = [[NSMutableArray alloc] init];
    _recentSearches = [[NSMutableArray alloc] init];
    
    // Register observers
    NSNotificationCenter*   center;
    center = [NSNotificationCenter defaultCenter];
    [center addObserver:self 
            selector:@selector(faviconAdded:) 
            name:SRFaviconAddedNotification 
            object:nil];
    
    return self;
}

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

//--------------------------------------------------------------//
#pragma mark -- Accessors --
//--------------------------------------------------------------//

- (NSMutableArray*)searchEngines
{
    return _searchEngines;
}

- (NSMutableArray*)recentSearches
{
    return _recentSearches;
}

- (void)removeAllRecenentSeraches
{
    [_recentSearches removeAllObjects];
    
    // Notify search engins list change
    [[NSNotificationCenter defaultCenter] 
            postNotificationName:SRSearchEnginesListChanged 
            object:self];
}

- (void)addRecentWord:(NSString*)word 
        engineName:(NSString*)engineName 
        URLString:(NSString*)URLString
{
    // Check private browse mode
    WebPreferences* preferences;
    BOOL            isPrivate;
    preferences = [SRPreferencesController defaultWebPreferences];
    isPrivate = [preferences privateBrowsingEnabled];
    if (isPrivate) {
        return;
    }
    
    if(word && engineName && URLString){
        NSString*   label;
        int limit=[[NSUserDefaults standardUserDefaults]integerForKey:SRSearchEngineRecentsLimit];
        if(limit <= 0 || limit > 128) limit=kSearchEngineRecentsLimitDefault;
        
        // Trim long word
        if([word length]>kSearchLabelTrimingLength){
            word=[[word substringToIndex:kSearchLabelTrimingLength-2]
                            stringByAppendingString:@"..."];
        }
        label=[NSString stringWithFormat:@"%@ (%@)", word, engineName];
        
        NSDictionary*   aDict=[NSDictionary dictionaryWithObjectsAndKeys:
                                word, @"word", label, @"label", URLString, @"url", nil];

        // Check same search
        int i;
        for(i=[_recentSearches count]-1; i>=0 ;i--){
            NSDictionary*   otherDict=[_recentSearches objectAtIndex:i];
            NSString*       otherURLString=[otherDict objectForKey:@"url"];
            if([URLString isEqualToString:otherURLString]){
                [_recentSearches removeObjectAtIndex:i];
                break;
            }
        }

        [_recentSearches insertObject:aDict atIndex:0];
        if([_recentSearches count]>limit) [_recentSearches removeLastObject];

        // Notify search engins list change
        [[NSNotificationCenter defaultCenter] 
                postNotificationName:SRSearchEnginesListChanged 
                object:_searchEngines];
    }
}

//--------------------------------------------------------------//
#pragma mark -- Persistence --
//--------------------------------------------------------------//

- (NSArray*)_createDefaultEngines
{
    // Get default search engines path
    NSString*   path;
    path = [[NSBundle mainBundle] pathForResource:@"SearchEngines" ofType:@"plist"];
    if (!path) {
        return nil;
    }
    
    // Load file
    return [NSArray arrayWithContentsOfFile:path];
}

- (void)loadSearchEngines
{
    NSArray*    searchEngines;
    
    // Get the paths of ~/Library/Shiira/SearchEngines.plist
    NSArray*	libraryPaths;
    NSString*	searchEnginePath;
    libraryPaths = NSSearchPathForDirectoriesInDomains(
            NSLibraryDirectory, NSUserDomainMask, YES);
    searchEnginePath = [[libraryPaths objectAtIndex:0] 
            stringByAppendingPathComponent:SRSearchEnginesFileName];
    
    // Check existense
    NSFileManager*	fileMgr;
    fileMgr = [NSFileManager defaultManager];
    if (![fileMgr fileExistsAtPath:searchEnginePath]) {
        searchEngines = [self _createDefaultEngines];
    }
    else {
        // Load search engines
        searchEngines = [NSArray arrayWithContentsOfFile:searchEnginePath];
        if (!searchEngines) {
            searchEngines = [self _createDefaultEngines];
        }
    }
    
    // Make search engines mutable
    if (![searchEngines isKindOfClass:[NSMutableArray class]]) {
        searchEngines = [NSMutableArray arrayWithArray:searchEngines];
    }
    
    // Set engines to instance variables
    [_searchEngines release];
    _searchEngines = [searchEngines retain];
    
    // Load recent
    int limit=[[NSUserDefaults standardUserDefaults]integerForKey:SRSearchEngineRecentsLimit];
    if(limit <= 0 || limit > 128) limit=kSearchEngineRecentsLimitDefault;
    NSArray*    array;
    array = [[NSUserDefaults standardUserDefaults] objectForKey:SRSearchEngineRecents];
    if (array) {
        _recentSearches = [NSMutableArray arrayWithArray:array];
    }
    else {
        _recentSearches = [NSMutableArray arrayWithCapacity:limit+1];
    }
    
    [_recentSearches retain];
}

- (void)saveSearchEngines
{
    // Get the paths of ~/Library/Shiira/SearchEngines.plist
    NSArray*	libraryPaths;
    NSString*	searchEnginePath;
    libraryPaths = NSSearchPathForDirectoriesInDomains(
            NSLibraryDirectory, NSUserDomainMask, YES);
    searchEnginePath = [[libraryPaths objectAtIndex:0] 
            stringByAppendingPathComponent:SRSearchEnginesFileName];
    
    // Check existense
    NSFileManager*	fileMgr;
    fileMgr = [NSFileManager defaultManager];
    if (![fileMgr fileExistsAtPath:searchEnginePath]) {
        // Create file
        SRCreateFile(searchEnginePath);
    }
    
    // Save search engines
    [_searchEngines writeToFile:searchEnginePath atomically:YES];

    // Save recents
    [[NSUserDefaults standardUserDefaults] setObject:_recentSearches forKey:SRSearchEngineRecents];
}

//--------------------------------------------------------------//
#pragma mark -- SRBookmarkIconDatabase notification --
//--------------------------------------------------------------//

- (void)faviconAdded:(NSNotification*)notification
{
#if 0
    // Get related URL string
    NSString*   URLString;
    URLString = [notification object];
    
    // Find search engine
    NSEnumerator*           enumerator;
    NSMutableDictionary*    searchEngine;
    enumerator = [_searchEngines objectEnumerator];
    while (searchEngine = [enumerator nextObject]) {
        // Get template URL
        NSString*   templateURL;
        templateURL = [searchEngine objectForKey:@"templateURL"];
        if ([templateURL isEqualToString:URLString]) {
            // Update search engine favicon
            NSAttributedString* attachedTitle;
            attachedTitle = SRFaviconAttachedString(
                    [searchEngine objectForKey:@"title"], 
                    [searchEngine objectForKey:@"templateURL"], 
                    NSMakeSize(16, 16));
            if (attachedTitle) {
                [searchEngine setValue:attachedTitle forKey:@"title"];
            }
            
            return;
        }
    }
#endif
}

@end

#pragma mark -

//--------------------------------------------------------------//
// SRSearchEnginesController
//--------------------------------------------------------------//

@interface SRSearchEnginesController (private)
- (void)_updateButtons;
- (void)_updateDetailView;
@end

@implementation SRSearchEnginesController

//--------------------------------------------------------------//
#pragma mark -- Initialize --
//--------------------------------------------------------------//

- (id)initWithWindowNibName:(NSString*)windowNibName owner:(id)owner
{
    self = [super initWithWindowNibName:windowNibName owner:owner];
    if (!self) {
        return self;
    }
    
    // Initialize member variables
    _draggedEngines = nil;
    
    // Set frame auto save name
    if (![[self window] frameAutosaveName]) {
        [[self window] setFrameAutosaveName:@"SRSearchEnginesFrame"];
    }
    
    NSUserDefaults* defaults;
    defaults = [NSUserDefaults standardUserDefaults];
    
    // Register key value observation
    [defaults addObserver:self 
            forKeyPath:SRIconUseFavicon 
            options:NSKeyValueObservingOptionNew 
            context:NULL];
    [defaults addObserver:self 
            forKeyPath:SRIconUseFaviconSearchEngine 
            options:NSKeyValueObservingOptionNew 
            context:NULL];
    
    return self;
}

- (void)awakeFromNib
{
    // Configure search engine outline
    NSTableColumn*      column;
    NSCell*             oldCell;
    SRImageTextCell*    cell;
    
    column = [_searchEnginesOutline tableColumnWithIdentifier:@"title"];
    oldCell = [column dataCell];
    cell = [[SRImageTextCell alloc] init];
    [cell autorelease];
    [cell setEditable:YES];
    [cell setFont:[oldCell font]];
    [column setDataCell:cell];
    
    [_searchEnginesOutline registerForDraggedTypes:[NSArray arrayWithObjects:
            SRSearchEnginePboardType, nil]];
    
    // Update appearances
    [self _updateButtons];
    [self _updateDetailView];
}

- (void)dealloc
{
    NSUserDefaults* defaults;
    defaults = [NSUserDefaults standardUserDefaults];
    [defaults removeObserver:self forKeyPath:SRIconUseFavicon];
    [defaults removeObserver:self forKeyPath:SRIconUseFaviconSearchEngine];
    
    [super dealloc];
}

//--------------------------------------------------------------//
#pragma mark -- Search engine --
//--------------------------------------------------------------//

- (NSMutableArray*)_parentArrayOfEngine:(NSDictionary*)engine 
        fromArray:(NSMutableArray*)array
{
    if ([array containsObject:engine]) {
        return array;
    }
    
    NSEnumerator*   enumerator;
    NSDictionary*   childEngine;
    enumerator = [array objectEnumerator];
    while (childEngine = [enumerator nextObject]) {
        NSMutableArray* children;
        children = [childEngine objectForKey:@"child"];
        if (children) {
            NSMutableArray* parent;
            parent = [self _parentArrayOfEngine:engine fromArray:children];
            if (parent) {
                return parent;
            }
        }
    }
    
    return nil;
}

- (NSArray*)selectedEngines
{
    // Get selected rows
    NSIndexSet* indexSet;
    indexSet = [_searchEnginesOutline selectedRowIndexes];
    if (!indexSet || [indexSet count] == 0) {
        return nil;
    }
    
    // Get selected rows
    NSMutableArray* searchEngines;
    unsigned int    index;
    searchEngines = [NSMutableArray array];
    index = [indexSet firstIndex];
    do {
        // Get selected search engine
        id  selectedObject;
        selectedObject = [_searchEnginesOutline itemAtRow:index];
        if (!selectedObject) {
            continue;
        }
        
        [searchEngines addObject:selectedObject];
    } while ((index = [indexSet indexGreaterThanIndex:index]) != NSNotFound);
    
    return searchEngines;
}

- (void)selectEngines:(NSArray*)engines
{
    // Create index set
    NSMutableIndexSet*  indexSet;
    NSEnumerator*       enumerator;
    NSDictionary*       engine;
    indexSet = [NSMutableIndexSet indexSet];
    enumerator = [engines objectEnumerator];
    while (engine = [enumerator nextObject]) {
        // Get row of engine
        int row;
        row = [_searchEnginesOutline rowForItem:engine];
        if (row != -1) {
            [indexSet addIndex:row];
        }
    }
    
    // Select engines
    [_searchEnginesOutline selectRowIndexes:indexSet byExtendingSelection:NO];
    [_searchEnginesOutline scrollRowToVisible:[indexSet lastIndex]];
    
    // Update appearance
    [self _updateButtons];
    [self _updateDetailView];
}

- (void)insertEngineAfterSelection:(NSDictionary*)engine
{
    // Get parent array for insertion
    NSMutableArray* root = [[SRSearchEnginesManager sharedInstance] searchEngines];
    NSMutableArray* parentArray = root;
    int index = [parentArray count];
    
    // Get selected engines
    NSArray*    selectedEngines;
    selectedEngines = [self selectedEngines];
    if (selectedEngines) {
        // Get last one
        NSDictionary*   engine;
        engine = [selectedEngines lastObject];
        
        parentArray = [self _parentArrayOfEngine:engine fromArray:root];
        if (!parentArray) {
            parentArray = root;
        }
        
        // Index of last one
        index = [parentArray indexOfObject:engine];
        if (index == NSNotFound) {
            index = [parentArray count];
        }
        
        // Increment one
        index += 1;
        if (index > [parentArray count]) {
            index = [parentArray count];
        }
    }
    
    // Insert folder
    [parentArray insertObject:engine atIndex:index];
    
    // Reload data
    [_searchEnginesOutline reloadData];
    
    // Select new engine
    [self selectEngines:[NSArray arrayWithObject:engine]];
    
    // Notify search engins list change
    [[NSNotificationCenter defaultCenter] 
            postNotificationName:SRSearchEnginesListChanged 
            object:[[SRSearchEnginesManager sharedInstance] searchEngines]];
}

- (void)addNewEngine
{
    // Cretate new engine
    NSMutableDictionary*    engine;
    engine = [NSMutableDictionary dictionary];
    
    // Create title
    NSString*   title;
    title = NSLocalizedString(@"Untitled", nil);
    [engine setObject:title forKey:@"title"];
    
    [engine setObject:[NSNumber numberWithBool:YES] forKey:@"isUsing"];
    [engine setObject:@"" forKey:@"templateURL"];
    [engine setObject:(NSString*)CFUUIDCreateString(NULL, CFUUIDCreate(NULL)) forKey:@"uuid"];
    
    // Insert engine
    [self insertEngineAfterSelection:engine];
}

- (void)addNewFolder
{
    // Cretate new folder
    NSMutableDictionary*    folder;
    folder = [NSMutableDictionary dictionary];
    
    // Create title
    NSString*   title;
    title = NSLocalizedString(@"Untitled", nil);
    [folder setObject:title forKey:@"title"];
    
    // Create child array
    NSMutableArray* children;
    children = [NSMutableArray array];
    [folder setObject:children forKey:@"child"];
    
    [folder setObject:[NSNumber numberWithBool:YES] forKey:@"isUsing"];
    [folder setObject:(NSString*)CFUUIDCreateString(NULL, CFUUIDCreate(NULL)) forKey:@"uuid"];
    
    // Insert folder
    [self insertEngineAfterSelection:folder];
}

- (void)addNewSeparator
{
    // Cretate new separator
    NSMutableDictionary*    separator;
    separator = [NSMutableDictionary dictionary];
    
    // Create separator
    [separator setObject:@"----------------" forKey:@"title"];
    
    [separator setObject:[NSNumber numberWithBool:YES] forKey:@"isSeparator"];
    [separator setObject:(NSString*)CFUUIDCreateString(NULL, CFUUIDCreate(NULL)) forKey:@"uuid"];
    
    // Insert separtor
    [self insertEngineAfterSelection:separator];
}

- (void)deleteSelectedEngines
{
    // Get selected engines
    NSArray*    selectedEngines;
    selectedEngines = [self selectedEngines];
    if (!selectedEngines || [selectedEngines count] == 0) {
        return;
    }
    
    // Get first row
    int row;
    row = [_searchEnginesOutline rowForItem:[selectedEngines objectAtIndex:0]];
    
    // Delete engines
    [[[SRSearchEnginesManager sharedInstance] searchEngines] 
            removeObjectsFromArrayRecursivelyIdenticalTo:selectedEngines];
    
    // Reload data
    [_searchEnginesOutline reloadData];
    
    // Select previous one
    if (row > 0) {
        row -= 1;
    }
    if (row >=0 && row < [_searchEnginesOutline numberOfRows]) {
        [_searchEnginesOutline selectRowIndexes:[NSIndexSet indexSetWithIndex:row] 
                byExtendingSelection:NO];
    }
    
    // Update appearance
    [self _updateButtons];
    [self _updateDetailView];
    
    // Notify search engins list change
    [[NSNotificationCenter defaultCenter] 
            postNotificationName:SRSearchEnginesListChanged 
            object:[[SRSearchEnginesManager sharedInstance] searchEngines]];
}

//--------------------------------------------------------------//
#pragma mark -- Appearance --
//--------------------------------------------------------------//

- (void)_updateButtons
{
    // Get selected engines
    NSArray*    selectedEngines;
    selectedEngines = [self selectedEngines];
    if (!selectedEngines || [selectedEngines count] == 0) {
        // Disable delete button
        [_deleteButton setEnabled:NO];
        
        return;
    }
    
    // Enable delete button
    [_deleteButton setEnabled:YES];
}

- (void)_updateDetailView
{
    // Get selected engines
    NSArray*    selectedEngines;
    selectedEngines = [self selectedEngines];
    if (!selectedEngines || [selectedEngines count] == 0) {
        // Reset all
        NSString*   noSelectString;
        noSelectString = NSLocalizedString(@"No selection", nil);
        
        [_nameTextField setStringValue:@""];
        [_nameTextField setEditable:NO];
        [[_nameTextField cell] setPlaceholderString:noSelectString];
        [_templateTextField setStringValue:@""];
        [_templateTextField setEditable:NO];
        [[_templateTextField cell] setPlaceholderString:noSelectString];
        [_keywordTextField setStringValue:@""];
        [_keywordTextField setEditable:NO];
        [_encodingTextField setStringValue:@""];
        
        return;
    }
    
    // Multiple selection
    if ([selectedEngines count] > 1) {
        NSString*   multiString;
        multiString = NSLocalizedString(@"Multiple selection", nil);
        
        [_nameTextField setStringValue:@""];
        [_nameTextField setEditable:NO];
        [[_nameTextField cell] setPlaceholderString:multiString];
        [_templateTextField setStringValue:@""];
        [_templateTextField setEditable:NO];
        [[_templateTextField cell] setPlaceholderString:multiString];
        [_keywordTextField setStringValue:@""];
        [_keywordTextField setEditable:NO];
        [_encodingTextField setStringValue:@""];
        
        return;
    }
    
    // Get search engine
    NSDictionary*   engine;
    engine = [selectedEngines objectAtIndex:0];
    
    // For separator
    id  boolObject;
    if ((boolObject = [engine objectForKey:@"isSeparator"]) && [boolObject boolValue]) {
        [_nameTextField setStringValue:@""];
        [_nameTextField setEditable:NO];
        [[_nameTextField cell] setPlaceholderString:@""];
        [_templateTextField setStringValue:@""];
        [_templateTextField setEditable:NO];
        [[_templateTextField cell] setPlaceholderString:@""];
        [_keywordTextField setStringValue:@""];
        [_keywordTextField setEditable:NO];
        [_encodingTextField setStringValue:@""];
        
        return;
    }
    
    // For folder
    if ([engine objectForKey:@"child"]) {
        [_nameTextField setStringValue:[engine objectForKey:@"title"]];
        [_nameTextField setEditable:YES];
        [[_nameTextField cell] setPlaceholderString:@""];
        [_templateTextField setStringValue:@""];
        [_templateTextField setEditable:NO];
        [[_templateTextField cell] setPlaceholderString:@""];
        [_keywordTextField setStringValue:@""];
        [_keywordTextField setEditable:NO];
        [_encodingTextField setStringValue:@""];
        
        return;
    }
    
    // Set search engine property
    [_nameTextField setStringValue:[engine objectForKey:@"title"]];
    [_nameTextField setEditable:YES];
    
    [_templateTextField setStringValue:[engine objectForKey:@"templateURL"]];
    [_templateTextField setEditable:YES];
    
    NSString*   keyword;
    keyword = [engine objectForKey:@"keyword"];
    if (!keyword) {
        keyword = @"";
    }
    [_keywordTextField setStringValue:keyword];
    [_keywordTextField setEditable:YES];
    
    NSString*   encodingString = nil;
    if ([engine objectForKey:@"encoding"]) {
        encodingString = [NSString localizedNameOfStringEncoding:
                [[engine objectForKey:@"encoding"] unsignedIntValue]];
    }
    if (!encodingString) {
        encodingString = @"";
    }
    [_encodingTextField setStringValue:encodingString];
}

//--------------------------------------------------------------//
#pragma mark -- NSOutlineView data source --
//--------------------------------------------------------------//

- (int)outlineView:(NSOutlineView*)outlineView 
        numberOfChildrenOfItem:(id)item
{
    NSArray*    engines = nil;
    
    // For root
    if (!item) {
        // Get root search engines
        engines = [[SRSearchEnginesManager sharedInstance] searchEngines];
    }
    else {
        // Get child engines
        engines = [item objectForKey:@"child"];
    }
    
    // Return number of engines
    if (!engines) {
        return 0;
    }
    return [engines count];
}

- (id)outlineView:(NSOutlineView*)outlineView 
        child:(int)index 
        ofItem:(id)item
{
    NSArray*    engines = nil;
    
    // For root
    if (!item) {
        // Get root search engines
        engines = [[SRSearchEnginesManager sharedInstance] searchEngines];
    }
    else {
        // Get child engines
        engines = [item objectForKey:@"child"];
    }
    
    // Return engine at index
    if (!engines) {
        return nil;
    }
    return [engines objectAtIndex:index];
}

- (BOOL)outlineView:(NSOutlineView*)outlineView 
        isItemExpandable:(id)item
{
    return [item objectForKey:@"child"] != nil;
}

- (id)outlineView:(NSOutlineView*)outlineView 
        objectValueForTableColumn:(NSTableColumn*)tableColumn 
        byItem:(id)item
{
    // Get identifier
    id  identifier;
    identifier = [tableColumn identifier];
    
    // Return value
    id  value;
    value = [item objectForKey:identifier];
    if (!value) {
        value = @"";
    }
    return value;
}

- (void)outlineView:(NSOutlineView*)outlineView 
        setObjectValue:(id)object 
        forTableColumn:(NSTableColumn*)tableColumn 
        byItem:(id)item
{
    // Get identifier
    id  identifier;
    identifier = [tableColumn identifier];
    
    // For separaotr
    id  boolObject;
    if ((boolObject = [item objectForKey:@"isSeparator"]) && [object boolValue]) {
        return;
    }
    
    [item setObject:object forKey:[tableColumn identifier]];
    
    // Notify search engins list change
    [[NSNotificationCenter defaultCenter] 
            postNotificationName:SRSearchEnginesListChanged 
            object:[[SRSearchEnginesManager sharedInstance] searchEngines]];
}

- (BOOL)outlineView:(NSOutlineView*)outlineView 
        acceptDrop:(id<NSDraggingInfo>)info 
        item:(id)item 
        childIndex:(int)index
{
    // Determine parent array
    NSMutableArray* parentArray = nil;
    if (item) {
        if ([item isKindOfClass:[NSDictionary class]]) {
            // Item should be folder engine
            id  array;
            array = [item objectForKey:@"child"];
            if (![array isMemberOfClass:[NSMutableArray class]]) {
                // Make array mutable
                array = [NSMutableArray arrayWithArray:array];
                [item setObject:array forKey:@"child"];
            }
            parentArray = array;
        }
    }
    if (!parentArray) {
        // Use root
        parentArray = [[SRSearchEnginesManager sharedInstance] searchEngines];
    }
    
    // Check index
    if (index == NSOutlineViewDropOnItemIndex) {
        index = 0;
    }
    if (index > [parentArray count]) {
        index = [parentArray count];
    }
    
    // Get dragged engines
    NSArray*    draggedEngines;
    draggedEngines = SRReadSearchEnginesFromPasteboard([info draggingPasteboard]);
    if (!draggedEngines) {
        return NO;
    }
    
    // Get operation mask
    NSDragOperation operationMask;
    operationMask = [info draggingSourceOperationMask];
    
    // Insert dragged engine into parent array
    [parentArray insertObjectsFromArray:draggedEngines atIndex:index];
    
    // For moving
    if (!(operationMask & NSDragOperationCopy)) {
        // Delete old engines
        if (_draggedEngines) {
            [[[SRSearchEnginesManager sharedInstance] searchEngines] 
                    removeObjectsFromArrayRecursivelyIdenticalTo:_draggedEngines];
        }
    }
    _draggedEngines = nil;
    
    // Reload data
    [_searchEnginesOutline reloadData];
    
    // Select new engines
    [self selectEngines:draggedEngines];
    
    // Notify search engins list change
    [[NSNotificationCenter defaultCenter] 
            postNotificationName:SRSearchEnginesListChanged 
            object:[[SRSearchEnginesManager sharedInstance] searchEngines]];
    
    return YES;
}

- (NSDragOperation)outlineView:(NSOutlineView*)outlineView 
        validateDrop:(id<NSDraggingInfo>)info 
        proposedItem:(id)item 
        proposedChildIndex:(int)index
{
    // Check index
    if (index == NSOutlineViewDropOnItemIndex) {
        return NSDragOperationNone;
    }
    
    // Get operation mask
    NSDragOperation operationMask;
    operationMask = [info draggingSourceOperationMask];
    
    // Copy
    if (operationMask & NSDragOperationCopy) {
        return NSDragOperationCopy;
    }
    
    // Move
    return NSDragOperationGeneric;
}

- (id)outlineView:(NSOutlineView*)outlineView 
        itemForPersistentObject:(id)object
{
    // Return unarchived bookmark
    return [NSUnarchiver unarchiveObjectWithData:object];
}

- (id)outlineView:(NSOutlineView*)outlineView 
        persistentObjectForItem:(id)item
{
    // Return archived bookmark
    return [NSArchiver archivedDataWithRootObject:item];
}

- (BOOL)outlineView:(NSOutlineView*)outlineView 
        writeItems:(NSArray*)items 
        toPasteboard:(NSPasteboard*)pboard
{
    // Write search engines to pasteboard
    SRWriteSearchEnginesToPasteboard(items, pboard);
    
    // Keep dragged items, but not retain it
    _draggedEngines = items;
    
    return YES;
}

//--------------------------------------------------------------//
#pragma mark -- NSOutlineView delegate --
//--------------------------------------------------------------//

- (BOOL)outlineView:(NSOutlineView*)outlineView 
        shouldEditTableColumn:(NSTableColumn*)tableColumn 
        item:(id)item
{
    // Get identifier
    id  identifier;
    identifier = [tableColumn identifier];
    
    // For folder
    if ([item objectForKey:@"child"]) {
        // Disable template and keyword cell
        if ([identifier isEqualToString:@"templateURL"] || 
            [identifier isEqualToString:@"keyword"])
        {
            return NO;
        }
    }
    // For separaotr
    id  object;
    if ((object = [item objectForKey:@"isSeparator"]) && [object boolValue]) {
        return NO;
    }
    
    return YES;
}

- (void)outlineView:(NSOutlineView*)outlineView 
        willDisplayCell:(id)cell 
        forTableColumn:(NSTableColumn*)tableColumn 
        item:(id)item
{
    NSUserDefaults* defaults;
    defaults = [NSUserDefaults standardUserDefaults];
    
    // Check favicon availability
    BOOL    isFaviconUsed;
    isFaviconUsed = [defaults boolForKey:SRIconUseFavicon] && [defaults boolForKey:SRIconUseFaviconSearchEngine];
    
    // Get identifier
    id  identifier;
    identifier = [tableColumn identifier];
    
    id  boolObject;
    
    // For checkbox column
    if ([identifier isEqualToString:@"isUsing"]) {
        // For separator
        if ((boolObject = [item objectForKey:@"isSeparator"]) && [boolObject boolValue]) {
            // Hide button
            [cell setTransparent:YES];
        }
        // Other
        else {
            // Show button
            [cell setTransparent:NO];
        }
    }
    
    // For title column
    if ([identifier isEqualToString:@"title"]) {
        // For folder
        if ([item objectForKey:@"child"]) {
            // Set folder icon
            if (isFaviconUsed) {
                [cell setImage:[[NSImage imageNamed:@"otherFolder"] copy]];
            }
            else {
                [cell setImage:nil];
            }
        }
        // For separator
        else if ((boolObject = [item objectForKey:@"isSeparator"]) && [boolObject boolValue]) {
            [cell setImage:nil];
        }
        // Other
        else {
            SRBookmarkIconDatabase* database;
            database = [SRBookmarkIconDatabase sharedInstance];
            
            // Get icon
            NSImage*    icon;
            icon = [[SRBookmarkIconDatabase sharedInstance] 
                    iconOrDefaultIconForURLString:[item objectForKey:@"templateURL"]];
            
            if (isFaviconUsed && icon) {
                [cell setImage:[icon copy]];
            }
            else {
                [cell setImage:nil];
            }
        }
    }
}

- (void)outlineViewSelectionDidChange:(NSNotification*)notification
{
    // Update appearances
    [self _updateButtons];
    [self _updateDetailView];
}

// Extentions
- (void)outlineViewDeleteSelectedItem:(NSOutlineView*)outlineView
{
    [self deleteSelectedEngines];
}

//--------------------------------------------------------------//
#pragma mark -- Actions --
//--------------------------------------------------------------//

- (void)searchEngineSelectedAction:(id)sender
{
    [self _updateButtons];
    [self _updateDetailView];
}

- (void)editTextAction:(id)sender
{
    // Get selected engines
    NSArray*                selectedEngines;
    NSMutableDictionary*    engine;
    selectedEngines = [self selectedEngines];
    if (!selectedEngines || [selectedEngines count] != 1) {
        return;
    }
    engine = [selectedEngines objectAtIndex:0];
    
    // Get string value
    NSString*   string;
    string = [sender stringValue];
    if (!string) {
        return;
    }
    
    // For name text field
    if (sender == _nameTextField) {
        [engine setObject:string forKey:@"title"];
    }
    // For tempalte text field
    BOOL    templateIsChanged = NO;
    if (sender == _templateTextField) {
        templateIsChanged = ![string isEqualToString:[engine objectForKey:@"templateURL"]];
        [engine setObject:string forKey:@"templateURL"];
    }
    // For keyword text field
    if (sender == _keywordTextField) {
        [engine setObject:string forKey:@"keyword"];
    }
    
    // Update appearances
    [_searchEnginesOutline reloadData];
    
    
    // Detect encoding
    if (templateIsChanged) {
        [self encodingAutoDetectAction:self];
    }
}

- (void)addNewEngineAction:(id)sender
{
    [self addNewEngine];
}

- (void)addNewFolderAction:(id)sender
{
    [self addNewFolder];
}

- (void)addNewSeparatorAction:(id)sender
{
    [self addNewSeparator];
}

- (void)deleteAction:(id)sender
{
    [self deleteSelectedEngines];
}

- (void)encodingAutoDetectAction:(id)sender
{
    // Get selected engine
    NSArray*                searchEngines;
    NSMutableDictionary*    searchEngine;
    searchEngines = [self selectedEngines];
    if (!searchEngines || [searchEngines count] != 1) {
        return;
    }
    searchEngine = [searchEngines objectAtIndex:0];
    
    // Get template URL
    NSString*   templateURL;
    templateURL = [searchEngine valueForKey:@"templateURL"];
    if (!templateURL) {
        return;
    }
    
    // Detect encoding
    SRPredictEncoding*  predictor;
    NSStringEncoding    encoding;
    predictor = [[SRPredictEncoding alloc] initWithTemplateURL:templateURL];
    [predictor autorelease];
    encoding = [predictor textEncoding];
    
    // Set encoding
    [searchEngine setValue:[NSNumber numberWithUnsignedInt:encoding] forKey:@"encoding"];
    [self _updateDetailView];
}

//--------------------------------------------------------------//
#pragma mark -- Key value observation --
//--------------------------------------------------------------//

- (void)observeValueForKeyPath:(NSString*)keyPath 
        ofObject:(id)object 
        change:(NSDictionary*)change 
        context:(void *)context
{
    // For NSUserDefault
    if (object == [NSUserDefaults standardUserDefaults]) {
        if ([keyPath isEqualToString:SRIconUseFavicon] || 
            [keyPath isEqualToString:SRIconUseFaviconSearchEngine])
        {
            // Update search outline
            [_searchEnginesOutline reloadData];
        }
    }
}

@end

#pragma mark -

//--------------------------------------------------------------//
#pragma mark -- Utilities --
//--------------------------------------------------------------//

BOOL SRIsSearchEngineTemplateURL(
        NSString* URLString)
{
    return [URLString rangeOfString:@"%@"].length != NSNotFound;
}

void SRWriteSearchEnginesToPasteboard(
        NSArray* engines, 
        NSPasteboard* pboard)
{
    // Copy searchn engines deeply
    NSMutableArray* copiedEngines;
    copiedEngines = [[NSMutableArray alloc] initWithArray:engines copyItemsDeeply:YES];
    [copiedEngines autorelease];
    
    // Declare paste board types
    NSArray*    types;
    types = [NSArray arrayWithObjects:
            SRSearchEnginePboardType, nil];
    [pboard declareTypes:types owner:nil];
    
    // Create search engine data
    NSData* searchEngineData;
    searchEngineData = [NSArchiver archivedDataWithRootObject:copiedEngines];
    [pboard setData:searchEngineData forType:SRSearchEnginePboardType];
}

NSArray* SRReadSearchEnginesFromPasteboard(
        NSPasteboard* pboard)
{
    // Get seach engien data
    NSData* searchEngineData;
    searchEngineData = [pboard dataForType:SRSearchEnginePboardType];
    if (!searchEngineData) {
        return nil;
    }
    
    // Unarchive data
    return [NSUnarchiver unarchiveObjectWithData:searchEngineData];
}
