/*
    Revver API Client v0.5.1

    Simple javascript abstraction for using Revver's JSON API.

    Features:

       * transparently handles cross domain callbacks

       * allows you to make calls using native javascript arguments

       * caches the results of API calls to reduce network latency

       * provides a pagination convenience which caches several pages per API
         call to further reduce network latency

       * tiny, 8k compressed

       * no dependencies on other libraries, freeing you to use which ever
         library you prefer!

    Todo:

       * support exception handling

    Example usage:

        var api = new Revver.API('https://api.revver.com/json/1.0');

        var callbacks = {
            onSuccess: function(result) {
                alert(result[0].title);
            }
        }

        api.open.collection.collect(593, ['title'], {limit:1}, callbacks);

        api.open.video.find({'owners': ['asi']}, ['title'], {limit:1}, callbacks);

        var pager = api.open.collection.collect.pager(593, ['title']);
        pager.page(15, 0, callbacks);

    Have fun!

    If you make something cool, or make a cool change, we'd love to hear about
    it!
        -- dev-support@revver.com
*/

// Setup some private name spaces
var Revver = new Object();

Revver.GLOBAL = new Object();
Revver.crossdomain = new Object();
Revver.util = new Object();
Revver.defer = new Object();
Revver.cache = new Object();

// Number of videos to fetch on every API call
Revver.GLOBAL.chunkSize = 32;


// Registry for keeping track of outstanding cross domain calls
Revver.crossdomain.registry = {};

// Generate a unique registery key, for tracking API calls.
Revver.crossdomain.getUniqueCallKey = function() {
    var timestamp = (new Date()).getTime();
    var extra = 1;
    while((timestamp + '.' + extra) in Revver.crossdomain.registry) {
        extra += 1;
    }
    var key = (timestamp + '.' + extra);
    Revver.crossdomain.registry[key] = null;
    return key;
}

// Make a cross domain call
Revver.crossdomain.call = function(url) {

    var key = Revver.crossdomain.getUniqueCallKey()

    // Request that the json request be wrapped with our callback function
    url += "&callback=" + escape('Revver.crossdomain.callback');
    url += "&callback_params=" + escape(key);
    url += "&noCache=" + (new Date()).getTime();

    // Create a script element for the call url, and insert it into the
    // document header.
    var scriptElement = document.createElement("script");
    scriptElement.setAttribute("src", url);
    document.getElementsByTagName("head").item(0).appendChild(scriptElement);

    // Save a deferred, and the scriptElement in the registry, to
    // be picked up by the Revver.crossdomain.callback
    var d = new Revver.defer.Deferred();
    Revver.crossdomain.registry[key] = [d, scriptElement];
    return d;
}

// This is the callback fired by the embedded script tag.  It takes care of
// cleaning up the registry, removing the script tag, and running callbacks
// with the response.
Revver.crossdomain.callback = function(result, key) {

    var registry = Revver.crossdomain.registry[key];

    var scriptElement = registry[1];
    document.getElementsByTagName("head").item(0).removeChild(scriptElement);
    delete Revver.crossdomain.registry[key];

    var d = registry[0];
    d.callback(result);
}


// Simple in memory cache with expiry
Revver.cache._cache = {};

Revver.cache.get = function(key) {
    if(!this._cache[key]) return null;

    /*
    var unpack = this._cache[key];
    var stamp = unpack[0];
    var 
    */
    return this._cache[key];
}

Revver.cache.set = function(key, value) {
    this._cache[key] = value;
}

Revver.cache.call = function(url) {

    var result = Revver.cache.get(url);
    if (result) return Revver.defer.succeed(result);
        
    var d = Revver.crossdomain.call(url);
    d.addCallback(
        function(result) {
            Revver.cache.set(url, result);
            return result;
        }
    );
    return d;
}

// A naive deferred implementation to help abstract chaining callbacks
Revver.defer.Deferred = function () {

    this.called = false;
    this.callbacks = new Array();
    this._im_a_deferred = true;

    this.addCallback = function (callback) {
        var args = Array();
        for(var i=1; i<arguments.length; i++) args[args.length] = arguments[i];

        this.callbacks[this.callbacks.length] = {method : callback, args : args};

        if(this.called) this._runCallbacks();
        return this;
    }

    this.callback = function(result) {
        if (this.called) {
            throw("Deferred:: callback already called");
        }
        this.called = true;
        this.result = result;
        this._runCallbacks();
    }

    this._runCallbacks = function() {
        while(this.callbacks.length) {
            var callback = this.callbacks.shift();

            var method = callback.method;
            var args   = callback.args;
            args.unshift(this.result);

            this.result = method.apply(method, args);
        }
    }
}

Revver.defer.DeferredList = function (dlist) {

    this._cb_Deferred = function(result, i) {

        this.results[i] = result;
        this.finishedCount += 1;

        if(this.finishedCount == this.results.length) {
            this.callback(this.results);
        }
    }

    this._inherit = Revver.defer.Deferred;
    this._inherit();

    this.finishedCount = 0;
    this.results = Array();
    for(var i=0; i<dlist.length; i++) {
        this.results[this.results.length] = null;
    }
    for(var i=0; i<dlist.length; i++) {
        dlist[i].addCallback(
            Revver.util.bindMethod(this, this._cb_Deferred), i);
    }
}

Revver.defer.succeed = function(result) {
    var d = new Revver.defer.Deferred();
    d.callback(result);
    return d;
}

Revver.defer.maybeDeferred = function(result) {
    if (typeof result == 'object') if (result._im_a_deferred) return result;
    return Revver.defer.succeed(result);
}


// Convenience for paginating through a result set.  Transparently caches
// several pages of data per API call
Revver.Pager = function(method, match, returnFields) {

    this.method = method;
    this.match = match;
    this.returnFields = returnFields;

    this.page = function(limit, offset, callbacks) {

        var chunkSize = Revver.GLOBAL.chunkSize;
        if (limit > chunkSize) chunkSize = limit;

        var firstChunk = Math.floor(offset/chunkSize);
        var secondChunk = Math.floor((offset+limit-1)/chunkSize);

        if (firstChunk == secondChunk) {
            var neededChunks = [firstChunk];
        } else {
            var neededChunks = [firstChunk, secondChunk];
        }

        var dlist = new Array();

        for (var x in neededChunks) {
            var neededChunk = neededChunks[x];

            var d = this.method(match, returnFields, {
                limit: chunkSize,
                offset: neededChunk*chunkSize,
                count: true
            });

            dlist[dlist.length] = d;
        }

        var d = new Revver.defer.DeferredList(dlist);

        d.addCallback(
            function(chunks) {
                var count = chunks[0][0];
                var videos = chunks[0][1];
                if (chunks.length == 2) {
                    for (var i=0; i<chunks[1][1].length; i++) {
                        videos[videos.length] = chunks[1][1][i];
                    }
                }

                var page = new Array();
                var start = offset - (firstChunk*chunkSize);

                for (var i=start; (i<start+limit) && (i<videos.length); i++) {
                    page[page.length] = videos[i];
                }
                return [count, page];
            }
        );

        if (callbacks) {
            d.addCallback(
                function(result) {
                    callbacks.onSuccess(result);
                }
            );
        }

        return d;
    }

    this.pump = function(limit, offset, callbacks) {
        return this.method(match, returnFields, {
            limit: limit,
            offset: offset,
            final: true
        }, callbacks);
    }
}


// Proxy object for calling Revver's JSON API
Revver.API = function(uri) {

    this.uri = uri;

    this.setNamespace = function(sNameSpace, method) {
        if (!sNameSpace || sNameSpace.length < 2) {
            return null;
        }
        var currentNS = this;
        var levels = sNameSpace.split(".");
        for (var i=0; i<levels.length-1; ++i) {
            currentNS[levels[i]] = currentNS[levels[i]] || {};
            currentNS = currentNS[levels[i]];
        }
        currentNS[levels[i]] = Revver.util.bindMethod(this, method);
    }

    this._getPager = function(method) {
        return function(match, returnFields) {
            return new Revver.Pager(method, match, returnFields);
        }
    }

    this.setNamespace('open.collection.collect',
        function(id, returnFields, options, callbacks) {
            return this.__callApiMethod(
                'open.collection.collect', [id, returnFields, options], callbacks);
        });
    this.open.collection.collect.pager = this._getPager(this.open.collection.collect);

    this.setNamespace('open.collection.find',
        function(query, returnFields, options, callbacks) {
            return this.__callApiMethod(
                'open.collection.find', [query, returnFields, options], callbacks);
        });
    this.open.collection.find.pager = this._getPager(this.open.collection.find);

    this.setNamespace('open.collection.get',
        function(id, returnFields, callbacks) {
            return this.__callApiMethod(
                'open.collection.get', [id, returnFields], callbacks);
        });

    this.setNamespace('open.video.find',
        function(query, returnFields, options, callbacks) {
            return this.__callApiMethod(
                'open.video.find', [query, returnFields, options], callbacks);
        });
    this.open.video.find.pager = this._getPager(this.open.video.find);

    this.setNamespace('open.video.get',
        function(query, returnFields, callbacks) {
            return this.__callApiMethod(
                'open.video.get', [query, returnFields], callbacks);
        });

    this.__callApiMethod = function(method, params, callbacks) {

        var url = this.uri + "?" + "method=" + method;
        url += "&params=" + escape(Revver.util.toJSONString.object(params));

        var d = Revver.cache.call(url);
        if (callbacks) {
            d.addCallback(
                function(result) {
                    callbacks.onSuccess(result);
                }
            );
        }
        return d;
    }   
}

// Truncate the supplied string, to the given length, respecting word boundaries
Revver.util.truncate = function(str, len) {
    if (str.length > len) {
        str = str.substring(0, len);
        str = str.replace(/\w+$/, '');
        str += '...';
    }
    return str;
}

// Bind a method to the supplied object
Revver.util.bindMethod = function(ob, method) {
    return function() {
        return method.apply(ob, arguments);
    }
}

// usage: Revver.util.toJSONString.object(yourObjectOrArray)
// This method produces a JSON text from an array or an object.
Revver.util.toJSONString = {
    m: {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    },

    array: function (x) {
        var a = ['['], b, f, i, l = x.length, v;
        for (i = 0; i < l; i += 1) {
            v = x[i];
            f = Revver.util.toJSONString[typeof v];
            if (f) {
                v = f(v);
                if (typeof v == 'string') {
                    if (b) {
                        a[a.length] = ',';
                    }
                    a[a.length] = v;
                    b = true;
                }
            }
        }
        a[a.length] = ']';
        return a.join('');
    },
    'boolean': function (x) {
        return String(x);
    },
    'null': function (x) {
        return "null";
    },
    number: function (x) {
        return isFinite(x) ? String(x) : 'null';
    },
    object: function (x) {
        if (x) {
            if (x instanceof Array) {
                return Revver.util.toJSONString.array(x);
            }
            var a = ['{'], b, f, i, v;
            for (i in x) {
                v = x[i];
                f = Revver.util.toJSONString[typeof v];
                if (f) {
                    v = f(v);
                    if (typeof v == 'string') {
                        if (b) {
                            a[a.length] = ',';
                        }
                        a.push(Revver.util.toJSONString.string(i), ':', v);
                        b = true;
                    }
                }
            }
            a[a.length] = '}';
            return a.join('');
        }
        return 'null';
    },
    string: function (x) {
        if (/["\\\x00-\x1f]/.test(x)) {
            x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                var c = Revver.util.toJSONString.m[b];
                if (c) {
                    return c;
                }
                c = b.charCodeAt();
                return '\\u00' +
                Math.floor(c / 16).toString(16) +
                (c % 16).toString(16);
            });
        }
        return '"' + x + '"';
    }
};
