/**
 * A Twitter library in JavaScript
 *
 * @package   codebird
 * @version   2.6.0
 * @author    Jublo Solutions <support@jublo.net>
 * @copyright 2010-2015 Jublo Solutions <support@jublo.net>
 * @license   http://opensource.org/licenses/GPL-3.0 GNU Public License 3.0
 * @link      https://github.com/jublonet/codebird-php
 */

/* jshint curly: true,
 eqeqeq: true,
 latedef: true,
 quotmark: double,
 undef: true,
 unused: true,
 trailing: true,
 laxbreak: true */
/* global window,
 document,
 navigator,
 console,
 Ti,
 ActiveXObject,
 module,
 define,
 require */
(function (undefined) {
    "use strict";

    /**
     * Array.indexOf polyfill
     */
    if (! Array.prototype.indexOf) {
        Array.prototype.indexOf = function (obj, start) {
            for (var i = (start || 0); i < this.length; i++) {
                if (this[i] === obj) {
                    return i;
                }
            }
            return -1;
        };
    }

    /**
     * A Twitter library in JavaScript
     *
     * @package codebird
     * @subpackage codebird-js
     */
    /* jshint -W098 */
    var Codebird = function () {
        /* jshint +W098 */

        /**
         * The OAuth consumer key of your registered app
         */
        var _oauth_consumer_key = null;

        /**
         * The corresponding consumer secret
         */
        var _oauth_consumer_secret = null;

        /**
         * The app-only bearer token. Used to authorize app-only requests
         */
        var _oauth_bearer_token = null;

        /**
         * The API endpoint base to use
         */
        var _endpoint_base = "https://api.twitter.com/";

        /**
         * The media API endpoint base to use
         */
        var _endpoint_base_media = "https://upload.twitter.com/";

        /**
         * The API endpoint to use
         */
        var _endpoint = _endpoint_base + "1.1/";

        /**
         * The media API endpoint to use
         */
        var _endpoint_media = _endpoint_base_media + "1.1/";

        /**
         * The API endpoint base to use
         */
        var _endpoint_oauth = _endpoint_base;

        /**
         * API proxy endpoint
         */
        var _endpoint_proxy = "https://api.jublo.net/codebird/";

        /**
         * The API endpoint to use for old requests
         */
        var _endpoint_old = _endpoint_base + "1/";

        /**
         * Use JSONP for GET requests in IE7-9
         */
        var _use_jsonp = (typeof navigator !== "undefined"
        && typeof navigator.userAgent !== "undefined"
        && (navigator.userAgent.indexOf("Trident/4") > -1
        || navigator.userAgent.indexOf("Trident/5") > -1
        || navigator.userAgent.indexOf("MSIE 7.0") > -1
        )
        );

        /**
         * Whether to access the API via a proxy that is allowed by CORS
         * Assume that CORS is only necessary in browsers
         */
        var _use_proxy = (typeof navigator !== "undefined"
        && typeof navigator.userAgent !== "undefined"
        );

        /**
         * The Request or access token. Used to sign requests
         */
        var _oauth_token = null;

        /**
         * The corresponding request or access token secret
         */
        var _oauth_token_secret = null;

        /**
         * The current Codebird version
         */
        var _version = "2.6.0";

        /**
         * Sets the OAuth consumer key and secret (App key)
         *
         * @param string key    OAuth consumer key
         * @param string secret OAuth consumer secret
         *
         * @return void
         */
        var setConsumerKey = function (key, secret) {
            _oauth_consumer_key = key;
            _oauth_consumer_secret = secret;
        };

        /**
         * Sets the OAuth2 app-only auth bearer token
         *
         * @param string token OAuth2 bearer token
         *
         * @return void
         */
        var setBearerToken = function (token) {
            _oauth_bearer_token = token;
        };

        /**
         * Gets the current Codebird version
         *
         * @return string The version number
         */
        var getVersion = function () {
            return _version;
        };

        /**
         * Sets the OAuth request or access token and secret (User key)
         *
         * @param string token  OAuth request or access token
         * @param string secret OAuth request or access token secret
         *
         * @return void
         */
        var setToken = function (token, secret) {
            _oauth_token = token;
            _oauth_token_secret = secret;
        };

        /**
         * Enables or disables CORS proxy
         *
         * @param bool use_proxy Whether to use CORS proxy or not
         *
         * @return void
         */
        var setUseProxy = function (use_proxy) {
            _use_proxy = !! use_proxy;
        };

        /**
         * Sets custom CORS proxy server
         *
         * @param string proxy Address of proxy server to use
         *
         * @return void
         */
        var setProxy = function (proxy) {
            // add trailing slash if missing
            if (! proxy.match(/\/$/)) {
                proxy += "/";
            }
            _endpoint_proxy = proxy;
        };

        /**
         * Parse URL-style parameters into object
         *
         * version: 1109.2015
         * discuss at: http://phpjs.org/functions/parse_str
         * +   original by: Cagri Ekin
         * +   improved by: Michael White (http://getsprink.com)
         * +    tweaked by: Jack
         * +   bugfixed by: Onno Marsman
         * +   reimplemented by: stag019
         * +   bugfixed by: Brett Zamir (http://brett-zamir.me)
         * +   bugfixed by: stag019
         * -    depends on: urldecode
         * +   input by: Dreamer
         * +   bugfixed by: Brett Zamir (http://brett-zamir.me)
         * %        note 1: When no argument is specified, will put variables in global scope.
         *
         * @param string str String to parse
         * @param array array to load data into
         *
         * @return object
         */
        var _parse_str = function (str, array) {
            var glue1 = "=",
                glue2 = "&",
                array2 = String(str).replace(/^&?([\s\S]*?)&?$/, "$1").split(glue2),
                i, j, chr, tmp, key, value, bracket, keys, evalStr,
                fixStr = function (str) {
                    return decodeURIComponent(str).replace(/([\\"'])/g, "\\$1").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
                };
            if (! array) {
                array = this.window;
            }

            for (i = 0; i < array2.length; i++) {
                tmp = array2[i].split(glue1);
                if (tmp.length < 2) {
                    tmp = [tmp, ""];
                }
                key = fixStr(tmp[0]);
                value = fixStr(tmp[1]);
                while (key.charAt(0) === " ") {
                    key = key.substr(1);
                }
                if (key.indexOf("\0") !== -1) {
                    key = key.substr(0, key.indexOf("\0"));
                }
                if (key && key.charAt(0) !== "[") {
                    keys = [];
                    bracket = 0;
                    for (j = 0; j < key.length; j++) {
                        if (key.charAt(j) === "[" && !bracket) {
                            bracket = j + 1;
                        } else if (key.charAt(j) === "]") {
                            if (bracket) {
                                if (!keys.length) {
                                    keys.push(key.substr(0, bracket - 1));
                                }
                                keys.push(key.substr(bracket, j - bracket));
                                bracket = 0;
                                if (key.charAt(j + 1) !== "[") {
                                    break;
                                }
                            }
                        }
                    }
                    if (!keys.length) {
                        keys = [key];
                    }
                    for (j = 0; j < keys[0].length; j++) {
                        chr = keys[0].charAt(j);
                        if (chr === " " || chr === "." || chr === "[") {
                            keys[0] = keys[0].substr(0, j) + "_" + keys[0].substr(j + 1);
                        }
                        if (chr === "[") {
                            break;
                        }
                    }
                    /* jshint -W061 */
                    evalStr = "array";
                    for (j = 0; j < keys.length; j++) {
                        key = keys[j];
                        if ((key !== "" && key !== " ") || j === 0) {
                            key = "'" + key + "'";
                        } else {
                            key = eval(evalStr + ".push([]);") - 1;
                        }
                        evalStr += "[" + key + "]";
                        if (j !== keys.length - 1 && eval("typeof " + evalStr) === "undefined") {
                            eval(evalStr + " = [];");
                        }
                    }
                    evalStr += " = '" + value + "';\n";
                    eval(evalStr);
                    /* jshint +W061 */
                }
            }
        };

        /**
         * Get allowed API methods, sorted by GET or POST
         * Watch out for multiple-method "account/settings"!
         *
         * @return array $apimethods
         */
        var getApiMethods = function () {
            var httpmethods = {
                GET: [
                    "account/settings",
                    "account/verify_credentials",
                    "application/rate_limit_status",
                    "blocks/ids",
                    "blocks/list",
                    "direct_messages",
                    "direct_messages/sent",
                    "direct_messages/show",
                    "favorites/list",
                    "followers/ids",
                    "followers/list",
                    "friends/ids",
                    "friends/list",
                    "friendships/incoming",
                    "friendships/lookup",
                    "friendships/lookup",
                    "friendships/no_retweets/ids",
                    "friendships/outgoing",
                    "friendships/show",
                    "geo/id/:place_id",
                    "geo/reverse_geocode",
                    "geo/search",
                    "geo/similar_places",
                    "help/configuration",
                    "help/languages",
                    "help/privacy",
                    "help/tos",
                    "lists/list",
                    "lists/members",
                    "lists/members/show",
                    "lists/memberships",
                    "lists/ownerships",
                    "lists/show",
                    "lists/statuses",
                    "lists/subscribers",
                    "lists/subscribers/show",
                    "lists/subscriptions",
                    "mutes/users/ids",
                    "mutes/users/list",
                    "oauth/authenticate",
                    "oauth/authorize",
                    "saved_searches/list",
                    "saved_searches/show/:id",
                    "search/tweets",
                    "statuses/home_timeline",
                    "statuses/mentions_timeline",
                    "statuses/oembed",
                    "statuses/retweeters/ids",
                    "statuses/retweets/:id",
                    "statuses/retweets_of_me",
                    "statuses/show/:id",
                    "statuses/user_timeline",
                    "trends/available",
                    "trends/closest",
                    "trends/place",
                    "users/contributees",
                    "users/contributors",
                    "users/profile_banner",
                    "users/search",
                    "users/show",
                    "users/suggestions",
                    "users/suggestions/:slug",
                    "users/suggestions/:slug/members",

                    // Internal
                    "users/recommendations",
                    "account/push_destinations/device",
                    "activity/about_me",
                    "activity/by_friends",
                    "statuses/media_timeline",
                    "timeline/home",
                    "help/experiments",
                    "search/typeahead",
                    "search/universal",
                    "discover/universal",
                    "conversation/show",
                    "statuses/:id/activity/summary",
                    "account/login_verification_enrollment",
                    "account/login_verification_request",
                    "prompts/suggest",

                    "beta/timelines/custom/list",
                    "beta/timelines/timeline",
                    "beta/timelines/custom/show"
                ],
                POST: [
                    "account/remove_profile_banner",
                    "account/settings__post",
                    "account/update_delivery_device",
                    "account/update_profile",
                    "account/update_profile_background_image",
                    "account/update_profile_banner",
                    "account/update_profile_colors",
                    "account/update_profile_image",
                    "blocks/create",
                    "blocks/destroy",
                    "direct_messages/destroy",
                    "direct_messages/new",
                    "favorites/create",
                    "favorites/destroy",
                    "friendships/create",
                    "friendships/destroy",
                    "friendships/update",
                    "lists/create",
                    "lists/destroy",
                    "lists/members/create",
                    "lists/members/create_all",
                    "lists/members/destroy",
                    "lists/members/destroy_all",
                    "lists/subscribers/create",
                    "lists/subscribers/destroy",
                    "lists/update",
                    "media/upload",
                    "mutes/users/create",
                    "mutes/users/destroy",
                    "oauth/access_token",
                    "oauth/request_token",
                    "oauth2/invalidate_token",
                    "oauth2/token",
                    "saved_searches/create",
                    "saved_searches/destroy/:id",
                    "statuses/destroy/:id",
                    "statuses/lookup",
                    "statuses/retweet/:id",
                    "statuses/update",
                    "statuses/update_with_media", // deprecated, use media/upload
                    "users/lookup",
                    "users/report_spam",

                    // Internal
                    "direct_messages/read",
                    "account/login_verification_enrollment__post",
                    "push_destinations/enable_login_verification",
                    "account/login_verification_request__post",

                    "beta/timelines/custom/create",
                    "beta/timelines/custom/update",
                    "beta/timelines/custom/destroy",
                    "beta/timelines/custom/add",
                    "beta/timelines/custom/remove"
                ]
            };
            return httpmethods;
        };

        /**
         * Main API handler working on any requests you issue
         *
         * @param string   fn            The member function you called
         * @param array    params        The parameters you sent along
         * @param function callback      The callback to call with the reply
         * @param bool     app_only_auth Whether to use app-only auth
         *
         * @return mixed The API reply encoded in the set return_format
         */

        var __call = function (fn, params, callback, app_only_auth) {
            if (typeof params === "undefined") {
                params = {};
            }
            if (typeof app_only_auth === "undefined") {
                app_only_auth = false;
            }
            if (typeof callback !== "function" && typeof params === "function") {
                callback = params;
                params = {};
                if (typeof callback === "boolean") {
                    app_only_auth = callback;
                }
            } else if (typeof callback === "undefined") {
                callback = function () {};
            }
            switch (fn) {
                case "oauth_authenticate":
                case "oauth_authorize":
                    return this[fn](params, callback);

                case "oauth2_token":
                    return this[fn](callback);
            }
            // reset token when requesting a new token (causes 401 for signature error on 2nd+ requests)
            if (fn === "oauth_requestToken") {
                setToken(null, null);
            }
            // parse parameters
            var apiparams = {};
            if (typeof params === "object") {
                apiparams = params;
            } else {
                _parse_str(params, apiparams); //TODO
            }

            // map function name to API method
            var method = "";
            var param, i, j;

            // replace _ by /
            var path = fn.split("_");
            for (i = 0; i < path.length; i++) {
                if (i > 0) {
                    method += "/";
                }
                method += path[i];
            }

            // undo replacement for URL parameters
            var url_parameters_with_underscore = ["screen_name", "place_id"];
            for (i = 0; i < url_parameters_with_underscore.length; i++) {
                param = url_parameters_with_underscore[i].toUpperCase();
                var replacement_was = param.split("_").join("/");
                method = method.split(replacement_was).join(param);
            }

            // replace AA by URL parameters
            var method_template = method;
            var match = method.match(/[A-Z_]{2,}/);
            if (match) {
                for (i = 0; i < match.length; i++) {
                    param = match[i];
                    var param_l = param.toLowerCase();
                    method_template = method_template.split(param).join(":" + param_l);
                    if (typeof apiparams[param_l] === "undefined") {
                        for (j = 0; j < 26; j++) {
                            method_template = method_template.split(String.fromCharCode(65 + j)).join("_" + String.fromCharCode(97 + j));
                        }
                        console.warn("To call the templated method \"" + method_template + "\", specify the parameter value for \"" + param_l + "\".");
                    }
                    method = method.split(param).join(apiparams[param_l]);
                    delete apiparams[param_l];
                }
            }

            // replace A-Z by _a-z
            for (i = 0; i < 26; i++) {
                method = method.split(String.fromCharCode(65 + i)).join("_" + String.fromCharCode(97 + i));
                method_template = method_template.split(String.fromCharCode(65 + i)).join("_" + String.fromCharCode(97 + i));
            }

            var httpmethod = _detectMethod(method_template, apiparams);
            var multipart = _detectMultipart(method_template);
            var internal = _detectInternal(method_template);

            return _callApi(
                httpmethod,
                method,
                apiparams,
                multipart,
                app_only_auth,
                internal,
                callback
            );
        };

        /**
         * Gets the OAuth authenticate URL for the current request token
         *
         * @return string The OAuth authenticate URL
         */
        var oauth_authenticate = function (params, callback) {
            if (typeof params.force_login === "undefined") {
                params.force_login = null;
            }
            if (typeof params.screen_name === "undefined") {
                params.screen_name = null;
            }
            if (_oauth_token === null) {
                console.warn("To get the authenticate URL, the OAuth token must be set.");
            }
            var url = _endpoint_oauth + "oauth/authenticate?oauth_token=" + _url(_oauth_token);
            if (params.force_login === true) {
                url += "&force_login=1";
                if (params.screen_name !== null) {
                    url += "&screen_name=" + params.screen_name;
                }
            }
            callback(url);
            return true;
        };

        /**
         * Gets the OAuth authorize URL for the current request token
         *
         * @return string The OAuth authorize URL
         */
        var oauth_authorize = function (params, callback) {
            if (typeof params.force_login === "undefined") {
                params.force_login = null;
            }
            if (typeof params.screen_name === "undefined") {
                params.screen_name = null;
            }
            if (_oauth_token === null) {
                console.warn("To get the authorize URL, the OAuth token must be set.");
            }
            var url = _endpoint_oauth + "oauth/authorize?oauth_token=" + _url(_oauth_token);
            if (params.force_login === true) {
                url += "&force_login=1";
                if (params.screen_name !== null) {
                    url += "&screen_name=" + params.screen_name;
                }
            }
            callback(url);
            return true;
        };

        /**
         * Gets the OAuth bearer token
         *
         * @return string The OAuth bearer token
         */

        var oauth2_token = function (callback) {
            if (_oauth_consumer_key === null) {
                console.warn("To obtain a bearer token, the consumer key must be set.");
            }

            if (typeof callback === "undefined") {
                callback = function () {};
            }

            var post_fields = "grant_type=client_credentials";
            var url = _endpoint_oauth + "oauth2/token";

            if (_use_proxy) {
                url = url.replace(
                    _endpoint_base,
                    _endpoint_proxy
                );
            }

            var xml = _getXmlRequestObject();
            if (xml === null) {
                return;
            }
            xml.open("POST", url, true);
            xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xml.setRequestHeader(
                (_use_proxy ? "X-" : "") + "Authorization",
                "Basic " + _base64_encode(_oauth_consumer_key + ":" + _oauth_consumer_secret)
            );

            xml.onreadystatechange = function () {
                if (xml.readyState >= 4) {
                    var httpstatus = 12027;
                    try {
                        httpstatus = xml.status;
                    } catch (e) {}
                    var response = "";
                    try {
                        response = xml.responseText;
                    } catch (e) {}
                    var reply = _parseApiReply(response);
                    reply.httpstatus = httpstatus;
                    if (httpstatus === 200) {
                        setBearerToken(reply.access_token);
                    }
                    callback(reply);
                }
            };
            xml.send(post_fields);

        };

        /**
         * Signing helpers
         */

        /**
         * URL-encodes the given data
         *
         * @param mixed data
         *
         * @return mixed The encoded data
         */
        var _url = function (data) {
            if ((/boolean|number|string/).test(typeof data)) {
                return encodeURIComponent(data).replace(/!/g, "%21").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A");
            } else {
                return "";
            }
        };

        /**
         * Gets the base64-encoded SHA1 hash for the given data
         *
         * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
         * in FIPS PUB 180-1
         * Based on version 2.1 Copyright Paul Johnston 2000 - 2002.
         * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
         * Distributed under the BSD License
         * See http://pajhome.org.uk/crypt/md5 for details.
         *
         * @param string data The data to calculate the hash from
         *
         * @return string The hash
         */
        var _sha1 = function () {
            function n(e, b) {
                e[b >> 5] |= 128 << 24 - b % 32;
                e[(b + 64 >> 9 << 4) + 15] = b;
                for (var c = new Array(80), a = 1732584193, d = -271733879, h = -1732584194,
                         k = 271733878, g = -1009589776, p = 0; p < e.length; p += 16) {
                    for (var o = a, q = d, r = h, s = k, t = g, f = 0; 80 > f; f++) {
                        var m;

                        if (f < 16) {
                            m = e[p + f];
                        } else {
                            m = c[f - 3] ^ c[f - 8] ^ c[f - 14] ^ c[f - 16];
                            m = m << 1 | m >>> 31;
                        }

                        c[f] = m;
                        m = l(l(a << 5 | a >>> 27, 20 > f ? d & h | ~d & k : 40 > f ? d ^
                        h ^ k : 60 > f ? d & h | d & k | h & k : d ^ h ^ k), l(
                            l(g, c[f]), 20 > f ? 1518500249 : 40 > f ? 1859775393 :
                                60 > f ? -1894007588 : -899497514));
                        g = k;
                        k = h;
                        h = d << 30 | d >>> 2;
                        d = a;
                        a = m;
                    }
                    a = l(a, o);
                    d = l(d, q);
                    h = l(h, r);
                    k = l(k, s);
                    g = l(g, t);
                }
                return [a, d, h, k, g];
            }

            function l(e, b) {
                var c = (e & 65535) + (b & 65535);
                return (e >> 16) + (b >> 16) + (c >> 16) << 16 | c & 65535;
            }

            function q(e) {
                for (var b = [], c = (1 << g) - 1, a = 0; a < e.length * g; a += g) {
                    b[a >> 5] |= (e.charCodeAt(a / g) & c) << 24 - a % 32;
                }
                return b;
            }
            var g = 8;
            return function (e) {
                var b = _oauth_consumer_secret + "&" + (null !== _oauth_token_secret ?
                        _oauth_token_secret : "");
                if (_oauth_consumer_secret === null) {
                    console.warn("To generate a hash, the consumer secret must be set.");
                }
                var c = q(b);
                if (c.length > 16) {
                    c = n(c, b.length * g);
                }
                b = new Array(16);
                for (var a = new Array(16), d = 0; d < 16; d++) {
                    a[d] = c[d] ^ 909522486;
                    b[d] = c[d] ^ 1549556828;
                }
                c = n(a.concat(q(e)), 512 + e.length * g);
                b = n(b.concat(c), 672);
                c = "";
                for (a = 0; a < 4 * b.length; a += 3) {
                    for (d = (b[a >> 2] >> 8 * (3 - a % 4) & 255) << 16 | (b[a + 1 >> 2] >>
                    8 * (3 - (a + 1) % 4) & 255) << 8 | b[a + 2 >> 2] >> 8 * (3 -
                    (a + 2) % 4) & 255, e = 0; 4 > e; e++) {
                        c = 8 * a + 6 * e > 32 * b.length ? c + "=" : c +
                        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
                            .charAt(d >> 6 * (3 - e) & 63);
                    }
                }
                return c;
            };
        }();

        /*
         * Gets the base64 representation for the given data
         *
         * http://phpjs.org
         * +   original by: Tyler Akins (http://rumkin.com)
         * +   improved by: Bayron Guevara
         * +   improved by: Thunder.m
         * +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
         * +   bugfixed by: Pellentesque Malesuada
         * +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
         * +   improved by: Rafał Kukawski (http://kukawski.pl)
         *
         * @param string data The data to calculate the base64 representation from
         *
         * @return string The base64 representation
         */
        var _base64_encode = function (a) {
            var d, e, f, b, g = 0,
                h = 0,
                i = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
                c = [];
            if (!a) {
                return a;
            }
            do {
                d = a.charCodeAt(g++);
                e = a.charCodeAt(g++);
                f = a.charCodeAt(g++);
                b = d << 16 | e << 8 | f;
                d = b >> 18 & 63;
                e = b >> 12 & 63;
                f = b >> 6 & 63;
                b &= 63;
                c[h++] = i.charAt(d) + i.charAt(e) + i.charAt(f) + i.charAt(b);
            } while (g < a.length);
            c = c.join("");
            a = a.length % 3;
            return (a ? c.slice(0, a - 3) : c) + "===".slice(a || 3);
        };

        /*
         * Builds a HTTP query string from the given data
         *
         * http://phpjs.org
         * +     original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
         * +     improved by: Legaev Andrey
         * +     improved by: Michael White (http://getsprink.com)
         * +     improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
         * +     improved by: Brett Zamir (http://brett-zamir.me)
         * +        revised by: stag019
         * +     input by: Dreamer
         * +     bugfixed by: Brett Zamir (http://brett-zamir.me)
         * +     bugfixed by: MIO_KODUKI (http://mio-koduki.blogspot.com/)
         *
         * @param string data The data to concatenate
         *
         * @return string The HTTP query
         */
        var _http_build_query = function (e, f, b) {
            function g(c, a, d) {
                var b, e = [];
                if (a === true) {
                    a = "1";
                } else if (a === false) {
                    a = "0";
                }
                if (null !== a) {
                    if (typeof a === "object") {
                        for (b in a) {
                            if (a[b] !== null) {
                                e.push(g(c + "[" + b + "]", a[b], d));
                            }
                        }
                        return e.join(d);
                    }
                    if (typeof a !== "function") {
                        return _url(c) + "=" + _url(a);
                    }
                    console.warn("There was an error processing for http_build_query().");
                } else {
                    return "";
                }
            }
            var d, c, h = [];
            if (! b) {
                b = "&";
            }
            for (c in e) {
                d = e[c];
                if (f && ! isNaN(c)) {
                    c = String(f) + c;
                }
                d = g(c, d, b);
                if (d !== "") {
                    h.push(d);
                }
            }
            return h.join(b);
        };

        /**
         * Generates a (hopefully) unique random string
         *
         * @param int optional length The length of the string to generate
         *
         * @return string The random string
         */
        var _nonce = function (length) {
            if (typeof length === "undefined") {
                length = 8;
            }
            if (length < 1) {
                console.warn("Invalid nonce length.");
            }
            var nonce = "";
            for (var i = 0; i < length; i++) {
                var character = Math.floor(Math.random() * 61);
                nonce += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".substring(character, character + 1);
            }
            return nonce;
        };

        /**
         * Sort array elements by key
         *
         * @param array input_arr The array to sort
         *
         * @return array The sorted keys
         */
        var _ksort = function (input_arr) {
            var keys = [], sorter, k;

            sorter = function (a, b) {
                var a_float = parseFloat(a),
                    b_float = parseFloat(b),
                    a_numeric = a_float + "" === a,
                    b_numeric = b_float + "" === b;
                if (a_numeric && b_numeric) {
                    return a_float > b_float ? 1 : a_float < b_float ? -1 : 0;
                } else if (a_numeric && !b_numeric) {
                    return 1;
                } else if (!a_numeric && b_numeric) {
                    return -1;
                }
                return a > b ? 1 : a < b ? -1 : 0;
            };

            // Make a list of key names
            for (k in input_arr) {
                if (input_arr.hasOwnProperty(k)) {
                    keys.push(k);
                }
            }
            keys.sort(sorter);
            return keys;
        };

        /**
         * Clone objects
         *
         * @param object obj    The object to clone
         *
         * @return object clone The cloned object
         */
        var _clone = function (obj) {
            var clone = {};
            for (var i in obj) {
                if (typeof(obj[i]) === "object") {
                    clone[i] = _clone(obj[i]);
                } else {
                    clone[i] = obj[i];
                }
            }
            return clone;
        };

        /**
         * Generates an OAuth signature
         *
         * @param string          httpmethod    Usually either 'GET' or 'POST' or 'DELETE'
         * @param string          method        The API method to call
         * @param array  optional params        The API call parameters, associative
         * @param bool   optional append_to_get Whether to append the OAuth params to GET
         *
         * @return string Authorization HTTP header
         */
        var _sign = function (httpmethod, method, params, append_to_get) {
            if (typeof params === "undefined") {
                params = {};
            }
            if (typeof append_to_get === "undefined") {
                append_to_get = false;
            }
            if (_oauth_consumer_key === null) {
                console.warn("To generate a signature, the consumer key must be set.");
            }
            var sign_params = {
                consumer_key:     _oauth_consumer_key,
                version:          "1.0",
                timestamp:        Math.round(new Date().getTime() / 1000),
                nonce:            _nonce(),
                signature_method: "HMAC-SHA1"
            };
            var sign_base_params = {};
            var value;
            for (var key in sign_params) {
                value = sign_params[key];
                sign_base_params["oauth_" + key] = _url(value);
            }
            if (_oauth_token !== null) {
                sign_base_params.oauth_token = _url(_oauth_token);
            }
            var oauth_params = _clone(sign_base_params);
            for (key in params) {
                value = params[key];
                sign_base_params[key] = value;
            }
            var keys = _ksort(sign_base_params);
            var sign_base_string = "";
            for (var i = 0; i < keys.length; i++) {
                key = keys[i];
                value = sign_base_params[key];
                sign_base_string += key + "=" + _url(value) + "&";
            }
            sign_base_string = sign_base_string.substring(0, sign_base_string.length - 1);
            var signature    = _sha1(httpmethod + "&" + _url(method) + "&" + _url(sign_base_string));

            params = append_to_get ? sign_base_params : oauth_params;
            params.oauth_signature = signature;
            keys = _ksort(params);
            var authorization = "";
            if (append_to_get) {
                for(i = 0; i < keys.length; i++) {
                    key = keys[i];
                    value = params[key];
                    authorization += key + "=" + _url(value) + "&";
                }
                return authorization.substring(0, authorization.length - 1);
            }
            authorization = "OAuth ";
            for (i = 0; i < keys.length; i++) {
                key = keys[i];
                value = params[key];
                authorization += key + "=\"" + _url(value) + "\", ";
            }
            return authorization.substring(0, authorization.length - 2);
        };

        /**
         * Detects HTTP method to use for API call
         *
         * @param string method The API method to call
         * @param array  params The parameters to send along
         *
         * @return string The HTTP method that should be used
         */
        var _detectMethod = function (method, params) {
            // multi-HTTP method endpoints
            switch (method) {
                case "account/settings":
                case "account/login_verification_enrollment":
                case "account/login_verification_request":
                    method = params.length ? method + "__post" : method;
                    break;
            }

            var apimethods = getApiMethods();
            for (var httpmethod in apimethods) {
                if (apimethods[httpmethod].indexOf(method) > -1) {
                    return httpmethod;
                }
            }
            console.warn("Can't find HTTP method to use for \"" + method + "\".");
        };

        /**
         * Detects if API call should use multipart/form-data
         *
         * @param string method The API method to call
         *
         * @return bool Whether the method should be sent as multipart
         */
        var _detectMultipart = function (method) {
            var multiparts = [
                // Tweets
                "statuses/update_with_media",

                // Users
                "account/update_profile_background_image",
                "account/update_profile_image",
                "account/update_profile_banner"
            ];
            return multiparts.indexOf(method) > -1;
        };

        /**
         * Build multipart request from upload params
         *
         * @param string method  The API method to call
         * @param array  params  The parameters to send along
         *
         * @return null|string The built multipart request body
         */
        var _buildMultipart = function (method, params) {
            // well, files will only work in multipart methods
            if (! _detectMultipart(method)) {
                return;
            }

            // only check specific parameters
            var possible_methods = [
                // Tweets
                "statuses/update_with_media",
                // Accounts
                "account/update_profile_background_image",
                "account/update_profile_image",
                "account/update_profile_banner"
            ];
            var possible_files = {
                // Tweets
                "statuses/update_with_media": "media[]",
                // Accounts
                "account/update_profile_background_image": "image",
                "account/update_profile_image": "image",
                "account/update_profile_banner": "banner"
            };
            // method might have files?
            if (possible_methods.indexOf(method) === -1) {
                return;
            }

            // check for filenames
            possible_files = possible_files[method].split(" ");

            var multipart_border = "--------------------" + _nonce();
            var multipart_request = "";
            for (var key in params) {
                multipart_request +=
                    "--" + multipart_border + "\r\n"
                    + "Content-Disposition: form-data; name=\"" + key + "\"";
                if (possible_files.indexOf(key) > -1) {
                    multipart_request +=
                        "\r\nContent-Transfer-Encoding: base64";
                }
                multipart_request +=
                    "\r\n\r\n" + params[key] + "\r\n";
            }
            multipart_request += "--" + multipart_border + "--";
            return multipart_request;
        };

        /**
         * Detects if API call is internal
         *
         * @param string method The API method to call
         *
         * @return bool Whether the method is defined in internal API
         */
        var _detectInternal = function (method) {
            var internals = [
                "users/recommendations"
            ];
            return internals.join(" ").indexOf(method) > -1;
        };

        /**
         * Detects if API call should use media endpoint
         *
         * @param string method The API method to call
         *
         * @return bool Whether the method is defined in media API
         */
        var _detectMedia = function (method) {
            var medias = [
                "media/upload"
            ];
            return medias.join(" ").indexOf(method) > -1;
        };

        /**
         * Detects if API call should use old endpoint
         *
         * @param string method The API method to call
         *
         * @return bool Whether the method is defined in old API
         */
        var _detectOld = function (method) {
            var olds = [
                "account/push_destinations/device"
            ];
            return olds.join(" ").indexOf(method) > -1;
        };

        /**
         * Builds the complete API endpoint url
         *
         * @param string method The API method to call
         *
         * @return string The URL to send the request to
         */
        var _getEndpoint = function (method) {
            var url;
            if (method.substring(0, 5) === "oauth") {
                url = _endpoint_oauth + method;
            } else if (_detectMedia(method)) {
                url = _endpoint_media + method + ".json";
            } else if (_detectOld(method)) {
                url = _endpoint_old + method + ".json";
            } else {
                url = _endpoint + method + ".json";
            }
            return url;
        };

        /**
         * Gets the XML HTTP Request object, trying to load it in various ways
         *
         * @return object The XMLHttpRequest object instance
         */
        var _getXmlRequestObject = function () {
            var xml = null;
            // first, try the W3-standard object
            if (typeof window === "object"
                && window
                && typeof window.XMLHttpRequest !== "undefined"
            ) {
                xml = new window.XMLHttpRequest();
                // then, try Titanium framework object
            } else if (typeof Ti === "object"
                && Ti
                && typeof Ti.Network.createHTTPClient !== "undefined"
            ) {
                xml = Ti.Network.createHTTPClient();
                // are we in an old Internet Explorer?
            } else if (typeof ActiveXObject !== "undefined"
            ) {
                try {
                    xml = new ActiveXObject("Microsoft.XMLHTTP");
                } catch (e) {
                    console.error("ActiveXObject object not defined.");
                }
                // now, consider RequireJS and/or Node.js objects
            } else if (typeof require === "function"
                && require
            ) {
                // look for xmlhttprequest module
                try {
                    var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
                    xml = new XMLHttpRequest();
                } catch (e1) {
                    // or maybe the user is using xhr2
                    try {
                        var XMLHttpRequest = require("xhr2");
                        xml = new XMLHttpRequest();
                    } catch (e2) {
                        console.error("xhr2 object not defined, cancelling.");
                    }
                }
            }
            return xml;
        };

        /**
         * Calls the API using cURL
         *
         * @param string          httpmethod    The HTTP method to use for making the request
         * @param string          method        The API method to call
         * @param array  optional params        The parameters to send along
         * @param bool   optional multipart     Whether to use multipart/form-data
         * @param bool   optional app_only_auth Whether to use app-only bearer authentication
         * @param bool   optional internal      Whether to use internal call
         * @param function        callback      The function to call with the API call result
         *
         * @return mixed The API reply, encoded in the set return_format
         */

        var _callApi = function (httpmethod, method, params, multipart, app_only_auth, internal, callback) {
            if (typeof params === "undefined") {
                params = {};
            }
            if (typeof multipart === "undefined") {
                multipart = false;
            }
            if (typeof app_only_auth === "undefined") {
                app_only_auth = false;
            }
            if (typeof callback !== "function") {
                callback = function () {};
            }
            if (internal) {
                params.adc            = "phone";
                params.application_id = 333903271;
            }

            var url           = _getEndpoint(method);
            var authorization = null;

            var xml = _getXmlRequestObject();
            if (xml === null) {
                return;
            }
            var post_fields;

            if (httpmethod === "GET") {
                var url_with_params = url;
                if (JSON.stringify(params) !== "{}") {
                    url_with_params += "?" + _http_build_query(params);
                }
                if (! app_only_auth) {
                    authorization = _sign(httpmethod, url, params);
                }

                // append auth params to GET url for IE7-9, to send via JSONP
                if (_use_jsonp) {
                    if (JSON.stringify(params) !== "{}") {
                        url_with_params += "&";
                    } else {
                        url_with_params += "?";
                    }
                    var callback_name = _nonce();
                    window[callback_name] = function (reply) {
                        reply.httpstatus = 200;

                        var rate = null;
                        if (typeof xml.getResponseHeader !== "undefined"
                            && xml.getResponseHeader("x-rate-limit-limit") !== ""
                        ) {
                            rate = {
                                limit: xml.getResponseHeader("x-rate-limit-limit"),
                                remaining: xml.getResponseHeader("x-rate-limit-remaining"),
                                reset: xml.getResponseHeader("x-rate-limit-reset")
                            };
                        }
                        callback(reply, rate);
                    };
                    params.callback = callback_name;
                    url_with_params = url + "?" + _sign(httpmethod, url, params, true);
                    var tag = document.createElement("script");
                    tag.type = "text/javascript";
                    tag.src = url_with_params;
                    var body = document.getElementsByTagName("body")[0];
                    body.appendChild(tag);
                    return;

                } else if (_use_proxy) {
                    url_with_params = url_with_params.replace(
                        _endpoint_base,
                        _endpoint_proxy
                    ).replace(
                        _endpoint_base_media,
                        _endpoint_proxy
                    );
                }
                xml.open(httpmethod, url_with_params, true);
            } else {
                if (_use_jsonp) {
                    console.warn("Sending POST requests is not supported for IE7-9.");
                    return;
                }
                if (multipart) {
                    if (! app_only_auth) {
                        authorization = _sign(httpmethod, url, {});
                    }
                    params = _buildMultipart(method, params);
                } else {
                    if (! app_only_auth) {
                        authorization = _sign(httpmethod, url, params);
                    }
                    params = _http_build_query(params);
                }
                post_fields = params;
                if (_use_proxy || multipart) { // force proxy for multipart base64
                    url = url.replace(
                        _endpoint_base,
                        _endpoint_proxy
                    ).replace(
                        _endpoint_base_media,
                        _endpoint_proxy
                    );
                }
                xml.open(httpmethod, url, true);
                if (multipart) {
                    xml.setRequestHeader("Content-Type", "multipart/form-data; boundary="
                    + post_fields.split("\r\n")[0].substring(2));
                } else {
                    xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                }
            }
            if (app_only_auth) {
                if (_oauth_consumer_key === null
                    && _oauth_bearer_token === null
                ) {
                    console.warn("To make an app-only auth API request, consumer key or bearer token must be set.");
                }
                // automatically fetch bearer token, if necessary
                if (_oauth_bearer_token === null) {
                    return oauth2_token(function () {
                        _callApi(httpmethod, method, params, multipart, app_only_auth, false, callback);
                    });
                }
                authorization = "Bearer " + _oauth_bearer_token;
            }
            if (authorization !== null) {
                xml.setRequestHeader((_use_proxy ? "X-" : "") + "Authorization", authorization);
            }
            xml.onreadystatechange = function () {
                if (xml.readyState >= 4) {
                    var httpstatus = 12027;
                    try {
                        httpstatus = xml.status;
                    } catch (e) {}
                    var response = "";
                    try {
                        response = xml.responseText;
                    } catch (e) {}
                    var reply = _parseApiReply(response);
                    reply.httpstatus = httpstatus;
                    var rate = null;
                    if (typeof xml.getResponseHeader !== "undefined"
                        && xml.getResponseHeader("x-rate-limit-limit") !== ""
                    ) {
                        rate = {
                            limit: xml.getResponseHeader("x-rate-limit-limit"),
                            remaining: xml.getResponseHeader("x-rate-limit-remaining"),
                            reset: xml.getResponseHeader("x-rate-limit-reset")
                        };
                    }
                    callback(reply, rate);
                }
            };
            xml.send(httpmethod === "GET" ? null : post_fields);
            return true;
        };

        /**
         * Parses the API reply to encode it in the set return_format
         *
         * @param string reply  The actual reply, JSON-encoded or URL-encoded
         *
         * @return array|object The parsed reply
         */
        var _parseApiReply = function (reply) {
            if (typeof reply !== "string" || reply === "") {
                return {};
            }
            if (reply === "[]") {
                return [];
            }
            var parsed;
            try {
                parsed = JSON.parse(reply);
            } catch (e) {
                parsed = {};
                if (reply.indexOf("<" + "?xml version=\"1.0\" encoding=\"UTF-8\"?" + ">") === 0) {
                    // we received XML...
                    // since this only happens for errors,
                    // don't perform a full decoding
                    parsed.request = reply.match(/<request>(.*)<\/request>/)[1];
                    parsed.error   = reply.match(/<error>(.*)<\/error>/)[1];
                } else {
                    // assume query format
                    var elements = reply.split("&");
                    for (var i = 0; i < elements.length; i++) {
                        var element = elements[i].split("=", 2);
                        if (element.length > 1) {
                            parsed[element[0]] = decodeURIComponent(element[1]);
                        } else {
                            parsed[element[0]] = null;
                        }
                    }
                }
            }
            return parsed;
        };

        return {
            setConsumerKey: setConsumerKey,
            getVersion: getVersion,
            setToken: setToken,
            setBearerToken: setBearerToken,
            setUseProxy: setUseProxy,
            setProxy: setProxy,
            getApiMethods: getApiMethods,
            __call: __call,
            oauth_authenticate: oauth_authenticate,
            oauth_authorize: oauth_authorize,
            oauth2_token: oauth2_token
        };
    };

    if (typeof module === "object"
        && module
        && typeof module.exports === "object"
    ) {
        // Expose codebird as module.exports in loaders that implement the Node
        // module pattern (including browserify). Do not create the global, since
        // the user will be storing it themselves locally, and globals are frowned
        // upon in the Node module world.
        module.exports = Codebird;
    } else {
        // Otherwise expose codebird to the global object as usual
        if (typeof window === "object"
            && window) {
            window.Codebird = Codebird;
        }

        // Register as a named AMD module, since codebird can be concatenated with other
        // files that may use define, but not via a proper concatenation script that
        // understands anonymous AMD modules. A named AMD is safest and most robust
        // way to register. Lowercase codebird is used because AMD module names are
        // derived from file names, and codebird is normally delivered in a lowercase
        // file name. Do this after creating the global so that if an AMD module wants
        // to call noConflict to hide this version of codebird, it will work.
        if (typeof define === "function" && define.amd) {
            define("codebird", [], function () { return Codebird; });
        }
    }

})();