/*

        Copyright 2006-2008 OpenAjax Alliance

        Licensed under the Apache License, Version 2.0 (the "License"); 
        you may not use this file except in compliance with the License. 
        You may obtain a copy of the License at
        
                http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software 
        distributed under the License is distributed on an "AS IS" BASIS, 
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
        See the License for the specific language governing permissions and 
        limitations under the License.
*/
if ( typeof OpenAjax.widget == "undefined" ) {
    OpenAjax.widget = {};
}

////////////////////////////////////////////////////////////////////////////////

OpenAjax.widget.Widget = function( id, properties, comm, dimensions, views )
{
    if ( ! id ) {
        return;
    }
    
    this.init( id, properties, views, comm );
    
    // Reference to the actual widget instance object.  If the widget has a
    // custom class, this is a reference to that object.  Otherwise, it is a
    // reference to 'this'.
    this.instance = null;
    this.inRemoval = false;
    this.dimensions = dimensions;
    this.currentViewName = views[0];

    var that = this;
    OpenAjax.widget.Widget.addEventListener( 'load', window, function() {
        // let the widget model know that the widget has loaded
        that.fireEvent( '_loaded' );
    });
}

OpenAjax.widget.Widget.prototype.init = function( id, properties, views, comm )
{
    this.gID = id;
    this.views = views;
    this.doBatch = false;
    this.batchChanges = null;

    if ( ! this.hubId ) {
        this.hubId = id;
    }
    this.comm = comm ? comm : "http://providers.openajax.org/inline";
    this.connectToHub();

    this.setProperties( properties );

    this.listeners = {};
    this.listenForEvents();
}

OpenAjax.widget.Widget.prototype.getId = function() 
{
    return this.gID;
}

/**
 * Sets the value of the given property name.
 * @param {String} propertyName  Name of the property to be updated
 * @param {*} value  The new value of the property
 */
OpenAjax.widget.Widget.prototype.setPropertyValue = function(propertyName, value)
{
    if ( this.properties ) {
        var prop = this.properties.getProperty(propertyName);
        if ( prop ) {
            prop.value(value);
            return;
        }
    }
    throw 'Undefined property:' + name;
}

/**
 * Returns the current value of the requested property name.
 * @param {String} propertyName  Name of the property to retrieve
 * @return The current value of the property names propertyName
 * @type *
 */
OpenAjax.widget.Widget.prototype.getPropertyValue = function(propertyName)
{
    if ( this.properties ) {
        var prop = this.properties.getProperty(propertyName);
        if ( prop ) {
            return prop.value();
        }
    }
    throw 'Undefined property:' + propertyName;
}

/**
 * Returns an array of the property names on the widget.
 * @return array of property names
 * @type [String]
 */
OpenAjax.widget.Widget.prototype.getPropertyNames = function()
{
    var names = [];
    for ( var name in this.properties.getProperties() ) {
        names.push( name );
    }
    return names;
}

/**
 * Register an event listener.
 * @param {String} type  A string representing the event type for which to
 *          listen.  The supported event types are as follows:
 *              load        - page has finished loading
 *              unload      - page is about to unload
 *              insert      - widget has been added to page
 *              remove      - widget is about to be removed from page
 *              modeChange  - mode (default, edit, help) has changed
 *              resize      - widget has been resized by container
 * @param {Function} listener  The function that is invoked when the given
 *          event occurs.
 */
OpenAjax.widget.Widget.prototype.registerCallback = function( type, listener )
{
    // Add listener
    if ( ! this.listeners[ type ] ) {
        this.listeners[ type ] = { handle: null, funcs: [] };
    }
    this.listeners[ type ].funcs.push( listener );

    // Only subscribe to each topic once per widget.  We save off each listener;
    // then, when the hub event comes in, we cycle through all of the listeners
    // for the given type and call each one.
    if ( this.listeners[ type ].funcs.length > 1 ) {
        return;
    }

    // We handle 'remove', 'viewChange' & 'unload' listeners differently.
    // See listenForEvents().
    if ( type == 'remove' || type == 'unload' ) {
        return;
    }

    var that = this;
    var callback = function( success, subHandle ) {
        if ( !success ) {
            // subscribe failed, so remove listener
            delete that.listeners[ type ];
            // XXX handle error
            alert( "subscribe failed" );
            return;
        }
        
        that.listeners[ type ].handle = subHandle;
    };
    
    // subscribe to "openajax.widget.<ID>.<TYPE>", unless type is 'load',
    // in which case we subscribe with ID = 'mashup'
    // - Widget listens for id = __ID
    // - WidgetModel listens for id = ID
    var id = (( this.gID === this.hubId ) ? "__" : "") + this.gID;
    if ( type == 'load' ) {
        id = 'mashup';
    }
    this.connHandle.subscribe( "openajax.widget." + id + "." + type,
            callback, OpenAjax.widget.Widget.bind(this, this.handleEvent) );
}

/**
 * Unregister the given event listener for the given event type.
 * @param {String} type  A string representing the event type for which to
 *          listen.
 * @param {Function} listener  The function to be unregistered.
 */
OpenAjax.widget.Widget.prototype.unregisterCallback = function( type, listener )
{
    var funcs = this.listeners[ type ].funcs;
    for ( var i = 0; i < funcs.length; i++ ) {
        if ( funcs[i] === listener ) {
            funcs.splice( i, 1 );
            // if there are no more listeners, then unsubscribe from the hub
            if ( funcs.length == 0 ) {
                this.listeners[ type ].handle.unsubscribe();
                delete this.listeners[ type ];
            }
            break;
        }
    }
}

/**
 * Returns the available width and height which the widget can request in the
 * {@link Widget#adjustDimensions} method.
 * @return An object with 'width' and 'height' properties representing the
 *          available pixels for use.  NOTE: If the container does not return a
 *          width or height, this signifies that the widget can ask for whatever
 *          size it would like when using the {@link Widget#adjustDimensions}
 *          method.
 */
OpenAjax.widget.Widget.prototype.getAvailableDimensions = function()
{
    return null;
}

/**
 * Returns the current width and height which the widget occupies in the
 * container.
 * @return An object with 'width' and 'height' properties, representing the
 *          current dimensions of the widget.
 */
OpenAjax.widget.Widget.prototype.getDimensions = function()
{
    return this.dimensions;
}

/**
 * Requests that the page size the widget to the supplied dimensions.
 * @param {Object} dimensions  An object that contains 'width' and 'height'
 *          properties which are integers. The width and height properties
 *          specify the requested dimensions to which to adjust the widget.
 *          NOTE: If either property, width or height, is not present, then the
 *          dimension is not changed.
 */
OpenAjax.widget.Widget.prototype.adjustDimensions = function( dimensions )
{
    this.fireEvent( '_adjustDimensions', dimensions );
}

/**
 * Returns the OpenAjax Hub 1.1 connection handle object used by this widget.
 * @return Connection handle object
 * @type Object
 */
OpenAjax.widget.Widget.prototype.getHubConnectionHandle = function()
{
    var hub = {};
    hub.handle = this.connHandle;
    hub.id = this.gID;
    hub.subscribe = function( topic, callback, eventCallback ) {
        var newTopic = topic + ".*";
        this.handle.subscribe( newTopic, callback, eventCallback );
    };
    hub.publish = function( topic, data ) {
        var newTopic = topic + "." + this.id;
        this.handle.publish( newTopic, data );
    }
    return hub;
}

/**
 * Returns an associative array whose key is the value of the view attribute of
 * a content node contained in the widget metadata and whose value is an
 * instance of an OpenAjax.widgets.View class.
 * @return list of supported views
 * @type Array
 */
OpenAjax.widget.Widget.prototype.getSupportedViews = function()
{
    if ( ! this.viewsList ) {
        this.viewsList = [];
        for ( var i = 0; i < this.views.length; i++) {
            var name = this.views[i];
            this.viewsList[ name ] = new OpenAjax.widget.View( name );
        }
    }
    return this.viewsList;
}

/**
 * The view parameter specifies the view instance to be displayed. If the
 * container grants the request, a 'viewChange' event is triggered.
 * @param {OpenAjax.widgets.View} view
 */
OpenAjax.widget.Widget.prototype.requestNavigateTo = function( view )
{
    this.changeView( view.name );
}

OpenAjax.widget.Widget.prototype.getProperty  = function(name)
{
    if ( this.properties ) {
        return this.properties.getProperty(name);
    }
    return null;
}

OpenAjax.widget.Widget.prototype.setProperties = function(propertiesCollection) 
{
    if ( ! propertiesCollection ) {
        return;
    }
    
    if ( typeof propertiesCollection.length === 'number' ) {  // is array
        this.properties = new OpenAjax.widget.WidgetProperties( propertiesCollection, this );
    } else if ( typeof propertiesCollection.setProperties === 'function' ) {  // is of type OpenAjax.widget.WidgetProperties
        this.properties = propertiesCollection;
    }
}

OpenAjax.widget.Widget.prototype.getProperties = function()
{
    if ( this.properties ) {
        var props =  this.properties.getProperties();
        var props_array = [];
        var i=0;
        for (var key in props) {
            props_array[i] = props[key];
            i++;
        }
        return props_array;
    }
    return null;
}

OpenAjax.widget.Widget.prototype.getPropertiesDatums = function()
{
    if ( this.properties ) {
        var props =  this.properties.getProperties();
        var props_array = new Array();
        var i=0;
        for (var key in props) {
            var property = props[key];
            props_array.push(property.datum);
            i++;
        }
        return props_array;
    }
    return null;
}

/*
 * Connect to the OpenAjax Hub instance using the appropriate
 * communications provider.
 */
OpenAjax.widget.Widget.prototype.connectToHub = function()
{
    this.connHandle = {};
    
    var that = this;
    function connectCallback( success, connHandle ) {
        if ( !success ) {
            // XXX handle error
            alert( "The widget " + that.hubId + " failed to connect to the parent" );
            return;
        }
        that.connHandle = connHandle;
    };
    
    OpenAjax.hub.connect({ providerName: this.comm,
            clientName: this.hubId, callback: connectCallback });
}

OpenAjax.widget.Widget.prototype.removeFromHub = function()
{
    for ( var type in this.listeners ) {
        var handle = this.listeners[ type ].handle
        if ( handle ) {
            handle.unsubscribe();
        }
        delete this.listeners[ type ];
    }
    
    this.removeSubHandle.unsubscribe();
    var that = this;
    this.connHandle.disconnect( function( success, connHandle ) {
        if ( ! success ) {
            console.log( "ERROR: failed to disconnect widget " + that.gID +
                    "from the hub" );
        }
    });
}

OpenAjax.widget.Widget.prototype.fireEvent = function( type, data )
{
    // publish to "openajax.widget.<ID>.<TYPE>"
    // - Widget publishes to = ID
    // - WidgetModel publishes to id = __ID
    if ( typeof data === 'undefined' ) {
        data = null;
    }
    this.connHandle.publish( "openajax.widget." + this.hubId + "." + type,
            JSON.stringify( data ) );
}

OpenAjax.widget.Widget.prototype.listenForEvents = function()
{
    // We only run our 'remove' routine after all of the other listeners that
    // were registered by the widget.  So instead of registering all 'remove'
    // listeners to the hub, we just register ours -- all other listeners are
    // just saved in the 'listeners' array.  When our handler gets called,
    // we cycle through the other listeners first, before running our routine.
    var that = this;
    this.connHandle.subscribe( "openajax.widget.__" + this.gID + ".remove",
            function( success, subHandle ) {
                if ( !success ) {
                    // XXX handle error
                    alert( "subscribe failed" );
                    return;
                }
                that.removeSubHandle = subHandle;
            },
            function( subHandle, topic, dataString ) {
                // handle the other listeners first
                that.handleEvent( subHandle, topic, null );

                // now run our 'remove' routine...
                // notify MashupMaker that we have run our 'remove' callbacks
                that.inRemoval = true;
                that.fireEvent( "_removed", null );
                that.removeFromHub();
                window[ that.gID ] = null; // XXX do we still add the widget to the 'window' obj?
            }
    );

    // Listen for property value changes from the WidgetModel.  If the given
    // property doesn't exist, then we create it.
    this.registerCallback( '_propValueChange', function( data ) {
        for ( var i = 0; i < data.length; i++ ) {
            var prop = this.getProperty( data[i].name );
            if ( ! prop ) {
                var datum = {};
                datum[ OpenAjax.widget.WidgetProperty.ATTRIBUTES.NAME ] = data[i].name;
                datum[ OpenAjax.widget.WidgetProperty.ATTRIBUTES.VALUE ] = data[i].value;
                this.properties.addProperty( datum );
            } else {
                prop.value( data[i].value, false );
            }
        }
    });

    // this.registerCallback( '_removeProp', function( data ) {
    // });
    
    // When using registerCallback(), the 'this' variable will point to the
    // widget instance, which could be the widget's custom JS class or the
    // actual Widget class instance.  For that reason, we use 'that' rather
    // than 'this'.
    this.registerCallback( 'resize', function( data ) {
        that.dimensions = data;
    });

    // For the 'unload' method, we directly listen to the document's 'unload'
    // event, rather than waiting for an event from the mashup.  This is
    // necessary so that IFRAMEd widgets can get this notification.
    OpenAjax.widget.Widget.addEventListener( 'unload', window,
            function() {
                if ( ! that.inRemoval ) {
                    that.handleEvent( null, 'unload', null );
                }
            }
    );
    
    // We receive the '_showView' event when the user has selected a different
    // view.  Later, this method calls any registered 'viewChange' callbacks.
    this.registerCallback( '_showView', this.changeView );
}

OpenAjax.widget.Widget.prototype.handleEvent = function( subHandle, topic, dataString )
{
    var eventType = topic.split(".").pop();
    var data = null;
    if ( dataString ) {
        data = JSON.parse( dataString );
    }
    
    var listeners = this.listeners[ eventType ];
    if ( ! listeners ) {
        return;
    }
    
    // The normal event types (i.e. 'load', 'insert', 'remove', etc) must run
    // in the context of the actual widget code (in case the widget has a
    // custom class).  However, the internal events (those that start with an
    // underscore) must run in the context of the Widget object.
    var context = this.instance;
    if ( eventType.charAt(0) == '_' ) {
        context = this;
    }
    
    for ( var i = 0; i < listeners.funcs.length; i++ ) {
        try {
            listeners.funcs[i].call( context, data );
        } catch( e ) {
            console.log( "Problem executing callback for '" + eventType + "' event" );
        }
    }
}

OpenAjax.widget.Widget.prototype.setBatchMode = function( doBatch )
{
    this.doBatch = doBatch;
    if ( doBatch ) {
        this.batchChanges = [];
    } else {
        // batch mode has been disabled, so aggregate any property changes
        // and send a notification event
        if ( this.batchChanges.length > 0 ) {
            this.fireEvent( "_propValueChange", this.batchChanges );
        }
        this.batchChanges = null;
    }
}

OpenAjax.widget.Widget.prototype.sendPropChangeNotification = function( prop )
{
    var data = { name: prop.name(), value: prop.value().toString() };
    if ( ! this.doBatch ) {
        this.fireEvent( "_propValueChange", [data] );
    } else {
        this.batchChanges.push( data );
    }
}

OpenAjax.widget.Widget.prototype.changeView = function( viewName )
{
    // hide the previous view and show the new view
    var prevView = document.getElementById( this.gID + "_view_" +
            this.currentViewName.replace( ":", "_" ) );
    var newView = document.getElementById( this.gID + "_view_" +
            viewName.replace( ":", "_" ) );
    prevView.style.display = "none";
    newView.style.display = "inline";

    var prevViewName = this.currentViewName;
    this.currentViewName = viewName;
    
    // notify any "viewChange" callbacks
    this.handleEvent( null, "viewChange",
            '{"previousView":"' + prevViewName + '","newView":"' + viewName + '"}' );
}

OpenAjax.widget.Widget.bind = function(toWhom, callback)
{
    var __method = callback;
    return function() {
        return __method.apply(toWhom, arguments);
    }
}

OpenAjax.widget.Widget.addEventListener = function(eventType, onWhom, callback) {
    if (onWhom.addEventListener) {
        onWhom.addEventListener(eventType, callback, false);
    } else {
        onWhom.attachEvent('on' + eventType, callback);
    }
}

/**
 * @param {String} id  The ID to be used for the created widget.
 * @param {Function} widgetClass  Name of widget class.  If not specified,
 *          uses OpenAjax.widget.Widget.
 * @param {array} properties  Array of properties for the wdget -- these are
 *          either read from persistent storage or the defaults as specified
 *          in the widget spec.
 * @param {String} comm  Identifier for Hub communications provider
 * @param {Object} dimensions  Initial dimensions of widget - {w, h}
 * @param {array} views  Array of view names
 *
 * @returns the widget instance
 * @type Object
 */
OpenAjax.widget.Widget.createWidgetObjectInstance = function( id, widgetClass,
        properties, comm, dimensions, views )
{
    var w = new OpenAjax.widget.Widget( id, properties, comm, dimensions, views );
    
    // instantiate the actual widget code, passing in the Widget obj
    var widget;
    if ( widgetClass ) {
        widget = eval( 'new ' + widgetClass + '( id, w );' );
    } else {
        widget = w;
    }
    
    // save reference to the actual widget in the Widget obj
    w.instance = widget;
    return widget;
}

////////////////////////////////////////////////////////////////////////////////

OpenAjax.widget.WidgetProperties = function( datums, widget )
{
    this.init( datums, widget );
}

OpenAjax.widget.WidgetProperties.prototype.init = function( datums, widget )
{
    this.widget = widget;

    this.properties = {};
    if ( datums ) {
        this.setProperties( datums );
    }
}

OpenAjax.widget.WidgetProperties.prototype.getProperty = function(name)
{
    if ( this.properties[name] != "undefined" ) {
        return this.properties[name];
    }
    return null;
}

OpenAjax.widget.WidgetProperties.prototype.getProperties = function()
{
    return this.properties;
}

OpenAjax.widget.WidgetProperties.prototype.setProperties = function( datums )
{
    for ( var i = 0 ; i < datums.length; i++ ) {
        this.addProperty( datums[i] );
    }
}

OpenAjax.widget.WidgetProperties.prototype.addProperty = function( datum )
{
    var preference = new OpenAjax.widget.WidgetProperty( datum, this.widget );
    if ( typeof datum["onchangePattern"] !== "undefined" && datum["onchangePattern"] != "" ) {
        preference._widgetHandlerName = this._applyOnchangePattern( preference.name(), datum["onchangePattern"] );
    }
    this.properties[ preference.name() ] = preference;
}

OpenAjax.widget.WidgetProperties.prototype._applyOnchangePattern = function( name, pattern )
{
    var tempname = pattern;
    tempname = tempname.replace( /{{property}}/g, name );
    tempname = tempname.replace( /{{propertyUCFirst}}/g, name.charAt(0).toUpperCase() + name.substr(1) );
    tempname = tempname.replace( /{{propertyLCFirst}}/g, name.charAt(0).toLowerCase() + name.substr(1) );
    return tempname;
}

////////////////////////////////////////////////////////////////////////////////

OpenAjax.widget.WidgetProperty = function( datum, widget )
{
    this.widget = widget;
    if ( datum ) {
        this.datum = {};
        this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.NAME] = datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.NAME];
        this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.DATATYPE] = datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.DATATYPE];
        this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.VALUE] = datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.VALUE];

        this._setValue( this.decodedValue(), false );
    }
}

OpenAjax.widget.WidgetProperty.ATTRIBUTES =  {
    NAME: "name",
    VALUE: "default",
    DATATYPE: "datatype"
}

OpenAjax.widget.WidgetProperty.prototype.name =  function()
{
    return this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.NAME];
}       

OpenAjax.widget.WidgetProperty.prototype.type = function()
{
    var type = this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.DATATYPE];
    if ( ! type ) {
        type = "String";
    }
                
    //type does not change so change the def of the function
    this.type = function(){
        return type;
    }
    return type;
}

OpenAjax.widget.WidgetProperty.prototype.decodedValue = function()
{
    var value = this._getValue();
    
    switch( this.type() ) {
        case "String" :
        case "Date" :
        case "RegExp" :
            return (typeof value == "undefined") ? "" : value;
        
        default : {
            try {
                return (typeof value == "undefined") ? null : JSON.parse( value );
            } catch( e ) {
                return null;
            }
        }
    }
}

OpenAjax.widget.WidgetProperty.prototype.encodedValue = function()
{
    var value = this._getValue();
    
    switch( this.type() ) {
        case "String" :
        case "Date" :
        case "RegExp" :
            return value;
        
        default : {
            try {
                return JSON.stringify( value );
            } catch( e ) {
                return "";
            }
        }
    }
}

/**
 * If no parameters are given, then this method returns the current value of
 * this property.  If a new value is specified, then this method sets the
 * property value to this new value, and returns the new value.
 *
 * @param {Object} newVal  If set, then the property value is set to this new
 *          value.  If not set, then this method returns the current value.
 * @param {boolean} doPropagate  If 'true', then value changes are synced
 *          between WidgetProperty and WidgetModelProperties.  Default is true.
 * @return The current (or newly set) value of this property.
 * @type Object
 */
OpenAjax.widget.WidgetProperty.prototype.value =  function( newVal, doPropagate )
{
    if ( typeof newVal === "undefined" ) {
        return this._getValue();
    }

    if ( this._setValue( newVal, doPropagate ) ) {
        //call implicit handler on widget if available on<Prop Name>Change
        if( !this._widgetHandlerName ){
            var propName = this.name();
            var name = propName.charAt(0).toUpperCase() + propName.substring(1);
            propName = "on" + name + "Change"; //camel-case
            this._widgetHandlerName = propName;
        }

        var instance = this.widget.instance;
        if ( typeof instance[ this._widgetHandlerName ] === 'function' ) {
            try{
                instance[ this._widgetHandlerName ].call( instance, this.value() );
            }catch(error){
                console.log( "Property <"+this.name()+">: Error invoking "+
                        this._widgetHandlerName+" callback on widget <"+this.widget.gID+">." );
            }
        }
    }
                    
    return this._getValue();
}

OpenAjax.widget.WidgetProperty.prototype._getValue = function()
{
    return this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.VALUE];
}

OpenAjax.widget.WidgetProperty.prototype._setValue = function( newVal, doPropagate )
{
    switch ( this.type() ) {
        case "Boolean" : {
            newVal = Boolean( newVal );
            break;
        }
        case "Number" : {
            newVal = Number( newVal );
            break;
        }
        case "Date" : {
            if ( typeof newVal === "string" ) {
                if ( newVal == "" ) {
                    // XXX Assume that an empty string default value for Date
                    // signifies 'now'
                    newVal = new Date();
                } else {
                    newVal = new Date( newVal );
                }
            }
            break;
        }
        case "RegExp" : {
            if ( typeof newVal === "string" ) {
                newVal = new RegExp( newVal );
            }
            break;
        }
    }

    if ( this._getValue() === newVal ) {
        return false;  // not changed so no event needed
    }

    this.datum[OpenAjax.widget.WidgetProperty.ATTRIBUTES.VALUE] = newVal;

    if ( typeof doPropagate === 'undefined' || doPropagate ) {
        this.widget.sendPropChangeNotification( this );
    }
    
    return true;
}

////////////////////////////////////////////////////////////////////////////////

OpenAjax.widget.View = function( name )
{
    this.name = name;
}

