/*
    -----------------------------------------------------------------------------------------------
    File Name:		AppShell.m
    Project:		Parakeet
    Author:		Greg Howell (with some help from Apple sample code)
    Revision Date:	20/08/2003 (0.1)
    
    Description:	AppShell object implementation.
                        Interrogates the Accessibility API and produces strings to be spoken
                        via the TTS API.
    -----------------------------------------------------------------------------------------------
*/

#import <Cocoa/Cocoa.h>
#import <AppKit/NSAccessibility.h>
#import <Carbon/Carbon.h>
#import "AppShell.h"

// Setting DEBUG_PARAKEET to TRUE allows Parakeet to print debugging
// information to the run window in Project Builder
// Setting DEBUG_PARAKEET to FALSE turns this feature off
#define DEBUG_PARAKEET FALSE

AppShell * gAppShell = NULL;		// an AppShell object called gAppShell, initialised to NULL

NSMutableString * lastTTSString;	// state variable that stores the last spoken string
NSMutableString * currentTTSString;	// variable that stores the string to be spoken
char * theTTSString;			// string used to call the TTS functions (a "C" string)

const UInt32 kShutupHotKeyIdentifier = 'sUIk';
const UInt32 kShutupHotKey = 109;	// corresponds to F10 (and command key)

EventHotKeyRef gMyHotKeyRef;		// hot key related
EventHotKeyID gMyHotKeyID;		// hot key related
EventHandlerUPP gAppHotKeyFunction;	// hot key related

// prototype of the callback process that lets us know when the TTS has finished
static pascal void OurSpeechDoneCallBackProc(SpeechChannel inSpeechChannel, long inRefCon);

// prototype for the shutup hot-key handler
pascal OSStatus ShutupHotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData);

@implementation AppShell

/*
    -----------------------------------------------------------------------------------------------
    updateCurrentUIElement
    -----------------------------------------------------------------------------------------------
*/
+ (void)updateCurrentUIElement
{
    [gAppShell updateCurrentUIElement];
}
/*
    -----------------------------------------------------------------------------------------------
    descriptionOfValue
    
    Deals with element title (eg. Mail) and role (eg. Application)
    -----------------------------------------------------------------------------------------------
*/
+ (NSString *)descriptionOfValue:(CFTypeRef)theValue beingVerbose:(BOOL)beVerbose
{
    NSString * theValueDescString = NULL;

    if (theValue) {
    
        if (AXValueGetType(theValue) != kAXValueIllegalType) {
            theValueDescString = [AppShell stringDescriptionOfAXValue:theValue beingVerbose:beVerbose];
        }
        else if (CFGetTypeID(theValue) == CFArrayGetTypeID()) {
            theValueDescString = [NSString stringWithFormat:@"<array of size %d>", [(NSArray *)theValue count]];
        }
        else if (CFGetTypeID(theValue) == AXUIElementGetTypeID()) {
            
            NSString *	uiElementRole  	= NULL;
        
            if (AXUIElementCopyAttributeValue( (AXUIElementRef)theValue, kAXRoleAttribute, (CFTypeRef *)&uiElementRole ) == kAXErrorSuccess) {
                NSString *	uiElementTitle  = NULL;
                
                uiElementTitle = [AppShell valueOfExistingAttribute:kAXTitleAttribute ofUIElement:(AXUIElementRef)theValue];
                
                #if 0
                // hack to work around cocoa app objects not having titles yet
                if (uiElementTitle == nil && [uiElementRole isEqualToString:(NSString *)kAXApplicationRole]) {
                    pid_t theAppPID = 0;
                    ProcessSerialNumber	theAppPSN = {0,0};
                    NSString * theAppName = NULL;
                    
                    if (AXUIElementGetPid( (AXUIElementRef)theValue, &theAppPID ) == kAXErrorSuccess
                        && GetProcessForPID( theAppPID, &theAppPSN ) == noErr
                        && CopyProcessName( &theAppPSN, (CFStringRef *)&theAppName ) == noErr ) {
                        uiElementTitle = theAppName;
                    }
                }
                #endif

                if (uiElementTitle != nil) {
                    currentTTSString = (NSMutableString *)uiElementTitle;
                    theValueDescString = [NSString stringWithFormat:@"<%@: Ò%@Ó>", uiElementRole, uiElementTitle];
                }
                else {
                    theValueDescString = [NSString stringWithFormat:@"<%@>", uiElementRole];
                }
                [uiElementRole release];
            }
            else {
                theValueDescString = [(id)theValue description];
            }
        }
        else {
            theValueDescString = [(id)theValue description];
        }
    }
    return theValueDescString;
}

/*
    -----------------------------------------------------------------------------------------------
    descriptionForUIElement
    -----------------------------------------------------------------------------------------------
*/
+ (NSString *)descriptionForUIElement:(AXUIElementRef)uiElement attribute:(NSString *)name beingVerbose:(BOOL)beVerbose
{
    NSString *	theValueDescString	= NULL;
    CFTypeRef	theValue;
    CFIndex	count;
    if (([name isEqualToString:NSAccessibilityChildrenAttribute]
            ||
         [name isEqualToString:NSAccessibilityRowsAttribute]
        )
            &&
        AXUIElementGetAttributeValueCount(uiElement, (CFStringRef)name, &count) == kAXErrorSuccess) {
        // No need to get the value of large arrays - we just display their size.
        // We don't want to do this with every attribute because AXUIElementGetAttributeValueCount
        // on non-array valued attributes will cause debug spewage.
        theValueDescString = [NSString stringWithFormat:@"<array of size %d>", count];
    } else if (AXUIElementCopyAttributeValue ( uiElement, (CFStringRef)name, &theValue ) == kAXErrorSuccess && theValue) {
        theValueDescString = [self descriptionOfValue:theValue beingVerbose:beVerbose];
    }
    return theValueDescString;
}
/*
    -----------------------------------------------------------------------------------------------
    valueOfExistingAttribute
    -----------------------------------------------------------------------------------------------
*/
+ (id)valueOfExistingAttribute:(CFStringRef)attribute ofUIElement:(AXUIElementRef)element
{
    id result = nil;
    NSArray *attrNames;
    
    if (AXUIElementCopyAttributeNames(element, (CFArrayRef *)&attrNames) == kAXErrorSuccess) {
        if ( [attrNames indexOfObject:(NSString *)attribute] != NSNotFound
                &&
        	AXUIElementCopyAttributeValue(element, attribute, (CFTypeRef *)&result) == kAXErrorSuccess
        ) {
            [result autorelease];
        }
        [attrNames release];
    }
    return result;
}
/*
    -----------------------------------------------------------------------------------------------
    stringDescriptionOFAXValue
    -----------------------------------------------------------------------------------------------
*/
+ (NSString *)stringDescriptionOfAXValue:(CFTypeRef)valueRef beingVerbose:(BOOL)beVerbose
{
    NSString *result = @"AXValue???";
    
    switch (AXValueGetType(valueRef)) {
        case kAXValueCGPointType: {
            CGPoint point;
            if (AXValueGetValue(valueRef, kAXValueCGPointType, &point)) {
                if (beVerbose)
                    result = [NSString stringWithFormat:@"<AXPointValue x=%g y=%g>", point.x, point.y];
                else
                    result = [NSString stringWithFormat:@"x=%g y=%g", point.x, point.y];
            }
            break;
        }
        case kAXValueCGSizeType: {
            CGSize size;
            if (AXValueGetValue(valueRef, kAXValueCGSizeType, &size)) {
                if (beVerbose)
                    result = [NSString stringWithFormat:@"<AXSizeValue w=%g h=%g>", size.width, size.height];
                else
                    result = [NSString stringWithFormat:@"w=%g h=%g", size.width, size.height];
            }
            break;
        }
        case kAXValueCGRectType: {
            CGRect rect;
            if (AXValueGetValue(valueRef, kAXValueCGRectType, &rect)) {
                if (beVerbose)
                    result = [NSString stringWithFormat:@"<AXRectValue  x=%g y=%g w=%g h=%g>", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height];
                else
                    result = [NSString stringWithFormat:@"x=%g y=%g w=%g h=%g", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height];
            }
            break;
        }
        case kAXValueCFRangeType: {
            CFRange range;
            if (AXValueGetValue(valueRef, kAXValueCFRangeType, &range)) {
                if (beVerbose)
                    result = [NSString stringWithFormat:@"<AXRangeValue pos=%ld len=%ld>", range.location, range.length];
                else
                    result = [NSString stringWithFormat:@"pos=%ld len=%ld", range.location, range.length];
            }
            break;
        }
        default:
            break;
    }
    return result;
}
/*
    -----------------------------------------------------------------------------------------------
    awakeFromNib
    
    - executes when the application is loaded (nib file loaded)
    - checks that the Accessibility APIs are turned on
    - sets up the application (key listening, TTS initialisation)
    -----------------------------------------------------------------------------------------------
*/
- (void)awakeFromNib
{
    EventTypeSpec eventType;
    
    // We first have to check if the Accessibility APIs are turned on.  If not, we have to tell
    // the user to do it (they'll need to authenticate to do it).  If you are an accessibility app
    // (i.e., if you are getting info about UI elements in other apps), the APIs won't work unless
    // the APIs are turned on.
    if (!AXAPIEnabled ())
    {
        NSRunAlertPanel(@"Error",@"Please check the 'Enable access for assistive devices' checkbox in the Universal Access System Preferences and relaunch Parakeet.",@"OK",NULL,NULL);
	// quit the application here as there is nothing more we can do
        [NSApp terminate:nil];
    }

    gAppShell = self;

    if(DEBUG_PARAKEET){
        printf("AppShell:awakeFromNib (DEBUGGING ENABLED)\n");
    }

    _systemWideElement = AXUIElementCreateSystemWide();
    
    gAppHotKeyFunction = NewEventHandlerUPP(ShutupHotKeyHandler);
    eventType.eventClass = kEventClassKeyboard;
    eventType.eventKind = kEventHotKeyPressed;
    InstallApplicationEventHandler(gAppHotKeyFunction, 1, &eventType, NULL, NULL);
    gMyHotKeyID.signature = kShutupHotKeyIdentifier;
    gMyHotKeyID.id = 1;
    // unfortunately we cannot simply remove the "cmdKey" parameter from this call
    // to make just F10 the key we are listening for
    RegisterEventHotKey(kShutupHotKey, cmdKey, gMyHotKeyID, GetApplicationEventTarget(), 0, &gMyHotKeyRef);

    // set the lastTTString variable to an initial value so comparisons further on don't fail
    // we need to "cast" our initialisation string as an NSMutableString
    // (this avoids a compiler warning but either way the code works fine?)
    // I have initialised with a dash character as this is unlikely to conflict with any generated
    // strings when it comes to performing string comparisons
    // Greg Howell 15/08/2003
    lastTTSString = (NSMutableString *)@"-";

    // create a new speech channel to use
    [self createNewSpeechChannel];
    [self performTimerBasedUpdate];
    
    // initialise the start/stop state so that we start speaking as soon
    // as the application is launched
    [_inspectorWindow setStartStopState:TRUE];
}
/*
    -----------------------------------------------------------------------------------------------
    performTimerBasedUpdate
    
    - application uses timer based updates to check what is going on with the user and the system
    -----------------------------------------------------------------------------------------------
*/
- (void)performTimerBasedUpdate
{
    [gAppShell updateCurrentUIElement];
    
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(performTimerBasedUpdate) userInfo:nil repeats:NO];
}
/*
    -----------------------------------------------------------------------------------------------
    setCurrentUIElement
    -----------------------------------------------------------------------------------------------
*/
- (void)setCurrentUIElement:(AXUIElementRef)uiElement
{
    if (uiElement)
        CFRetain( uiElement );
    
    if (_currentUIElementRef)
        CFRelease( _currentUIElementRef );
        
	_currentUIElementRef = uiElement;
}
/*
    -----------------------------------------------------------------------------------------------
    currentUIElement
    -----------------------------------------------------------------------------------------------
*/
- (AXUIElementRef)currentUIElement
{
    return _currentUIElementRef;
}
/*
    -----------------------------------------------------------------------------------------------
    updateCurrentUIElement
    -----------------------------------------------------------------------------------------------
*/
- (void)updateCurrentUIElement
{
    // Point object
    Point pointAsCarbonPoint;

    // The current mouse position with origin at top left.
    GetMouse( &pointAsCarbonPoint );
        
    // Only ask for the UIElement under the mouse if has moved since the last check.
    // Prevents unnecessary updating of information.
    if (pointAsCarbonPoint.h != _lastMousePoint.h || pointAsCarbonPoint.v != _lastMousePoint.v) {
        
        CGPoint	pointAsCGPoint;
        AXUIElementRef newElement = NULL;

        pointAsCGPoint.x = pointAsCarbonPoint.h;
        pointAsCGPoint.y = pointAsCarbonPoint.v;
    
        // Ask Accessibility API for UI Element under the mouse
        // And update the display if a different UIElement
        if (AXUIElementCopyElementAtPosition( _systemWideElement, pointAsCGPoint.x, pointAsCGPoint.y, &newElement ) == kAXErrorSuccess && newElement && ([self currentUIElement] == NULL || ! CFEqual( [self currentUIElement], newElement ))) {
                    
            [self setCurrentUIElement:newElement];
            [_inspectorWindow displayInfoForUIElement:newElement];
            
            // display the current application name (GH)
            [_inspectorWindow setCurrentAppString:currentTTSString];
            // set the TTS string (GH)
            [self setTTSString:currentTTSString];
        }
        // Update _lastMousePoint
        _lastMousePoint = pointAsCarbonPoint;
    }
}

/*
    -----------------------------------------------------------------------------------------------
    createNewSpeechChannel
	
    - Create a new speech channel for the given voice spec.  A nil voice spec pointer
      causes the speech channel to use the default voice.  Any existing speech channel
      for this window is closed first.
    
    - Borrowed from Apple's Cocoa Speech Synthesis Example (24/07/2003)
    - Modified by Greg Howell (31/07/2003)
    -----------------------------------------------------------------------------------------------
*/
- (void)createNewSpeechChannel
{
    OSErr theErr = noErr;

    // Dispose of the current one, if it exists
    if (fCurSpeechChannel) {
        theErr = DisposeSpeechChannel(fCurSpeechChannel);
        if (theErr != noErr)
           NSRunAlertPanel(@"DisposeSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Ok", NULL, NULL);
    
        fCurSpeechChannel = NULL;
    }
		
    // Create a speech channel
    if (theErr == noErr) {
    	theErr = NewSpeechChannel(NULL, &fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"NewSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Ok", NULL, NULL);
	}
    
	// Setup our refcon to the document controller object so we have access within our Speech callbacks
	if (theErr == noErr) {
            theErr = SetSpeechInfo(fCurSpeechChannel, soRefCon, (Ptr)self);
            if (theErr != noErr)
                NSRunAlertPanel(@"SetSpeechInfo(soRefCon)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Ok", NULL, NULL);
	}
    
    if(DEBUG_PARAKEET){
        printf("AppShell:createNewSpeechChannel\n");
    }
}

/*
    -----------------------------------------------------------------------------------------------
    startSpeakingText
	
    - Speaks the text.
    
    - Modified code from Apple's Cocoa Speech Synthesis Example (24/07/2003)
    - Added ability to check start/stop TTS state information (20/08/2003)
    -----------------------------------------------------------------------------------------------
*/
- (void)startSpeakingText
{
    OSErr theErr = noErr; // store any errors that we might encounter
    
    // check if we can speak by accessing the start/stop TTS state stored
    // in the InspectorWindow object
    if ([_inspectorWindow getStartStopState] == TRUE){
        // set up the callback (TTS has finished)
        // this is done via the SetSpeechInfo method
        if (theErr == noErr){
            theErr = SetSpeechInfo(fCurSpeechChannel, soSpeechDoneCallBack, OurSpeechDoneCallBackProc);
            if (theErr != noErr)
                NSRunAlertPanel(@"SetSpeechInfo(soSpeechDoneCallBack)",[NSString stringWithFormat:@"Error #%d returned.", theErr], @"Ok", NULL, NULL);
        }
    
        // speak the string
        // strlen only works on "C" strings
        theErr = SpeakText(fCurSpeechChannel,theTTSString,strlen(theTTSString));
    }
}

/*
    -----------------------------------------------------------------------------------------------
    speechIsDone
	
    - This method is called by the OurSpeechDoneCallBackProc process.
    -----------------------------------------------------------------------------------------------
*/
- (void)speechIsDone
{
    // do nothing for the time being
    if(DEBUG_PARAKEET){
        printf("AppShell:speechIsDone\n");
    }
}

/*
    -----------------------------------------------------------------------------------------------
    setTTSString
	
    - Method to set the TTS string (calls the TTS routine if required)
    - Coded by Greg Howell (13/08/2003)
    -----------------------------------------------------------------------------------------------
*/
- (void)setTTSString:(NSMutableString *)stringToSpeak
{
    // declare local NSMutableString to store the last string spoken
    NSMutableString *tempLast = [[NSString alloc] initWithString:lastTTSString]; 
    // declare local NSMutableString to store the next string to be spoken
    NSMutableString *tempNext = [[NSString alloc] initWithString:stringToSpeak];

    // perform a comparison on the two strings
    BOOL bCompare = [tempLast isEqualToString:tempNext]; 

    // if they are the same we don't speak the string
    // this stops us repeating ourselves (as that gets a tad annoying)
    
    if (bCompare != TRUE){
        // convert the NSString provided into a "C" string
        // you need to call the cString method AND cast the result
        // as a (char *)
        theTTSString = (char *)[tempNext cString];
    
        // set the last string variable to the string we are about to say
        // could go in the SpeechIsDone method but we will do it here to make sure
        // it happens
        lastTTSString = tempNext;
    
        // call startSpeakingText
        [self startSpeakingText];
    }


}

/*
    -----------------------------------------------------------------------------------------------
    toggleShutup
	
    - Called when the user activates our hot-key
    - Coded by Greg Howell (20/08/2003)
    -----------------------------------------------------------------------------------------------
*/
- (void)toggleShutup:(id)sender
{
    if ([_inspectorWindow getStartStopState] == TRUE)
    {
        [_inspectorWindow setStartStopState:FALSE];	// set to FALSE (shut up)
    }
    else
    {
        [_inspectorWindow setStartStopState:TRUE];	// set to TRUE (stop being shut up)
    }
}

@end

/*
    -----------------------------------------------------------------------------------------------
    OurSpeechDoneCallBackProc
    
    - Called by speech channel when all speech has been generated.
    - Modified code from Apple's Cocoa Speech Synthesis Example (24/07/2003)
    -----------------------------------------------------------------------------------------------
*/
pascal void OurSpeechDoneCallBackProc(SpeechChannel inSpeechChannel, long inRefCon)
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    // calls the AppShell method "speechIsDone" when TTS has completed
    [(AppShell *)inRefCon performSelectorOnMainThread:@selector(speechIsDone) withObject:NULL waitUntilDone:false];
    [pool release];
}
/*
    -----------------------------------------------------------------------------------------------
    ShutupHotKeyHandler

    - Called when the user activates our hot-key
    - Modified code from Apple's UIElementInspector Example (20/08/2003)
    -----------------------------------------------------------------------------------------------
*/
pascal OSStatus ShutupHotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData){
    
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:gAppShell selector:@selector(toggleShutup:) userInfo:nil repeats:NO];
    
    return noErr;
}
