//
//  FSAAppList.m
//  F-Script Anywhere
//
//  Created by Nicholas Riley on Fri Feb 01 2002.
//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
//

/*

 F-Script Anywhere is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 F-Script Anywhere is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with F-Script Anywhere; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/

#import "FSAAppList.h"
#import "FSAApp.h"
#import "libMatch.h"
#import "DeVercruesseProcessManager.h"
#import "NJRLabeledImageCell.h"
#import "NSTableView-NJRExtensions.h"

NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";

NSString *FSACheckMarkCharacter;

static const char *FSACocoaFrameworks[] = {
    "/System/Library/Frameworks/AppKit.framework",
    "/System/Library/Frameworks/Foundation.framework",
    "/System/Library/Frameworks/Cocoa.framework",
    NULL
};

@implementation FSAAppList

+ (void)load;
{
    FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
}

- (void)awakeFromNib;
{
    processManager = [DeVercruesseProcessManager defaultManager];
    cocoaApps = [[NSMutableArray alloc] init];
    patchedApps = [[NSMutableSet alloc] init];
    appsByPID = [[NSMutableDictionary alloc] init];

    [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
        setDataCell: [NJRLabeledImageCell cell]];
    [[tableView window] setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];

    [self update];
    [[tableView window] makeFirstResponder: tableView];
}

- (void)dealloc;
{
    // don't release processManager, we don't own it
    [cocoaApps release];
    [patchedApps release];
    [appsByPID release];
    [super dealloc];
}

- (pid_t)selectedProcessID;
{
    int row = [tableView selectedRow];
    if (row == -1) return -1;

    return [[cocoaApps objectAtIndex: row] pid];
}

- (void)didPatchProcessID:(pid_t)pid;
{
    [patchedApps addObject: [appsByPID objectForKey: [NSNumber numberWithInt: pid]]];
    [tableView reloadData];
    [installButton setEnabled: NO];
}

- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
{
    NSString *bundleExecutableLoc = [app executableLoc];
    if (bundleExecutableLoc == NULL)
        return NO;
    return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
}

- (void)addApp:(DeVercruesseProcess *)app;
{
    if ( ([app isCarbon] || [app isCocoa]) // XXX OS X 10.1.2 (and earlier?) bug, Cocoa reported as Carbon
         && ![app isBackgroundOnly]
         && [self appIsCocoa: app]) {
        [cocoaApps addObject: app];
    }
    [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
}

// XXX should insert/resort on launch too; this is harder because of synchronization issues

- (void)update;
{
    NSEnumerator *e;
    NSArray *allApps;
    DeVercruesseProcess *app;

    [cocoaApps removeAllObjects];
    [appsByPID removeAllObjects];
    // [processManager update]; // [processManager processes] sends update

    allApps = [processManager processes];
    e = [allApps objectEnumerator];

    while ( (app = [e nextObject]) != nil) {
        [self addApp: app];
    }

    [tableView noteNumberOfRowsChanged];
    [tableView reloadData];
    // XXX this is broken, it doesn't update properly
    [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
}

- (void)applicationLaunchedWithProcessID:(pid_t)pid;
{
    NSNumber *pidNum = [NSNumber numberWithInt: pid];
    DeVercruesseProcess *app = [appsByPID objectForKey: pidNum];

    if (app == nil) {
        [self update];
    }
}

- (void)applicationQuitWithProcessID:(pid_t)pid;
{
    NSNumber *pidNum = [NSNumber numberWithInt: pid];
    DeVercruesseProcess *app = [appsByPID objectForKey: pidNum];

    if (app != nil) {
        [cocoaApps removeObject: app];
        [appsByPID removeObjectForKey: pidNum];
        [patchedApps removeObject: app];
    }

    [tableView reloadData];
}

@end

@implementation FSAAppList (NSTableViewDelegate)

- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
{
    if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
        DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];

        NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
        [(NJRLabeledImageCell *)cell setImage: [app img]];
        [(NJRLabeledImageCell *)cell setImageCacheSource: app];
    }
}

- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
{
    BOOL canInstall = NO;

    if (row != -1) {
        canInstall = ![patchedApps containsObject: [cocoaApps objectAtIndex: row]];
    }

    [installButton setEnabled: canInstall];
    
    return YES;
}

@end

@implementation FSAAppList (NSTableDataSource)

- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
    return [cocoaApps count];
}

- (id)tableView:(NSTableView *)aTableView
    objectValueForTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    NSString *columnIdentifier = [aTableColumn identifier];
    
    if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
        return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
    } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
        return [patchedApps containsObject: [cocoaApps objectAtIndex: rowIndex]] ? FSACheckMarkCharacter : @"";
    }
    return nil;
}

@end

@implementation FSAAppList (NSWindowDelegate)

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
{
    NSWindow *window = [tableView window];
    NSRect frame = [window frame];
    NSScrollView *scrollView = [tableView enclosingScrollView];
    float displayedHeight = [[scrollView contentView] bounds].size.height;
    float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
    float heightExcess;

    if (heightChange >= 0 && heightChange <= 1) {
        // either the window is already optimal size, or it's too big
        float rowHeight = [tableView cellHeight];
        heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
    }

    frame.size.height += heightChange;

    if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
         (heightExcess = [window maxSize].height - frame.size.height) < 1) {
        heightChange += heightExcess;
        frame.size.height += heightExcess;
    }

    frame.origin.y -= heightChange;

    return frame;
}

- (BOOL)windowShouldClose:(id)sender;
{
    if ([patchedApps count] != 0) {
        NSMutableString *message =  [@"F-Script Anywhere is installed in the following applications:\n\n" mutableCopy];
        NSEnumerator *e = [patchedApps objectEnumerator];
        DeVercruesseProcess *app;
        int retval;

        while ( (app = [e nextObject]) != nil) {
            [message appendFormat: @"    ¥ %@\n", [app name]];
        }

        [message appendString: @"\nIf F-Script Anywhere quits now, these applications will be forced to quit, and any changes you have made in them will be lost.\n\nPlease quit these applications before quitting F-Script Anywhere, or click Force Quit to continue."];

        retval = NSRunAlertPanel(@"Force applications to quit?", message, @"DonÕt Quit", @"Force Quit", nil);
        if (retval != NSAlertAlternateReturn) {
            return NO;
        }

        [(FSAApp *)NSApp unloadBundles: self];
    }

    return YES;
}

@end

@implementation FSAAppList (NSApplicationDelegate)

- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
{
    static NSMenu *dockMenu = nil;
    NSMenuItem *menuItem;
    DeVercruesseProcess *frontApp = [processManager frontProcess];
    NSString *appName = [frontApp name];
    NSString *status = nil;
    // XXX workaround for broken dock menu sender
    NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
    NSInvocation *inv;

    if (dockMenu != nil) {
        // XXX release invocation
        [[[dockMenu itemAtIndex: 0] target] release];
        [dockMenu removeItemAtIndex: 0];
    } else {
        dockMenu = [[NSMenu alloc] init];
    }

    NSAssert(frontApp != nil && appName != nil, @"CanÕt obtain information on the frontmost application");

    if ([patchedApps containsObject: frontApp]) {
        status = [NSString stringWithFormat: @"Installed in Ò%@Ó", appName];
    } else if (![cocoaApps containsObject: frontApp]) {
        status = [NSString stringWithFormat: @"CanÕt install because Ò%@Ó is not a Cocoa application", appName];
    }

    if (status == nil) {
        menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: @"Install in Ò%@Ó", appName]
                                       action: @selector(invoke)
                                keyEquivalent: @""];
        inv = [NSInvocation invocationWithMethodSignature: sig];
        [inv setSelector: @selector(installBundleInFrontmostApp:)];
        [inv setTarget: NSApp];
        [inv setArgument: &menuItem atIndex: 2];
        [menuItem setTag: [frontApp pid]];
        [menuItem setTarget: [inv retain]];
    } else {
        menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
        [menuItem setEnabled: NO];
    }

    return dockMenu;
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
{
    return YES;
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
{
    return ([self windowShouldClose: self] ? NSTerminateNow : NSTerminateCancel);
}

@end