import { statSync } from 'fs';
import { dirname, extname, resolve, sep } from 'path';
import { sync as nodeResolveSync } from 'resolve';
import { createFilter } from 'rollup-pluginutils';
import { EXTERNAL, PREFIX, HELPERS_ID, HELPERS } from './helpers.js';
import defaultResolver from './defaultResolver.js';
import { checkFirstpass, checkEsModule, transformCommonjs } from './transform.js';
import { getName } from './utils.js';

function getCandidatesForExtension ( resolved, extension ) {
	return [
		resolved + extension,
		resolved + `${sep}index${extension}`
	];
}

function getCandidates ( resolved, extensions ) {
	return extensions.reduce(
		( paths, extension ) => paths.concat( getCandidatesForExtension ( resolved, extension ) ),
		[resolved]
	);
}

// Return the first non-falsy result from an array of
// maybe-sync, maybe-promise-returning functions
function first ( candidates ) {
	return function ( ...args ) {
		return candidates.reduce( ( promise, candidate ) => {
			return promise.then( result => result != null ?
				result :
				Promise.resolve( candidate( ...args ) ) );
		}, Promise.resolve() );
	};
}

function startsWith ( str, prefix ) {
	return str.slice( 0, prefix.length ) === prefix;
}


export default function commonjs ( options = {} ) {
	const extensions = options.extensions || ['.js'];
	const filter = createFilter( options.include, options.exclude );
	const ignoreGlobal = options.ignoreGlobal;

	const customNamedExports = {};
	if ( options.namedExports ) {
		Object.keys( options.namedExports ).forEach( id => {
			let resolvedId;

			try {
				resolvedId = nodeResolveSync( id, { basedir: process.cwd() });
			} catch ( err ) {
				resolvedId = resolve( id );
			}

			customNamedExports[ resolvedId ] = options.namedExports[ id ];
		});
	}

	const esModulesWithoutDefaultExport = [];

	const allowDynamicRequire = !!options.ignore; // TODO maybe this should be configurable?

	const ignoreRequire = typeof options.ignore === 'function' ?
		options.ignore :
		Array.isArray( options.ignore ) ? id => ~options.ignore.indexOf( id ) :
			() => false;

	let entryModuleIdsPromise = null;

	function resolveId ( importee, importer ) {
		if ( importee === HELPERS_ID ) return importee;

		if ( importer && startsWith( importer, PREFIX ) ) importer = importer.slice( PREFIX.length );

		const isProxyModule = startsWith( importee, PREFIX );
		if ( isProxyModule ) importee = importee.slice( PREFIX.length );

		return resolveUsingOtherResolvers( importee, importer ).then( resolved => {
			if ( resolved ) return isProxyModule ? PREFIX + resolved : resolved;

			resolved = defaultResolver( importee, importer );

			if ( isProxyModule ) {
				if ( resolved ) return PREFIX + resolved;
				return EXTERNAL + importee; // external
			}

			return resolved;
		});
	}

	const sourceMap = options.sourceMap !== false;

	const commonjsModules = new Map();
	let resolveUsingOtherResolvers;

	return {
		name: 'commonjs',

		options ( options ) {
			const resolvers = ( options.plugins || [] )
				.map( plugin => {
					if ( plugin.resolveId === resolveId ) {
						// substitute CommonJS resolution logic
						return ( importee, importer ) => {
							if ( importee[0] !== '.' || !importer ) return; // not our problem

							const resolved = resolve( dirname( importer ), importee );
							const candidates = getCandidates( resolved, extensions );

							for ( let i = 0; i < candidates.length; i += 1 ) {
								try {
									const stats = statSync( candidates[i] );
									if ( stats.isFile() ) return candidates[i];
								} catch ( err ) { /* noop */ }
							}
						};
					}

					return plugin.resolveId;
				})
				.filter( Boolean );

			const isExternal = id => options.external ?
				Array.isArray( options.external ) ? ~options.external.indexOf( id ) :
					options.external(id) :
				false;

			resolvers.unshift( id => isExternal( id ) ? false : null );

			resolveUsingOtherResolvers = first( resolvers );

			const entryModules = [].concat( options.input || options.entry );
			entryModuleIdsPromise = Promise.all(
				entryModules.map( entry => resolveId( entry ))
			);
		},

		resolveId,

		load ( id ) {
			if ( id === HELPERS_ID ) return HELPERS;

			// generate proxy modules
			if ( startsWith( id, EXTERNAL ) ) {
				const actualId = id.slice( EXTERNAL.length );
				const name = getName( actualId );

				return `import ${name} from ${JSON.stringify( actualId )}; export default ${name};`;
			}

			if ( startsWith( id, PREFIX ) ) {
				const actualId = id.slice( PREFIX.length );
				const name = getName( actualId );

				if (commonjsModules.has( actualId ))
					return `import { __moduleExports } from ${JSON.stringify( actualId )}; export default __moduleExports;`;
				else if (esModulesWithoutDefaultExport.indexOf(actualId) !== -1)
					return `import * as ${name} from ${JSON.stringify( actualId )}; export default ${name};`;
				else
					return `import * as ${name} from ${JSON.stringify( actualId )}; export default ( ${name} && ${name}['default'] ) || ${name};`;
			}
		},

		transform ( code, id ) {
			if ( !filter( id ) ) return null;
			if ( extensions.indexOf( extname( id ) ) === -1 ) return null;

			return entryModuleIdsPromise.then( (entryModuleIds) => {
				const {isEsModule, hasDefaultExport, ast} = checkEsModule( code, id );
				if ( isEsModule ) {
					if ( !hasDefaultExport )
						esModulesWithoutDefaultExport.push( id );
					return;
				}

				// it is not an ES module but not a commonjs module, too.
				if ( !checkFirstpass( code, ignoreGlobal ) ) {
					esModulesWithoutDefaultExport.push( id );
					return;
				}

				const transformed = transformCommonjs( code, id, entryModuleIds.indexOf(id) !== -1, ignoreGlobal, ignoreRequire, customNamedExports[ id ], sourceMap, allowDynamicRequire, ast );
				if ( !transformed ) return;

				commonjsModules.set( id, true );
				return transformed;
			});
		}
	};
}
