"use strict";
// MARK: - Parsing Regular Expressions
Object.defineProperty(exports, "__esModule", { value: true });
exports.URL = exports.QueryHandling = void 0;
const optional_1 = require("../types/optional");
const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i;
const queryParamRegex = /([^=?&]+)=?([^&]*)/g;
const componentOrder = ["hash", "query", "pathname", "host"];
/**
 * Defines how query parameters should be parsed and encoded.
 */
var QueryHandling;
(function (QueryHandling) {
    /**
     * Handle according to `application/x-www-form-urlencoded` rules (HTML forms).
     *
     * This is the **default decoding mode** for backward compatibility.
     *
     * **Example:**
     * ```typescript
     * // Input: "?search=hello+world&category=news+articles"
     * // Parsed: { search: "hello world", category: "news articles" }
     * // Output: "?search=hello+world&category=news+articles"
     * ```
     *
     * @see {@link https://url.spec.whatwg.org/#concept-urlencoded-parser WHATWG URL Standard}
     */
    QueryHandling["FORM_ENCODED"] = "form-encoded";
    /**
     * Handle according to RFC 3986 URI specification rules.
     *
     * This is the **default encoding mode** for backward compatibility.
     *
     * **Example:**
     * ```typescript
     * // Input: "?search=hello+world&math=2+2%3D4"
     * // Parsed: { search: "hello+world", math: "2+2=4" }
     * // Output: "?search=hello+world&math=2+2%3D4"
     * ```
     *
     * @see {@link https://tools.ietf.org/html/rfc3986#section-3.4 RFC 3986 Section 3.4}
     */
    QueryHandling["RFC3986"] = "rfc3986";
})(QueryHandling = exports.QueryHandling || (exports.QueryHandling = {}));
class URL {
    constructor(url, options) {
        var _a;
        this.query = {};
        this.queryHandling = options === null || options === void 0 ? void 0 : options.queryHandling;
        if ((0, optional_1.isNothing)(url)) {
            return;
        }
        // Split the protocol from the rest of the urls
        let remainder = url;
        const match = protocolRegex.exec(url);
        if ((0, optional_1.isSome)(match)) {
            // Pull out the protocol
            let protocol = match[1];
            if (protocol !== null && protocol !== undefined) {
                protocol = protocol.split(":")[0];
            }
            this.protocol = protocol !== null && protocol !== void 0 ? protocol : undefined;
            // Save the remainder
            remainder = (_a = match[3]) !== null && _a !== void 0 ? _a : undefined;
        }
        // Then match each component in a specific order
        let parse = { remainder: remainder, result: undefined };
        for (const component of componentOrder) {
            if (parse === undefined || parse.remainder === undefined) {
                break;
            }
            switch (component) {
                case "hash": {
                    parse = splitUrlComponent(parse.remainder, "#", "suffix");
                    this.hash = parse === null || parse === void 0 ? void 0 : parse.result;
                    break;
                }
                case "query": {
                    parse = splitUrlComponent(parse.remainder, "?", "suffix");
                    if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) {
                        this.query = URL.queryFromString(parse.result, this.queryHandling);
                    }
                    break;
                }
                case "pathname": {
                    parse = splitUrlComponent(parse.remainder, "/", "suffix");
                    if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) {
                        // Replace the initial /, since paths require it
                        this.pathname = "/" + parse.result;
                    }
                    break;
                }
                case "host": {
                    const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix");
                    const userInfo = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.result;
                    const hostPort = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.remainder;
                    if (userInfo !== undefined) {
                        const userInfoSplit = userInfo.split(":");
                        this.username = decodeURIComponent(userInfoSplit[0]);
                        this.password = decodeURIComponent(userInfoSplit[1]);
                    }
                    if (hostPort !== undefined) {
                        const hostPortSplit = hostPort.split(":");
                        this.host = hostPortSplit[0];
                        this.port = hostPortSplit[1];
                    }
                    break;
                }
                default: {
                    throw new Error("Unhandled case!");
                }
            }
        }
    }
    get(component) {
        switch (component) {
            // Exhaustive match to make sure TS property minifiers and other
            // transformer plugins do not break this code.
            case "protocol":
                return this.protocol;
            case "username":
                return this.username;
            case "password":
                return this.password;
            case "port":
                return this.port;
            case "pathname":
                return this.pathname;
            case "query":
                return this.query;
            case "hash":
                return this.hash;
            default:
                // The fallback for component which is not a property of URL object.
                return this[component];
        }
    }
    set(component, value) {
        if (value === undefined) {
            return this;
        }
        if (component === "query") {
            if (typeof value === "string") {
                value = URL.queryFromString(value, this.queryHandling);
            }
        }
        switch (component) {
            // Exhaustive match to make sure TS property minifiers and other
            // transformer plugins do not break this code.
            case "protocol":
                this.protocol = value;
                break;
            case "username":
                this.username = value;
                break;
            case "password":
                this.password = value;
                break;
            case "port":
                this.port = value;
                break;
            case "pathname":
                this.pathname = value;
                break;
            case "query":
                this.query = value;
                break;
            case "hash":
                this.hash = value;
                break;
            default:
                // The fallback for component which is not a property of URL object.
                this[component] = value;
                break;
        }
        return this;
    }
    append(component, value) {
        let existingValue = this.get(component);
        let newValue;
        if (component === "query") {
            if (existingValue === undefined) {
                existingValue = {};
            }
            if (typeof value === "string") {
                value = URL.queryFromString(value, this.queryHandling);
            }
            if (typeof existingValue === "string") {
                newValue = { existingValue, ...value };
            }
            else {
                newValue = { ...existingValue, ...value };
            }
        }
        else {
            if (existingValue === undefined) {
                existingValue = "";
            }
            let existingValueString = existingValue;
            if (existingValueString === undefined) {
                existingValueString = "";
            }
            let newValueString = existingValueString;
            if (component === "pathname") {
                const pathLength = existingValueString.length;
                if (pathLength === 0 || existingValueString[pathLength - 1] !== "/") {
                    newValueString += "/";
                }
            }
            // The component is not "query" so we treat value as string.
            // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-plus-operands
            newValueString += value;
            newValue = newValueString;
        }
        return this.set(component, newValue);
    }
    param(key, value) {
        if (key === null) {
            return this;
        }
        if (this.query === undefined) {
            this.query = {};
        }
        if (value === undefined) {
            // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
            delete this.query[key];
        }
        else {
            this.query[key] = value;
        }
        return this;
    }
    removeParam(key) {
        if (key === undefined || this.query === undefined) {
            return this;
        }
        if (key in this.query) {
            // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
            delete this.query[key];
        }
        return this;
    }
    path(value) {
        return this.append("pathname", value);
    }
    pathExtension() {
        var _a, _b;
        // Extract path extension if one exists
        if (this.pathname === undefined) {
            return undefined;
        }
        const lastFilenameComponents = (_b = (_a = this.pathname
            .split("/")
            .filter((item) => item.length > 0) // Remove any double or trailing slashes
            .pop()) === null || _a === void 0 ? void 0 : _a.split(".")) !== null && _b !== void 0 ? _b : [];
        if (lastFilenameComponents.filter(function (part) {
            return part !== "";
        }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"])
        ) {
            return undefined;
        }
        return lastFilenameComponents.pop();
    }
    /**
     * Returns the path components of the URL
     * @returns An array of non-empty path components from `urls`.
     */
    pathComponents() {
        if (this.pathname === undefined) {
            return [];
        }
        return this.pathname.split("/").filter((component) => component.length > 0);
    }
    /**
     * Same as toString
     *
     * @returns A string representation of the URL
     */
    build() {
        return this.toString();
    }
    /**
     * Converts the URL to a string
     *
     * @returns A string representation of the URL
     */
    toString() {
        let url = "";
        if (this.protocol !== undefined) {
            url += this.protocol + "://";
        }
        if (this.username !== undefined) {
            url += encodeURIComponent(this.username);
            if (this.password !== undefined) {
                url += ":" + encodeURIComponent(this.password);
            }
            url += "@";
        }
        if (this.host !== undefined) {
            url += this.host;
            if (this.port !== undefined) {
                url += ":" + this.port;
            }
        }
        if (this.pathname !== undefined) {
            url += this.pathname;
        }
        if (this.query !== undefined && Object.keys(this.query).length !== 0) {
            url += "?" + URL.toQueryString(this.query, this.queryHandling);
        }
        if (this.hash !== undefined) {
            url += "#" + this.hash;
        }
        return url;
    }
    // ----------------
    // Static API
    // ----------------
    /**
     * Converts a string into a query dictionary
     * @param query - The string to parse
     * @returns The query dictionary containing the key-value pairs in the query string
     */
    static queryFromString(query, queryHandling = QueryHandling.FORM_ENCODED) {
        const result = {};
        let parseResult = queryParamRegex.exec(query);
        while (parseResult !== null && parseResult.length >= 3) {
            let key = parseResult[1];
            let value = parseResult[2];
            // We support the legacy query format for "application/x-www-form-urlencoded" which can represent spaces as "+" symbols.
            // https://url.spec.whatwg.org/#concept-urlencoded-parser
            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
            //
            // For RFC3986 mode, plus signs remain as literal plus signs
            if (queryHandling === QueryHandling.FORM_ENCODED) {
                key = key.replace(/\+/g, " ");
                value = value.replace(/\+/g, " ");
            }
            const decodedKey = decodeURIComponent(key);
            const decodedValue = decodeURIComponent(value);
            result[decodedKey] = decodedValue;
            parseResult = queryParamRegex.exec(query);
        }
        return result;
    }
    /**
     * Converts a query dictionary into a query string
     *
     * @param query - The query dictionary
     * @returns The string representation of the query dictionary
     */
    static toQueryString(query, queryHandling = QueryHandling.RFC3986) {
        let queryString = "";
        let first = true;
        for (const key of Object.keys(query)) {
            if (!first) {
                queryString += "&";
            }
            first = false;
            queryString += URL.encodeQueryComponent(key, queryHandling);
            const value = query[key];
            if (value !== null && value.length > 0) {
                queryString += "=" + URL.encodeQueryComponent(value, queryHandling);
            }
        }
        return queryString;
    }
    /**
     * Encode a query parameter key or value according to the specified mode.
     * @param component - The key or value to encode
     * @param queryHandling - The encoding mode
     * @returns The encoded component
     */
    static encodeQueryComponent(component, queryHandling) {
        if (queryHandling === QueryHandling.FORM_ENCODED) {
            // For form-encoded: encode with encodeURIComponent, then convert %20 back to +
            return encodeURIComponent(component).replace(/%20/g, "+");
        }
        else {
            // For RFC 3986: standard percent-encoding (spaces become %20)
            return encodeURIComponent(component);
        }
    }
    static from(url) {
        return new URL(url);
    }
    /**
     * Convenience method to instantiate a URL from numerous (optional) components
     * @param protocol - The protocol type
     * @param host - The host name
     * @param path - The path
     * @param query - The query
     * @param hash - The hash
     * @param options - Configuration options for URL construction
     * @returns The new URL object representing the URL
     */
    static fromComponents(protocol, host, path, query, hash, options) {
        const url = new URL(undefined, options);
        url.protocol = protocol;
        url.host = host;
        url.pathname = path;
        url.query = query !== null && query !== void 0 ? query : {};
        url.hash = hash;
        return url;
    }
}
exports.URL = URL;
// MARK: - Helpers
function splitUrlComponent(input, marker, style) {
    const index = input.indexOf(marker);
    let result;
    let remainder = input;
    if (index !== -1) {
        const prefix = input.slice(0, index);
        const suffix = input.slice(index + marker.length, input.length);
        if (style === "prefix") {
            result = prefix;
            remainder = suffix;
        }
        else {
            result = suffix;
            remainder = prefix;
        }
    }
    return {
        result: result,
        remainder: remainder,
    };
}
//# sourceMappingURL=urls.js.map