//
//  ICeCoffEEServicePrefController.m
//  ICeCoffEE APE
//
//  Created by Nicholas Riley on Fri Jun 06 2003.
//  Copyright (c) 2003 Nicholas Riley. All rights reserved.
//

#import "ICeCoffEEShared.h"
#import "ICeCoffEEServices.h"
#import "ICeCoffEEServicePrefController.h"
#import "ICeCoffEEMenuOutlineView.h"
#import "ICeCoffEENonHighlightingButtonCell.h"
#import "ICeCoffEEInvertingTextFieldCell.h"
#import "ICeCoffEELabeledIconCell.h"
#import <objc/objc.h>
#import <ApplicationEnhancer/ApplicationEnhancer.h>
#import <QuartzCore/QuartzCore.h>

const int ICCF_SERVICE_UNKNOWN = 0;
const int ICCF_SERVICE_SHOWN = 1;
const int ICCF_SERVICE_HIDDEN = 2;
const int ICCF_SERVICE_MIXED = 3;

static NSDictionary *ICCF_SERVICE_OPTION_HIDDEN;

static float ICCF_TableViewCellHeight(NSTableView *tableView) {
    return ([tableView rowHeight] + [tableView intercellSpacing].height);
}

static NSMutableDictionary *keyEquivalents;
static NSDictionary *userKeyEquivalents;

static void ICCF_RemoveSingleKeyEquivalents() {
    NSMutableArray *singleKeys = [[NSMutableArray alloc] init];
    NSEnumerator *e = [[keyEquivalents allKeys] objectEnumerator];
    NSString *keyEquivalent;
    ICLog(@"before ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
    while ( (keyEquivalent = [e nextObject]) != nil) {
	if ([[keyEquivalents objectForKey: keyEquivalent] count] == 1)
	    [singleKeys addObject: keyEquivalent];
    }
    [keyEquivalents removeObjectsForKeys: singleKeys];
    [singleKeys release];
    ICLog(@"after ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
}

static inline unsigned ICCF_CountForKeyEquivalent(NSString *keyEquivalent) {
    if (keyEquivalent == nil) return 0;
    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
    return (setOrNil == nil) ? 0 : [setOrNil count];
}

static inline void ICCF_AddKeyEquivalentForItem(NSMenuItem *item) {
    NSString *keyEquivalent = [item toolTip];
    if (keyEquivalent == nil) return;
    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
    if (setOrNil == nil) return;
    [setOrNil addObject: item];
}

static inline void ICCF_RemoveKeyEquivalentForItem(NSMenuItem *item) {
    NSString *keyEquivalent = [item toolTip];
    if (keyEquivalent == nil) return;
    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
    if (setOrNil == nil) return;
    [setOrNil removeObject: item];
}

static int ICCF_GetServiceState(NSMenuItem *item) {
    return [item tag];
}

static void ICCF_SetServiceState(NSMenuItem *item, int state) {
    [item setTag: state];
}

static inline void ICCF_UpdateKeyEquivalentForItem(NSMenuItem *item, int state) {
    return; // XXX disabled until we can affect menubar Services menu(s)
    int oldState = ICCF_GetServiceState(item);
    if ((oldState == ICCF_SERVICE_UNKNOWN || oldState == ICCF_SERVICE_SHOWN) && state == ICCF_SERVICE_HIDDEN)
	ICCF_RemoveKeyEquivalentForItem(item);
    else if (oldState == ICCF_SERVICE_HIDDEN && (state == ICCF_SERVICE_SHOWN || state == ICCF_SERVICE_UNKNOWN))
	ICCF_AddKeyEquivalentForItem(item);
}


static inline NSCellStateValue ICCF_ServiceItemState(NSMenuItem *item) {
    int state = ICCF_GetServiceState(item);
    if (state == ICCF_SERVICE_HIDDEN)
	return NSOffState;
    if (state == ICCF_SERVICE_MIXED)
	    return NSMixedState;
    return NSOnState;
}

static void ICCF_PropagateServiceStateChange(NSMenu *menu, int state) {
    NSEnumerator *e = [[menu itemArray] objectEnumerator];
    NSMenuItem *item;
    NSMenu *submenu;

    while ( (item = [e nextObject]) != nil) {
        submenu = [item submenu];
        if (submenu != nil)
	    ICCF_PropagateServiceStateChange(submenu, state);
	else
	    ICCF_UpdateKeyEquivalentForItem(item, state);
	
	ICCF_SetServiceState(item, state);
    }
}

static NSCellStateValue ICCF_PropagateServiceState(NSMenuItem *item, NSMenuItem *changedItem) {
    NSMenu *submenu = [item submenu];
    if (submenu == nil) return ICCF_ServiceItemState(item);

    if (item == changedItem) ICCF_PropagateServiceStateChange(submenu, [item tag]);

    BOOL areOn = NO, areOff = NO;
    NSEnumerator *e = [[submenu itemArray] objectEnumerator];
    NSMenuItem *subItem;
    while ( (subItem = [e nextObject]) != nil) {
        switch (ICCF_PropagateServiceState(subItem, changedItem)) {
            case NSOnState: if (!areOff) { areOn = YES; continue; }
                break;
            case NSOffState: if (!areOn) { areOff = YES; continue; }
                break;
            case NSMixedState:
                break;
        }
	ICCF_SetServiceState(item, ICCF_SERVICE_MIXED);
        return NSMixedState;
    }
    if (areOn) {
	ICCF_SetServiceState(item, ICCF_SERVICE_SHOWN);
        return NSOnState;
    } else {
	ICCF_SetServiceState(item, ICCF_SERVICE_HIDDEN);
        return NSOffState;
    }
}

static NSMutableDictionary *ICCF_RetainedServiceOptionsDictionary(NSMenu *menu) {
    NSEnumerator *e = [[menu itemArray] objectEnumerator];
    NSMenuItem *item;
    NSMenu *submenu;
    NSMutableDictionary *dict = nil, *subDict = nil, *submenuDict = nil;

    while ( (item = [e nextObject]) != nil) {
        submenu = [item submenu];
        if (ICCF_ServiceItemState(item) == NSOffState) {
            subDict = [ICCF_SERVICE_OPTION_HIDDEN retain];
        } else if (submenu != nil) {
            submenuDict = ICCF_RetainedServiceOptionsDictionary(submenu);
            if (submenuDict == nil)
                continue;
            subDict = [[NSDictionary alloc] initWithObjectsAndKeys: submenuDict, kICServiceSubmenu, nil];
            [submenuDict release];
        } else continue;
        if (dict == nil) {
            dict = [[NSMutableDictionary alloc] init];
        }
        [dict setObject: subDict forKey: [item title]];
        [subDict release];
    }
    return dict;
}

static void ICCF_RestoreServiceOptionsDictionary(NSMenu *menu, NSDictionary *dict) {
    NSEnumerator *e = [dict keyEnumerator];
    NSString *itemTitle;
    NSDictionary *subDict, *submenuDict;
    NSMenuItem *item;
    NSMenu *submenu;

    // XXX handle exceptions
    while ( (itemTitle = [e nextObject]) != nil) {
        item = (NSMenuItem *)[menu itemWithTitle: itemTitle];
        if (item == nil) continue;
        subDict = [dict objectForKey: itemTitle];
        if ([[subDict objectForKey: (NSString *)kICServiceHidden] boolValue]) {
	    ICCF_SetServiceState(item, ICCF_SERVICE_HIDDEN);
	    ICCF_RemoveKeyEquivalentForItem(item);
        }
        if ( (submenu = [item submenu]) != nil) {
            submenuDict = [subDict objectForKey: (NSString *)kICServiceSubmenu];
            if ([submenuDict count] == 0)
                ICCF_PropagateServiceStateChange(submenu, ICCF_SERVICE_HIDDEN);
            else
                ICCF_RestoreServiceOptionsDictionary(submenu, submenuDict);
        }
    }
}

static void ICCF_AddServiceKeyEquivalentsAndIcons(NSMenu *menu, NSDictionary *serviceInfo) {
    if (serviceInfo == nil) return;
    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
    NSMenuItem *menuItem;
    NSMenu *submenu;
    NSDictionary *itemInfo = nil;
    while ( (menuItem = [enumerator nextObject]) != nil) {
        itemInfo = [serviceInfo objectForKey: [menuItem title]];
	if (itemInfo == nil) continue;
	
	if ( (submenu = [menuItem submenu]) != nil) {
	    ICCF_AddServiceKeyEquivalentsAndIcons(submenu, [itemInfo objectForKey: (NSString *)kICServiceSubmenu]);
	} else {
	    NSString *keyEquivalent = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceShortcut];
	    if (keyEquivalent == nil) {
		keyEquivalent = [userKeyEquivalents objectForKey: [menuItem title]];
	    } else if ([keyEquivalent length] != 1) {
		keyEquivalent = nil;
	    } else {
		// XXX Inconsistency between Cocoa and Carbon: always command-shift in Carbon, not in Cocoa.  Since we only patch Cocoa for the moment, keep as is.
		unichar key = [keyEquivalent characterAtIndex: 0];
		if ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember: key])
		    keyEquivalent = [NSString stringWithFormat: @"$@%c", key];
		else
		    keyEquivalent = [NSString stringWithFormat: @"@%c", key];
	    }
	    if (keyEquivalent != nil) {
		[menuItem setToolTip: keyEquivalent];
		NSMutableSet *equivalentItems = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
		if (equivalentItems == nil) {
		    equivalentItems = [[NSMutableSet alloc] initWithObjects: menuItem, nil];
		    [keyEquivalents setObject: equivalentItems forKey: keyEquivalent];
		    [equivalentItems release];
		} else {
		    [equivalentItems addObject: menuItem];
		}
	    }
	}

        NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath];
        if (bundlePath == NULL) continue;
        IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath);
        if (serviceIcon == NULL) continue;
        [menuItem _setIconRef: serviceIcon];
        ReleaseIconRef(serviceIcon);
    }
}

@implementation ICeCoffEEServicePrefController

#pragma mark class initialization

+ (void)initialize;
{
    ICCF_SERVICE_OPTION_HIDDEN = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool: YES], kICServiceHidden, nil];
}

#pragma mark initialize-release

- (id)initWithParentWindow:(NSWindow *)parent;
{
    if ( (self = [self initWithWindowNibName: @"Select services"])) {
        NSWindow *window = [self window]; // connect outlets
	[serviceOutline setAutoresizesOutlineColumn: NO];
	
        NSButtonCell *checkBoxCell = [[ICeCoffEENonHighlightingButtonCell alloc] init];
        [checkBoxCell setButtonType: NSSwitchButton];
        [checkBoxCell setImagePosition: NSImageOnly];
        [checkBoxCell setAllowsMixedState: YES];
	
        [[serviceOutline tableColumnWithIdentifier: @"show"] setDataCell: checkBoxCell];
        [checkBoxCell release];

        NSTextFieldCell *textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"service"] dataCell];
	[textFieldCell setWraps: YES];
	[[serviceOutline tableColumnWithIdentifier: @"service"] setDataCell:
	    [ICeCoffEELabeledIconCell copyFromTextFieldCell: textFieldCell]];
	
        textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"key"] dataCell];
        ((struct objc_object *)textFieldCell)->isa = [ICeCoffEEInvertingTextFieldCell class];
	
	[serviceOutline noteNumberOfRowsChanged]; // or we get no active scroll bar or initial selection

        [window setResizeIncrements: NSMakeSize(1, ICCF_TableViewCellHeight(serviceOutline))];
        if (parent != nil) {
            [NSApp beginSheet: window modalForWindow: parent modalDelegate: self didEndSelector: nil contextInfo: nil];
        } else {
            [window center];
            [window makeKeyAndOrderFront: nil];
        }
    }
    return self;
}

- (void)dealloc;
{
    [keyEquivalents release]; keyEquivalents = nil;
    [userKeyEquivalents release]; userKeyEquivalents = nil;
    [servicesMenu release]; servicesMenu = nil;
    [closedTriangle release]; closedTriangle = nil;
    [selectedClosedTriangle release]; selectedClosedTriangle = nil;
    [openTriangle release]; openTriangle = nil;
    [selectedOpenTriangle release]; selectedOpenTriangle = nil;
    [super dealloc];
}

#pragma mark actions

- (IBAction)showAll:(NSButton *)sender;
{
    ICCF_PropagateServiceStateChange(servicesMenu, 0);
    [serviceOutline reloadData];
}

- (IBAction)hideAll:(NSButton *)sender;
{
    ICCF_PropagateServiceStateChange(servicesMenu, ICCF_SERVICE_HIDDEN);
    [serviceOutline reloadData];
}

- (void)closeWithReturnCode:(int)returnCode;
{
    if ([[self window] isSheet]) {
        [NSApp endSheet: [self window] returnCode: NSRunAbortedResponse];
    }
    [self close];
}

- (IBAction)cancel:(NSButton *)sender;
{
    [self closeWithReturnCode: NSRunAbortedResponse];
}

- (IBAction)saveChanges:(NSButton *)sender;
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *icDefaults = [[defaults persistentDomainForName: (NSString *)kICBundleIdentifier] mutableCopy];
    NSDictionary *serviceOptions = ICCF_RetainedServiceOptionsDictionary(servicesMenu);
    if (serviceOptions != nil) {
        [icDefaults setObject: serviceOptions forKey: (NSString *)kICServiceOptions];
        [serviceOptions release];
    } else {
        [icDefaults removeObjectForKey: (NSString *)kICServiceOptions];
    }
    [defaults setPersistentDomain: icDefaults forName: (NSString *)kICBundleIdentifier];
    [defaults synchronize];
    APEMessageBroadcast(kICBundleIdentifier, kICPreferencesChanged, NULL);
    [icDefaults release];
    [self closeWithReturnCode: NSRunStoppedResponse];
}

@end

@implementation ICeCoffEEServicePrefController (NSOutlineViewDataSource)

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;
{
    if (servicesMenu == nil) {
	keyEquivalents = [[NSMutableDictionary alloc] init];
	userKeyEquivalents = [[[NSUserDefaults standardUserDefaults] dictionaryForKey: @"NSUserKeyEquivalents"] retain];
	servicesMenu = [[NSMenu alloc] initWithTitle: @""];
	ICCF_SetServicesMenu(servicesMenu);
	ICCF_AddServiceKeyEquivalentsAndIcons(servicesMenu, ICCF_GetServicesInfo());
	ICCF_RemoveSingleKeyEquivalents();
	
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSDictionary *icDefaults = [defaults persistentDomainForName: (NSString *)kICBundleIdentifier];
        NSDictionary *serviceOptions = [icDefaults objectForKey: (NSString *)kICServiceOptions];
        ICCF_RestoreServiceOptionsDictionary(servicesMenu, serviceOptions);
    }
    return [(item == nil ? servicesMenu : [item submenu]) numberOfItems];
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;
{
    return (item == nil ? YES : [item submenu] != nil);
}

- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item;
{
    return [(item == nil ? servicesMenu : [item submenu]) itemAtIndex: index];
}

NSAttributedString *ICCF_KeyEquivalentAttributedString(NSString *self, unsigned count);

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
{
    if ([[tableColumn identifier] isEqualToString: @"service"]) {
	static NSDictionary *attrDict = nil;
	if (attrDict == nil) { // XXX we leak this, but so does the Apple sample code...
	    NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
	    [style setLineBreakMode: NSLineBreakByTruncatingMiddle];
	    attrDict = [[NSDictionary alloc] initWithObjectsAndKeys: style, NSParagraphStyleAttributeName, nil];
	    [style release];
	}
        return [[[NSAttributedString alloc] initWithString: [item title] attributes: attrDict] autorelease];
    } else if ([[tableColumn identifier] isEqualToString: @"show"]) {
        int state = ICCF_GetServiceState(item);
        if ([item submenu] != nil && state == ICCF_SERVICE_UNKNOWN) {
            return [NSNumber numberWithInt: ICCF_PropagateServiceState(item, nil)];
        }
        return [NSNumber numberWithInt: ICCF_ServiceItemState(item)];
    } else if ([[tableColumn identifier] isEqualToString: @"key"]) {
        NSString *equivalent = [item toolTip];
        if (equivalent == nil)
	    return nil;
        return ICCF_KeyEquivalentAttributedString(equivalent, ICCF_CountForKeyEquivalent(equivalent));
    }
    return nil;
}

- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
{
    if ([[tableColumn identifier] isEqualToString: @"show"]) {
	int newState = [object boolValue] ? ICCF_SERVICE_SHOWN : ICCF_SERVICE_HIDDEN;
	ICCF_UpdateKeyEquivalentForItem(item, newState);
	ICCF_SetServiceState(item, newState);
        
        NSMenu *submenu = [item menu];
        if (submenu == servicesMenu) {
            ICCF_PropagateServiceState(item, item);
        } else {
            NSMenu *supermenu;
            while ( (supermenu = [submenu supermenu]) != servicesMenu) {
                submenu = supermenu;
            }
            ICCF_PropagateServiceState((NSMenuItem *)[supermenu itemAtIndex: [supermenu indexOfItemWithSubmenu: submenu]], item);
        }
        [outlineView reloadData];
    }
}

@end

@implementation ICeCoffEEServicePrefController (NSOutlineViewDelegate)

- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item;
{
    if (![[tableColumn identifier] isEqualToString: @"service"])
	return;
    [(ICeCoffEELabeledIconCell *)cell setIconRef: [item _iconRef]];
}

static NSImage *ICCF_InvertedImage(NSImage *image) {
    NSImage *invertedImage = [[NSImage alloc] initWithSize: [image size]];
    NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData: [image TIFFRepresentation]];
    CIImage *source = [[CIImage alloc] initWithBitmapImageRep: imageRep];
    CIColor *black = [[CIColor alloc] initWithColor: [NSColor blackColor]];
							     
    CIFilter *monochromeFilter = [CIFilter filterWithName: @"CIColorMonochrome"];
    [monochromeFilter setValue: source forKey: @"inputImage"]; 
    [monochromeFilter setValue: [NSNumber numberWithFloat: 1.0] forKey: @"inputIntensity"];
    [monochromeFilter setValue: black forKey: @"inputColor"];
    
    CIFilter *invertFilter = [CIFilter filterWithName: @"CIColorInvert"];
    [invertFilter setValue: [monochromeFilter valueForKey: @"outputImage"] forKey: @"inputImage"]; 
    
    CIFilter *maskToAlphaFilter = [CIFilter filterWithName: @"CIMaskToAlpha"];
    [maskToAlphaFilter setValue: [invertFilter valueForKey: @"outputImage"] forKey: @"inputImage"]; 
    
    [invertedImage lockFocus];
    CIContext *context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort]
						 options: nil];
    CIImage *result = [maskToAlphaFilter valueForKey: @"outputImage"];
    [context drawImage: result atPoint: CGPointZero fromRect: [result extent]];
    [invertedImage unlockFocus];

    [source release];
    [black release];

    return invertedImage;
}

- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item;
{
    static BOOL isInverted = NO;

    if (closedTriangle == nil) {
	closedTriangle = [[cell image] retain];
	openTriangle = [[cell alternateImage] retain];
	selectedClosedTriangle = ICCF_InvertedImage(closedTriangle);
	selectedOpenTriangle = ICCF_InvertedImage(openTriangle);
    }
    
    if (![outlineView isRowSelected: [outlineView rowForItem: item]] ||
	!ICCF_ViewHasKeyboardFocus(outlineView)) {
	if (!isInverted)
	    return;
	
	[cell setImage: closedTriangle];
	[cell setAlternateImage: openTriangle];
	isInverted = NO;
	return;
    }
    
    // not checking for isInverted is intentional - images reset when triangle is flipped
    [cell setImage: selectedClosedTriangle];
    [cell setAlternateImage: selectedOpenTriangle];
    isInverted = YES;
}

@end

@implementation ICeCoffEEServicePrefController (NSWindowDelegate)

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
{
    NSWindow *window = [serviceOutline window];
    NSRect frame = [window frame];
    NSScrollView *scrollView = [serviceOutline 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 = ICCF_TableViewCellHeight(serviceOutline);
        heightChange = (rowHeight * [serviceOutline 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 ICeCoffEEServicePrefController (NSWindowNotifications)

- (void)windowWillClose:(NSNotification *)notification;
{
    [self autorelease];
}

@end