//
//  ApplicationController.m
//  CyTV Client
//
//  Created by Andreas Junghans on Sun Oct 26 2003.
//  Copyright (c) 2003-2004 Andreas Junghans. All rights reserved.
//

#import "ApplicationController.h"
#import "StreamsDataSource.h"
#import "BackgroundHelper.h"
#import <Carbon/Carbon.h>

static NSString* _serverKeyFile = @"~/Library/Application Support/CyTV/server_keys";

@implementation ApplicationController


/**
 * Initializes this instance.
 */

- (id)init {
    if (self = [super init]) {
        _urlFetcher = [[URLFetcher alloc] init];
        NSMutableDictionary* hotKeyMap = [[NSMutableDictionary alloc] init];
        EventHotKeyID hotKeyID = {0, 0};
        EventHotKeyRef ref;
        RegisterEventHotKey(0x7c, cmdKey+optionKey, hotKeyID, GetApplicationEventTarget(), 0, &ref);
        [hotKeyMap setObject:[NSNumber numberWithInt:hotKeySkipForward]
                   forKey:[NSNumber numberWithInt:(int)ref]];
        RegisterEventHotKey(0x7b, cmdKey+optionKey, hotKeyID, GetApplicationEventTarget(), 0, &ref);
        [hotKeyMap setObject:[NSNumber numberWithInt:hotKeySkipBackward]
                   forKey:[NSNumber numberWithInt:(int)ref]];
        RegisterEventHotKey(0x7e, cmdKey+optionKey, hotKeyID, GetApplicationEventTarget(), 0, &ref);
        [hotKeyMap setObject:[NSNumber numberWithInt:hotKeyChannelUp]
                   forKey:[NSNumber numberWithInt:(int)ref]];
        RegisterEventHotKey(0x7d, cmdKey+optionKey, hotKeyID, GetApplicationEventTarget(), 0, &ref);
        [hotKeyMap setObject:[NSNumber numberWithInt:hotKeyChannelDown]
                   forKey:[NSNumber numberWithInt:(int)ref]];
        _hotKeyMap = hotKeyMap;
        _currentStreamIndex = -1;
        return self;
    }
    return nil;
}


/**
 * Returns a hot key code from an event.
 */

- (int)getHotKey:(NSEvent *)event {
    return [[_hotKeyMap objectForKey:[NSNumber numberWithInt:[event data1]]]
            intValue];
}


/**
 * Hot key processing.
 */

- (void)processHotKey:(NSEvent *)theEvent {
    if (_backgroundHelper != nil) {
        int hotKey = [self getHotKey:theEvent];
        if (hotKey == hotKeySkipForward) {
            [_backgroundHelper skip:[preferencesController skipForwardTime]];
            [self setOverlay:overlaySkipForward];
        } else if (hotKey == hotKeySkipBackward) {
            [_backgroundHelper skip:-[preferencesController skipBackTime]];
            [self setOverlay:overlaySkipBackward];
        } else if (hotKey == hotKeyChannelUp) {
            [_backgroundHelper channelUp];
            [self setOverlay:overlayChannelUp];
        } else if (hotKey == hotKeyChannelDown) {
            [_backgroundHelper channelDown];
            [self setOverlay:overlayChannelDown];
        }
    }
}


/**
 * Start the background helper thread.
 */

- (void)initBackgroundHelperWithBaseURL: (NSString*)baseURL
                               clientId: (NSString*)clientId
                               cacheURL: (NSString*)cacheURL {
    NSPort* port1;
    NSPort* port2;
    NSArray* paramsArray;
    
    _baseURL = [baseURL retain];
    _clientId = [clientId retain];
    
    port1 = [NSPort port];
    port2 = [NSPort port];
    _backgroundHelperConnection = [[NSConnection alloc]
                                   initWithReceivePort: port1
                                   sendPort: port2];
    [_backgroundHelperConnection setRootObject: self];
    paramsArray = [NSArray arrayWithObjects: port2, port1, baseURL, clientId,
                                             cacheURL, nil];
    
    [NSThread detachNewThreadSelector: @selector(connectWithParams:)
              toTarget: [BackgroundHelper class]
              withObject: paramsArray];
}


/**
 * Try to start the info collector in the background helper thread.
 * If _backgroundHelper is still nil, nothing happens (but as soon
 * as the helper object is available, the collector is started).
 *
 * This method doesn't perform any immediate UI updates.
 */

- (void)startInfoCollector {
    _infoCollectorRunning = TRUE;
    [_backgroundHelper startInfoCollector];
}


/**
 * Stop the info collector in the background helper thread.
 *
 * This method doesn't perform any immediate UI updates.
 */

- (void)stopInfoCollector {
    _infoCollectorRunning = FALSE;
    [_backgroundHelper stopInfoCollector];
}


/**
 * Try to start the channel scanner in the background helper thread.
 * If _backgroundHelper is still nil, nothing happens (but as soon
 * as the helper object is available, the scanner is started).
 *
 * This method doesn't perform any immediate UI updates.
 */

- (void)startChannelScanner {
    [channelUpButton setEnabled: FALSE];
    [channelDownButton setEnabled: FALSE];
    [channelPopUpButton setEnabled: FALSE];
    [recordButton setEnabled: FALSE];
    _channelScannerRunning = TRUE;
    [_backgroundHelper startChannelScanner];
    [scanButton setState: NSOnState];
}


/**
 * Stop the channel scanner in the background helper thread.
 *
 * This method doesn't perform any immediate UI updates.
 */

- (void)stopChannelScanner {
    _channelScannerRunning = FALSE;
    [_backgroundHelper stopChannelScanner];
    [recordButton setEnabled: TRUE];
    [channelUpButton setEnabled: TRUE];
    [channelDownButton setEnabled: TRUE];
    if ([channelPopUpButton numberOfItems] > 1) {
        [channelPopUpButton setEnabled: TRUE];
    }
    [scanButton setState: NSOffState];
}


/**
 * Helper method to start/stop background services
 * in case their current state doesn't match the
 * expected one. For example, when the application
 * is not active, the info collector is stopped. When
 * the application becomes active again, calling
 * checkServices ensures that the expected state of
 * the info collector (and other services) is restored.
 */

- (void)checkServices {
    if (_infoCollectorRunning) {
        [_backgroundHelper startInfoCollector];
    } else {
        [_backgroundHelper stopInfoCollector];
    }
    if (_channelScannerRunning) {
        [_backgroundHelper startChannelScanner];
    } else {
        [_backgroundHelper stopChannelScanner];
    }
}


/**
 * This method tells us that the background helper is ready.
 * If requested to run but not already running, the info collector
 * and the channel scanner are started here.
 */

- (void)setServer:(id)serverObject {
    [serverObject setProtocolForProxy: @protocol(BackgroundHelperProtocol)];
    _backgroundHelper = (id <BackgroundHelperProtocol>) [serverObject retain];
    [self checkServices];
}


/**
 * Updates the states of all buttons of the remote control
 * according to the current state.
 */

- (void)updateButtonsWithRecordingState:(BOOL)recordingState {
    if (recordingState) {
        if (_channelScannerRunning) {
            [self stopChannelScanner];
        }
        [scanButton setEnabled: FALSE];
        [scanButton setState: NSOffState];
        [recordButton setEnabled: TRUE];
        [recordButton setState: NSOnState];
        [channelPopUpButton setEnabled: FALSE];
        [channelUpButton setEnabled: FALSE];
        [channelDownButton setEnabled: FALSE];
    } else {
        [channelDownButton setEnabled: TRUE];
        [channelUpButton setEnabled: TRUE];
        [recordButton setState: NSOffState];
        [scanButton setEnabled: TRUE];
        if (_channelScannerRunning) {
            [recordButton setEnabled: FALSE];
            [channelUpButton setEnabled: FALSE];
            [channelDownButton setEnabled: FALSE];
            [channelPopUpButton setEnabled: FALSE];
            [scanButton setState: NSOnState];
        } else {
            [recordButton setEnabled: TRUE];
            [channelUpButton setEnabled: TRUE];
            [channelDownButton setEnabled: TRUE];
            if ([channelPopUpButton numberOfItems] > 1) {
                [channelPopUpButton setEnabled: TRUE];
            }
            [scanButton setState: NSOffState];
        }
    }
}


/**
 * Updates the channel selection.
 */

- (void)updateSelectedChannel:(int)channel {
    int index = [[channelPopUpButton menu] indexOfItemWithTag: channel];
    if (index != -1) {
        [channelPopUpButton selectItemAtIndex: index];
    }
}


/**
 * Initialize the formatters for the recording listing.
 */

- (void)initStreamInfo {
	NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
	NSDateFormatter* formatter = [[[NSDateFormatter alloc]
								   initWithDateFormat:
								    [NSString stringWithFormat:@"%@ %@",
									 [userDefaults stringForKey:NSShortDateFormatString],
									 [userDefaults stringForKey:NSTimeFormatString]]
								   allowNaturalLanguage:YES]
								  autorelease];
	[[streamsDateColumn dataCell] setFormatter:formatter];
    [self sortStreamsByColumn:streamsDateColumn];
}


/**
 * Update the user interface (initiated by the background helper
 * thread).
 */

- (void)updateStreamInfo:(NSString*)info cacheState:(double)cacheState {
    // always update the cache state, no matter what
    [cacheStateBar setDoubleValue:cacheState];
    
    if (!_infoCollectorRunning) {
        // this can happen when we try to stop the collector
        // but a distributed object event for updateWithInfo()
        // is already in the queue
        return;
    }
    if (info == nil) {
        // when the application isn't active, only the
        // cache state is updated
        return;
    }
    
    //NSString* firstVariableStreamId = nil;
    if (_ignoreNextStreamInfo) {
        _ignoreNextStreamInfo = FALSE;
    } else {
        //[cacheStateBar setDoubleValue:cacheState];
        StreamsDataSource* dataSource = [streamsTableView dataSource];
        BOOL oldRecordingState = [dataSource isRecording];
        int oldChannel = [dataSource currentChannel];
        NSString* oldSelectedStream = nil;
        int selectedRow = [streamsTableView selectedRow];
        if (selectedRow != -1) {
            oldSelectedStream = [[dataSource streamIdForRow:selectedRow] retain];
        }
        [dataSource updateWithInfo:info channels:channelPopUpButton];
        [streamsTableView reloadData];
        if ([streamsTableView numberOfRows] <= 0) {
            printf("Recording info has 0 rows!");
        }
        _currentStreamIndex = [dataSource currentStreamIndex];
        if (oldSelectedStream != nil) {
            BOOL changeSelection = FALSE;
            if (selectedRow >= [streamsTableView numberOfRows]) {
                changeSelection = TRUE;
            } else if (![[dataSource streamIdForRow:selectedRow]
                         isEqualToString:oldSelectedStream]) {
                changeSelection = TRUE;
            }
            if (changeSelection) {
                int rowToSelect = [dataSource rowForStreamId:oldSelectedStream];
                if (rowToSelect != -1) {
                    [streamsTableView selectRow:rowToSelect
                     byExtendingSelection:NO];
                }
            }
            [oldSelectedStream release];
        }
        if (_currentStreamIndex == -1) {
            _currentQuality = nil;
            [nowPlaying setStringValue:@""];
        } else {
            _currentQuality = [dataSource streamQualityForRow:_currentStreamIndex];
            NSString* recordingFlag =
                [dataSource recordingFlagForRow:_currentStreamIndex];
            if ([recordingFlag isEqualToString:@"L"]) {
                [nowPlaying setStringValue:@"Live TV (buffered)"];
            } else {
                [nowPlaying setStringValue:
                 [dataSource titleForRow:_currentStreamIndex]];
            }
        }
        [streamPosSlider setDoubleValue: [dataSource currentStreamPosition]];
        //printf("current stream position: %f\n", [dataSource currentStreamPosition]);
        BOOL newRecordingState = [dataSource isRecording];
        int newChannel = [dataSource currentChannel];
        if (newRecordingState != oldRecordingState) {
            [self updateButtonsWithRecordingState: newRecordingState];
            if (newChannel == -1) {
                // if we have no info about the current channel
                // in the info package we got (which can happen
                // if the server decides that asking for the
                // current channel is currently too resource-
                // consuming)
                NSString* info = [_urlFetcher fetchURL: [_baseURL stringByAppendingString: @"?getCurrentChannel"]];
                [self updateSelectedChannel: [info intValue]];
            }
        }
        if (newChannel != oldChannel) {
            [self updateSelectedChannel: newChannel];
        }
    }
    //return firstVariableStreamId;
}


/**
 * Selects the currently playing stream in the list
 * of streams.
 */

- (IBAction)selectCurrentStream:(id)sender {
    if (_currentStreamIndex != -1) {
        [streamsTableView selectRow:_currentStreamIndex
                        byExtendingSelection:NO];
        [streamsTableView scrollRowToVisible:_currentStreamIndex];
    }
}


/**
 * Handles a quality switch.
 *
 * If the current stream quality is unknown or the current and
 * new qualities are incompatible, false is returned. Otherwise,
 * true is returned.
 *
 * The current quality is set to unknown in this method and only
 * updated over time. This way, trying to play streams that do
 * no longer exist won't result in a stuck player (showing
 * nothing).
 */

- (BOOL)checkQuality {
    NSString* currentQuality = _currentQuality;
    _currentQuality = nil;
    if (currentQuality != nil) {
        StreamsDataSource* dataSource = [streamsTableView dataSource];
        int selectedRow = [streamsTableView selectedRow];
        if (selectedRow != -1) {
            NSString* newQuality = [dataSource streamQualityForRow: selectedRow];
            if ([newQuality isEqualToString:currentQuality]) {
                return true;
            }
            if (([newQuality isEqualToString:@"HQ"] &&
                 [currentQuality isEqualToString:@"SQ"]) ||
                ([newQuality isEqualToString:@"SQ"] &&
                 [currentQuality isEqualToString:@"HQ"])) {
                return true;
            }
        }
    }
    return false;
}


/**
 * Returns the id of the currently selected stream.
 */

- (NSString*)getSelectedStreamId {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    int selectedRow = [streamsTableView selectedRow];
    if (selectedRow == -1) {
        return @"";
    }
    return [dataSource streamIdForRow: selectedRow];
}


/**
 * Returns the size (in bytes) of the currently selected stream.
 */

- (double)getSelectedStreamSize {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    int selectedRow = [streamsTableView selectedRow];
    if (selectedRow == -1) {
        return -1;
    }
    return [dataSource streamSizeForRow: selectedRow];
}


/**
 * Returns the quality of the currently selected stream.
 */

- (NSString*)getSelectedStreamQuality {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    int selectedRow = [streamsTableView selectedRow];
    if (selectedRow == -1) {
        return @"";
    }
    return [dataSource streamQualityForRow: selectedRow];
}


/**
 * Returns the id of the currently playing stream.
 */

-  (NSString*)getCurrentStreamId {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    if (_currentStreamIndex == -1) {
        return @"";
    }
    return [dataSource streamIdForRow:_currentStreamIndex];
}


/**
 * Returns the size (in bytes) of the currently playing stream.
 */

- (double)getCurrentStreamSize {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    if (_currentStreamIndex == -1) {
        return -1;
    }
    return [dataSource streamSizeForRow:_currentStreamIndex];
}


/**
 * Returns the quality of the currently playing stream.
 */

- (NSString*)getCurrentStreamQuality {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    if (_currentStreamIndex == -1) {
        return @"";
    }
    return [dataSource streamQualityForRow:_currentStreamIndex];
}


/**
 * Returns the id of the latest available stream.
 */

- (NSString*)getLatestStreamId {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    if ([streamsTableView numberOfRows] == 0) {
        return @"";
    }
    return [dataSource streamIdForRow: [streamsTableView numberOfRows] - 1];
}


/**
 * Returns the size (in bytes) of the latest available stream.
 */

- (double)getLatestStreamSize {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    if ([streamsTableView numberOfRows] == 0) {
        return -1;
    }
    return [dataSource streamSizeForRow: [streamsTableView numberOfRows] - 1];
}


/**
 * Helper method (will probably disappear).
 */

- (void)ignoreNextStreamInfo {
    _ignoreNextStreamInfo = TRUE;
}


/**
 * Helper method (retrieves the contents of a URL).
 */

- (NSString*)getURL:(NSString*)url {
    return [_urlFetcher fetchURL: url];
}


/**
 * Notifies the application controller that the application
 * became active.
 */

- (void)becameActive {
    [self checkServices];
}


/**
 * Notifies the application controller that the application
 * became inactive.
 */

- (void)becameInactive {
    [_backgroundHelper stopInfoCollector];
}


/**
 * Returns the controller for the server list.
 */

- (ChooseServerController*)chooseServerController
{
    return chooseServerController;
}


/**
 * Skips relative to the current position.
 */

- (void)skip:(int)skipTime {
    if (_clientId != nil && _baseURL != nil) {
        [_urlFetcher fetchURL: [_baseURL stringByAppendingString:
            [NSString stringWithFormat:@"?skip&%i&clientId=%@", skipTime, _clientId]]];
    }
}


/**
 * Channel up.
 */

- (void)channelUp {
    if (_baseURL != nil) {
        [_urlFetcher fetchURL: [_baseURL stringByAppendingString:@"?channelUp"]];
    }
}


/**
 * Channel down.
 */

- (void)channelDown {
    if (_baseURL != nil) {
        [_urlFetcher fetchURL: [_baseURL stringByAppendingString:@"?channelDown"]];
    }
}


/**
 * Sets the overlay to display (and restarts the overlay timer for
 * fading).
 */

- (void)setOverlay:(int)overlayConstant {
    [_overlayTimer invalidate];
    _overlayTimer = nil;
    _timerCounter = 0;
    [overlayView setOverlay:overlayConstant];
    [overlayWindow display];
    [overlayWindow orderFront:nil];
    [overlayWindow setAlphaValue:1.0];
    
    // timer
    _overlayTimer = [NSTimer scheduledTimerWithTimeInterval: 0.02
                                    target: self
                                    selector: @selector(handleOverlay:)
                                    userInfo: nil
                                    repeats: YES];
}


/**
 * Timer method for overlay display.
 */

- (void)handleOverlay:(NSTimer*)timer {
    ++_timerCounter;
    if (_timerCounter >= 75) {
        [_overlayTimer invalidate];
        _overlayTimer = nil;
        [overlayWindow orderOut:nil];
    } else if (_timerCounter > 50) {
        [overlayWindow setAlphaValue:(75-_timerCounter)/25.0];
        //[overlayWindow display];
    }
}


/**
 * Parse the server keys file.
 */

- (NSMutableDictionary*)loadServerKeys {
    NSData* keysFileData =
        [NSData dataWithContentsOfFile:
         [_serverKeyFile stringByExpandingTildeInPath]];
    NSString* keysFileString =
        [[[NSString alloc] initWithData:keysFileData
                               encoding:NSUTF8StringEncoding]
         autorelease];
    NSArray* keyLines = [keysFileString componentsSeparatedByString:@"\n"];
    NSMutableDictionary* retVal = [[[NSMutableDictionary alloc] init] autorelease];
    int i;
    for (i = 0; i < [keyLines count]; ++i) {
        NSArray* keyComponents =
            [[keyLines objectAtIndex:i] componentsSeparatedByString:@" "];
        if ([keyComponents count] != 3) {
            continue;
        }
        NSArray* hosts = [[keyComponents objectAtIndex:0]
                          componentsSeparatedByString:@","];
        int j;
        for (j = 0; j < [hosts count]; ++j) {
            [retVal setObject:[NSArray arrayWithObjects:
                               [keyComponents objectAtIndex:1],
                               [keyComponents objectAtIndex:2],
                               nil]
                       forKey:[hosts objectAtIndex:j]];
        }
    }
    return retVal;
}


/**
 * Save the server keys from a dictionary.
 */

- (void)saveServerKeys:(NSDictionary*)keys {
    NSEnumerator* keyEnumerator = [keys keyEnumerator];
    NSString* key;
    NSArray* value;
    NSString* result = @"";
    do {
        key = [keyEnumerator nextObject];
        value = [keys objectForKey:key];
        if (key != nil) {
            result = [result stringByAppendingFormat:@"%@ %@ %@\n",
                      key,
                      [value objectAtIndex: 0],
                      [value objectAtIndex: 1]];
        }
    } while (key != nil);
    if (![[result dataUsingEncoding:NSUTF8StringEncoding]
            writeToFile:[_serverKeyFile stringByExpandingTildeInPath]
             atomically:YES]) {
        NSRunAlertPanel(@"Cannot save the key file",
                        @"Please check if you have permission to write the file '%@'.",
                        nil, nil, nil,
                        [_serverKeyFile stringByExpandingTildeInPath]);
    }
}


/**
 * Import an SSH server key.
 */

- (void)importServerKeyForHost:(NSString*)host {
    NSOpenPanel* openPanel = [NSOpenPanel openPanel];
    [openPanel setRequiredFileType:@"ssh_key"];
    [openPanel setTitle:[NSString stringWithFormat:@"Import server key for '%@'", host]];
    [openPanel setCanSelectHiddenExtension:YES];
    [openPanel setAllowsMultipleSelection:NO];
    [openPanel setCanChooseDirectories:NO];
    if ([openPanel runModal] != NSFileHandlingPanelOKButton) {
        return;
    }
    NSData* importFileData =
        [NSData dataWithContentsOfFile:[openPanel filename]];
    NSString* importFileString =
        [[[NSString alloc] initWithData:importFileData
                            encoding:NSUTF8StringEncoding]
            autorelease];
    NSArray* importKeyComponents =
        [importFileString componentsSeparatedByString:@" "];
    if ([importKeyComponents count] != 2) {
        NSRunAlertPanel(@"Cannot parse the key file",
                        @"Please make sure you select a file that was exported in CyTV server.",
                        nil, nil, nil);
        return;
    }
    NSMutableDictionary* serverKeys = [self loadServerKeys];
    if ([serverKeys objectForKey:host] != nil) {
        int retVal = NSRunAlertPanel(@"Overwrite key?",
                                        @"There is already a key for host '%@' - do you want to overwrite it?",
                                        @"Overwrite", @"Don't overwrite", nil, host);
        if (retVal != NSAlertDefaultReturn) {
            return;
        }
    }
    [serverKeys setObject:[NSArray arrayWithObjects:
                            [importKeyComponents objectAtIndex:0],
                            [[[importKeyComponents objectAtIndex:1]
							  componentsSeparatedByString:@"\n"]
							 objectAtIndex:0],
                            nil]
                    forKey:host];
    [self saveServerKeys:serverKeys];
}


/**
 * Sort the recordings.
 */

- (void)sortStreamsByColumn:(NSTableColumn*)sortColumn {
    if (sortColumn == _sortedBy) {
        _sortedDescending = !_sortedDescending;
    } else {
        [streamsTableView setIndicatorImage:nil inTableColumn:_sortedBy];
        _sortedBy = sortColumn;
    }
    [streamsTableView setIndicatorImage:
     (_sortedDescending ? [NSImage imageNamed:@"NSDescendingSortIndicator"]
                        : [NSImage imageNamed:@"NSAscendingSortIndicator"])
     inTableColumn:_sortedBy];
    [streamsTableView setHighlightedTableColumn:_sortedBy];
}

- (void)sortStreams:(NSTableColumn*)clickedColumn {
    StreamsDataSource* dataSource = [streamsTableView dataSource];
    NSString* oldSelectedStream = nil;
    int selectedRow = [streamsTableView selectedRow];
    if (selectedRow != -1) {
        oldSelectedStream = [[dataSource streamIdForRow:selectedRow] retain];
    }
    [self sortStreamsByColumn:clickedColumn];
    [dataSource sortStreamsByColumn:clickedColumn descending:_sortedDescending];
    [streamsTableView reloadData];
    _currentStreamIndex = [dataSource currentStreamIndex];
    if (oldSelectedStream != nil) {
        int rowToSelect = [dataSource rowForStreamId:oldSelectedStream];
        if (rowToSelect != -1) {
            [streamsTableView selectRow:rowToSelect
             byExtendingSelection:NO];
        }
        [oldSelectedStream release];
    }
}

-(void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn {
    [self sortStreams:tableColumn];
}

- (NSDate*)testDate {
	return [NSDate dateWithString:@"2004-08-31 17:29:00 +0000"];
}

@end
