'use strict';

var errors = require('./errors.js'),
    isFunction = require('lodash/isFunction'),
    isObjectLike = require('lodash/isObjectLike'),
    isString = require('lodash/isString'),
    isUndefined = require('lodash/isUndefined');


module.exports = function (options) {

    var errorText = 'Please verify options'; // For better minification because this string is repeating

    if (!isObjectLike(options)) {
        throw new TypeError(errorText);
    }

    if (!isFunction(options.PromiseImpl)) {
        throw new TypeError(errorText + '.PromiseImpl');
    }

    if (!isUndefined(options.constructorMixin) && !isFunction(options.constructorMixin)) {
        throw new TypeError(errorText + '.PromiseImpl');
    }

    var PromiseImpl = options.PromiseImpl;
    var constructorMixin = options.constructorMixin;


    var plumbing = {};

    plumbing.init = function (requestOptions) {

        var self = this;

        self._rp_promise = new PromiseImpl(function (resolve, reject) {
            self._rp_resolve = resolve;
            self._rp_reject = reject;
            if (constructorMixin) {
                constructorMixin.apply(self, arguments); // Using arguments since specific Promise libraries may pass additional parameters
            }
        });

        self._rp_callbackOrig = requestOptions.callback;
        requestOptions.callback = self.callback = function RP$callback(err, response, body) {
            plumbing.callback.call(self, err, response, body);
        };

        if (isString(requestOptions.method)) {
            requestOptions.method = requestOptions.method.toUpperCase();
        }

        requestOptions.transform = requestOptions.transform || plumbing.defaultTransformations[requestOptions.method];

        self._rp_options = requestOptions;
        self._rp_options.simple = requestOptions.simple !== false;
        self._rp_options.resolveWithFullResponse = requestOptions.resolveWithFullResponse === true;
        self._rp_options.transform2xxOnly = requestOptions.transform2xxOnly === true;

    };

    plumbing.defaultTransformations = {
        HEAD: function (body, response, resolveWithFullResponse) {
            return resolveWithFullResponse ? response : response.headers;
        }
    };

    plumbing.callback = function (err, response, body) {

        var self = this;

        var origCallbackThrewException = false, thrownException = null;

        if (isFunction(self._rp_callbackOrig)) {
            try {
                self._rp_callbackOrig.apply(self, arguments); // TODO: Apply to self mimics behavior of request@2. Is that also right for request@next?
            } catch (e) {
                origCallbackThrewException = true;
                thrownException = e;
            }
        }

        var is2xx = !err && /^2/.test('' + response.statusCode);

        if (err) {

            self._rp_reject(new errors.RequestError(err, self._rp_options, response));

        } else if (self._rp_options.simple && !is2xx) {

            if (isFunction(self._rp_options.transform) && self._rp_options.transform2xxOnly === false) {

                (new PromiseImpl(function (resolve) {
                    resolve(self._rp_options.transform(body, response, self._rp_options.resolveWithFullResponse)); // transform may return a Promise
                }))
                    .then(function (transformedResponse) {
                        self._rp_reject(new errors.StatusCodeError(response.statusCode, body, self._rp_options, transformedResponse));
                    })
                    .catch(function (transformErr) {
                        self._rp_reject(new errors.TransformError(transformErr, self._rp_options, response));
                    });

            } else {
                self._rp_reject(new errors.StatusCodeError(response.statusCode, body, self._rp_options, response));
            }

        } else {

            if (isFunction(self._rp_options.transform) && (is2xx || self._rp_options.transform2xxOnly === false)) {

                (new PromiseImpl(function (resolve) {
                    resolve(self._rp_options.transform(body, response, self._rp_options.resolveWithFullResponse)); // transform may return a Promise
                }))
                    .then(function (transformedResponse) {
                        self._rp_resolve(transformedResponse);
                    })
                    .catch(function (transformErr) {
                        self._rp_reject(new errors.TransformError(transformErr, self._rp_options, response));
                    });

            } else if (self._rp_options.resolveWithFullResponse) {
                self._rp_resolve(response);
            } else {
                self._rp_resolve(body);
            }

        }

        if (origCallbackThrewException) {
            throw thrownException;
        }

    };

    plumbing.exposePromiseMethod = function (exposeTo, bindTo, promisePropertyKey, methodToExpose, exposeAs) {

        exposeAs = exposeAs || methodToExpose;

        if (exposeAs in exposeTo) {
            throw new Error('Unable to expose method "' + exposeAs + '"');
        }

        exposeTo[exposeAs] = function RP$exposed() {
            var self = bindTo || this;
            return self[promisePropertyKey][methodToExpose].apply(self[promisePropertyKey], arguments);
        };

    };

    plumbing.exposePromise = function (exposeTo, bindTo, promisePropertyKey, exposeAs) {

        exposeAs = exposeAs || 'promise';

        if (exposeAs in exposeTo) {
            throw new Error('Unable to expose method "' + exposeAs + '"');
        }

        exposeTo[exposeAs] = function RP$promise() {
            var self = bindTo || this;
            return self[promisePropertyKey];
        };

    };

    return plumbing;

};
