import { IMovieSearchInfo } from "@autoplex-api/torrent-search";
import { sleep } from "@autoplex/utils";
import Torrent from "../Torrent";

/**
 * Media type flags
 */
export enum MediaType {
	None    = 0x0,
	Movies  = 0x1,
	TvShows = 0x2
}

/**
 * The search method type
 */
type SearchMethod = (...args: any[]) => Promise<Torrent[]>;

/**
 * The base implementation for a torrent provider
 */
export default abstract class Provider
{
	/**
	 * The name of the provider
	 */
	public abstract readonly NAME: string;

	/**
	 * Indicate what media types the provider supports
	 */
	public abstract readonly PROVIDES: MediaType;

	/**
	 * How much to throttle requests
	 */
	protected abstract readonly THROTTLE: number;

	/**
	 * Retry search requests if necessary
	 */
	protected readonly SEARCH_RETRIES: number = 0;

	/**
	 * Indicate if the API is currently authenticated
	 */
	protected async isAuthenticated() {
		return true;
	}

	/**
	 * Authenticate the API if necessary. Invoked once before each findMovieTorrents request
	 */
	protected async authenticate(): Promise<boolean> {
		return true;
	}

	/**
	 * Search for movies by the provided IMDb ID
	 */
	protected async searchMovieImdb(imdbId: string, movie: IMovieSearchInfo): Promise<Torrent[]> {
		return [];
	}

	/**
	 * Search for movies by the title and year
	 */
	protected async searchMovieString(movie: IMovieSearchInfo): Promise<Torrent[]> {
		return [];
	}

	// ---------------------------------------------------------------------------------------------

	/**
	 * Indicate if the provider is currently searching for a torrent
	 */
	protected isSearching = false;

	/**
	 * Maintain a queue of search requests
	 */
	protected searchQueue: (() => void)[] = [];

	/**
	 * Create a torrent object
	 */
	protected createTorrent(movie: IMovieSearchInfo, name: string, size?: number, seeders?: number,
	                        link?: string)
	{
		return new Torrent(this.NAME, movie, name, size, seeders, link);
	}

	/**
	 * Lock the search
	 */
	protected async lock() {
		if (this.isSearching) {
			await new Promise<void>(resolve => this.searchQueue.push(resolve));
		}
		this.isSearching = true;
	}

	/**
	 * Unlock the search
	 */
	protected unlock(delay: number = 0) {
		setTimeout(() => {
			let next = this.searchQueue.splice(0, 1)[0];
			if (next === undefined) {
				this.isSearching = false;
				return;
			}
			next();
		}, delay);
	}

	/**
	 * Await for authentication if necessary
	 */
	protected async awaitAuthentication() {
		while (!await this.isAuthenticated()) {
			try {
				await this.authenticate();
			} catch(e) {
				console.error("Failed to authenticate", e);
			}
			await sleep(this.THROTTLE);
		}
	}

	// Public Interface ----------------------------------------------------------------------------

	/**
	 * Search for content with a number of retries
	 */
	protected async search<M extends SearchMethod>(searchMethod: M, args: Parameters<M>,
	                                               results: Torrent[])
	{
		for (let i = 0; i <= this.SEARCH_RETRIES; i++) {
			try {
				await this.awaitAuthentication();
				let torrents = await searchMethod.apply(this, args);
				return results.concat(torrents);
			} catch(e) {
				await sleep(this.THROTTLE);
				continue;
			}
		}
		return results;
	}

	/**
	 * Search for movies
	 */
	public async searchMovie(movie: IMovieSearchInfo) {
		// Accumulate a mutex lock
		await this.lock();

		// Create the results list
		let results: Torrent[] = [];

		// If an IMDb ID is available, search for movies by the IMDb ID
		if (movie.imdbId !== null) {
			results = await this.search(this.searchMovieImdb, [movie.imdbId, movie], results);
		}

		// Search by string (if supported)
		results = await this.search(this.searchMovieString, [movie], results);

		// Unlock and return the results
		this.unlock(this.THROTTLE);
		return results;
	}
}
