//
//  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
 
 */

#include <sys/types.h>
#include <sys/sysctl.h>
#import "FSAAppList.h"
#import "FSAnywhere.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 * const FSATableColumnIdentifier_always = @"always";

NSString *FSACheckMarkCharacter;
NSImage *FSACheckMarkImage;

NSString *FSAEllipsisCharacter;
NSImage *FSAEllipsisImage;

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

static int sysctlbyname_with_pid (const char *name, pid_t pid, 
				  void *oldp, size_t *oldlenp, 
				  void *newp, size_t newlen);
int is_pid_native (pid_t pid);

@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);
    }
    FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
    FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
    if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
        [FSAEllipsisImage release];
        FSAEllipsisImage = nil;
    }
}

- (void)awakeFromNib;
{
    NSWindow *window = [tableView window];
    
    processManager = [DeVercruesseProcessManager defaultManager];
    cocoaApps = [[NSMutableArray alloc] init];
    patchedApps = [[NSMutableSet alloc] init];
    patchingApps = [[NSMutableSet alloc] init];
    appsByPID = [[NSMutableDictionary alloc] init];
    alwaysApps = [[NSMutableArray array] retain];
    
    [[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])];
    [tableView setTarget:self];
    [tableView setAction:@selector(clickInTable:)];
    
    [self update];
    [window makeFirstResponder: tableView];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
					     selector:@selector(update)
						 name:NSUserDefaultsDidChangeNotification
					       object:nil];
}

- (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)_processStatusChanged;
{
    [tableView reloadData];
//    [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
}

- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
{
    return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
}

- (void)didPatchProcessID:(pid_t)pid;
{
    DeVercruesseProcess *app = [self _applicationForPID: pid];
    if(app){
	[patchingApps removeObject: app];
	[patchedApps addObject: app];
    }
    [self _processStatusChanged];
}

- (void)isPatchingProcessID:(pid_t)pid;
{
    [patchingApps addObject: [self _applicationForPID: pid]];
    [self _processStatusChanged];
}

- (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)){
	close(fd);
	return NO;
    } else
	close(fd);
    
    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);
}

-(BOOL)appIsNative:(DeVercruesseProcess *)app
{
    return is_pid_native([app pid]);
}

- (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]))) {
	if([self appIsNative:app]){
	    [cocoaApps addObject: app];
	    if(finishedLaunch){
		if([alwaysApps containsObject:[app name]] && ![patchedApps containsObject:app])
		    [NSApp installBundleInAppWithPID:[app pid]];
	    }
	}
    }
    [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] unneeded: [processManager processes] sends update
    
    allApps = [processManager processes];
    e = [allApps objectEnumerator];
    
    while ( (app = [e nextObject]) != nil) {
        [self addApp: app];
    }

    if([[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]){
	[[NSUserDefaults standardUserDefaults] synchronize];
	[alwaysApps removeAllObjects];
	[alwaysApps addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]];
	[tableView reloadData];
    }
    [tableView noteNumberOfRowsChanged];
    [self _processStatusChanged];
}

- (NSArray *)cocoaAppProcessIDs;
{
    NSEnumerator *e = [cocoaApps objectEnumerator];
    NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
    DeVercruesseProcess *app;
    while ( (app = [e nextObject]) != nil) {
        [pids addObject: [NSNumber numberWithInt: [app pid]]];
    }
    return pids;
}

- (void)applicationLaunchedWithProcessID:(pid_t)pid;
{
    if ([self _applicationForPID: pid] == nil) {
        [self update];
    }
}

- (void)applicationQuitWithProcessID:(pid_t)pid;
{
    DeVercruesseProcess *app = [self _applicationForPID: pid];
    
    if (app != nil) {
        [cocoaApps removeObject: app];
        [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
        [patchedApps removeObject: app];
    }
    
    [tableView noteNumberOfRowsChanged];
    [self _processStatusChanged];
}

@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) {
        DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
        canInstall = !([patchedApps containsObject: app] ||
                       [patchingApps containsObject: app]);
    }
    
    [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]) {
        DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
        if ([patchedApps containsObject: app]) {
            if (FSACheckMarkImage == nil)
                return FSACheckMarkCharacter;
            else
                return FSACheckMarkImage;
        }
        if ([patchingApps containsObject: app]) {
            if (FSAEllipsisImage == nil)
                return FSAEllipsisCharacter;
            else
                return FSAEllipsisImage;
        }
    } else if([columnIdentifier isEqualToString:FSATableColumnIdentifier_always]){
	if([alwaysApps containsObject:[[cocoaApps objectAtIndex: rowIndex] name]])
	    return [NSNumber numberWithBool:YES];
	else
	    return [NSNumber numberWithBool:NO];
    }
    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;
}

@end

@implementation FSAAppList (NSApplicationDelegate)

- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
{
    static NSMenu *dockMenu = nil;
    id<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: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
    } else if (![cocoaApps containsObject: frontApp]) {
        status = [NSString stringWithFormat: NSLocalizedString(@"Can't install because '%@' is not a Cocoa application", "Dock menu disabled item displayed when frontmost app not Cocoa, app name parameter"), appName];
    }
    
    if (status == nil) {
        menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), 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;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    id anApp;
    id e = [cocoaApps objectEnumerator];
    while(anApp = [e nextObject]){
	if([alwaysApps containsObject:[anApp name]]){
	    [NSApp installBundleInAppWithPID:[anApp pid]];
	}
    }
    finishedLaunch = YES;
}

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

-(void)clickInTable:(id)sender
{
    if([sender clickedColumn] == 2){
	if([sender clickedRow] >= 0){
	    NSString *appName = [[cocoaApps objectAtIndex:[sender clickedRow]] name];
	    if(![alwaysApps containsObject:appName]){
		[NSApp installBundleInAppWithPID:[[cocoaApps objectAtIndex:[sender clickedRow]] pid]];
		[alwaysApps addObject:appName];
	    } else
		[alwaysApps removeObject:appName];
	    [[NSUserDefaults standardUserDefaults] setObject:alwaysApps forKey:@"AlwaysApps"];
	}
    }
}

@end

static int sysctlbyname_with_pid (const char *name, pid_t pid, 
				  void *oldp, size_t *oldlenp, 
				  void *newp, size_t newlen)
{
    if (pid == 0) {
        if (sysctlbyname(name, oldp, oldlenp, newp, newlen) == -1)  {
            fprintf(stderr, "sysctlbyname_with_pid(0): sysctlbyname  failed:"
		    "%s\n", strerror(errno));
            return -1;
        }
    } else {
        int mib[CTL_MAXNAME];
        size_t len = CTL_MAXNAME;
        if (sysctlnametomib(name, mib, &len) == -1) {
            fprintf(stderr, "sysctlbyname_with_pid: sysctlnametomib  failed:"
		    "%s\n", strerror(errno));
            return -1;
        }
        mib[len] = pid;
        len++;
        if (sysctl(mib, len, oldp, oldlenp, newp, newlen) == -1)  {
            fprintf(stderr, "sysctlbyname_with_pid: sysctl  failed:"
                    "%s\n", strerror(errno));
            return -1;
        }
    }
    return 0;
}

int is_pid_native (pid_t pid)
{
    int ret = 0;
    size_t sz = sizeof(ret);
    
    if (sysctlbyname_with_pid("sysctl.proc_native", pid, 
			      &ret, &sz, NULL, 0) == -1) {
	if (errno == ENOENT) {
            // sysctl doesn't exist, which means that this version of Mac OS 
            // pre-dates Rosetta, so the application must be native.
            return 1;
        }
        fprintf(stderr, "is_pid_native: sysctlbyname_with_pid  failed:" 
                "%s\n", strerror(errno));
        return -1;
    }
    return ret;
}	
