'use strict';

/*@
Ox.Request <o> Basic request controller
# FIXME: options is not a property, just documenting defaults
# options <o> Options object
#     timeout <n|60000>   request timeout
#     type    <s|"POST">  request type, possible values POST, GET, PUT, DELETE
#     url     <s>         request url
@*/

Ox.Request = (function() {

    var cache = {},
        pending = {},
        requests = {},
        self = {
            options: {
                cache: true,
                timeout: 60000,
                type: 'POST',
                url: '/api/'
            }
        },
        $element;

    return {

        /*@
        bindEvent <f> Bind event
        @*/
        bindEvent: function() {
            if (!$element) {
                $element = Ox.Element();
            }
            $element.bindEvent.apply($element, arguments);
        },

        /*@
        cancel <f> cancel pending requests
            ()  -> <u>  cancel all requests
            (fn)  -> <u>  cancel all requests where function returns true
            (id)  -> <u>  cancel request by id
        @*/
        cancel: function() {
            if (arguments.length == 0) {
                // cancel all requests
                requests = {};
            } else if (Ox.isFunction(arguments[0])) {
                // cancel with function
                var fn = arguments[0];
                Ox.forEach(requests, function(req, id) {
                    if (fn(req)) {
                        delete requests[id];
                    }
                });
            } else {
                // cancel by id
                delete requests[arguments[0]];
            }
            $element && $element.triggerEvent('request', {
                requests: Ox.len(requests)
            });
        },

        /*@
        clearCache <f> clear cached results
            () -> <u> ...
        @*/
        clearCache: function(query) {
            if (!query) {
                cache = {};
            } else {
                cache = Ox.filter(cache, function(val, key) {
                    return key.indexOf(query) == -1;
                });
            }
        },

        /*@
        options <f> get/set options
            ()        -> <o> get options
            (options) -> <o> set options
            options <o> Options Object
        @*/
        options: function() {
            return Ox.getset(self.options, arguments, function() {}, this);
        },

        /*@
        requests <f> pending requests
            () -> <n> returns number of requests
        @*/
        requests: function() {
            return Ox.len(requests);
        },

        /*@
        send <f> send request
            (options) -> <n> returns request id
            options <o> Options Object
                age <n|-1> cache age
                id <n|Ox.uid()> request id
                timeout <n|self.options.timeout> overwrite default timeout
                type <n|self.options.timeout> overwrite default type
                url <n|self.options.timeout> overwrite default url
        @*/
        send: function(options) {

            var data,
                options = Ox.extend({
                    age: -1,
                    callback: null,
                    id: Ox.uid(),
                    timeout: self.options.timeout,
                    type: self.options.type,
                    url: self.options.url
                }, options),
                req = JSON.stringify({
                    url: options.url,
                    data: options.data
                });

            if (pending[req]) {
                wait();
            } else {
                requests[options.id] = {
                    url: options.url,
                    data: options.data
                };
                if (cache[req] && (
                    options.age == -1
                    || options.age > +new Date() - cache[req].time
                )) {
                    data = cache[req].data;
                    setTimeout(function() {
                        callback(data, true);
                    });
                } else {
                    pending[req] = true;
                    $.ajax({
                        beforeSend: function(request) {
                            var csrftoken = Ox.Cookies('csrftoken');
                            if (csrftoken) {
                                request.setRequestHeader('X-CSRFToken', csrftoken);
                            }
                        },
                        complete: complete,
                        data: options.data,
                        // dataType: 'json',
                        timeout: options.timeout,
                        type: options.type,
                        url: options.url
                    });
                }
                $element && $element.triggerEvent('request', {
                    requests: Ox.len(requests)
                });
            }

            function callback(data, success) {
                if (requests[options.id]) {
                    delete requests[options.id];
                    $element && $element.triggerEvent('request', {
                        requests: Ox.len(requests)
                    });
                    if (success) {
                        options.callback && options.callback(data);
                    } else {
                        $element && $element.triggerEvent('error', data);
                    }
                }
            }

            function complete(request) {
                var $dialog, data;
                try {
                    data = JSON.parse(request.responseText);
                } catch (error) {
                    try {
                        data = {
                            status: {
                                code: request.status,
                                text: request.statusText
                            }
                        };
                    } catch (error) {
                        data = {
                            status: {
                                code: '500',
                                text: 'Unknown Error'
                            }
                        };
                    }
                }
                if (Ox.contains([200, 404, 409], data.status.code)) {
                    // we have to include not found and conflict
                    // so that handlers can handle these cases
                    if (self.options.cache) {
                        cache[req] = {
                            data: data,
                            time: Ox.getTime()
                        };
                    }
                    callback(data, true);
                } else {
                    callback(data, false);
                }
                delete pending[req];
            }

            function wait() {
                setTimeout(function() {
                    if (pending[req]) {
                        wait();
                    } else {
                        Ox.Request.send(options);
                    }
                });
            }

            return options.id;

        },

        /*@
        unbindEvent <f> Unbind event
        @*/
        unbindEvent: function() {
            $element && $element.unbindEvent.apply($element, arguments);
        }

    };

}());