/*!
 * Lightweight URL manipulation with JavaScript
 * This library is independent of any other libraries and has pretty simple
 * interface and lightweight code-base.
 * Some ideas of query string parsing had been taken from Jan Wolter
 * @see http://unixpapa.com/js/querystring.html
 *
 * @license MIT
 * @author Mykhailo Stadnyk <mikhus@gmail.com> and contributors
 * @see https://github.com/Mikhus/domurl/graphs/contributors
 */
(function (ns) {
    'use strict';

    var RX_PROTOCOL = /^[a-z]+:/;
    var RX_PORT = /[-a-z0-9]+(\.[-a-z0-9])*:\d+/i;
    var RX_CREDS = /\/\/(.*?)(?::(.*?))?@/;
    var RX_WIN = /^win/i;
    var RX_PROTOCOL_REPL = /:$/;
    var RX_QUERY_REPL = /^\?/;
    var RX_HASH_REPL = /^#/;
    var RX_PATH = /(.*\/)/;
    var RX_PATH_FIX = /^\/{2,}/;
    // https://news.ycombinator.com/item?id=3939454
    var RX_PATH_IE_FIX = /(^\/?)/;
    var RX_SINGLE_QUOTE = /'/g;
    var RX_DECODE_1 = /%([ef][0-9a-f])%([89ab][0-9a-f])%([89ab][0-9a-f])/gi;
    var RX_DECODE_2 = /%([cd][0-9a-f])%([89ab][0-9a-f])/gi;
    var RX_DECODE_3 = /%([0-7][0-9a-f])/gi;
    var RX_PLUS = /\+/g;
    var RX_PATH_SEMI = /^\w:$/;
    var RX_URL_TEST = /[^/#?]/;

    // configure given url options
    function urlConfig (url) {
        var config = {
            path: true,
            query: true,
            hash: true
        };

        if (!url) {
            return config;
        }

        if (RX_PROTOCOL.test(url)) {
            config.protocol = true;
            config.host = true;

            if (RX_PORT.test(url)) {
                config.port = true;
            }

            if (RX_CREDS.test(url)) {
                config.user = true;
                config.pass = true;
            }
        }

        return config;
    }

    var isNode = typeof window === 'undefined' &&
        typeof global !== 'undefined' &&
        typeof require === 'function';
    var isIe = !isNode && ns.navigator && ns.navigator.userAgent &&
        ~ns.navigator.userAgent.indexOf('MSIE');

    // Trick to bypass Webpack's require at compile time
    var nodeRequire = isNode ? ns['require'] : null;

    // mapping between what we want and <a> element properties
    var map = {
        protocol: 'protocol',
        host: 'hostname',
        port: 'port',
        path: 'pathname',
        query: 'search',
        hash: 'hash'
    };

    // jscs: disable
    /**
     * default ports as defined by http://url.spec.whatwg.org/#default-port
     * We need them to fix IE behavior,
     * @see https://github.com/Mikhus/jsurl/issues/2
     */
    // jscs: enable
    var defaultPorts = {
        ftp: 21,
        gopher: 70,
        http: 80,
        https: 443,
        ws: 80,
        wss: 443
    };

    var _currNodeUrl;
    function getCurrUrl() {
        if (isNode) {
            if (!_currNodeUrl) {
                _currNodeUrl = ('file://' +
                    (process.platform.match(RX_WIN) ? '/' : '') +
                    nodeRequire('fs').realpathSync('.')
                );
            }
            return _currNodeUrl;
        } else if (document.location.href === 'about:srcdoc') {
            return self.parent.document.location.href;
        } else {
            return document.location.href;
        }
    }

    function parse (self, url, absolutize) {
        var link, i, auth;

        if (!url) {
            url = getCurrUrl();
        }

        if (isNode) {
            link = nodeRequire('url').parse(url);
        }

        else {
            link = document.createElement('a');
            link.href = url;
        }

        var config = urlConfig(url);

        auth = url.match(RX_CREDS) || [];

        for (i in map) {
            if (config[i]) {
                self[i] = link[map[i]] || '';
            } else {
                self[i] = '';
            }
        }

        // fix-up some parts
        self.protocol = self.protocol.replace(RX_PROTOCOL_REPL, '');
        self.query = self.query.replace(RX_QUERY_REPL, '');
        self.hash = decode(self.hash.replace(RX_HASH_REPL, ''));
        self.user = decode(auth[1] || '');
        self.pass = decode(auth[2] || '');
        /* jshint ignore:start */
        self.port = (
            // loosely compare because port can be a string
            defaultPorts[self.protocol] == self.port || self.port == 0
        ) ? '' : self.port; // IE fix, Android browser fix
        /* jshint ignore:end */

        if (!config.protocol && RX_URL_TEST.test(url.charAt(0))) {
            self.path = url.split('?')[0].split('#')[0];
        }

        if (!config.protocol && absolutize) {
            // is IE and path is relative
            var base = new Url(getCurrUrl().match(RX_PATH)[0]);
            var basePath = base.path.split('/');
            var selfPath = self.path.split('/');
            var props = ['protocol', 'user', 'pass', 'host', 'port'];
            var s = props.length;

            basePath.pop();

            for (i = 0; i < s; i++) {
                self[props[i]] = base[props[i]];
            }

            while (selfPath[0] === '..') { // skip all "../
                basePath.pop();
                selfPath.shift();
            }

            self.path =
                (url.charAt(0) !== '/' ? basePath.join('/') : '') +
                '/' + selfPath.join('/')
            ;
        }

        self.path = self.path.replace(RX_PATH_FIX, '/');
        isIe && (self.path = self.path.replace(RX_PATH_IE_FIX, '/'));

        self.paths(self.paths());

        self.query = new QueryString(self.query);
    }

    function encode (s) {
        return encodeURIComponent(s).replace(RX_SINGLE_QUOTE, '%27');
    }

    function decode (s) {
        s = s.replace(RX_PLUS, ' ');
        s = s.replace(RX_DECODE_1, function (code, hex1, hex2, hex3) {
            var n1 = parseInt(hex1, 16) - 0xE0;
            var n2 = parseInt(hex2, 16) - 0x80;

            if (n1 === 0 && n2 < 32) {
                return code;
            }

            var n3 = parseInt(hex3, 16) - 0x80;
            var n = (n1 << 12) + (n2 << 6) + n3;

            if (n > 0xFFFF) {
                return code;
            }

            return String.fromCharCode(n);
        });
        s = s.replace(RX_DECODE_2, function (code, hex1, hex2) {
            var n1 = parseInt(hex1, 16) - 0xC0;

            if (n1 < 2) {
                return code;
            }

            var n2 = parseInt(hex2, 16) - 0x80;

            return String.fromCharCode((n1 << 6) + n2);
        });

        return s.replace(RX_DECODE_3, function (code, hex) {
            return String.fromCharCode(parseInt(hex, 16));
        });
    }

    /**
     * Class QueryString
     *
     * @param {string} qs - string representation of QueryString
     * @constructor
     */
    function QueryString (qs) {
        var parts = qs.split('&');

        for (var i = 0, s = parts.length; i < s; i++) {
            var keyVal = parts[i].split('=');
            var key = decodeURIComponent(keyVal[0].replace(RX_PLUS, ' '));

            if (!key) {
                continue;
            }

            var value = keyVal[1] !== undefined ? decode(keyVal[1]) : null;

            if (this[key] === undefined) {
                this[key] = value;
            } else {
                if (!(this[key] instanceof Array)) {
                    this[key] = [this[key]];
                }

                this[key].push(value);
            }
        }
    }

    /**
     * Converts QueryString object back to string representation
     *
     * @returns {string}
     */
    QueryString.prototype.toString = function () {
        var s = '';
        var e = encode;
        var i, ii;

        for (i in this) {
            var w = this[i];

            if (w instanceof Function || w === undefined) {
                continue;
            }

            if (w instanceof Array) {
                var len = w.length;

                if (!len) {
                    // Parameter is an empty array, so treat as
                    // an empty argument
                    s += (s ? '&' : '') + e(i) + '=';
                    continue;
                }

                for (ii = 0; ii < len; ii++) {
                    var v = w[ii];
                    if (v === undefined) {
                        continue;
                    }
                    s += s ? '&' : '';
                    s += e(i) + (v === null
                        ? ''
                        : '=' + e(v));
                }
                continue;
            }

            // Plain value
            s += s ? '&' : '';
            s += e(i) + (w === null ? '' : '=' + e(w));
        }

        return s;
    };

    /**
     * Class Url
     *
     * @param {string} [url] - string URL representation
     * @param {boolean} [noTransform] - do not transform to absolute URL
     * @constructor
     */
    function Url (url, noTransform) {
        parse(this, url, !noTransform);
    }

    /**
     * Clears QueryString, making it contain no params at all
     *
     * @returns {Url}
     */
    Url.prototype.clearQuery = function () {
        for (var key in this.query) {
            if (!(this.query[key] instanceof Function)) {
                delete this.query[key];
            }
        }

        return this;
    };

    /**
     * Returns total number of parameters in QueryString
     *
     * @returns {number}
     */
    Url.prototype.queryLength = function () {
        var count = 0;

        for (var key in this.query) {
            if (!(this.query[key] instanceof Function)) {
                count++;
            }
        }

        return count;
    };

    /**
     * Returns true if QueryString contains no parameters, false otherwise
     *
     * @returns {boolean}
     */
    Url.prototype.isEmptyQuery = function () {
        return this.queryLength() === 0;
    };

    /**
     *
     * @param {Array} [paths] - an array pf path parts (if given will modify
     *                          Url.path property
     * @returns {Array} - an array representation of the Url.path property
     */
    Url.prototype.paths = function (paths) {
        var prefix = '';
        var i = 0;
        var s;

        if (paths && paths.length && paths + '' !== paths) {
            if (this.isAbsolute()) {
                prefix = '/';
            }

            for (s = paths.length; i < s; i++) {
                paths[i] = !i && RX_PATH_SEMI.test(paths[i])
                    ? paths[i]
                    : encode(paths[i]);
            }

            this.path = prefix + paths.join('/');
        }

        paths = (this.path.charAt(0) === '/' ?
            this.path.slice(1) : this.path).split('/');

        for (i = 0, s = paths.length; i < s; i++) {
            paths[i] = decode(paths[i]);
        }

        return paths;
    };

    /**
     * Performs URL-specific encoding of the given string
     *
     * @method Url#encode
     * @param {string} s - string to encode
     * @returns {string}
     */
    Url.prototype.encode = encode;

    /**
     * Performs URL-specific decoding of the given encoded string
     *
     * @method Url#decode
     * @param {string} s - string to decode
     * @returns {string}
     */
    Url.prototype.decode = decode;

    /**
     * Checks if current URL is an absolute resource locator (globally absolute
     * or absolute path to current server)
     *
     * @returns {boolean}
     */
    Url.prototype.isAbsolute = function () {
        return this.protocol || this.path.charAt(0) === '/';
    };

    /**
     * Returns string representation of current Url object
     *
     * @returns {string}
     */
    Url.prototype.toString = function () {
        return (
            (this.protocol && (this.protocol + '://')) +
            (this.user && (
                encode(this.user) + (this.pass && (':' + encode(this.pass))
            ) + '@')) +
            (this.host && this.host) +
            (this.port && (':' + this.port)) +
            (this.path && this.path) +
            (this.query.toString() && ('?' + this.query)) +
            (this.hash && ('#' + encode(this.hash)))
        );
    };

    ns[ns.exports ? 'exports' : 'Url'] = Url;
}(typeof module !== 'undefined' && module.exports ? module : window));
