/*
 * AuthenticationController.m
 *
 * $Header$
 *
 * Copyright 2004 Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 * require a specific license from the United States Government.
 * It is the responsibility of any person or organization contemplating
 * export to obtain such a license before exporting.
 * 
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#import "KerberosAgentController.h"
#import "AuthenticationController.h"
#import "ChangePasswordController.h"
#import "PrompterController.h"
#import "LifetimeSlider.h"
#import "LifetimeFormatter.h"
#import "ErrorAlert.h"

#define AuthControllerString(key) NSLocalizedStringFromTable (key, @"AuthenticationController", NULL)

@implementation AuthenticationController

// ---------------------------------------------------------------------------

- (id) init
{
    if ((self = [super initWithWindowNibName: @"AuthenticationController"])) {
        dprintf ("AuthenticationController initializing");
        
        // Set these to NULL so we don't free random memory on release
        result = klNoErr;
        preferences = NULL;
        state.doesMinimize = NO;
        state.isMinimized = NO;
        state.callerProvidedPrincipal = NULL;
        state.serviceNameString = NULL;
        state.callerNameString = NULL;
        state.callerIconImage = NULL;
        acquiredPrincipal = NULL;
        acquiredCacheName = NULL;
        
        preferences = [[Preferences sharedPreferences] retain];
        if (preferences == NULL) {
            [self release];
            return NULL;
        }
        
        state.lifetime          = [preferences defaultLifetime];
        state.startTime         = 0;
        state.forwardable       = [preferences defaultForwardable];
        state.proxiable         = [preferences defaultProxiable];
        state.addressless       = [preferences defaultAddressless];
        state.renewable         = [preferences defaultRenewable];
        state.renewableLifetime = [preferences defaultRenewableLifetime];

        acquiredCacheName = [[NSMutableString alloc] init];
        if (acquiredCacheName == NULL) {
            [self release];
            return NULL;
        }        
    }
    
    return self;
}

// ---------------------------------------------------------------------------

- (void) dealloc
{
    dprintf ("AuthenticationController deallocating");
    if (preferences                   != NULL) { [preferences release]; }
    if (state.callerProvidedPrincipal != NULL) { [state.callerProvidedPrincipal release]; }
    if (state.serviceNameString       != NULL) { [state.serviceNameString release]; }
    if (state.callerNameString        != NULL) { [state.callerNameString release]; }
    if (state.callerIconImage         != NULL) { [state.callerIconImage release]; }
    if (acquiredPrincipal             != NULL) { [acquiredPrincipal release]; }
    if (acquiredCacheName             != NULL) { [acquiredCacheName release]; }
    [super dealloc];
}

#pragma mark -- Loading --

// ---------------------------------------------------------------------------

- (void) windowDidLoad
{
    dprintf ("Entering AuthenticationController's windowDidLoad");
    
    // Message text
    if (state.callerNameString != NULL) {
        [headerTextField setStringValue: 
            [NSString stringWithFormat: AuthControllerString (@"AuthControllerApplicationRequest"), state.callerNameString]];
    } else {
        [headerTextField setStringValue: AuthControllerString (@"AuthControllerRequest")];
    }
    
    // Set up the dialog icon:
    NSImage *applicationIconImage = [NSImage imageNamed: @"NSApplicationIcon"];
    [kerberosIconImageView setImage: applicationIconImage];
    if (state.callerIconImage != NULL) {
        [kerberosIconImageView setBadgeImage: state.callerIconImage];
    }

    // remember the size of the window frame size with and without the options visible
    maximizedFrameHeight = [[[self window] contentView] frame].size.height;
    minimizedFrameHeight = [bannerBox frame].size.height;
        
    // Default Principal
    BOOL callerProvidedPrincipal = (state.callerProvidedPrincipal != NULL);
    
    [callerProvidedNameTextField  setEnabled: callerProvidedPrincipal];
    [callerProvidedRealmTextField setEnabled: callerProvidedPrincipal];
    [nameTextField                setEnabled: !callerProvidedPrincipal];
    [realmComboBox                setEnabled: !callerProvidedPrincipal];
    
    [callerProvidedNameTextField  setHidden: !callerProvidedPrincipal];
    [callerProvidedRealmTextField setHidden: !callerProvidedPrincipal];
    [nameTextField                setHidden: callerProvidedPrincipal];
    [realmComboBox                setHidden: callerProvidedPrincipal];
    
    if (callerProvidedPrincipal) {
#warning "No support for '\@' in realm names"
        NSString *principalString = [state.callerProvidedPrincipal displayStringForKLVersion: kerberosVersion_V5];
        NSRange separator = [principalString rangeOfString: @"@" options: (NSLiteralSearch | NSBackwardsSearch)];
        
        [callerProvidedNameTextField  setStringValue: [principalString substringToIndex: separator.location]];
        [callerProvidedRealmTextField setStringValue: [principalString substringFromIndex: separator.location + separator.length]];
    } else {
        [nameTextField setObjectValue: [preferences defaultName]];
        
        NSArray *realmsArray = [preferences realms];
        [realmComboBox removeAllItems];
        unsigned int i;
        for (i = 0; i < [realmsArray count]; i++) {
            NSString *realm = [realmsArray objectAtIndex: i];
            [realmComboBox addItemWithObjectValue: realm];
        }
        [realmComboBox selectItemWithObjectValue: [preferences defaultRealm]];
        [realmComboBox setObjectValue: [realmComboBox numberOfItems] > 0 ? [realmComboBox objectValueOfSelectedItem] : @""];
        [realmComboBox setNumberOfVisibleItems: [realmsArray count]];
        [realmComboBox setCompletes: YES];
    }   

    // If we have no username and the username field is editable, put the cursor there;
    // otherwise put in in the password text field.
    if (!callerProvidedPrincipal && ([[nameTextField stringValue] length] == 0)) {
        [[self window] setInitialFirstResponder: nameTextField];
    } else {
        [[self window] setInitialFirstResponder: passwordSecureTextField];
    }
}

#pragma mark -- Notifications --

// ---------------------------------------------------------------------------

- (void) controlTextDidChange: (NSNotification *) notification
{
    [self updateOKButtonState];
}

// ---------------------------------------------------------------------------

- (void) comboBoxSelectionDidChange: (NSNotification *) notification
{
    if ([notification object] == realmComboBox) {
        // For some reason the string value doesn't immediately get set for us but 
        // we need it for updateOKButton to do the right thing
        [realmComboBox setStringValue: [realmComboBox objectValueOfSelectedItem]];
        [self updateOKButtonState];
    }
}

// ---------------------------------------------------------------------------

- (void) comboBoxWillDismiss: (NSNotification *) notification
{
    [self updateOKButtonState];
}

// ---------------------------------------------------------------------------

- (void) windowDidBecomeKey: (NSNotification *) notification
{
    if (state.doesMinimize && ([notification object] == [self window])) {
        [self maximizeWindow];
    }
}

// ---------------------------------------------------------------------------

- (void) windowDidResignKey: (NSNotification *) notification
{
    // Only minimize if none of our windows are key anymore since we could be in the about box
    if (state.doesMinimize && ([notification object] == [self window]) && ([NSApp keyWindow] == NULL)) {
        [self minimizeWindow];
    }
}

#pragma mark -- Actions --

// ---------------------------------------------------------------------------

- (IBAction) changePassword: (id) sender
{
    Principal *dialogPrincipal = [self principal];
    if (dialogPrincipal != NULL) {
        ChangePasswordController *controller = [[ChangePasswordController alloc] initWithPrincipal: dialogPrincipal];
        if (controller != NULL) {
            [controller runSheetModalForWindow: [self window]];
            [controller release];
        }
    } else {
        [ErrorAlert alertForError: klBadPrincipalErr
                           action: KerberosChangePasswordAction
                   modalForWindow: [self window]];        
    }
}

// ---------------------------------------------------------------------------

- (IBAction) showOptions: (id) sender
{
    if (state.isMinimized) {
        NSLog (@"Warning! Calling toggleOptions while window is minimized!");
    }
    
    SetupLifetimeSlider (lifetimeSlider, lifetimeTextField,
                         [preferences lifetimeMaximum], [preferences lifetimeMinimum], 
                         state.lifetime);
    
    [forwardableCheckbox setState: state.forwardable ? NSOnState : NSOffState];
    [addresslessCheckbox setState: state.addressless ? NSOnState : NSOffState];
    [renewableCheckbox   setState: state.renewable   ? NSOnState : NSOffState];
    
    SetupLifetimeSlider (renewableSlider, renewableTextField,
                         [preferences renewableLifetimeMaximum], [preferences renewableLifetimeMinimum], 
                         state.renewableLifetime);
    
    [self renewableCheckboxWasHit: self];

    [NSApp beginSheet: optionsSheet 
       modalForWindow: [self window] 
        modalDelegate: self
       didEndSelector: NULL
          contextInfo: NULL];
}

// ---------------------------------------------------------------------------

- (IBAction) renewableCheckboxWasHit: (id) sender
{
    BOOL enabled = ([renewableCheckbox state] == NSOnState);
    NSColor *enabledColor = enabled ? [NSColor blackColor] : [NSColor grayColor];
    
    if ([renewableSlider maxValue] > [renewableSlider minValue]) {
        [renewableTextField  setTextColor: enabledColor];
        [renewableSlider     setEnabled: enabled];
    }
}

// ---------------------------------------------------------------------------

- (IBAction) optionsOK: (id) sender
{
    // Save dialog state to controller
    [self setForwardable: ([forwardableCheckbox state] == NSOnState)];
    [self setAddressless: ([addresslessCheckbox state] == NSOnState)];
    [self setRenewable:   ([renewableCheckbox   state] == NSOnState)];
    
    LifetimeFormatter *lifetimeFormatter = [lifetimeTextField formatter];
    [self setLifetime: [lifetimeFormatter lifetimeForControl: lifetimeSlider]];
    
    LifetimeFormatter *renewableFormatter =  [renewableTextField formatter];
    [self setRenewableLifetime: [renewableFormatter lifetimeForControl: renewableSlider]];

    [NSApp endSheet: optionsSheet returnCode: klNoErr];
    [optionsSheet orderOut: self];
}

// ---------------------------------------------------------------------------

- (IBAction) optionsCancel: (id) sender
{
    // Toss dialog state
    [NSApp endSheet: optionsSheet returnCode: klUserCanceledErr];
    [optionsSheet orderOut: self];
}

// ---------------------------------------------------------------------------

- (IBAction) ok: (id) sender
{
    // Get tickets
    KLStatus err = [self getTickets];

    if (err == klNoErr) {
        // If we got tickets, save dialog state to preferences
        [self loginOptionsToPreferences];
        
        [self stopWithCode: err];
    } else if (err != klUserCanceledErr) {
        [ErrorAlert alertForError: err
                           action: KerberosGetTicketsAction
                   modalForWindow: [self window]];
    }
}

// ---------------------------------------------------------------------------

- (IBAction) cancel: (id) sender
{
    [self stopWithCode: klUserCanceledErr];
}

#pragma mark -- Settings and Options --

// ---------------------------------------------------------------------------

- (void) setDoesMinimize: (BOOL) doesMinimize
{
    state.doesMinimize = doesMinimize;
}

// ---------------------------------------------------------------------------

- (void) setCallerNameString: (NSString *) callerNameString
{
    if (state.callerNameString != NULL) { [state.callerNameString release]; }
    state.callerNameString = [callerNameString retain];
}

// ---------------------------------------------------------------------------

- (void) setCallerIcon: (NSImage *) callerIconImage
{
    if (state.callerIconImage != NULL) { [state.callerIconImage release]; }
    state.callerIconImage = [callerIconImage retain];
}
// ---------------------------------------------------------------------------

- (void) setCallerProvidedPrincipal: (Principal *) callerProvidedPrincipal
{
    if (state.callerProvidedPrincipal != NULL) { [state.callerProvidedPrincipal release]; }
    state.callerProvidedPrincipal = [callerProvidedPrincipal retain];
}

// ---------------------------------------------------------------------------

- (void) setServiceName: (NSString *) serviceNameString
{
    if (state.serviceNameString != NULL) { [state.serviceNameString release]; }
    state.serviceNameString = [serviceNameString retain];
}

// ---------------------------------------------------------------------------

- (void) setStartTime: (time_t) startTime
{
    state.startTime = startTime;
}

// ---------------------------------------------------------------------------

- (void) setLifetime: (time_t) lifetime
{
    state.lifetime = lifetime;
}

// ---------------------------------------------------------------------------

- (void) setForwardable: (BOOL) forwardable
{
    state.forwardable = forwardable;
}

// ---------------------------------------------------------------------------

- (void) setProxiable: (BOOL) proxiable
{
    state.proxiable = proxiable;
}

// ---------------------------------------------------------------------------

- (void) setAddressless: (BOOL) addressless
{
    state.addressless = addressless;
}

// ---------------------------------------------------------------------------

- (void) setRenewable: (BOOL) renewable
{
    state.renewable = renewable;
}

// ---------------------------------------------------------------------------

- (void) setRenewableLifetime: (time_t) renewableLifetime
{
    state.renewableLifetime = renewableLifetime;
}

#pragma mark -- Miscellaneous --

// ---------------------------------------------------------------------------
// Note: may return NULL

- (Principal *) principal
{
    if (state.callerProvidedPrincipal != NULL) {
        return state.callerProvidedPrincipal;
    } else {
        NSString *principalString = [NSString stringWithFormat: @"%@@%@", [nameTextField stringValue], [realmComboBox stringValue]];
        return [[[Principal alloc] initWithString: principalString klVersion: kerberosVersion_V5] autorelease];
    }
}

// ---------------------------------------------------------------------------

- (void) updateOKButtonState
{
    BOOL havePrincipal = NO;
    
    if ([nameTextField isHidden]) {
        havePrincipal = ([[passwordSecureTextField stringValue] length] > 0);
    } else {
        havePrincipal = (([[nameTextField           stringValue] length] > 0) && 
                         ([[realmComboBox           stringValue] length] > 0) &&
                         ([[passwordSecureTextField stringValue] length] > 0));
    }
    
    [okButton setEnabled: havePrincipal];
}

// ---------------------------------------------------------------------------

- (BOOL) validateMenuItem: (id <NSMenuItem>) menuItem
{
    if (menuItem == changePasswordMenuItem) {
        return ([nameTextField isHidden] || 
                (([[nameTextField stringValue] length] > 0) && 
                 ([[realmComboBox stringValue] length] > 0)));
    } else {
        return YES;
    }
}

// ---------------------------------------------------------------------------

- (void) setWindowContentHeight: (float) newHeight
{
    NSRect contentRect = [[self window] contentRectForFrameRect: [[self window] frame]];
    float currentHeight = contentRect.size.height;
    
    if (newHeight != currentHeight) {
        contentRect.size.height = newHeight;
        
        NSRect frameRect = [[self window] frameRectForContentRect: contentRect];
        if (newHeight > currentHeight) {
            frameRect.origin.y -= (newHeight - currentHeight); // move down
        } else {
            frameRect.origin.y += (currentHeight - newHeight); // move up
        }
        [[self window] setFrame: frameRect display: YES animate: YES]; 
    }
}

// ---------------------------------------------------------------------------

- (void) minimizeWindow
{
    if (state.doesMinimize && !state.isMinimized) {
        // minimize view
        [okButton        setAutoresizingMask: NSViewMinYMargin];
        [cancelButton    setAutoresizingMask: NSViewMinYMargin];
        [gearPopupButton setAutoresizingMask: NSViewMinYMargin];
        
        [self setWindowContentHeight: minimizedFrameHeight];
        
        state.isMinimized = YES;
    }
}

// ---------------------------------------------------------------------------

- (void) maximizeWindow
{
    if (state.isMinimized) {
        // maximize view
        [self setWindowContentHeight: maximizedFrameHeight];
        
        [okButton        setAutoresizingMask: NSViewMinXMargin | NSViewMaxYMargin];
        [cancelButton    setAutoresizingMask: NSViewMinXMargin | NSViewMaxYMargin];
        [gearPopupButton setAutoresizingMask: NSViewMinXMargin | NSViewMaxYMargin];
                
        state.isMinimized = NO;
    }
}

// ---------------------------------------------------------------------------

- (void) loginOptionsToPreferences
{
    if ([preferences rememberPrincipalFromLastLogin]) {
        if (state.callerProvidedPrincipal != NULL) {
            [preferences setDefaultName:  [callerProvidedNameTextField stringValue]];
            [preferences setDefaultRealm: [callerProvidedRealmTextField stringValue]];
        } else {
            [preferences setDefaultName:  [nameTextField stringValue]];
            [preferences setDefaultRealm: [realmComboBox stringValue]];
        }
    }
    
    if ([preferences rememberOptionsFromLastLogin]) {
        [preferences setDefaultLifetime:          state.lifetime];
        [preferences setDefaultForwardable:       state.forwardable];
        [preferences setDefaultAddressless:       state.addressless];
        [preferences setDefaultRenewable:         state.renewable];
        [preferences setDefaultRenewableLifetime: state.renewableLifetime];
    }
}

// ---------------------------------------------------------------------------

- (int) getTickets
{
    KLStatus err = klNoErr;
    KLLoginOptions loginOptions = NULL;
    
    // Get Tickets:
    acquiredPrincipal = [[self principal] retain];
    if (acquiredPrincipal == NULL) { err = klBadPrincipalErr; }
    
    if (err == klNoErr) {
        err = KLCreateLoginOptions (&loginOptions);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetTicketLifetime (loginOptions, state.lifetime);
    }
    
    if (err == klNoErr) {
        if (state.renewable) {
            err = KLLoginOptionsSetRenewableLifetime (loginOptions, state.renewableLifetime);
        } else {
            err = KLLoginOptionsSetRenewableLifetime (loginOptions, 0L);
        }
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetTicketStartTime (loginOptions, state.startTime);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetServiceName (loginOptions, ((state.serviceNameString == NULL) ? 
                                                           NULL : [state.serviceNameString UTF8String]));
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetForwardable (loginOptions, state.forwardable);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetProxiable (loginOptions, state.proxiable);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetAddressless (loginOptions, state.addressless);
    }
    
    if (err == klNoErr) {
        __KLSetApplicationPrompter (GraphicalKerberosPrompter);
        err = [acquiredPrincipal getTicketsWithPassword: [passwordSecureTextField stringValue]
                                           loginOptions: loginOptions
                                              cacheName: acquiredCacheName];
        if (err == KRB5KDC_ERR_KEY_EXP) {
            // Ask the user if s/he wants to change the password
            NSString *question = AuthControllerString (@"AuthControllerStringPasswordExpired");
            BOOL changePassword = [ErrorAlert alertForYNQuestion: question
                                                          action: KerberosGetTicketsAction
                                                  modalForWindow: [self window]];
            if (changePassword) {
                ChangePasswordController *controller = 
                    [[ChangePasswordController alloc] initWithPrincipal: acquiredPrincipal];
                if (controller != NULL) {
                    if ([controller runSheetModalForWindow: [self window]] == klNoErr) {
                        err = [acquiredPrincipal getTicketsWithPassword: [controller newPassword]
                                                           loginOptions: loginOptions
                                                              cacheName: acquiredCacheName];                    
                        
                    }
                    [controller release];
                }
            }
        }
    }
    
    if (loginOptions != NULL) { KLDisposeLoginOptions (loginOptions); }
    
    return err;
}

// ---------------------------------------------------------------------------

- (Principal *) acquiredPrincipal
{
    return acquiredPrincipal;
}

// ---------------------------------------------------------------------------

- (NSString *) acquiredCacheName
{
    return acquiredCacheName;
}

// ---------------------------------------------------------------------------

- (int) runWindow
{
    [[NSApp delegate] addActiveWindow: [self window]];
    
    // Popup or not:
    if (state.doesMinimize) {
        [self minimizeWindow];
        
        NSPoint frameOriginPoint;
        NSRect sFrameRect = [[NSScreen mainScreen] frame];
        NSSize wFrameSize = [[self window] frame].size;
        frameOriginPoint.x = ((sFrameRect.origin.x + sFrameRect.size.width)  - wFrameSize.width  - 100);
        frameOriginPoint.y = ((sFrameRect.origin.y + sFrameRect.size.height) - wFrameSize.height - 100);
        
        [[self window] setFrameOrigin: frameOriginPoint];
        
    } else {
        [self maximizeWindow];
        [[self window] center];
    }

    [self showWindow: self];
    [NSApp run];
    [self close];
    
    [[NSApp delegate] removeActiveWindow: [self window]];
    
    return result;
}

// ---------------------------------------------------------------------------

- (void) stopWithCode: (int) returnCode
{
    result = returnCode;
    [NSApp stop: self];
}

@end
