/**
 * Ext.ux.CssProxy
 * Version 1.0
 * Copyright (c) 2009 - David W Davis - http://xant.us/
 *
 * Adapted from CSSHttpRequest for Extjs
 * 
 * CSSHttpRequest
 * Copyright 2008 nb.io - http://nb.io/
 * http://nb.io/hacks/csshttprequest/
 * Licensed under Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html
 */

/**
 * @class Ext.ux.CssProxy
 * @extends Ext.data.DataProxy
 * An implementation of Ext.data.DataProxy that reads a data object from a URL which may be in a domain
 * other than the originating domain of the running page.<br>
 * <p>
 * <b>Note that if you are retrieving data from a page that is in a domain that is NOT the same as the originating domain
 * of the running page, you must use Ext.data.ScriptTagProxy or this class, rather than HttpProxy.</b><br>
 * <p>
 * The content passed back from a server resource requested by a CssProxy <b>must</b> be specially encoded css.
 * <p>
 * Data is encoded on the server into URI-encoded 2KB chunks and serialized into CSS rules with a modified
 * data: URI scheme. The selector should be in the form #c<N>, where N is an integer index in [0,].
 * The response is decoded and returned to the callback function as a string:
 * <p>
 * <pre><code>
 * #c0 { background: url(data:,Hello%20World!); }
 * #c1 { background: url(data:,I.m%20text%20encoded%20in%20CSS!); }
 * #c2 { background: url(data:,I%20like%20arts%20and%20crafts.); }
 * <pre></code>
 * <p>
 * You can pass a simple reader to Ext.ux.CssProxy and handle the data directly, almost like you would with Ext.Ajax.
 * For example this fetches a json response and returns an object:
 * <pre><code>
 * 
 * SimpleReader = function() {
 *    SimpleReader.superclass.constructor.apply(this, arguments);
 * };
 * Ext.extend(SimpleReader, Ext.data.DataReader, {
 *    read: function(r) { return r.responseText; },
 *    readRecords: function(o) { return o; }
 * });
 *
 * var conn = new Ext.ux.CssProxy({ url: "http://s.nb.io/hacks/csshttprequest/time-json/" });
 * conn.load(
 *     { },                    // params
 *     new SimpleReader(),     // reader
 *     function(r) {           // callback
 *         r = Ext.decode(r);  // convert to json
 *         alert('Happy '+r.year+'!  iso:'+r.isoformat);
 *     }
 * );
 * </pre></code>
 *
 * @constructor
 * @param {Object} config A configuration object.
 */
Ext.namespace('Ext.ux');

Ext.ux.CssProxy = function(config){
    Ext.ux.CssProxy.superclass.constructor.call(this);
    Ext.apply(this, config);
    this.doc = document.documentElement;
    
    /**
     * @event loadexception
     * Fires if an exception occurs in the Proxy during data loading.  This event can be fired for one of two reasons:
     * <ul><li><b>The load call timed out.</b>  This means the load callback did not execute within the time limit
     * specified by {@link #timeout}.  In this case, this event will be raised and the
     * fourth parameter (read error) will be null.</li>
     * <li><b>The load succeeded but the reader could not read the response.</b>  This means the server returned
     * data, but the configured Reader threw an error while reading the data.  In this case, this event will be 
     * raised and the caught error will be passed along as the fourth parameter of this event.</li></ul>
     * Note that this event is also relayed through {@link Ext.data.Store}, so you can listen for it directly
     * on any Store instance.
     * @param {Object} this
     * @param {Object} options The loading options that were specified (see {@link #load} for details).  If the load
     * call timed out, this parameter will be null.
     * @param {Object} arg The callback's arg object passed to the {@link #load} function
     * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data.
     * If the load call returned success: false, this parameter will be null.
     */
};

Ext.ux.CssProxy.TRANS_ID = 1000;
Ext.ux.CssProxy.sandbox = function(x) { };
Ext.ux.CssProxy.MATCH_ORDINAL = /#c(\d+)/;
Ext.ux.CssProxy.MATCH_URL = /url\("?data\:[^,]*,([^")]+)"?\)/; // "

Ext.extend(Ext.ux.CssProxy, Ext.data.DataProxy, {
    /**
     * @cfg {String} url The URL from which to request the data object.
     */
    /**
     * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds.
     */
    timeout: 30000,
    /**
     *  @cfg {Boolean} nocache (optional) Defaults to true. Disable caching by adding a unique parameter
     * name to the request.
     */
    nocache: true,

    /**
     * Load data from the configured URL, read the data object into
     * a block of Ext.data.Records using the passed Ext.data.DataReader implementation, and
     * process that block using the passed callback.
     * @param {Object} params An object containing properties which are to be used as HTTP parameters
     * for the request to the remote server.
     * @param {Ext.data.DataReader} reader The Reader object which converts the data
     * object into a block of Ext.data.Records.
     * @param {Function} callback The function into which to pass the block of Ext.data.Records.
     * The function must be passed <ul>
     * <li>The Record block object</li>
     * <li>The "arg" argument from the load function</li>
     * <li>A boolean success indicator</li>
     * </ul>
     * @param {Object} scope The scope in which to call the callback
     * @param {Object} arg An optional argument which is passed to the callback as its second parameter.
     */
    load: function(params, reader, callback, scope, arg){
        if (this.fireEvent('beforeload', this, params) !== false) {

            if (this.autoAbort !== false) {
                this.abort();
            }

            var p = Ext.urlEncode(Ext.apply(params, this.extraParams));

            var url = this.url;
            url += (url.indexOf('?') != -1 ? '&' : '?') + p;
            if (this.nocache) {
                url += '&_dc=' + (new Date().getTime());
            }
            var transId = ++Ext.ux.CssProxy.TRANS_ID;
            
            var trans = {
                id: transId,
                cb: 'chrCallback'+transId,
                frameId: 'chrIframe'+transId,
                params: params,
                arg: arg,
                url: url,
                callback: callback,
                scope: scope,
                reader: reader
            };
            
            var iframe = document.createElement( 'iframe' );
            iframe.setAttribute( 'id', trans.frameId );
            iframe.style.position = 'absolute';
            iframe.style.left = iframe.style.top = '-1000px';
            iframe.style.width = iframe.style.height = 0;
            this.doc.appendChild( iframe );

            trans.document = iframe.contentDocument || iframe.contentWindow.document;
            
            window[trans.cb] = this.handleResponse.createDelegate( this, [trans] );

            trans.document.open('text/html', false);
            trans.document.write("<html><head>");
            trans.document.write("<link rel='stylesheet' type='text/css' media='print, csshttprequest' href='" + Ext.util.Format.htmlEncode(url) + "' />");
            trans.document.write("</head><body>");
            trans.document.write("<script type='text/javascript'>");
            trans.document.write("(function(){var w = window; var p = w.parent; p.Ext.ux.CssProxy.sandbox(w); w.onload = function(){p." + trans.cb + "();};})();");
            trans.document.write("</script>");
            trans.document.write("</body></html>");
            trans.document.close();
            
            trans.timeoutId = this.handleFailure.defer( this.timeout, this, [trans] );

            this.trans = trans;
        }else{
            callback.call(scope||this, null, arg, false);
        }
    },

    // private
    isLoading: function(){
        return this.trans ? true : false;
    },

    /**
     * Abort the current server request.
     */
    abort: function(){
        if(this.isLoading()){
            this.destroyTrans(this.trans);
        }
    },

    // private
    destroyTrans: function(trans, isLoaded){
        window.setTimeout(function() {
            try { this.doc.removeChild(document.getElementById(trans.frameId)); } catch(e) {};
        }, 0);
        clearTimeout(trans.timeoutId);
        if(isLoaded){
            window[trans.cb] = undefined;
            try{
                delete window[trans.cb];
            }catch(e){}
        }else{
            // if hasn't been loaded, wait for load to remove it to prevent script error
            window[trans.cb] = function(){
                window[trans.cb] = undefined;
                try{
                    delete window[trans.cb];
                }catch(e){}
            };
        }
    },

    // private
    handleResponse: function(trans){
        var o = this.parseCSS(trans);
        this.trans = false;
        this.destroyTrans(trans, true);
        var result;
        try {
            result = trans.reader.read({ responseText: o });
            //result = trans.reader.read(o);
        }catch(e){
            this.fireEvent('loadexception', this, o, trans.arg, e);
            trans.callback.call(trans.scope||window, null, trans.arg, false);
            return;
        }
        this.fireEvent('load', this, o, trans.arg);
        trans.callback.call(trans.scope||window, result, trans.arg, true);
    },

    // private
    handleFailure: function(trans){
        this.trans = false;
        this.destroyTrans(trans, false);
        this.fireEvent('loadexception', this, null, trans.arg);
        trans.callback.call(trans.scope||window, null, trans.arg, false);
    },
    
    // private
    parseCSS: function(trans) {
        var data = [];
        try {
            // Safari, IE and same-domain Firefox
            var rules = trans.document.styleSheets[0].cssRules || trans.document.styleSheets[0].rules;
            for ( var i = 0, len = rules.length; i < len; i++ ) {
                try {
                    var r = rules.item ? rules.item(i) : rules[i];
                    var ord = r.selectorText.match(Ext.ux.CssProxy.MATCH_ORDINAL)[1];
                    var val = r.style.backgroundImage.match(Ext.ux.CssProxy.MATCH_URL)[1];
                    data[ord] = val;
                } catch(e) {};
            }
        } catch(e) {
            // catch same-domain exception
            trans.document.getElementsByTagName('link')[0].setAttribute('media', 'screen');
            var x = trans.document.createElement('div');
            x.innerHTML = 'x';
            trans.document.body.appendChild(x);
            var ord = 0;
            try {
                while (1) {
                    x.id = 'c' + ord;
                    var style = trans.document.defaultView.getComputedStyle(x, null);
                    var bg = style['background-image'] || style.backgroundImage || style.getPropertyValue('background-image');
                    var val = bg.match(Ext.ux.CssProxy.MATCH_URL)[1];
                    data[ord] = val;
                    ord++;
                }
            } catch(e) {};
        }
        return decodeURIComponent(data.join(''));
    }

});

