/*
SRFindController.m

Author: Makoto Kinoshita

Copyright 2004-2006 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 "SRBrowserController.h"
#import "SRFindController.h"
#import "SRPageController.h"
#import "SRDefaultKeys.h"

#import "SRMenu.h"

// Frame auto save name
NSString*   SRFindPanelFrameAutoSaveName = @"SRFindPanelFrameAutoSaveName";

@implementation SRFindController

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

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

- (id)init
{
    self = [super initWithWindowNibName:@"FindPanel"];
    if (!self) {
        return nil;
    }
    
    // Initialize instance variables
    _foundRanges = [[NSMutableArray array] retain];
    
    return self;
}

- (void)windowDidLoad
{
    // Configure window
    HMPanel*    panel;
    panel = (HMPanel*)[self window];
    [panel setPassMenuValidationToDelegate:YES];
    if (![panel frameAutosaveName]) {
        [panel setFrameAutosaveName:SRFindPanelFrameAutoSaveName];
    }
    
    // Hide text field
    [_notFoundTextField setHidden:YES];
}

- (void)dealloc
{
    [_foundRanges release], _foundRanges = nil;
    
    [super dealloc];
}

//--------------------------------------------------------------//
#pragma mark -- String value --
//--------------------------------------------------------------//

- (NSString*)stringValue
{
    return [_findTextField stringValue];
}

- (void)setStringValue:(NSString*)string
{
    // Set text
    [_findTextField setStringValue:string];
    
    // Clear found ranges
    [_foundRanges removeAllObjects];
    [_outlineView reloadData];
}

//--------------------------------------------------------------//
#pragma mark -- Web view --
//--------------------------------------------------------------//

- (WebView*)frontWebView
{
    // Get main window controller
    NSWindowController* windowController;
    windowController = [[NSApp mainWindow] windowController];
    if (!windowController) {
        return nil;
    }
    if (![windowController isKindOfClass:[SRBrowserController class]]) {
        return nil;
    }
    
    // Get web view
    return [[(SRBrowserController*)windowController selectedPageController] webView];
}

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

- (void)showWindow:(id)sender
{
    // Focus on text field
    [[self window] makeFirstResponder:_findTextField];
    
    [super showWindow:sender];
}

- (void)closeWindowAction:(id)sender
{
    [[self window] performClose:sender];
}

- (void)_findText:(NSString*)text withWebView:(WebView*)webView options:(unsigned)options
{
    // Get text
    NSEnumerator*   enumerator;
    DOMDocument*    domDocument;
    enumerator = [[webView DOMDocuments] objectEnumerator];
    while (domDocument = [enumerator nextObject]) {
        NSMutableString*    buffer;
        NSMutableArray*     textNodes;
        NSMutableArray*     ranges;
        buffer = [NSMutableString string];
        textNodes = [NSMutableArray array];
        ranges = [NSMutableArray array];
        
        DOMTreeWalker*  walker;
        DOMNode*        node;
        walker = [domDocument createTreeWalker:
                [domDocument documentElement] :DOM_SHOW_ALL :nil :NO];
        while (node = [walker nextNode]) {
            if ([node respondsToSelector:@selector(boundingBox)]) {
                if (NSIsEmptyRect([node boundingBox])) {
                    continue;
                }
            }
            else {
                // Skip head and script node
                if ([node isKindOfClass:[DOMHTMLHeadElement class]] || 
                    [node isKindOfClass:[DOMComment class]] || 
                    [node isKindOfClass:[DOMHTMLScriptElement class]])
                {
                    DOMNode*    rootNode;
                    rootNode = [walker root];
                    while (node && node != rootNode) {
                        DOMNode*    nextNode;
                        nextNode = [walker nextSibling];
                        if (nextNode) {
                            [walker setCurrentNode:nextNode];
                            break;
                        }
                        
                        node = [[walker parentNode] nextSibling];
                    }
                    
                    if (!node || node == rootNode) {
                        break;
                    }
                    
                    continue;
                }
            }
            
            // For text node
            if ([node isKindOfClass:[DOMText class]] && 
                ![node isKindOfClass:[DOMCDATASection class]])
            {
                // Append string
                NSString*   data;
                data = [(DOMText*)node data];
                if ([data length] > 0) {
                    unsigned int    start, length;
                    start = [buffer length];
                    length = [data length];
                    [buffer appendString:data];
                    
                    [textNodes addObject:node];
                    [ranges addObject:[NSValue valueWithRange:NSMakeRange(start, length)]];
                }
            }
            // For block node
            else if ([node isKindOfClass:[DOMHTMLDivElement class]] || 
                [node isKindOfClass:[DOMHTMLDListElement class]] || 
                [node isKindOfClass:[DOMHTMLFormElement class]] || 
                [node isKindOfClass:[DOMHTMLHeadElement class]] || 
                [node isKindOfClass:[DOMHTMLHRElement class]] || 
                [node isKindOfClass:[DOMHTMLMenuElement class]] || 
                [node isKindOfClass:[DOMHTMLOListElement class]] || 
                [node isKindOfClass:[DOMHTMLParagraphElement class]] || 
                [node isKindOfClass:[DOMHTMLPreElement class]] || 
                [node isKindOfClass:[DOMHTMLTableElement class]] || 
                [node isKindOfClass:[DOMHTMLTableCaptionElement class]] || 
                [node isKindOfClass:[DOMHTMLTableCellElement class]] || 
                [node isKindOfClass:[DOMHTMLTableColElement class]] || 
                [node isKindOfClass:[DOMHTMLTableRowElement class]] || 
                [node isKindOfClass:[DOMHTMLTableSectionElement class]] || 
                [node isKindOfClass:[DOMHTMLUListElement class]])
            {
                // Append new line
                [buffer appendString:@"\n"];
            }
        }
        
        // Find text
        NSRange range, searchRange;
        int     i = 0;
        searchRange.location = 0;
        searchRange.length = [buffer length];
        while (YES) {
            // Find substring
            range = [buffer rangeOfString:text options:options range:searchRange];
            if (range.location == NSNotFound) {
                break;
            }
            
            int start, end;
            start = range.location;
            end = range.location + range.length;
            
            // Get start and end node
            DOMNode*    startNode = nil;
            DOMNode*    endNode = nil;
            NSRange     startRange, endRange;
            for (i; i < [ranges count]; i++) {
                NSRange r;
                r = [[ranges objectAtIndex:i] rangeValue];
                
                // Start node
                if (!startNode && r.location <= start && r.location + r.length > start) {
                    startNode = [textNodes objectAtIndex:i];
                    startRange = r;//range;
                }
                
                // End node
                if (!endNode && r.location <= end && r.location + r.length > end) {
                    endNode = [textNodes objectAtIndex:i];
                    endRange = r;//range;
                }
                
                if (startNode && endNode) {
                    break;
                }
            }
            
            if (!startNode || !endNode) {
                break;
            }
            
            // Create DOM range
            DOMRange*   domRange;
            domRange = [domDocument createRange];
            [domRange setStart:startNode :start - startRange.location];
            [domRange setEnd:endNode :end - endRange.location];
            
            // Get string
            NSMutableString*    data;
            data = [NSMutableString string];
            [data appendString:[buffer substringWithRange:NSMakeRange(start, end - start)]];
            
            int startIndex, endIndex, index, localStartIndex;
            startIndex = start - 20;
            if (startIndex < 0) {
                startIndex = 0;
            }
            endIndex = end + 20;
            if (endIndex > [buffer length]) {
                startIndex = [buffer length];
            }
            
            index = start;
            while (index > startIndex) {
                unichar c;
                c = [buffer characterAtIndex:index];
                if ([[NSCharacterSet newLineCharacterSet] characterIsMember:c]) {
                    index++;
                    break;
                }
                index--;
            }
            [data insertString:
                    [buffer substringWithRange:NSMakeRange(index, start - index)] atIndex:0];
            localStartIndex = start - index;
            
            index = end;
            while (index < endIndex) {
                unichar c;
                c = [buffer characterAtIndex:index];
                if ([[NSCharacterSet newLineCharacterSet] characterIsMember:c]) {
                    break;
                }
                index++;
            }
            [data appendString:
                    [buffer substringWithRange:NSMakeRange(end, index - end)]];
                    
            // Add found range
            NSMutableDictionary*    rangeDict;
            rangeDict = [NSMutableDictionary dictionary];
            [rangeDict setObject:data forKey:@"data"];
            [rangeDict setObject:[NSValue valueWithRange:NSMakeRange(localStartIndex, range.length)] 
                    forKey:@"range"];
            [rangeDict setObject:domRange forKey:@"domRange"];
            
            [_foundRanges addObject:rangeDict];
            
            // Update search range
            searchRange.location = range.location + range.length;
            searchRange.length = [buffer length] - searchRange.location;
        }
    }
}

- (void)findTextAction:(id)sender
{
    // Get find text
    NSString*   string;
    string = [_findTextField stringValue];
    if (!string || [string length] == 0) {
        return;
    }
    
    // Get main window controller
    NSWindowController* windowController;
    windowController = [[NSApp mainWindow] windowController];
    if (!windowController) {
        return;
    }
    
    // For SRBrowserController
    if ([windowController isKindOfClass:[SRBrowserController class]]) {
        SRBrowserController*    browserController;
        browserController = (SRBrowserController*)windowController;
        
        // Get web view
        WebView*    webView;
        webView = [[browserController selectedPageController] webView];
        if (!webView) {
            return;
        }
        
        // Get frames
        NSArray*    frames;
        frames = [webView webFrames];
        
        // Unmark all text
        NSEnumerator*   enumerator;
        WebFrame*       frame;
        frames = [webView webFrames];
        enumerator = [frames objectEnumerator];
        while (frame = [enumerator nextObject]) {
            id  documentView;
            documentView = [[frame frameView] documentView];
            
            if ([documentView respondsToSelector:
                    @selector(unmarkAllTextMatches)])
            {
                [documentView unmarkAllTextMatches];
            }
        }
        
        // Remove old ranges
        [_foundRanges removeAllObjects];
        
        // Decide find options
        BOOL    ignoreCase;
        ignoreCase = [[NSUserDefaults standardUserDefaults] boolForKey:SRFindIgnoreCase];
        
        unsigned    options = 0;
        if (ignoreCase) {
            options |= NSCaseInsensitiveSearch;
        }
        
        // Find txet
        [self _findText:string withWebView:webView options:options];
        
        // Set marker
        frames = [webView webFrames];
        enumerator = [frames objectEnumerator];
        while (frame = [enumerator nextObject]) {
            id  documentView;
            documentView = [[frame frameView] documentView];
            
            if ([documentView respondsToSelector:
                    @selector(setMarkedTextMatchesAreHighlighted:)])
            {
                [documentView setMarkedTextMatchesAreHighlighted:YES];
            }
            if ([documentView respondsToSelector:
                    @selector(markAllMatchesForText:caseSensitive:limit:)])
            {
                [documentView markAllMatchesForText:string caseSensitive:!ignoreCase limit:0];
            }
        }
#if 0
        // Find text
        NSEnumerator*   enumerator;
        DOMDocument*    domDocument;
        enumerator = [[webView DOMDocuments] objectEnumerator];
        while (domDocument = [enumerator nextObject]) {
            DOMNodeIterator*    iterator;
            DOMText*            textNode;
            iterator = [domDocument createNodeIterator:[domDocument documentElement] 
                    :DOM_SHOW_TEXT :nil :NO];
            while (textNode = (DOMText*)[iterator nextNode]) {
                // Get data
                NSString*   data;
                data = [textNode data];
                
                // Find text
                NSRange range;
                range = [data rangeOfString:string options:options];
                if (range.location != NSNotFound) {
                    DOMRange*   domRange;
                    domRange = [domDocument createRange];
                    [domRange setStart:textNode :range.location];
                    [domRange setEnd:textNode :range.location + range.length];
                    
                    // Add found range
                    NSMutableDictionary*    rangeDict;
                    rangeDict = [NSMutableDictionary dictionary];
                    [rangeDict setObject:data forKey:@"data"];
                    [rangeDict setObject:[NSValue valueWithRange:range] forKey:@"range"];
                    [rangeDict setObject:domRange forKey:@"domRange"];
                    
                    [_foundRanges addObject:rangeDict];
                }
            }
        }
#endif

        // Reload data
        [_outlineView reloadData];
        
        // Select first row
        if ([_foundRanges count] > 0) {
            [_outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] 
                    byExtendingSelection:NO];
            [_outlineView scrollRowToVisible:0];
        }
        
#if 0
        // Find string
        if ([webView searchFor:string direction:direction caseSensitive:NO wrap:YES]) {
            [_notFoundTextField setHidden:YES];
            if (flag) {
                [[self window] orderOut:self];
            }
        }
        else {
            [_notFoundTextField setHidden:NO];
            NSBeep();
        }
#endif
    }

#if 0
    // For SRSourceWindowController
    else if ([windowController isKindOfClass:[SRSourceWindowController class]]) {
        id textView=[(SRSourceWindowController*)windowController selectedTextView];
        if (!textView || ![textView respondsToSelector:@selector(searchFor:direction:caseSensitive:wrap:)]) {
            return;
        }
        
        // Find string
        if ([textView searchFor:string direction:direction caseSensitive:NO wrap:YES]) {
            [_notFoundTextField setHidden:YES];
            if (flag) {
                [[self window] orderOut:self];
            }
        }
        else {
            [_notFoundTextField setHidden:NO];
            NSBeep();
        }
    }
#endif
}

- (void)findNextAction:(id)sender
{
    // Find text
    if ([_foundRanges count] == 0) {
        [self findTextAction:sender];
        return;
    }
    
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
    }
    
    // For single tab
    else {
        // Select next row
        int row;
        row = [_outlineView selectedRow];
        if (row != -1) {
            row++;
            if (row >= [_foundRanges count]) {
                row = 0;
            }
            
            [_outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] 
                    byExtendingSelection:NO];
            [_outlineView scrollRowToVisible:row];
        }
    }
}

- (void)findPreviousAction:(id)sender
{
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
    }
    
    // For single tab
    else {
        // Select next row
        int row;
        row = [_outlineView selectedRow];
        if (row != -1) {
            row--;
            if (row < 0) {
                row = [_foundRanges count] - 1;
            }
            
            [_outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] 
                    byExtendingSelection:NO];
            [_outlineView scrollRowToVisible:row];
        }
    }
}

- (void)useSelectionForFindAction:(id)sender
{
    // Get web view
    WebView*    webView;
    webView = [self frontWebView];
    if (webView) {
        // Get selected string
        NSString*   selectedString;
        selectedString = [webView selectedString];
        if (selectedString) {
            [self setStringValue:selectedString];
        }
        
        return;
    }
#if 0
    // For SRSourceWindowController
    if ([windowController isKindOfClass:[SRSourceWindowController class]]) {
        id textView=[(SRSourceWindowController*)windowController selectedTextView];
        if (!textView || ![textView respondsToSelector:@selector(searchFor:direction:caseSensitive:wrap:)]) {
            return;
        }
        
        // Get selected string
        NSRange range;
        range = [textView selectedRange];
        if (range.length == 0) {
            return;
        }
        
        NSString*   selectedString;
        selectedString = [[[textView textStorage] string] substringWithRange:range];
        if (selectedString) {
            [self setStringValue:selectedString];
        }
        
        return;
    }
#endif
}

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

- (int)outlineView:(NSOutlineView*)outlineView 
        numberOfChildrenOfItem:(id)item
{
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
        if (!item) {
            return [_foundRanges count];
        }
        return [item count];
    }
    
    // For single tab
    else {
        if (!item) {
            return [_foundRanges count];
        }
    }
    
    return 0;
}

- (id)outlineView:(NSOutlineView*)outlineView 
        child:(int)index 
        ofItem:(id)item
{
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
        if (!item) {
            return [_foundRanges objectAtIndex:index];
        }
        return [item objectAtIndex:index];
    }
    
    // For single tab
    else {
        if (!item) {
            return [_foundRanges objectAtIndex:index];
        }
    }
    
    return nil;
}

- (BOOL)outlineView:(NSOutlineView*)outlineView 
        isItemExpandable:(id)item
{
    return [item isKindOfClass:[NSArray class]];
}

- (id)outlineView:(NSOutlineView*)outlineView 
        objectValueForTableColumn:(NSTableColumn*)tableColumn 
        byItem:(id)item
{
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
    }
    
    // For single tab
    else {
        // For rangeDict
        if ([item isKindOfClass:[NSDictionary class]]) {
            // Get data and range
            NSString*   data;
            NSRange     range;
            data = [item objectForKey:@"data"];
            range = [[item objectForKey:@"range"] rangeValue];
            
            // Create attributed string
            NSMutableAttributedString*  attrStr;
            attrStr = [[[NSMutableAttributedString alloc] initWithString:data] autorelease];
            [attrStr addAttribute:NSFontAttributeName 
                    value:[NSFont boldSystemFontOfSize:11] range:range];
            
            return attrStr;
        }
    }
    
    return nil;
}

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

- (void)outlineViewSelectionDidChange:(NSNotification*)notification
{
    NSUserDefaults* defaults;
    defaults = [NSUserDefaults standardUserDefaults];
    
    // Get selected item
    id  item;
    item = [_outlineView itemAtRow:[_outlineView selectedRow]];
    
    // For all tabs
    if ([[NSUserDefaults standardUserDefaults] boolForKey:SRFindInAllTabs]) {
    }
    
    // For single tab
    else {
        if ([item isKindOfClass:[NSDictionary class]]) {
            // Get DOM range
            DOMRange*   domRange;
            domRange = [item objectForKey:@"domRange"];
            
            // Check web view
            WebView*    webView;
            webView = [self frontWebView];
            if ([[webView DOMDocuments] containsObject:
                    [[domRange startContainer] ownerDocument]])
            {
                // Select range
                //[defaults setBool:YES forKey:HMUseAltHighlightColor];
                [webView setSelectedDOMRange:domRange affinity:NSSelectionAffinityDownstream];
                [NSApp sendAction:@selector(jumpToSelection:) to:nil from:self];
                //[defaults setBool:NO forKey:HMUseAltHighlightColor];
            }
        }
    }
}

//--------------------------------------------------------------//
#pragma mark -- Menu item validation --
//--------------------------------------------------------------//

- (BOOL)validateMenuItem:(id<NSMenuItem>)menuItem
{
    // Get tag
    int tag;
    tag = [menuItem tag];
    
    switch (tag) {
    case SRCloseWindowTag: {
        [menuItem setKeyEquivalent:@"w"];
        [menuItem setKeyEquivalentModifierMask:NSCommandKeyMask];
        return YES;
    }
    case SRCloseTabTag: {
        [menuItem setKeyEquivalent:@""];
        return YES;
    }
    }
    
    return YES;
}

//--------------------------------------------------------------//
#pragma mark -- NSWindow delegate --
//--------------------------------------------------------------//

- (void)windowDidBecomeKey:(NSNotification*)notification
{
    // Update file menu
    [SRFileMenu() update];
}

@end
