/**
 * Unisource Andromeda v. 2.0
 * 
 * Copyright 2007 by High-Touch Communications Inc.
 * 
 * 
 * This software was developed internally by High-Touch Communications Inc.
 * for Unisource. In no event shall this source code be published.
 */
 
 
/**
* Provides a way to enhance existing class methods with
* an automatic retry mechanism
*/
RetryHandler = Class.create(    
    {
        /**
        * The default number of attempts for a given operation
        *
        * @var Integer
        */
        defaultLimit: 5,
        
        /**
        * The object to which retry functionality has been attached
        * 
        * @var object
        */
        target : null,
        
        /**
        * Methods to which retry functionality has been added
        *
        * @var object
        */
        methods : {},
        
        
        /**
        * A data store that collects all exceptions encountered in retry attempts
        *
        * @var object
        */
        exceptions : {},
        
        /**
        * Creates a new retry instance and associates it with an object
        *
        * @var object target  the target to which retry functionality is being added
        * @var object methods the definitions of methods to which the mechanism is being applied
        *
        * Sample : {
        *   pay : RetryHandler.RETRY, // default settings
        *   sendMessage : {
        *       limit : 10,                                  // the number of times to retry
        *       condition : function() {return foo == bar; } // the condition that must evaluate to true in order to retry
        *   }
        * }
        * All settings are optional
        */
        initialize : function(target, methods) {
            this.target = target;
            if (methods) {
                this.enhanceMethods(methods);
            }
        },
        
        /**
        * Enhances the methods of an object with a retry mechanism using
        * the definitions provided
        *
        * @param object methods method definitions. See the constructor for an example.
        */
        enhanceMethods : function(methods) {
            $H(methods).each(
                function(definition) {
                    if (this.target[definition.key]) {
                        this.enhanceMethod(definition.key, definition.value);
                    }
                }.bind(this)
            );
        },
        
        /**
        * Enhances a method on the target class with the ability to retry
        * by replacing it with a lambda that calls the original method. 
        *
        * @param String methodName the name of the method to enhance
        */
        enhanceMethod : function(methodName, definition) {
            if (!definition.limit) {
                definition.limit = this.defaultLimit;
            }
            
            if (!definition.condition) {
                definition.condition = function() {return true;}
            }
            
            if (!definition.type) {
                definition.type = RetryHandler.SYNCHRONOUS;
            }
            
            if (!definition.onFailure) {
                definition.onFailure = function(){};
            }
            
            // Remember the original method for future reference
            var originalMethod = this.target[methodName];

            var register = this.register[definition.type].bind(this);
            register(methodName, definition);                            
        },
        
        register : {
            asynchronous : function(methodName, definition) {                
                var originalMethod = this.target[methodName];
                var returnValue;
                
                var observer = new Object();                
                new EventHandler(observer);                
                var target = this.target;
                
                var hardFailureHandler = null;
                var hardFailure = false;

                originalMethod.fail = function(onHardFailure) {
                    
                    if (onHardFailure) {
                        hardFailureHandler = onHardFailure;
                    }
                    observer.fire(RetryHandler.Event.RECOVERABLE_FAILURE);
                }
                
                
                        
                this.target[methodName] = function() {
                    var retryCount = 0;
                    var myArguments = arguments;
                    myArguments.observer = observer;
                
                    
                    observer.clearEvents();
                    
                    observer.observe(
                        RetryHandler.Event.RECOVERABLE_FAILURE,
                        function() {
                            retryCount++;
                            if (retryCount < definition.limit && definition.condition()) {
                                returnValue = originalMethod.apply(target, myArguments);
                            } else {
                                observer.fire(RetryHandler.Event.HARD_FAILURE)
                            }
                        }
                    );
                    
                    observer.observe(
                        RetryHandler.Event.HARD_FAILURE,
                        function() {            
                            if (hardFailureHandler) hardFailureHandler();
                            hardFailure = true;
                        }
                    );
                    returnValue = originalMethod.apply(target, myArguments);
                    if (!hardFailure) {
                        return returnValue;
                    }
                    
                }
                
                
            },
            synchronous : function(methodName, definition) {
                var originalMethod = this.target[methodName];
                
                this.target[methodName] = function() {
                    var retry = true;
                    var retryCount = 0;
                    var success = false;
                    var lastException = null;
                    var returnValue;
                    
                    while(retry) {
                        success = false;
                        try {
                            returnValue = originalMethod.apply(this.target, arguments);
                            success = true;
                        } catch (e) {
                            if (!this.exceptions[methodName]) {
                                this.exceptions[methodName] = [];
                            }
                            this.exceptions[methodName].push(e);
                            lastException = e;
                        }
                        retryCount++;
                        success = success && definition.condition;
                        retry = ((!success) && (retryCount < limit));                    
                    }
                }
            
                // In case of failure, re-throw the last exception
                if (success) {
                    return returnValue;
                } else {
                    definition.onFailure();
                    if (lastException) {
                        throw lastException;
                    }
                } 
                
            }
        
        }
        
    }
);

RetryHandler.RETRY = {};
RetryHandler.SYNCHRONOUS = 'synchronous';
RetryHandler.ASYNCHRONOUS = 'asynchronous';

RetryHandler.Event = 
{
    RECOVERABLE_FAILURE: 'recoverable-failure',
    HARD_FAILURE: 'hard-failure'
}
