/**
 * This code is derived from the following projects:
 *
 * 1. dynamic-importmap (https://github.com/keller-mark/dynamic-importmap)
 * 2. es-module-shims (https://github.com/guybedford/es-module-shims)
 *
 * The original implementation was created by Guy Bedford in es-module-shims,
 * then adapted by Mark Keller in dynamic-importmap, and further modified
 * for use in this project.
 *
 * Both projects are licensed under the MIT license.
 *
 * MIT License: https://opensource.org/licenses/MIT
 */

const backslashRegEx = /\\/g;

function isURL( url: string ) {
	if ( url.indexOf( ':' ) === -1 ) {
		return false;
	}
	try {
		new URL( url );
		return true;
	} catch {
		return false;
	}
}

function resolveIfNotPlainOrUrl( relUrl, parentUrl ) {
	const hIdx = parentUrl.indexOf( '#' ),
		qIdx = parentUrl.indexOf( '?' );
	if ( hIdx + qIdx > -2 ) {
		parentUrl = parentUrl.slice(
			0,
			// eslint-disable-next-line no-nested-ternary
			hIdx === -1 ? qIdx : qIdx === -1 || qIdx > hIdx ? hIdx : qIdx
		);
	}
	if ( relUrl.indexOf( '\\' ) !== -1 ) {
		relUrl = relUrl.replace( backslashRegEx, '/' );
	}
	// protocol-relative
	if ( relUrl[ 0 ] === '/' && relUrl[ 1 ] === '/' ) {
		return parentUrl.slice( 0, parentUrl.indexOf( ':' ) + 1 ) + relUrl;
	}
	// relative-url
	else if (
		( relUrl[ 0 ] === '.' &&
			( relUrl[ 1 ] === '/' ||
				( relUrl[ 1 ] === '.' &&
					( relUrl[ 2 ] === '/' ||
						( relUrl.length === 2 && ( relUrl += '/' ) ) ) ) ||
				( relUrl.length === 1 && ( relUrl += '/' ) ) ) ) ||
		relUrl[ 0 ] === '/'
	) {
		const parentProtocol = parentUrl.slice(
			0,
			parentUrl.indexOf( ':' ) + 1
		);
		// Disabled, but these cases will give inconsistent results for deep backtracking
		//if (parentUrl[parentProtocol.length] !== '/')
		//  throw new Error('Cannot resolve');
		// read pathname from parent URL
		// pathname taken to be part after leading "/"
		let pathname;
		if ( parentUrl[ parentProtocol.length + 1 ] === '/' ) {
			// resolving to a :// so we need to read out the auth and host
			if ( parentProtocol !== 'file:' ) {
				pathname = parentUrl.slice( parentProtocol.length + 2 );
				pathname = pathname.slice( pathname.indexOf( '/' ) + 1 );
			} else {
				pathname = parentUrl.slice( 8 );
			}
		} else {
			// resolving to :/ so pathname is the /... part
			pathname = parentUrl.slice(
				parentProtocol.length +
					( parentUrl[ parentProtocol.length ] === '/' )
			);
		}

		if ( relUrl[ 0 ] === '/' ) {
			return (
				parentUrl.slice( 0, parentUrl.length - pathname.length - 1 ) +
				relUrl
			);
		}

		// join together and split for removal of .. and . segments
		// looping the string instead of anything fancy for perf reasons
		// '../../../../../z' resolved to 'x/y' is just 'z'
		const segmented =
			pathname.slice( 0, pathname.lastIndexOf( '/' ) + 1 ) + relUrl;

		const output = [];
		let segmentIndex = -1;
		for ( let i = 0; i < segmented.length; i++ ) {
			// busy reading a segment - only terminate on '/'
			if ( segmentIndex !== -1 ) {
				if ( segmented[ i ] === '/' ) {
					output.push( segmented.slice( segmentIndex, i + 1 ) );
					segmentIndex = -1;
				}
				continue;
			}
			// new segment - check if it is relative
			else if ( segmented[ i ] === '.' ) {
				// ../ segment
				if (
					segmented[ i + 1 ] === '.' &&
					( segmented[ i + 2 ] === '/' || i + 2 === segmented.length )
				) {
					output.pop();
					i += 2;
					continue;
				}
				// ./ segment
				else if (
					segmented[ i + 1 ] === '/' ||
					i + 1 === segmented.length
				) {
					i += 1;
					continue;
				}
			}
			// it is the start of a new segment
			while ( segmented[ i ] === '/' ) {
				i++;
			}
			segmentIndex = i;
		}
		// finish reading out the last segment
		if ( segmentIndex !== -1 ) {
			output.push( segmented.slice( segmentIndex ) );
		}
		return (
			parentUrl.slice( 0, parentUrl.length - pathname.length ) +
			output.join( '' )
		);
	}
}

function resolveUrl( relUrl, parentUrl ) {
	return (
		resolveIfNotPlainOrUrl( relUrl, parentUrl ) ||
		( isURL( relUrl )
			? relUrl
			: resolveIfNotPlainOrUrl( './' + relUrl, parentUrl ) )
	);
}

function getMatch( path, matchObj ) {
	if ( matchObj[ path ] ) {
		return path;
	}
	let sepIndex = path.length;
	do {
		const segment = path.slice( 0, sepIndex + 1 );
		if ( segment in matchObj ) {
			return segment;
		}
	} while ( ( sepIndex = path.lastIndexOf( '/', sepIndex - 1 ) ) !== -1 );
}

function applyPackages( id, packages ) {
	const pkgName = getMatch( id, packages );
	if ( pkgName ) {
		const pkg = packages[ pkgName ];
		if ( pkg === null ) {
			return;
		}
		return pkg + id.slice( pkgName.length );
	}
}

function resolveImportMap( importMap, resolvedOrPlain, parentUrl ) {
	let scopeUrl = parentUrl && getMatch( parentUrl, importMap.scopes );
	while ( scopeUrl ) {
		const packageResolution = applyPackages(
			resolvedOrPlain,
			importMap.scopes[ scopeUrl ]
		);
		if ( packageResolution ) {
			return packageResolution;
		}
		scopeUrl = getMatch(
			scopeUrl.slice( 0, scopeUrl.lastIndexOf( '/' ) ),
			importMap.scopes
		);
	}
	return (
		applyPackages( resolvedOrPlain, importMap.imports ) ||
		( resolvedOrPlain.indexOf( ':' ) !== -1 && resolvedOrPlain )
	);
}

function resolveAndComposePackages(
	packages,
	outPackages,
	baseUrl,
	parentMap
) {
	for ( const p in packages ) {
		const resolvedLhs = resolveIfNotPlainOrUrl( p, baseUrl ) || p;
		const target = packages[ p ];
		if ( typeof target !== 'string' ) {
			continue;
		}
		const mapped = resolveImportMap(
			parentMap,
			resolveIfNotPlainOrUrl( target, baseUrl ) || target,
			baseUrl
		);
		if ( mapped ) {
			outPackages[ resolvedLhs ] = mapped;
			continue;
		}
		// console.warn(
		// 	`Mapping "${ p }" -> "${ packages[ p ] }" does not resolve`
		// );
	}
}

function resolveAndComposeImportMap( json, baseUrl, parentMap ) {
	const outMap = {
		imports: Object.assign( {}, parentMap.imports ),
		scopes: Object.assign( {}, parentMap.scopes ),
	};

	if ( json.imports ) {
		resolveAndComposePackages(
			json.imports,
			outMap.imports,
			baseUrl,
			parentMap
		);
	}

	if ( json.scopes ) {
		for ( const s in json.scopes ) {
			const resolvedScope = resolveUrl( s, baseUrl );
			resolveAndComposePackages(
				json.scopes[ s ],
				outMap.scopes[ resolvedScope ] ||
					( outMap.scopes[ resolvedScope ] = {} ),
				baseUrl,
				parentMap
			);
		}
	}

	return outMap;
}

let importMap = { imports: {}, scopes: {} };

// TODO: check if this baseURI should change per document, and so
// it need to be passed as a parameter to methods like `resolve`.
const baseUrl = document.baseURI;
const pageBaseUrl = baseUrl;

/**
 * Extends the internal dynamic import map with the passed one.
 *
 * @param importMapIn         Import map.
 * @param importMapIn.imports Imports declaration.
 * @param importMapIn.scopes  Scopes declaration.
 */
export function addImportMap( importMapIn: {
	imports?: Record< string, string >;
	scopes?: Record< string, any >;
} ) {
	importMap = resolveAndComposeImportMap(
		importMapIn,
		pageBaseUrl,
		importMap
	);
}

/**
 * Resolves the URL of the passed module ID against the current internal
 * dynamic import map.
 *
 * @param id        Module ID.
 * @param parentUrl Parent URL, in case the module ID is relative.
 * @return Resolved module URL.
 */
export function resolve( id: string, parentUrl: string ): string {
	const urlResolved = resolveIfNotPlainOrUrl( id, parentUrl );
	return resolveImportMap( importMap, urlResolved || id, parentUrl ) || id;
}
