import * as http from "../http";
import * as types from "../utils/types";

module XMLHttpRequestResponseType {
    export var empty = "";
    export var text = "text";
    export var json = "json";
}

export class XMLHttpRequest {
    public UNSENT = 0;
    public OPENED = 1;
    public HEADERS_RECEIVED = 2;
    public LOADING = 3;
    public DONE = 4;

    public onload: () => void;
    public onerror: (any) => void;

    private _options: http.HttpRequestOptions;
    private _readyState: number;
    private _status: number;
    private _response: any;
    private _responseTextReader: Function;
    private _headers: any;
    private _errorFlag: boolean;
    private _responseType: string = "";

    public onreadystatechange: Function;

    constructor() {
        this._readyState = this.UNSENT;
    }

    public open(method: string, url: string, async?: boolean, user?: string, password?: string) {
        if (types.isString(method) && types.isString(url)) {
            this._options = { url: url, method: method };
            this._options.headers = {};

            if (types.isString(user)) {
                this._options.headers["user"] = user;
            }

            if (types.isString(password)) {
                this._options.headers["password"] = password;
            }

            this._setReadyState(this.OPENED);
        }
    }

    public abort() {
        this._errorFlag = true;

        this._response = null;
        this._responseTextReader = null;
        this._headers = null;
        this._status = null;

        if (this._readyState === this.UNSENT || this._readyState === this.OPENED || this._readyState === this.DONE) {
            this._readyState = this.UNSENT;
        } else {
            this._setReadyState(this.DONE);
        }
    }

    public send(data?: any) {
        this._errorFlag = false;
        this._response = null;
        this._responseTextReader = null;
        this._headers = null;
        this._status = null;

        if (types.isDefined(this._options)) {
            if (types.isString(data) && this._options.method !== 'GET') {
                //The Android Java HTTP lib throws an exception if we provide a
                //a request body for GET requests, so we avoid doing that.
                //Browser implementations silently ignore it as well.
                this._options.content = data;
            } else if (data instanceof FormData) {
                this._options.content = (<FormData>data).toString();
            }

            http.request(this._options).then(r => {
                if (!this._errorFlag) {
                    this._loadResponse(r);
                }

            }).catch(e => {
                this._errorFlag = true;
                this._setReadyState(this.DONE, e);
            });
        }
    }

    private _loadResponse(r) {
        this._status = r.statusCode;
        this._response = r.content.raw + "";
        this._headers = r.headers;
        this._setReadyState(this.HEADERS_RECEIVED);

        this._setReadyState(this.LOADING);

        this._setResponseType();

        this._responseTextReader = () => r.content.toString();
        this._addToStringOnResponse();

        if (this.responseType === XMLHttpRequestResponseType.json) {
            this._response = JSON.parse(this.responseText);
        } else if (this.responseType === XMLHttpRequestResponseType.text) {
            this._response = this.responseText;
        }

        this._setReadyState(this.DONE);
    }

    private _addToStringOnResponse() {
        // Add toString() method to ease debugging and
        // make Angular2 response.text() method work properly.
        if (types.isObject(this.response)) {
            Object.defineProperty(this._response, "toString", {
                configurable: true,
                enumerable: false,
                writable: true,
                value: () => this.responseText
            });
        }
    }

    private textTypes: string[] = [
        'text/plain',
        'application/xml',
        'text/html'
    ];

    private isTextContentType(contentType: string): boolean {
        let result = false;
        for(let i = 0; i < this.textTypes.length; i++) {
            if (contentType.toLowerCase().indexOf(this.textTypes[i]) >= 0) {
                result = true;
                break;
            }
        }
        return result;
    }

    private _setResponseType() {
        const header = this.getResponseHeader('Content-Type');
        const contentType = header && header.toLowerCase();

        if (contentType) {
            if (contentType.indexOf('application/json') >= 0 || contentType.indexOf('+json') >= 0) {
                this.responseType = XMLHttpRequestResponseType.json;
            } else if (this.isTextContentType(contentType)) {
                this.responseType = XMLHttpRequestResponseType.text;
            }
        } else {
            this.responseType = XMLHttpRequestResponseType.text;
        }
    }

    private _listeners: Map<string, Array<Function>> = new Map<string, Array<Function>>();

    public addEventListener(eventName: string, handler: Function) {
        if (eventName !== 'load' && eventName !== 'error') {
            throw new Error('Event not supported: ' + eventName);
        }

        let handlers = this._listeners.get(eventName) || [];
        handlers.push(handler);
        this._listeners.set(eventName, handlers);
    }

    public removeEventListener(eventName: string, toDetach: Function) {
        let handlers = this._listeners.get(eventName) || [];
        handlers = handlers.filter((handler) => handler !== toDetach);
        this._listeners.set(eventName, handlers);
    }

    private emitEvent(eventName: string, ...args: Array<any>) {
        let handlers = this._listeners.get(eventName) || [];
        handlers.forEach((handler) => {
            handler(...args);
        });
    }

    public setRequestHeader(header: string, value: string) {
        if (types.isDefined(this._options) && types.isString(header) && types.isString(value)) {
            this._options.headers[header] = value;
        }
    }

    public getAllResponseHeaders(): string {
        if (this._readyState < 2 || this._errorFlag) {
            return "";
        }

        let result = "";

        for (let i in this._headers) {
            // Cookie headers are excluded
            if (i !== "set-cookie" && i !== "set-cookie2") {
                result += i + ": " + this._headers[i] + "\r\n";
            }
        }
        return result.substr(0, result.length - 2);
    }

    public getResponseHeader(header: string): string {
        if (types.isString(header) && this._readyState > 1
            && this._headers
            && !this._errorFlag
        ) {
            header = header.toLowerCase();
            for (let i in this._headers) {
                if (i.toLowerCase() === header) {
                    return this._headers[i];
                }
            }
        }

        return null;
    }

    public overrideMimeType(mime: string) {
        //
    }

    get readyState(): number {
        return this._readyState;
    }

    public get responseType(): string {
        return this._responseType;
    }

    public set responseType(value: string) {
        if (value === XMLHttpRequestResponseType.empty || value in XMLHttpRequestResponseType) {
            this._responseType = value;
        } else {
            throw new Error(`Response type of '${value}' not supported.`);
        }
    }

    private _setReadyState(value: number, error?: any) {
        if (this._readyState !== value) {
            this._readyState = value;

            if (types.isFunction(this.onreadystatechange)) {
                this.onreadystatechange();
            }
        }

        if (this._readyState === this.DONE) {
            if (this._errorFlag) {
                if (types.isFunction(this.onerror)) {
                    this.onerror(error);
                }
                this.emitEvent('error', error);
            } else {
                if (types.isFunction(this.onload)) {
                    this.onload();
                }
                this.emitEvent('load');
            }
        }
    }

    get responseText(): string {
        if (types.isFunction(this._responseTextReader)) {
            return this._responseTextReader();
        }

        return "";
    }

    get response(): any {
        return this._response;
    }

    get status(): number {
        return this._status;
    }

    get statusText(): string {
        if (this._readyState === this.UNSENT || this._readyState === this.OPENED || this._errorFlag) {
            return "";
        }
        return statuses[this._status];
    }
}

const statuses = {
    100: "Continue",
    101: "Switching Protocols",
    200: "OK",
    201: "Created",
    202: "Accepted",
    203: "Non - Authoritative Information",
    204: "No Content",
    205: "Reset Content",
    206: "Partial Content",
    300: "Multiple Choices",
    301: "Moved Permanently",
    302: "Found",
    303: "See Other",
    304: "Not Modified",
    305: "Use Proxy",
    307: "Temporary Redirect",
    400: "Bad Request",
    401: "Unauthorized",
    402: "Payment Required",
    403: "Forbidden",
    404: "Not Found",
    405: "Method Not Allowed",
    406: "Not Acceptable",
    407: "Proxy Authentication Required",
    408: "Request Timeout",
    409: "Conflict",
    410: "Gone",
    411: "Length Required",
    412: "Precondition Failed",
    413: "Request Entity Too Large",
    414: "Request - URI Too Long",
    415: "Unsupported Media Type",
    416: "Requested Range Not Satisfiable",
    417: "Expectation Failed",
    500: "Internal Server Error",
    501: "Not Implemented",
    502: "Bad Gateway",
    503: "Service Unavailable",
    504: "Gateway Timeout",
    505: "HTTP Version Not Supported"
};

export class FormData {
    private _data: Map<string, any>;

    constructor() {
        this._data = new Map<string, any>();
    }

    append(name: string, value: any) {
        this._data.set(name, value);
    }

    toString(): string {
        let arr = new Array<string>();

        this._data.forEach(function (value, name, map) {
            arr.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
        });

        return arr.join("&");
    }
}
