//
//  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 "FSAnywhere.h"
#import "FSAAppList.h"
#import "FSAApp.h"
#import "libMatch.h"
#import "DeVercruesseProcessManager.h"
#import "NJRLabeledImageCell.h"
#import "NSTableView-NJRExtensions.h"

// for appIsPEF:
#import <Carbon/Carbon.h>
#import <fcntl.h>
#import <unistd.h>

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

NSString *FSACheckMarkCharacter;
NSImage *FSACheckMarkImage;

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

@implementation FSAAppList

+ (void)initialize;
{
    FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
    FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
    if (FSACheckMarkImage == nil) {
        FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
        if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
            [FSACheckMarkImage release];
            FSACheckMarkImage = nil;
        }
        FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
    }
}

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

    [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
        setDataCell: [NJRLabeledImageCell cell]];
    if (FSACheckMarkImage != nil)
        [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
            setDataCell: [[[NSImageCell alloc] init] autorelease]];
    [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];

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

- (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)appIsPEF:(DeVercruesseProcess *)app;
{
    NSString *bundleExecutableLoc = [app executableLoc];
    const char *bundleExecutablePath;
    int fd;
    PEFContainerHeader pefHeader;

    if (bundleExecutableLoc == NULL)
        return NO;
    
    if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
	return NO;

    if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
        return NO;

    if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader))
	return NO;
    
    if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
	return NO;

    return YES;
}

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

- (void)addApp:(DeVercruesseProcess *)app;
{
    /* Try to determine if the application is a foreground Cocoa application.
       In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
       but we don't support such hybrids because the menu items we add depend
       on Cocoa dispatch mechanisms.
    
       The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
       10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
       are Carbon apps.  So we use some code extracted from otool to check
       whether the application links to the Foundation, AppKit or Cocoa
       frameworks.  This problem is fixed in Jaguar, except that certain CFM
       Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
       Conversely, the appIsCocoa: code works on _most_ applications, but
       Jaguar always correctly identifies Cocoa apps as isCocoa.

       So, our checks go like this:
         Is the application background-only?
         Is the application Cocoa or Carbon according to the CPS flavor?
           If it's Cocoa, is it a CFM app?  If so, CPS is lying to us.
           If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
             If so, it's really a Cocoa app.  If not, it's a Carbon app.
    
       Be careful not to call appIsCocoa: on a Classic application, you will
       crash.
    */

    /*
    if ([app isCocoa] || [app isCarbon]) {
        NSLog(@"%@ |%@%@%@%@%@", [app name],
              [app isBackgroundOnly] ? @" bgOnly" : @"",
              [app isCocoa] ? @" isCocoa" : @"",
              [app isCarbon] ? @" isCarbon" : @"",
	      [self appIsPEF: app] ? @" appIsPEF" : @"",
              [self appIsCocoa: app] ? @" appIsCocoa" : @"");
    }
    */
          
    if ( ![app isBackgroundOnly] &&
	 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
	   ( [app isCarbon] && [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]) {
        BOOL isPatched = [patchedApps containsObject: [cocoaApps objectAtIndex: rowIndex]];
        if (FSACheckMarkImage == nil)
            return isPatched ? FSACheckMarkCharacter : @"";
        else
            return isPatched ? FSACheckMarkImage : nil;
    }
    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
