'use strict';
const fs = require('fs');
const path = require('path');
const url = require('url');
const pify = require('pify');
const importLazy = require('import-lazy')(require);

const binCheck = importLazy('bin-check');
const binVersionCheck = importLazy('bin-version-check');
const download = importLazy('download');
const osFilterObj = importLazy('os-filter-obj');

const statAsync = pify(fs.stat);
const chmodAsync = pify(fs.chmod);

/**
 * Initialize a new `BinWrapper`
 *
 * @param {Object} options
 * @api public
 */
module.exports = class BinWrapper {
	constructor(options = {}) {
		this.options = options;

		if (this.options.strip <= 0) {
			this.options.strip = 0;
		} else if (!this.options.strip) {
			this.options.strip = 1;
		}
	}

	/**
	 * Get or set files to download
	 *
	 * @param {String} src
	 * @param {String} os
	 * @param {String} arch
	 * @api public
	 */
	src(src, os, arch) {
		if (arguments.length === 0) {
			return this._src;
		}

		this._src = this._src || [];
		this._src.push({
			url: src,
			os,
			arch
		});

		return this;
	}

	/**
	 * Get or set the destination
	 *
	 * @param {String} dest
	 * @api public
	 */
	dest(dest) {
		if (arguments.length === 0) {
			return this._dest;
		}

		this._dest = dest;
		return this;
	}

	/**
	 * Get or set the binary
	 *
	 * @param {String} bin
	 * @api public
	 */
	use(bin) {
		if (arguments.length === 0) {
			return this._use;
		}

		this._use = bin;
		return this;
	}

	/**
	 * Get or set a semver range to test the binary against
	 *
	 * @param {String} range
	 * @api public
	 */
	version(range) {
		if (arguments.length === 0) {
			return this._version;
		}

		this._version = range;
		return this;
	}

	/**
	 * Get path to the binary
	 *
	 * @api public
	 */
	path() {
		return path.join(this.dest(), this.use());
	}

	/**
	 * Run
	 *
	 * @param {Array} cmd
	 * @api public
	 */
	run(cmd = ['--version']) {
		return this.findExisting().then(() => {
			if (this.options.skipCheck) {
				return;
			}

			return this.runCheck(cmd);
		});
	}

	/**
	 * Run binary check
	 *
	 * @param {Array} cmd
	 * @api private
	 */
	runCheck(cmd) {
		return binCheck(this.path(), cmd).then(works => {
			if (!works) {
				throw new Error(`The \`${this.path()}\` binary doesn't seem to work correctly`);
			}

			if (this.version()) {
				return binVersionCheck(this.path(), this.version());
			}

			return Promise.resolve();
		});
	}

	/**
	 * Find existing files
	 *
	 * @api private
	 */
	findExisting() {
		return statAsync(this.path()).catch(error => {
			if (error && error.code === 'ENOENT') {
				return this.download();
			}

			return Promise.reject(error);
		});
	}

	/**
	 * Download files
	 *
	 * @api private
	 */
	download() {
		const files = osFilterObj(this.src() || []);
		const urls = [];

		if (files.length === 0) {
			return Promise.reject(new Error('No binary found matching your system. It\'s probably not supported.'));
		}

		files.forEach(file => urls.push(file.url));

		return Promise.all(urls.map(url => download(url, this.dest(), {
			extract: true,
			strip: this.options.strip
		}))).then(result => {
			const resultingFiles = flatten(result.map((item, index) => {
				if (Array.isArray(item)) {
					return item.map(file => file.path);
				}

				const parsedUrl = url.parse(files[index].url);
				const parsedPath = path.parse(parsedUrl.pathname);

				return parsedPath.base;
			}));

			return Promise.all(resultingFiles.map(fileName => {
				return chmodAsync(path.join(this.dest(), fileName), 0o755);
			}));
		});
	}
};

function flatten(arr) {
	return arr.reduce((acc, elem) => {
		if (Array.isArray(elem)) {
			acc.push(...elem);
		} else {
			acc.push(elem);
		}

		return acc;
	}, []);
}
