'use strict';
const fs = require('fs');
const path = require('path');
const Promise = require('bluebird');
const slash = require('slash');
const globby = require('globby');
const flatten = require('lodash.flatten');
const autoBind = require('auto-bind');
const defaultIgnore = require('ignore-by-default').directories();
const multimatch = require('multimatch');

function handlePaths(files, excludePatterns, globOptions) {
	// Convert Promise to Bluebird
	files = Promise.resolve(globby(files.concat(excludePatterns), globOptions));

	const searchedParents = new Set();
	const foundFiles = new Set();

	function alreadySearchingParent(dir) {
		if (searchedParents.has(dir)) {
			return true;
		}

		const parentDir = path.dirname(dir);

		if (parentDir === dir) {
			// We have reached the root path
			return false;
		}

		return alreadySearchingParent(parentDir);
	}

	return files
		.map(file => {
			file = path.resolve(globOptions.cwd, file);

			if (fs.statSync(file).isDirectory()) {
				if (alreadySearchingParent(file)) {
					return null;
				}

				searchedParents.add(file);

				let pattern = path.join(file, '**', '*.js');

				if (process.platform === 'win32') {
					// Always use `/` in patterns, harmonizing matching across platforms
					pattern = slash(pattern);
				}

				return handlePaths([pattern], excludePatterns, globOptions);
			}

			// `globby` returns slashes even on Windows. Normalize here so the file
			// paths are consistently platform-accurate as tests are run.
			return path.normalize(file);
		})
		.then(flatten)
		.filter(file => file && path.extname(file) === '.js')
		.filter(file => {
			if (path.basename(file)[0] === '_' && globOptions.includeUnderscoredFiles !== true) {
				return false;
			}

			return true;
		})
		.map(file => path.resolve(file))
		.filter(file => {
			const alreadyFound = foundFiles.has(file);
			foundFiles.add(file);
			return !alreadyFound;
		});
}

const defaultExcludePatterns = () => [
	'!**/node_modules/**',
	'!**/fixtures/**',
	'!**/helpers/**'
];

const defaultIncludePatterns = () => [
	'test.js',
	'test-*.js',
	'test',
	'**/__tests__',
	'**/*.test.js'
];

const defaultHelperPatterns = () => [
	'**/__tests__/helpers/**/*.js',
	'**/__tests__/**/_*.js',
	'**/test/helpers/**/*.js',
	'**/test/**/_*.js'
];

const getDefaultIgnorePatterns = () => defaultIgnore.map(dir => `${dir}/**/*`);

// Used on paths before they're passed to multimatch to harmonize matching
// across platforms
const matchable = process.platform === 'win32' ? slash : (path => path);

class AvaFiles {
	constructor(options) {
		options = options || {};

		let files = (options.files || []).map(file => {
			// `./` should be removed from the beginning of patterns because
			// otherwise they won't match change events from Chokidar
			if (file.slice(0, 2) === './') {
				return file.slice(2);
			}

			return file;
		});

		if (files.length === 0) {
			files = defaultIncludePatterns();
		}

		this.excludePatterns = defaultExcludePatterns();
		this.files = files;
		this.sources = options.sources || [];
		this.cwd = options.cwd || process.cwd();

		autoBind(this);
	}

	findTestFiles() {
		return handlePaths(this.files, this.excludePatterns, {
			cwd: this.cwd,
			cache: Object.create(null),
			statCache: Object.create(null),
			realpathCache: Object.create(null),
			symlinks: Object.create(null)
		});
	}

	findTestHelpers() {
		return handlePaths(defaultHelperPatterns(), ['!**/node_modules/**'], {
			cwd: this.cwd,
			includeUnderscoredFiles: true,
			cache: Object.create(null),
			statCache: Object.create(null),
			realpathCache: Object.create(null),
			symlinks: Object.create(null)
		});
	}

	isSource(filePath) {
		let mixedPatterns = [];
		const defaultIgnorePatterns = getDefaultIgnorePatterns();
		const overrideDefaultIgnorePatterns = [];

		let hasPositivePattern = false;
		this.sources.forEach(pattern => {
			mixedPatterns.push(pattern);

			// TODO: Why not just `pattern[0] !== '!'`?
			if (!hasPositivePattern && pattern[0] !== '!') {
				hasPositivePattern = true;
			}

			// Extract patterns that start with an ignored directory. These need to be
			// rematched separately.
			if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
				overrideDefaultIgnorePatterns.push(pattern);
			}
		});

		// Same defaults as used for Chokidar
		if (!hasPositivePattern) {
			mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
		}

		filePath = matchable(filePath);

		// Ignore paths outside the current working directory.
		// They can't be matched to a pattern.
		if (/^\.\.\//.test(filePath)) {
			return false;
		}

		const isSource = multimatch(filePath, mixedPatterns).length === 1;
		if (!isSource) {
			return false;
		}

		const isIgnored = multimatch(filePath, defaultIgnorePatterns).length === 1;
		if (!isIgnored) {
			return true;
		}

		const isErroneouslyIgnored = multimatch(filePath, overrideDefaultIgnorePatterns).length === 1;
		if (isErroneouslyIgnored) {
			return true;
		}

		return false;
	}

	isTest(filePath) {
		const excludePatterns = this.excludePatterns;
		const initialPatterns = this.files.concat(excludePatterns);

		// Like in `api.js`, tests must be `.js` files and not start with `_`
		if (path.extname(filePath) !== '.js' || path.basename(filePath)[0] === '_') {
			return false;
		}

		// Check if the entire path matches a pattern
		if (multimatch(matchable(filePath), initialPatterns).length === 1) {
			return true;
		}

		// Check if the path contains any directory components
		const dirname = path.dirname(filePath);
		if (dirname === '.') {
			return false;
		}

		// Compute all possible subpaths. Note that the dirname is assumed to be
		// relative to the working directory, without a leading `./`.
		const subpaths = dirname.split(/[\\/]/).reduce((subpaths, component) => {
			const parent = subpaths[subpaths.length - 1];

			if (parent) {
				// Always use `/`` to makes multimatch consistent across platforms
				subpaths.push(`${parent}/${component}`);
			} else {
				subpaths.push(component);
			}

			return subpaths;
		}, []);

		// Check if any of the possible subpaths match a pattern. If so, generate a
		// new pattern with **/*.js.
		const recursivePatterns = subpaths
			.filter(subpath => multimatch(subpath, initialPatterns).length === 1)
			// Always use `/` to makes multimatch consistent across platforms
			.map(subpath => `${subpath}/**/*.js`);

		// See if the entire path matches any of the subpaths patterns, taking the
		// excludePatterns into account. This mimicks the behavior in api.js
		return multimatch(matchable(filePath), recursivePatterns.concat(excludePatterns)).length === 1;
	}

	getChokidarPatterns() {
		let paths = [];
		let ignored = [];

		this.sources.forEach(pattern => {
			if (pattern[0] === '!') {
				ignored.push(pattern.slice(1));
			} else {
				paths.push(pattern);
			}
		});

		// Allow source patterns to override the default ignore patterns. Chokidar
		// ignores paths that match the list of ignored patterns. It uses anymatch
		// under the hood, which supports negation patterns. For any source pattern
		// that starts with an ignored directory, ensure the corresponding negation
		// pattern is added to the ignored paths.
		const overrideDefaultIgnorePatterns = paths
			.filter(pattern => defaultIgnore.indexOf(pattern.split('/')[0]) >= 0)
			.map(pattern => `!${pattern}`);

		ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);

		if (paths.length === 0) {
			paths = ['package.json', '**/*.js', '**/*.snap'];
		}

		paths = paths.concat(this.files);

		return {
			paths,
			ignored
		};
	}
}

module.exports = AvaFiles;
module.exports.defaultIncludePatterns = defaultIncludePatterns;
module.exports.defaultExcludePatterns = defaultExcludePatterns;