"use strict";
//*******************************************************************************************************
// significant portions of this file copied from: VSO\src\Vssf\WebPlatform\Platform\Scripts\VSS\WebApi\RestClient.ts
//*******************************************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.VsoClient = exports.InvalidApiResourceVersionError = void 0;
/// Imports of 3rd Party ///
const url = require("url");
const path = require("path");
class InvalidApiResourceVersionError {
    constructor(message) {
        this.name = "Invalid resource version";
        this.message = message;
    }
}
exports.InvalidApiResourceVersionError = InvalidApiResourceVersionError;
/**
 * Base class that should be used (derived from) to make requests to VSS REST apis
 */
class VsoClient {
    constructor(baseUrl, restClient) {
        this.baseUrl = baseUrl;
        this.basePath = url.parse(baseUrl).pathname;
        this.restClient = restClient;
        this._locationsByAreaPromises = {};
        this._initializationPromise = Promise.resolve(true);
    }
    autoNegotiateApiVersion(location, requestedVersion) {
        let negotiatedVersion;
        let apiVersion;
        let apiVersionString;
        if (requestedVersion) {
            let apiVersionRegEx = new RegExp('(\\d+(\\.\\d+)?)(-preview(\\.(\\d+))?)?');
            // Need to handle 3 types of api versions + invalid apiversion
            // '2.1-preview.1' = ["2.1-preview.1", "2.1", ".1", -preview.1", ".1", "1"]
            // '2.1-preview' = ["2.1-preview", "2.1", ".1", "-preview", undefined, undefined]
            // '2.1' = ["2.1", "2.1", ".1", undefined, undefined, undefined]
            let isPreview = false;
            let resourceVersion;
            let regExExecArray = apiVersionRegEx.exec(requestedVersion);
            if (regExExecArray) {
                if (regExExecArray[1]) {
                    // we have an api version
                    apiVersion = +regExExecArray[1];
                    apiVersionString = regExExecArray[1];
                    if (regExExecArray[3]) {
                        // requesting preview
                        isPreview = true;
                        if (regExExecArray[5]) {
                            // we have a resource version
                            resourceVersion = +regExExecArray[5];
                        }
                    }
                    // compare the location version and requestedversion
                    if (apiVersion <= +location.releasedVersion
                        || (!resourceVersion && apiVersion <= +location.maxVersion && isPreview)
                        || (resourceVersion && apiVersion <= +location.maxVersion && resourceVersion <= +location.resourceVersion)) {
                        negotiatedVersion = requestedVersion;
                    }
                    // else fall back to latest version of the resource from location
                }
            }
        }
        if (!negotiatedVersion) {
            // Use the latest version of the resource if the api version was not specified in the request or if the requested version is higher then the location's supported version
            if (apiVersion < +location.maxVersion) {
                negotiatedVersion = apiVersionString + "-preview";
            }
            else if (location.maxVersion === location.releasedVersion) {
                negotiatedVersion = location.maxVersion;
            }
            else {
                negotiatedVersion = location.maxVersion + "-preview." + location.resourceVersion;
            }
        }
        return negotiatedVersion;
    }
    /**
     * Gets the route template for a resource based on its location ID and negotiates the api version
     */
    getVersioningData(apiVersion, area, locationId, routeValues, queryParams) {
        let requestUrl;
        return this.beginGetLocation(area, locationId)
            .then((location) => {
            if (!location) {
                throw new Error("Failed to find api location for area: " + area + " id: " + locationId);
            }
            apiVersion = this.autoNegotiateApiVersion(location, apiVersion);
            requestUrl = this.getRequestUrl(location.routeTemplate, location.area, location.resourceName, routeValues, queryParams);
            return {
                apiVersion: apiVersion,
                requestUrl: requestUrl
            };
        });
    }
    /**
     * Sets a promise that is waited on before any requests are issued. Can be used to asynchronously
     * set the request url and auth token manager.
     */
    _setInitializationPromise(promise) {
        if (promise) {
            this._initializationPromise = promise;
        }
    }
    /**
     * Gets information about an API resource location (route template, supported versions, etc.)
     *
     * @param area resource area name
     * @param locationId Guid of the location to get
     */
    beginGetLocation(area, locationId) {
        return this._initializationPromise.then(() => {
            return this.beginGetAreaLocations(area);
        }).then((areaLocations) => {
            return areaLocations[(locationId || "").toLowerCase()];
        });
    }
    beginGetAreaLocations(area) {
        let areaLocationsPromise = this._locationsByAreaPromises[area];
        if (!areaLocationsPromise) {
            let requestUrl = this.resolveUrl(VsoClient.APIS_RELATIVE_PATH + "/" + area);
            areaLocationsPromise = this.restClient.options(requestUrl)
                .then((res) => {
                if (!res.result) {
                    return {};
                }
                let locationsLookup = {};
                let resourceLocations = res.result.value;
                let i;
                for (i = 0; i < resourceLocations.length; i++) {
                    let resourceLocation = resourceLocations[i];
                    locationsLookup[resourceLocation.id.toLowerCase()] = resourceLocation;
                }
                // If we have completed successfully, cache the response.
                this._locationsByAreaPromises[area] = areaLocationsPromise;
                return locationsLookup;
            });
        }
        return areaLocationsPromise;
    }
    resolveUrl(relativeUrl) {
        return url.resolve(this.baseUrl, path.join(this.basePath, relativeUrl));
    }
    queryParamsToStringHelper(queryParams, prefix) {
        if (queryParams == null || queryParams.length === 0) {
            return '';
        }
        let queryString = '';
        if (Array.isArray(queryParams)) {
            // Remove trailing '.' from prefix if it exists, otherwise use the prefix as-is
            const paramName = prefix.endsWith('.') ? prefix.slice(0, -1) : prefix;
            const values = queryParams.map(value => value.toString()).join(',');
            queryString += paramName + '=' + encodeURIComponent(values) + '&';
            return queryString;
        }
        if (typeof (queryParams) !== 'string') {
            for (let property in queryParams) {
                if (queryParams.hasOwnProperty(property)) {
                    const prop = queryParams[property];
                    const newPrefix = prefix + encodeURIComponent(property.toString()) + '.';
                    queryString += this.queryParamsToStringHelper(prop, newPrefix);
                }
            }
        }
        if (queryString === '' && prefix.length > 0) {
            // Date.prototype.toString() returns a string that is not valid for the REST API.
            // Need to specially call `toUTCString()` instead for such cases
            const queryValue = typeof queryParams === 'object' && 'toUTCString' in queryParams ? queryParams.toUTCString() : queryParams.toString();
            // Will always need to chop period off of end of prefix, if it exists
            const paramName = prefix.endsWith('.') ? prefix.slice(0, -1) : prefix;
            queryString = paramName + '=' + encodeURIComponent(queryValue) + '&';
        }
        return queryString;
    }
    queryParamsToString(queryParams) {
        const queryString = '?' + this.queryParamsToStringHelper(queryParams, '');
        // Will always need to slice either a ? or & off of the end
        return queryString.slice(0, -1);
    }
    getRequestUrl(routeTemplate, area, resource, routeValues, queryParams) {
        // Add area/resource route values (based on the location)
        routeValues = routeValues || {};
        if (!routeValues.area) {
            routeValues.area = area;
        }
        if (!routeValues.resource) {
            routeValues.resource = resource;
        }
        // Replace templated route values
        let relativeUrl = this.replaceRouteValues(routeTemplate, routeValues);
        // Append query parameters to the end
        if (queryParams) {
            relativeUrl += this.queryParamsToString(queryParams);
        }
        // Resolve the relative url with the base
        return url.resolve(this.baseUrl, path.join(this.basePath, relativeUrl));
    }
    // helper method copied directly from VSS\WebAPI\restclient.ts
    replaceRouteValues(routeTemplate, routeValues) {
        let result = "", currentPathPart = "", paramName = "", insideParam = false, charIndex, routeTemplateLength = routeTemplate.length, c;
        for (charIndex = 0; charIndex < routeTemplateLength; charIndex++) {
            c = routeTemplate[charIndex];
            if (insideParam) {
                if (c == "}") {
                    insideParam = false;
                    if (routeValues[paramName]) {
                        currentPathPart += encodeURIComponent(routeValues[paramName]);
                    }
                    else {
                        // Normalize param name in order to capture wild-card routes
                        let strippedParamName = paramName.replace(/[^a-z0-9]/ig, '');
                        if (routeValues[strippedParamName]) {
                            currentPathPart += encodeURIComponent(routeValues[strippedParamName]);
                        }
                    }
                    paramName = "";
                }
                else {
                    paramName += c;
                }
            }
            else {
                if (c == "/") {
                    if (currentPathPart) {
                        if (result) {
                            result += "/";
                        }
                        result += currentPathPart;
                        currentPathPart = "";
                    }
                }
                else if (c == "{") {
                    if ((charIndex + 1) < routeTemplateLength && routeTemplate[charIndex + 1] == "{") {
                        // Escaped '{'
                        currentPathPart += c;
                        charIndex++;
                    }
                    else {
                        insideParam = true;
                    }
                }
                else if (c == '}') {
                    currentPathPart += c;
                    if ((charIndex + 1) < routeTemplateLength && routeTemplate[charIndex + 1] == "}") {
                        // Escaped '}'
                        charIndex++;
                    }
                }
                else {
                    currentPathPart += c;
                }
            }
        }
        if (currentPathPart) {
            if (result) {
                result += "/";
            }
            result += currentPathPart;
        }
        return result;
    }
}
exports.VsoClient = VsoClient;
VsoClient.APIS_RELATIVE_PATH = "_apis";
VsoClient.PREVIEW_INDICATOR = "-preview.";
