
/**
 * @enum{string}
**/
var HttpRequestMethod = {
    GET: "GET",
    POST: "POST",
    PUT: "PUT",
    DELETE: "DELETE"
};


/**
 * @enum{string}
**/
var HttpRequestResponseType = {
    ARRAYBUFFER: "arraybuffer",
    BLOB: "blob",
    DOCUMENT: "document",
    JSON: "json",
    TEXT: "text"
};

/**
 * @abstract
 * @constructor
 * @param {HttpRequestMethod|string} urlOrMethod url or method to use (default: GET)
 * @param {string=} url
**/
function HttpRequest(urlOrMethod, url) {
    /** @const @type {string} */
    this.url = url || urlOrMethod;

    /** @const @type {HttpRequestMethod} */
    this.method = url ? /** @type {HttpRequestMethod} */ (urlOrMethod) : HttpRequestMethod.GET;

    /** @type {Array<function(number, string, *)>|undefined} */
    this._callback;

    /** @type {Array<function(number, string, *)>|undefined} */
    this._callbackError;

    /** @type {Array<function(number, string, *)>|undefined} */
    this._callbackSuccess;
}

/**
 * @param {Array<function(number, string, *)>|undefined} callbacks
 * @param {number} statusCode
 * @param {string} statusText
 * @param {*} response
**/
HttpRequest.fireCallbacks = function(callbacks, statusCode, statusText, response) {
    if (callbacks)
        callbacks.forEach(function(cb) {
            cb(statusCode, statusText, response);
        });
};


/**
 * @param {function(number, string, (string|Object|null))} cb callback (statusCode statusText responseText)
 * @param {Object=} ctx
 * @return {HttpRequest}
**/
HttpRequest.prototype.callback = function(cb, ctx) {
    if (!this._callback)
        this._callback = [];
    cb.bind(ctx || this);
    this._callback.push(cb);
    return this;
};

/**
 * @param {function(number, string, (string|Object|null))} cb callback (statusCode statusText responseText)
 * @param {Object=} ctx
 * @return {HttpRequest}
**/
HttpRequest.prototype.callbackSuccess = function(cb, ctx) {
    if (!this._callbackSuccess)
        this._callbackSuccess = [];
    cb.bind(ctx || this);
    this._callbackSuccess.push(cb);
    return this;
};

/**
 * @param {function(number, string, (string|Object|null))} cb callback (statusCode statusText responseText)
 * @param {Object=} ctx
 * @return {HttpRequest}
**/
HttpRequest.prototype.callbackError = function(cb, ctx) {
    if (!this._callbackError)
        this._callbackError = [];
    cb.bind(ctx || this);
    this._callbackError.push(cb);
    return this;
};

/**
 * @abstract
 * @param {number} timeo
 * @return {HttpRequest}
**/
HttpRequest.prototype.setTimeout = function(timeo) {};

/**
 * @abstract
 * @param {HttpRequestResponseType} respType
 * @return {HttpRequest}
**/
HttpRequest.prototype.setResponseType = function(respType) {};

/**
 * @abstract
 * @param {string|FormData=} payload
 * @return {HttpRequest}
**/
HttpRequest.prototype.send = function(payload) {};

/**
 * @constructor
 * @extends {HttpRequest}
 * @param {HttpRequestMethod|string} urlOrMethod url or method to use (default: GET)
 * @param {string=} url
**/
function XHRHttpRequest(urlOrMethod, url) {
    HttpRequest.call(this, urlOrMethod, url);

    /** @const @type {XMLHttpRequest} */
    this.xhr = new XMLHttpRequest();

    this.xhr.onreadystatechange = (function(e) {
        if (this.xhr.readyState === 4) {
            if (Math.floor(this.xhr.status / 100) === 2) {
                HttpRequest.fireCallbacks(this._callbackSuccess, this.xhr.status, this.xhr.statusText, this.xhr.response);
            } else {
                HttpRequest.fireCallbacks(this._callbackError, this.xhr.status, this.xhr.statusText, this.xhr.response);
            }
            HttpRequest.fireCallbacks(this._callback, this.xhr.status, this.xhr.statusText, this.xhr.response);
        }
    }).bind(this);
}

XHRHttpRequest.prototype = Object.create(HttpRequest.prototype);
XHRHttpRequest.prototype.constructor = XHRHttpRequest;

/**
 * @param {number} timeo
 * @return {HttpRequest}
**/
XHRHttpRequest.prototype.setTimeout = function(timeo) {
    this.xhr.timeout = timeo;
    return this;
};

/**
 * @param {HttpRequestResponseType} respType
 * @return {HttpRequest}
**/
XHRHttpRequest.prototype.setResponseType = function(respType) {
    this.xhr.responseType = respType;
    return this;
};

/**
 * @param {string|FormData=} payload
 * @return {HttpRequest}
**/
XHRHttpRequest.prototype.send = function(payload) {
    this.xhr.open(this.method, this.url, true);
    this.xhr.send(payload);
    return this;
};

/**
 * @constructor
 * @extends {HttpRequest}
 * @param {HttpRequestMethod|string} urlOrMethod url or method to use (default: GET)
 * @param {string=} url
**/
function NativeHttpRequest(urlOrMethod, url) {
    HttpRequest.call(this, urlOrMethod, url);

    /** @type {number} */
    this.timeo = 5000;

    /** @type {HttpRequestResponseType} */
    this.responseType = HttpRequestResponseType.TEXT;
}

NativeHttpRequest.prototype = Object.create(HttpRequest.prototype);
NativeHttpRequest.prototype.constructor = XHRHttpRequest;

/**
 * @param {number} timeo
 * @return {HttpRequest}
**/
NativeHttpRequest.prototype.setTimeout = function(timeo) {
    this.timeo = timeo;
    return this;
};

/**
 * @param {HttpRequestResponseType} respType
 * @return {HttpRequest}
**/
NativeHttpRequest.prototype.setResponseType = function(respType) {
    this.responseType = respType;
    return this;
};

/**
 * @param {string|FormData=} payload
 * @return {string|null}
**/
NativeHttpRequest.prototype.stringifyPayload = function(payload) {
    if (!payload) return null;
    if (typeof payload === "string") return payload;

    var obj = {};
    for (var i in payload)
        obj[i] = payload[i];
    return JSON.stringify(obj);
};

/** @type {function(string):string} */
NativeHttpRequest.makeAbsoluteUrl = (function() {
    var a = document.createElement('a');
    return function(link) {
        a.href = link;
        return a.href;
    };
})();

/**
 * @param {string|FormData=} payload
 * @return {HttpRequest}
**/
NativeHttpRequest.prototype.send = function(payload) {
    __native.sendHttpRequest(this.method, NativeHttpRequest.makeAbsoluteUrl(this.url), this.timeo, this.responseType, this.stringifyPayload(payload), CALLBACK.makeCallback(function(statusCode, statusText, respText) {
        var resp;

        try {
            resp = this.responseType === HttpRequestResponseType.TEXT ? respText : JSON.parse(respText);
        } catch (e) {
            resp = respText;
        }

        if (__native.isDebug())
            console.log(this, {
                statusCode: statusCode,
                statusText: statusText,
                resp: respText
            });
        if (Math.floor(statusCode / 100) === 2) {
            HttpRequest.fireCallbacks(this._callbackSuccess, statusCode, statusText, resp);
        } else {
            HttpRequest.fireCallbacks(this._callbackError, statusCode, statusText, resp);
        }
        HttpRequest.fireCallbacks(this._callback, statusCode, statusText, resp);
    }, this));
    return this;
};

/**
 * @param {HttpRequestMethod|string} urlOrMethod
 * @param {string=} url
 * @return {HttpRequest}
**/
function HttpRequestWrapper(urlOrMethod, url) {
    return isNative() ? new NativeHttpRequest(urlOrMethod, url) : new XHRHttpRequest(urlOrMethod, url);
}

HttpRequestWrapper.encode = function(str) {
	return encodeURIComponent(str);
}

