text: const request = require('superagent');
  const userAgent = `node-osu v${require('../package.json').version} (https://github.com/troke12/node-osu)`;
  const Beatmap = require('./Base/Beatmap.js');
  const Score = require('./Base/Score.js');
  const Match = require('./Base/Match.js');
  const User = require('./Base/User.js');
  
  class Api {
  	/**
  	 * Creates a new node-osu object
  	 * @param {String} apiKey your osu api key
  	 * @param {Object} [options]
  	 * @param {String} [options.baseUrl="https://osu.ppy.sh/api"] Sets the base api url
  	 * @param {Boolean} [options.notFoundAsError=true] Throw an error on not found instead of returning nothing
  	 * @param {Boolean} [options.completeScores=false] When fetching scores also fetch the beatmap they are for (Allows getting accuracy)
  	 * @param {Boolean} [options.parseNumeric=false] Parse numeric properties into numbers. May have overflow
  	 */
  	constructor(apiKey, options = { }) {
  		this.apiKey = apiKey;
  		this.baseUrl = options.baseUrl || 'https://osu.ppy.sh/api';
  		this.notFoundAsError = options.notFoundAsError === undefined ? true : !!options.notFoundAsError;
  		this.completeScores = !!options.completeScores;
  		this.parseNumeric = !!options.parseNumeric;
  	}
  
  	get config() {
  		return {
  			notFoundAsError: this.notFoundAsError,
  			completeScores: this.completeScores,
  			parseNumeric: this.parseNumeric
  		};
  	}
  
  	/**
  	 * Makes an api call
  	 * @param {String} endpoint
  	 * @param {Object} options
  	 * @param {Date} [options.since] Return all beatmaps ranked or loved since this date
  	 * @param {String} [options.s] Specify a beatmapSetId to return metadata from
  	 * @param {String} [options.b] Specify a beatmapId to return metadata from
  	 * @param {String} [options.u] Specify a userId or a username to return metadata from
  	 * @param {"string"|"id"} [options.type] Specify if `u` is a userId or a username
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {0|1} [options.a] Specify whether converted beatmaps are included
  	 * @param {String} [options.h] The beatmap hash
  	 * @param {Number} [options.limit] Amount of results. Default and maximum are 500
  	 * @param {Number} [options.mods] Mods that apply to the beatmap requested. Default is 0
  	 * @param {Number} [options.event_days] Max number of days between now and last event date. Range of 1-31. Default value is 1
  	 * @param {String} [options.mp] Match id to get information from
  	 * @returns {Promise<Object>} The response body
  	 */
  	async apiCall(endpoint, options) {
  		if (!this.apiKey)
  			throw new Error('apiKey not set');
  		options.k = this.apiKey;
  
  		try {
  			const resp = await request.get(this.baseUrl + endpoint)
  				.set('User-Agent', userAgent)
  				.query(options);
  
  			return resp.body;
  		} catch (error) {
  			throw new Error(error.response || error);
  		}
  	}
  
  	/**
  	 * Returns a not found error or the response, depending on the config
  	 * @param {Object} response
  	 * @returns {Object}
  	 */
  	notFound(response) {
  		if (this.notFoundAsError)
  			throw new Error('Not found');
  
  		return response;
  	}
  
  	/**
  	 * Returns an array of Beatmap objects
  	 * @param {Object} options
  	 * @param {String} options.b Specify a beatmapId to return metadata from
  	 * @param {Date} [options.since] Return all beatmaps ranked or loved since this date
  	 * @param {String} [options.s] Specify a beatmapSetId to return metadata from
  	 * @param {String} [options.u] Specify a userId or a username to return metadata from
  	 * @param {"string"|"id"} [options.type] Specify if `u` is a userId or a username
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {0|1} [options.a] Specify whether converted beatmaps are included
  	 * @param {String} [options.h] The beatmap hash
  	 * @param {Number} [options.limit] Amount of results. Default and maximum are 500
  	 * @param {Number} [options.mods] Mods that apply to the beatmap requested. Default is 0
  	 * @returns {Promise<Beatmap[]>}
  	 */
  	async getBeatmaps(options) {
  		const resp = await this.apiCall('/get_beatmaps', options);
  
  		if (resp.length === 0)
  			return this.notFound(resp);
  
  		return resp.map(bm => new Beatmap(this.config, bm));
  	}
  
  	/**
  	 * Returns a User object
  	 * @param {Object} options
  	 * @param {String} options.u Specify a userId or a username to return metadata from
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
  	 * @param {Number} [options.event_days] Max number of days between now and last event date. Range of 1-31. Default value is 1
  	 * @returns {Promise<User>}
  	 */
  	async getUser(options) {
  		const resp = await this.apiCall('/get_user', options);
  
  		if (resp.length === 0)
  			return this.notFound(resp);
  
  		return new User(this.config, resp[0]);
  	}
  
  	/**
  	 * Returns an array of Score objects
  	 * @param {Object} options
  	 * @param {String} options.b Specify a beatmapId to return score information from
  	 * @param {String} [options.u] Specify a userId or a username to return information for
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
  	 * @param {Number} [options.limit] Amount of results from the top (range between 1 and 100 - defaults to 50)
  	 * @returns {Promise<Score[]>}
  	 */
  	async getScores(options) {
  		const resp = await this.apiCall('/get_scores', options);
  
  		if (resp.length === 0)
  			return this.notFound(resp);
  
  		if (!this.completeScores)
  			return resp.map(sc => new Score(this.config, sc));
  
  		const beatmaps = await this.getBeatmaps({ b: options.b });
  		return resp.map(sc => new Score(this.config, sc, beatmaps[0]));
  	}
  
  	/**
  	 * Returns an array of Score objects
  	 * @param {Object} options
  	 * @param {String} options.u Specify a userId or a username to return best scores from
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {"string"|"id"} [options.type] Specify if u is a user_id or a username
  	 * @param {Number} [options.limit] Amount of results (range between 1 and 100 - defaults to 10)
  	 * @returns {Promise<Score[]>}
  	 */
  	async getUserBest(options) {
  		const resp = await this.apiCall('/get_user_best', options);
  
  		if (resp.length === 0)
  			return this.notFound(resp);
  
  		if (!this.completeScores)
  			return resp.map(sc => new Score(this.config, sc));
  
  		const scores = resp.map(sc => new Score(this.config, sc));
  		for (const score of scores)
  			score.beatmap = (await this.getBeatmaps({ b: score.beatmapId }))[0];
  
  		return scores;
  	}
  
  	/**
  	 * Returns an array of Score objects.
  	 * Will return not found if the user has not submitted any scores in the past 24 hours
  	 * @param {Object} options
  	 * @param {String} options.u Specify a userId or a username to return recent plays from
  	 * @param {0|1|2|3} [options.m] Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {"string"|"id"} [options.type] Specify if `u` is a user_id or a username
  	 * @param {Number} [options.limit] Amount of results (range between 1 and 50 - defaults to 10)
  	 * @returns {Promise<Score[]>}
  	 */
  	async getUserRecent(options) {
  		const resp = await this.apiCall('/get_user_recent', options);
  
  		if (resp.length === 0)
  			return this.notFound(resp);
  
  		if (!this.completeScores)
  			return resp.map(sc => new Score(this.config, sc));
  
  		const scores = resp.map(sc => new Score(this.config, sc));
  		for (const score of scores)
  			score.beatmap = (await this.getBeatmaps({ b: score.beatmapId }))[0];
  
  		return scores;
  	}
  
  	/**
  	 * Returns a Match object.
  	 * @param {Object} options
  	 * @param {String} options.mp Match id to get information from
  	 * @returns {Promise<Match>}
  	 */
  	async getMatch(options) {
  		const resp = await this.apiCall('/get_match', options);
  
  		if (resp.match === 0)
  			return this.notFound(resp);
  
  		return new Match(this.config, resp);
  	}
  
  	/**
  	 * Returns a replay object. **Do not spam this endpoint.**
  	 * @param {Object} options
  	 * @param {0|1|2|3} options.m Mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
  	 * @param {String} options.b The beatmapId in which the replay was played
  	 * @param {String} options.u The user that has played the beatmap (required)
  	 * @param {"string"|"id"} [options.type] Specify if u is a userId or a username
  	 * @param {Number} [options.mods] Specify a mod or mod combination
  	 *
  	 */
  	async getReplay(options) {
  		return await this.apiCall('/get_replay', options);
  	}
  }
  
  module.exports = Api;
  