// import libraries
import * as request from 'request';

import RequestAPI = request.RequestAPI;
import Request = request.Request;
import CoreOptions = request.CoreOptions;
import RequiredUriUrl = request.RequiredUriUrl;
import RequestResponse = request.RequestResponse;
import RequestCallback = request.RequestCallback;
import Cookie = request.Cookie;

import {Observable} from 'rxjs/Observable';
import {RxCookieJar} from './RxCookieJar';

// native javascript's objects typings
declare const Object: any;

/**
 * Class definition
 */
export class RxHttpRequest {
    // private property to store singleton instance
    private static _instance: RxHttpRequest;
    // private property to store request API object
    private _request: RequestAPI<Request, CoreOptions, RequiredUriUrl>;

    /**
     * Returns singleton instance
     *
     * @return {RxHttpRequest}
     */
    static instance(): RxHttpRequest {
        if (!(RxHttpRequest._instance instanceof RxHttpRequest)) {
            RxHttpRequest._instance = new RxHttpRequest(request);
        }

        return RxHttpRequest._instance;
    }

    /**
     * Class constructor
     */
    constructor(req: RequestAPI<Request, CoreOptions, RequiredUriUrl>) {
        // check request parameter
        this._checkRequestParam(req);

        // set request object
        this._request = req;
    }

    /**
     * Returns private attribute _request
     *
     * @return {RequestAPI<Request, CoreOptions, RequiredUriUrl>}
     */
    get request(): RequestAPI<Request, CoreOptions, RequiredUriUrl> {
        return this._request;
    }

    /**
     * This method returns a wrapper around the normal rx-http-request API that defaults to whatever options
     * you pass to it.
     * It does not modify the global rx-http-request API; instead, it returns a wrapper that has your default settings
     * applied to it.
     * You can call .defaults() on the wrapper that is returned from rx-http-request.defaults to add/override defaults
     * that were previously defaulted.
     *
     * @param options
     *
     * @return {RxHttpRequest}
     */
    defaults(options: CoreOptions): RxHttpRequest {
        return new RxHttpRequest(this._request.defaults(options));
    }

    /**
     * Function to do a GET HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    get(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('get', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a POST HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    post(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('post', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a PUT HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    put(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('put', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a PATCH HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    patch(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('patch', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a DELETE HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    delete(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('del', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a HEAD HTTP request
     *
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     */
    head(uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> this.call.apply(this, [].concat('head', <string> uri,
            <CoreOptions> Object.assign({}, options || {})));
    }

    /**
     * Function to do a HTTP request for given method
     *
     * @param method {string}
     * @param uri {string}
     * @param options {CoreOptions}
     *
     * @return {Observable<RxHttpRequestResponse>}
     *
     * @private
     */
    call(method: string, uri: string, options?: CoreOptions): Observable<RxHttpRequestResponse> {
        return <Observable<RxHttpRequestResponse>> Observable.create((observer) => {
            // build params array
            const params = [].concat(<string> uri, <CoreOptions> Object.assign({}, options || {}),
                <RequestCallback> (error: any, response: RequestResponse, body: any) => {
                    if (error) {
                        return observer.error(error);
                    }

                    observer.next(<RxHttpRequestResponse> Object.assign({}, {response: <RequestResponse> response, body: <any> body}));
                    observer.complete();
                });

            // call request method
            try {
                this._request[<string> method].apply(<RequestAPI<Request, CoreOptions, RequiredUriUrl>> this._request, params);
            } catch (error) {
                observer.error(error);
            }
        });
    }

    /**
     * Function that creates a new rx cookie jar
     *
     * @return {Observable<RxCookieJar>}
     */
    jar(): Observable<RxCookieJar> {
        return Observable.create((observer) => {
            observer.next(new RxCookieJar(this._request.jar()));
            observer.complete();
        });
    }

    /**
     * Function that creates a new cookie
     *
     * @param str {string}
     *
     * @return {Observable<Cookie>}
     */
    cookie(str: string): Observable<Cookie> {
        return <Observable<Cookie>> Observable.create((observer) => {
            observer.next(this._request.cookie(<string> str));
            observer.complete();
        });
    }

    /**
     * Function to check existing function in request API passed in parameter for a new instance
     *
     * @param req {RequestAPI<Request, CoreOptions, RequiredUriUrl>}
     *
     * @private
     */
    private _checkRequestParam(req: RequestAPI<Request, CoreOptions, RequiredUriUrl>) {
        // check existing function in API
        if (!req ||
            Object.prototype.toString.call( req.get ) !== '[object Function]' ||
            Object.prototype.toString.call( req.head ) !== '[object Function]' ||
            Object.prototype.toString.call( req.post ) !== '[object Function]' ||
            Object.prototype.toString.call( req.put ) !== '[object Function]' ||
            Object.prototype.toString.call( req.patch ) !== '[object Function]' ||
            Object.prototype.toString.call( req.del ) !== '[object Function]' ||
            Object.prototype.toString.call( req.defaults ) !== '[object Function]' ||
            Object.prototype.toString.call( req.jar ) !== '[object Function]' ||
            Object.prototype.toString.call( req.cookie ) !== '[object Function]') {
            throw new TypeError('Parameter must be a valid `request` module API');
        }
    }
}

/**
 * Export {RxHttpRequest} instance
 */
export const RxHR: RxHttpRequest = RxHttpRequest.instance();

/**
 * Export response interface
 */
export interface RxHttpRequestResponse {
    response: RequestResponse;
    body: any;
}
