/**
 * Forge SDK
 * The Forge Platform contains an expanding collection of web service components that can be used with Autodesk cloud-based products or your own technologies. Take advantage of Autodesk’s expertise in design and engineering.
 *
 * Contact: forge.help@autodesk.com
 *
 * NOTE: This class is auto generated by the swagger code generator program.
 * https://github.com/swagger-api/swagger-codegen.git
 * Do not edit the class manually.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*jshint esversion: 9 */

module.exports = (function () {
	'use strict';

	const axios = require('axios');

	/**
	 * @module ApiClient
	 */

	/**
	 * Manages low level client-server communications, parameter marshalling, etc. There should not be any need for an
	 * application to use this class directly - the *Api and model classes provide the public API for the service. The
	 * contents of this file should be regarded as internal but are documented for completeness.
	 * @alias module:ApiClient
	 * @class
	 */
	const exports = function (basepath) {
		/**
		 * The base URL against which to resolve every API call's (relative) path.
		 * @type {String}
		 * @default https://developer.api.autodesk.com
		 */
		this.basePath = (basepath || 'https://developer.api.autodesk.com').replace(/\/+$/, '');


		/**
		 * The default HTTP headers to be included for all API calls.
		 * @type {Array.<String>}
		 * @default {}
		 */
		this.defaultHeaders = {};

		/**
		 * The default HTTP timeout for all API calls.
		 * @type {Number}
		 * @default 60000
		 */
		this.timeout = 60000;
	};

	exports.prototype.switchServerPath = function (basepath) {
		if (basepath !== undefined)
			this.basePath = basepath.replace(/\/+$/, '');
	};

	/**
	 * Returns a string representation for an actual parameter.
	 * @param param The actual parameter.
	 * @returns {String} The string representation of <code>param</code>.
	 */
	exports.prototype.paramToString = function (param) {
		if (param === undefined || param === null) {
			return ('');
		}
		if (param instanceof Date) {
			return (param.toJSON());
		}
		return (param.toString());
	};

	/**
	 * Builds full URL by appending the given path to the base URL and replacing path parameter place-holders with parameter values.
	 * NOTE: query parameters are not handled here.
	 * @param {String} path The path to append to the base URL.
	 * @param {Object} pathParams The parameter values to append.
	 * @returns {String} The encoded path with parameter values substituted.
	 */
	exports.prototype.buildUrl = function (path, pathParams) {
		const _this = this;
		if (!path.match(/^\//))
			path = '/' + path;
		let url = (this.basePath + path).replace(/\{([\w-]+)}/g, function (fullMatch, key) {
			let value = fullMatch;
			if (pathParams.hasOwnProperty(key))
				value = _this.paramToString(pathParams[key]);
			return (encodeURIComponent(value));
		});
		return (url);
	};

	/**
	 * Checks whether the given content type represents JSON.<br>
	 * JSON content type examples:<br>
	 * <ul>
	 * <li>application/json</li>
	 * <li>application/json; charset=UTF8</li>
	 * <li>APPLICATION/JSON</li>
	 * </ul>
	 * @param {String} contentType The MIME content type to check.
	 * @returns {Boolean} <code>true</code> if <code>contentType</code> represents JSON, otherwise <code>false</code>.
	 */
	exports.prototype.isJsonMime = function (contentType) {
		return (Boolean(contentType !== undefined && contentType !== null && contentType.match(/^application\/(vnd.api\+)?json(;.*)?$/i)));
	};

	/**
	 * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first.
	 * @param {Array.<String>} contentTypes
	 * @returns {String} The chosen content type, preferring JSON.
	 */
	exports.prototype.jsonPreferredMime = function (contentTypes) {
		for (let i = 0; i < contentTypes.length; i++) {
			if (this.isJsonMime(contentTypes[i]))
				return contentTypes[i];
		}
		return (contentTypes[0]);
	};

	/**
	 * Checks whether the given parameter value represents file-like content.
	 * @param param The parameter to check.
	 * @returns {Boolean} <code>true</code> if <code>param</code> represents a file.
	 */
	exports.prototype.isFileParam = function (param) {
		const type = typeof param;
		if ((type === 'number') || (type === 'boolean') || (type === 'string') || (type === 'undefined')) {
			return false;
		}
		return (param instanceof require('fs').ReadStream) || (typeof Buffer === 'function' && param instanceof Buffer);
	};

	/**
	 * Normalizes parameter values:
	 * <ul>
	 * <li>remove nils</li>
	 * <li>keep files and arrays</li>
	 * <li>format to string with `paramToString` for other cases</li>
	 * </ul>
	 * @param {Object.<String, Object>} params The parameters as object properties.
	 * @returns {Object.<String, Object>} normalized parameters.
	 */
	exports.prototype.normalizeParams = function (params) {
		const newParams = {};
		for (const key in params) {
			if (params.hasOwnProperty(key) && params[key] !== undefined && params[key] !== null) {
				const value = params[key];
				if (this.isFileParam(value) || Array.isArray(value)) {
					newParams[key] = value;
				} else {
					newParams[key] = this.paramToString(value);
				}
			}
		}
		return (newParams);
	};

	/**
	 * Enumeration of collection format separator strategies.
	 * @enum {String}
	 * @readonly
	 */
	exports.CollectionFormatEnum = {
		/**
		 * Comma-separated values. Value: <code>csv</code>
		 * @const
		 */
		CSV: ',',
		/**
		 * Space-separated values. Value: <code>ssv</code>
		 * @const
		 */
		SSV: ' ',
		/**
		 * Tab-separated values. Value: <code>tsv</code>
		 * @const
		 */
		TSV: '\t',
		/**
		 * Pipe(|)-separated values. Value: <code>pipes</code>
		 * @const
		 */
		PIPES: '|',
		/**
		 * Native array. Value: <code>multi</code>
		 * @const
		 */
		MULTI: 'multi'
	};

	/**
	 * Builds a string representation of an array-type actual parameter, according to the given collection format.
	 * @param {Array} param An array parameter.
	 * @param {module:ApiClient.CollectionFormatEnum} collectionFormat The array element separator strategy.
	 * @returns {String|Array} A string representation of the supplied collection, using the specified delimiter. Returns
	 * <code>param</code> as is if <code>collectionFormat</code> is <code>multi</code>.
	 */
	exports.prototype.buildCollectionParam = function buildCollectionParam(param, collectionFormat) {
		if (param === undefined || param === null) {
			return null;
		}
		switch (collectionFormat) {
			case 'csv':
				return param.map(this.paramToString).join(',');
			case 'ssv':
				return param.map(this.paramToString).join(' ');
			case 'tsv':
				return param.map(this.paramToString).join('\t');
			case 'pipes':
				return param.map(this.paramToString).join('|');
			case 'multi':
				// return the array directly
				return param.map(this.paramToString);
			default:
				throw new Error('Unknown collection format: ' + collectionFormat);
		}
	};

	/**
	 * Applies authentication header to the request.
	 * @param {Object} requestParams - The requestParams object created by a <code>axios()</code> call.
	 * @param {Object} headers - The headers that passed to this method
	 * @param {Object} oauth2client - OAuth2 client that has a credentials object
	 * @param {Object} credentials - The credentials object
	 */
	exports.prototype.applyAuthToRequest = function (requestParams, headers, oauth2client, credentials) {

		const _this = this;

		function setAuthHeader(credentials) {
			if (credentials && credentials.access_token)
				headers['Authorization'] = 'Bearer ' + credentials.access_token; // jshint ignore:line
		}

		return new Promise(function (resolve, reject) {
			// if the request doesn't require authentication, just resolve the promise
			if (!credentials || (credentials && !credentials.access_token))
				return (resolve());

			// let's see if the token is already expired?
			// be careful access tokens are validated once teh query was received by the server, not when emitted
			// for this reason, we need to aknowledge the time to upload payload/file/etc... (300 == 5min)
			if (oauth2client && oauth2client.autoRefresh && credentials.expires_at - 300000 <= Date.now()) {
				// set the correct promiseObj, for 2 or 3 legged token
				let isCredentialsTypeTwoLegged = credentials.refresh_token === undefined;
				const getCredentialsPromise = isCredentialsTypeTwoLegged ?
					oauth2client.authenticate() // 2-legged: create a new credentials object
					:
					oauth2client.refreshToken(credentials); // 3-legged: use refresh

				getCredentialsPromise.then(function (newCredentials) {
					_this.debug('credentials were refreshed, new credentials:', newCredentials);

					// For a 2-legged token just update the credentials object
					if (isCredentialsTypeTwoLegged) {
						oauth2client.setCredentials(newCredentials);
					}
					setAuthHeader(newCredentials);
					resolve();
				}, function (err) {
					reject(err);
				});
			} else {
				setAuthHeader(credentials);
				_this.debug('set current credentials to header', credentials);
				resolve();
			}
		});
	};

	/**
	 * Enable working in debug mode
	 * To activate, simple set ForgeSdk.setDebug(true);
	 */
	exports.prototype.debug = function debug() {
		if (this.isDebugMode) {
			const args = Array.prototype.slice.call(arguments);
			console.log(...args);
		}
	};

	exports.prototype.warn = function warn() {
		if (this.isDebugMode) {
			const args = Array.prototype.slice.call(arguments);
			console.warn(args);
		}
	};

	exports.prototype.error = function error() {
		if (this.isDebugMode) {
			const args = Array.prototype.slice.call(arguments);
			console.error(['\x1b[31mError:', ...args, '\x1b[0m']);
		}
	};

	/**
	 * Is this obj a readable stream
	 */
	exports.isReadableStream = function (obj) {
		return (obj && typeof obj.pipe === 'function' && typeof obj._read === 'function' && typeof obj._readableState === 'object');
	};

	/**
	 * Is this obj a writable stream
	 */
	exports.isWritableStream = function (obj) {
		return (obj && typeof obj.pipe === 'function' && typeof obj._write === 'function' && typeof obj._writableState === 'object');
	};

	/**
	 * Invokes the REST service using the supplied settings and parameters.
	 * @param {String} path The base URL to invoke.
	 * @param {String} httpMethod The HTTP method to use.
	 * @param {Object.<String, String>} pathParams A map of path parameters and their values.
	 * @param {Object.<String, Object>} queryParams A map of query parameters and their values.
	 * @param {Object.<String, Object>} headerParams A map of header parameters and their values.
	 * @param {Object.<String, Object>} formParams A map of form parameters and their values.
	 * @param {Object} bodyParam The value to pass as the request body.
	 * @param {Array.<String>} contentTypes An array of request MIME types.
	 * @param {Array.<String>} accepts An array of acceptable response MIME types.
	 * @param {(String|Array|Object|Function)} returnType The required type to return; can be a string for simple types or the
	 *    constructor for a complex type.
	 * @param {Object} oauth2client oauth2client for the call
	 * @param {Object} credentials credentials for the call
	 * @param {String} responseType The type of the response data
	 * @returns {Object} A Promise object.
	 */
	exports.prototype.callApi = function callApi(path, httpMethod, pathParams,
		queryParams, headerParams, formParams, bodyParam, contentTypes, accepts,
		returnType, oauth2client, credentials, responseType
	) {

		const _this = this;
		const requestParams = {};
		requestParams.uri = this.buildUrl(path, pathParams);
		requestParams.method = httpMethod;
		const headers = {};
		requestParams.qs = this.normalizeParams(queryParams);
		requestParams.timeout = this.timeout;
		requestParams.responseType = responseType;

		const contentType = this.jsonPreferredMime(contentTypes);
		if (contentType)
			headers['Content-Type'] = contentType;

		if (contentType === 'application/x-www-form-urlencoded') {
			requestParams.form = this.normalizeParams(formParams);
		} else if (contentType === 'multipart/form-data') {
			requestParams.formData = this.normalizeParams(formParams);
		} else if (bodyParam) {
			requestParams.body = bodyParam;
			if (this.isJsonMime(contentType))
				requestParams.json = true;
		}

		if (accepts.length > 0) {
			headers['Accept'] = accepts.join(','); // jshint ignore:line
			for (let i = 0; i < accepts.length; i++) {
				if (accepts[i] === 'application/octet-stream')
					requestParams.encoding = null;
			}
		}
		if (headerParams['Accept-Encoding'] === 'gzip, deflate')
			requestParams.encoding = null;
		headerParams['User-Agent'] = 'forge-apis/0.9.12 (nodejs)';
		_this.debug('request params were', requestParams);

		return new Promise(function (resolve, reject) {
			_this.applyAuthToRequest(requestParams, headers, oauth2client, credentials).then(function () {

				// headerParams optional overrides
				requestParams.headers = Object.assign(headers, headerParams);
				requestParams.headers = Object.assign(requestParams.headers, _this.defaultHeaders);
				requestParams.agentOptions = {
					secureProtocol: 'TLSv1_2_method' // 'TLSv1.2'
				};

				requestParams.headers && Object.keys(requestParams.headers).map((key) => {
					if (requestParams.headers[key] === undefined)
						delete requestParams.headers[key];
				}); // jshint ignore:line

				axios({
					method: requestParams.method,
					url: requestParams.uri,
					headers: requestParams.headers,
					params: requestParams.qs || {},
					maxContentLength: Infinity,
					maxBodyLength: Infinity,
					data: requestParams.body,
					responseType: requestParams.responseType
				})
					.then((response) => {
						if (response.statusCode >= 400) {
							_this.debug('error response', {
								statusCode: response.status || response.statusCode,
								statusMessage: response.statusText || response.statusMessage,
								headers: response.headers,
							});
							reject({
								statusCode: response.status || response.statusCode,
								statusMessage: response.statusText || response.statusMessage,
								headers: response.headers,
								statusBody: response.data,
							});
						} else {
							resolve({
								statusCode: response.status || response.statusCode,
								headers: response.headers,
								body: response.data,
							});
						}
					})
					.catch((err) => {
						err.statusCode = err.status;
						reject(err);
					});
			}, function (err) {
				throw new Error(err.toString());
			});
		});
	};

	/**
	 * Parses an ISO-8601 string representation of a date value.
	 * @param {String} str The date value as a string.
	 * @returns {Date} The parsed date object.
	 */
	exports.parseDate = function (str) {
		return (new Date(str.replace(/T/i, ' ')));
	};

	/**
	 * Converts a value to the specified type.
	 * @param {(String|Object)} data The data to convert, as a string or object.
	 * @param {(String|Array.<String>|Object.<String, Object>|Function)} type The type to return. Pass a string for simple types
	 * or the constructor function for a complex type. Pass an array containing the type name to return an array of that type. To
	 * return an object, pass an object with one property whose name is the key type and whose value is the corresponding value type:
	 * all properties on <code>data<code> will be converted to this type.
	 * @returns {Object} An instance of the specified type.
	 */
	exports.convertToType = function (data, type) {
		switch (type) {
			case 'Boolean':
				return (Boolean(data));
			case 'Integer':
				return (parseInt(data, 10));
			case 'Number':
				return (parseFloat(data));
			case 'String':
				return (String(data));
			case 'Date':
				return (this.parseDate(String(data)));
			default:
				if (type === Object) {
					// generic object, return directly
					return (data);
				} else if (typeof type === 'function') {
					// for model type like: User
					return (type.constructFromObject(data));
				} else if (Array.isArray(type)) {
					// for array type like: ['String']
					const itemType = type[0];
					return (data.map(function (item) {
						return (exports.convertToType(item, itemType));
					}));
				} else if (typeof type === 'object') {
					// for plain object type like: {'String': 'Integer'}
					let keyType, valueType;
					for (const k in type) {
						if (type.hasOwnProperty(k)) {
							keyType = k;
							valueType = type[k];
							break;
						}
					}
					const result = {};
					for (const j in data) {
						if (data.hasOwnProperty(j)) {
							const key = exports.convertToType(j, keyType);
							result[key] = exports.convertToType(data[j], valueType);
						}
					}
					return (result);
				} else {
					// for unknown type, return the data directly
					return (data);
				}
		}
	};


	exports.version = '0.9.12';

	exports.userAgentHeaders = {
		'User-Agent': `forge-apis/${exports.version} nodejs api wrappers library`,
	};

	/**
	 * The default API client implementation.
	 * @type {module:ApiClient}
	 */
	exports.instance = new exports();

	return (exports);
}());