import * as ts from 'typescript';

import { compileDts } from './compile-dts';
import { TypesUsageEvaluator } from './types-usage-evaluator';
import {
	ExportType,
	getActualSymbol,
	getClosestModuleLikeNode,
	getClosestSourceFileLikeNode,
	getDeclarationsForExportedValues,
	getDeclarationsForSymbol,
	getImportExportReferencedSymbol,
	getExportsForSourceFile,
	getExportsForStatement,
	getImportModuleName,
	getNodeName,
	getNodeOwnSymbol,
	getNodeSymbol,
	getRootSourceFile,
	getSymbolExportStarDeclarations,
	hasNodeModifier,
	isAmbientModule,
	isDeclareGlobalStatement,
	isDeclareModule,
	isNodeNamedDeclaration,
	SourceFileExport,
	splitTransientSymbol,
} from './helpers/typescript';

import {
	getFileModuleInfo,
	getModuleLikeModuleInfo,
	getReferencedModuleInfo,
	ModuleCriteria,
	ModuleInfo,
	ModuleType,
} from './module-info';

import { generateOutput, ModuleImportsSet, OutputInputData, StatementSettings } from './generate-output';

import {
	normalLog,
	verboseLog,
	warnLog,
} from './logger';
import { CollisionsResolver } from './collisions-resolver';

export interface CompilationOptions {
	/**
	 * EXPERIMENTAL!
	 * Allows disable resolving of symlinks to the original path.
	 * By default following is enabled.
	 * @see https://github.com/timocov/dts-bundle-generator/issues/39
	 */
	followSymlinks?: boolean;

	/**
	 * Path to the tsconfig file that will be used for the compilation.
	 */
	preferredConfigPath?: string;
}

export interface OutputOptions {
	/**
	 * Sort output nodes in ascendant order.
	 */
	sortNodes?: boolean;

	/**
	 * Name of the UMD module.
	 * If specified then `export as namespace ModuleName;` will be emitted.
	 */
	umdModuleName?: string;

	/**
	 * Enables inlining of `declare global` statements contained in files which should be inlined (all local files and packages from inlined libraries).
	 */
	inlineDeclareGlobals?: boolean;

	/**
	 * Enables inlining of `declare module` statements of the global modules
	 * (e.g. `declare module 'external-module' {}`, but NOT `declare module './internal-module' {}`)
	 * contained in files which should be inlined (all local files and packages from inlined libraries)
	 */
	inlineDeclareExternals?: boolean;

	/**
	 * Allows remove "Generated by dts-bundle-generator" comment from the output
	 */
	noBanner?: boolean;

	/**
	 * Enables stripping the `const` keyword from every direct-exported (or re-exported) from entry file `const enum`.
	 * This allows you "avoid" the issue described in https://github.com/microsoft/TypeScript/issues/37774.
	 */
	respectPreserveConstEnum?: boolean;

	/**
	 * By default all interfaces, types and const enums are marked as exported even if they aren't exported directly.
	 * This option allows you to disable this behavior so a node will be exported if it is exported from root source file only.
	 */
	exportReferencedTypes?: boolean;
}

export interface LibrariesOptions {
	/**
	 * Array of package names from node_modules to inline typings from.
	 * Used types will be inlined into the output file.
	 */
	inlinedLibraries?: string[];

	/**
	 * Array of package names from node_modules to import typings from.
	 * Used types will be imported using `import { First, Second } from 'library-name';`.
	 * By default all libraries will be imported (except inlined libraries and libraries from @types).
	 */
	importedLibraries?: string[];

	/**
	 * Array of package names from @types to import typings from via the triple-slash reference directive.
	 * By default all packages are allowed and will be used according to their usages.
	 */
	allowedTypesLibraries?: string[];
}

export interface EntryPointConfig {
	/**
	 * Path to input file.
	 */
	filePath: string;

	libraries?: LibrariesOptions;

	/**
	 * Fail if generated dts contains class declaration.
	 */
	failOnClass?: boolean;

	output?: OutputOptions;
}

export function generateDtsBundle(entries: readonly EntryPointConfig[], options: CompilationOptions = {}): string[] {
	normalLog('Compiling input files...');

	const { program, rootFilesRemapping } = compileDts(entries.map((entry: EntryPointConfig) => entry.filePath), options.preferredConfigPath, options.followSymlinks);
	const typeChecker = program.getTypeChecker();

	const typeRoots = ts.getEffectiveTypeRoots(program.getCompilerOptions(), {});

	const sourceFiles = program.getSourceFiles().filter((file: ts.SourceFile) => {
		return !program.isSourceFileDefaultLibrary(file);
	});

	const typesUsageEvaluator = new TypesUsageEvaluator(sourceFiles, typeChecker);

	// eslint-disable-next-line complexity
	return entries.map((entryConfig: EntryPointConfig) => {
		normalLog(`Processing ${entryConfig.filePath}`);

		const newRootFilePath = rootFilesRemapping.get(entryConfig.filePath);
		if (newRootFilePath === undefined) {
			throw new Error(`Cannot remap root source file ${entryConfig.filePath}`);
		}

		const rootSourceFile = getRootSourceFile(program, newRootFilePath);
		const rootSourceFileSymbol = typeChecker.getSymbolAtLocation(rootSourceFile);
		if (rootSourceFileSymbol === undefined) {
			throw new Error(`Symbol for root source file ${newRootFilePath} not found`);
		}

		const librariesOptions: LibrariesOptions = entryConfig.libraries || {};

		const criteria: ModuleCriteria = {
			allowedTypesLibraries: librariesOptions.allowedTypesLibraries,
			importedLibraries: librariesOptions.importedLibraries,
			inlinedLibraries: librariesOptions.inlinedLibraries || [],
			typeRoots,
		};

		const rootFileExports = getExportsForSourceFile(typeChecker, rootSourceFileSymbol);
		const rootFileExportSymbols = rootFileExports.map((exp: SourceFileExport) => exp.symbol);

		interface CollectingResult extends Omit<OutputInputData, 'statements'> {
			statements: ts.Statement[];
		}

		const collectionResult: CollectingResult = {
			typesReferences: new Set(),
			imports: new Map(),
			statements: [],
			renamedExports: new Map(),
			wrappedNamespaces: new Map(),
		};

		const outputOptions: OutputOptions = entryConfig.output || {};
		const inlineDeclareGlobals = Boolean(outputOptions.inlineDeclareGlobals);
		const inlineDeclareExternals = Boolean(outputOptions.inlineDeclareExternals);

		const collisionsResolver = new CollisionsResolver(typeChecker);

		function updateResultForAnyModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void {
			// contains a set of modules that were visited already
			// can be used to prevent infinite recursion in updating results in re-exports
			const visitedModules = new Set<string>();

			function updateResultForExternalExport(exportAssignment: ts.ExportAssignment | ts.ExportDeclaration): void {
				// if we have `export =` or `export * from` somewhere so we can decide that every declaration of exported symbol in this way
				// is "part of the exported module" and we need to update result according every member of each declaration
				// but treat they as current module (we do not need to update module info)
				for (const declaration of getDeclarationsForExportedValues(exportAssignment, typeChecker)) {
					if (ts.isVariableDeclaration(declaration)) {
						// variables will be processed separately anyway so no need to process them again here
						continue;
					}

					let exportedDeclarations: readonly ts.Statement[] = [];

					if (ts.isExportDeclaration(exportAssignment) && ts.isSourceFile(declaration)) {
						const referencedModule = getReferencedModuleInfo(exportAssignment, criteria, typeChecker);
						if (referencedModule !== null) {
							if (visitedModules.has(referencedModule.fileName)) {
								continue;
							}

							visitedModules.add(referencedModule.fileName);
						}

						exportedDeclarations = declaration.statements;
					} else if (ts.isModuleDeclaration(declaration)) {
						if (declaration.body !== undefined && ts.isModuleBlock(declaration.body)) {
							const referencedModule = getReferencedModuleInfo(declaration, criteria, typeChecker);
							if (referencedModule !== null) {
								if (visitedModules.has(referencedModule.fileName)) {
									continue;
								}

								visitedModules.add(referencedModule.fileName);
							}

							exportedDeclarations = declaration.body.statements;
						}
					} else {
						exportedDeclarations = [declaration as unknown as ts.Statement];
					}

					updateResultImpl(exportedDeclarations);
				}
			}

			// eslint-disable-next-line complexity
			function updateResultImpl(statementsToProcess: readonly ts.Statement[]): void {
				for (const statement of statementsToProcess) {
					// we should skip import statements
					if (statement.kind === ts.SyntaxKind.ImportDeclaration || statement.kind === ts.SyntaxKind.ImportEqualsDeclaration) {
						continue;
					}

					if (isDeclareModule(statement)) {
						updateResultForModuleDeclaration(statement, currentModule);

						// if a statement is `declare module "module" {}` then don't process it below
						// as it is handled already in `updateResultForModuleDeclaration`
						// but if it is `declare module Module {}` then it can be used in types and imports
						// so in this case it needs to be checked for "usages" below
						if (ts.isStringLiteral(statement.name)) {
							continue;
						}
					}

					if (currentModule.type === ModuleType.ShouldBeUsedForModulesOnly) {
						continue;
					}

					if (isDeclareGlobalStatement(statement) && inlineDeclareGlobals && currentModule.type === ModuleType.ShouldBeInlined) {
						collectionResult.statements.push(statement);
						continue;
					}

					if (ts.isExportDeclaration(statement)) {
						if (currentModule.type === ModuleType.ShouldBeInlined) {
							continue;
						}

						// `export * from`
						if (statement.exportClause === undefined) {
							updateResultForExternalExport(statement);
							continue;
						}

						// `export { val }`
						if (ts.isNamedExports(statement.exportClause) && currentModule.type === ModuleType.ShouldBeImported) {
							updateImportsForStatement(statement);
							continue;
						}
					}

					if (ts.isExportAssignment(statement) && statement.isExportEquals && currentModule.type !== ModuleType.ShouldBeInlined) {
						updateResultForExternalExport(statement);
						continue;
					}

					if (!isNodeUsed(statement)) {
						continue;
					}

					switch (currentModule.type) {
						case ModuleType.ShouldBeReferencedAsTypes:
							// while a node might be "used" somewhere via transitive nodes
							// we need to add types reference only if a node is treated as "should be imported"
							// because otherwise we might have lots of false-positive references
							forEachNodeThatShouldBeImported(statement, () => addTypesReference(currentModule.typesLibraryName));
							break;

						case ModuleType.ShouldBeImported:
							updateImportsForStatement(statement);
							break;

						case ModuleType.ShouldBeInlined:
							if (ts.isVariableStatement(statement)) {
								for (const variableDeclaration of statement.declarationList.declarations) {
									if (ts.isIdentifier(variableDeclaration.name)) {
										collisionsResolver.addTopLevelIdentifier(variableDeclaration.name);
										continue;
									}

									for (const element of variableDeclaration.name.elements) {
										if (!ts.isOmittedExpression(element) && ts.isIdentifier(element.name)) {
											collisionsResolver.addTopLevelIdentifier(element.name);
										}
									}
								}
							} else if (isNodeNamedDeclaration(statement)) {
								const statementName = getNodeName(statement);
								if (statementName !== undefined) {
									collisionsResolver.addTopLevelIdentifier(statementName as ts.Identifier | ts.DefaultKeyword);
								}
							}

							collectionResult.statements.push(statement);
							break;
					}
				}
			}

			updateResultImpl(statements);
		}

		function isReferencedModuleImportable(statement: ts.ExportDeclaration | ts.ImportDeclaration): boolean {
			return getReferencedModuleInfo(statement, criteria, typeChecker)?.type === ModuleType.ShouldBeImported;
		}

		function handleExportDeclarationFromRootModule(exportDeclaration: ts.ExportDeclaration): void {
			function handleExportStarStatement(exportStarStatement: ts.ExportDeclaration, visitedSymbols: Set<ts.Symbol> = new Set()): void {
				if (exportStarStatement.moduleSpecifier === undefined || exportStarStatement.exportClause !== undefined) {
					throw new Error(`Invalid export-star declaration statement provided, ${exportStarStatement.getText()}`);
				}

				const importModuleSpecifier = getImportModuleName(exportStarStatement);
				if (importModuleSpecifier === null) {
					return;
				}

				const referencedModuleInfo = getReferencedModuleInfo(exportStarStatement, criteria, typeChecker);
				if (referencedModuleInfo === null) {
					return;
				}

				switch (referencedModuleInfo.type) {
					case ModuleType.ShouldBeInlined: {
						// `export * from './inlined-module'`
						const referencedModuleSymbol = getNodeOwnSymbol(exportStarStatement.moduleSpecifier, typeChecker);
						const referencedSourceFileExportStarSymbol = referencedModuleSymbol.exports?.get(ts.InternalSymbolName.ExportStar);
						if (referencedSourceFileExportStarSymbol !== undefined) {
							if (visitedSymbols.has(referencedSourceFileExportStarSymbol)) {
								return;
							}

							visitedSymbols.add(referencedSourceFileExportStarSymbol);

							// we need to go recursive for all `export * from` statements and add all that are from imported modules
							for (const exportDecl of getSymbolExportStarDeclarations(referencedSourceFileExportStarSymbol)) {
								handleExportStarStatement(exportDecl, visitedSymbols);
							}
						}

						break;
					}

					case ModuleType.ShouldBeImported: {
						// `export * from 'importable-package'`
						collectionResult.statements.push(exportStarStatement);
						break;
					}
				}
			}

			interface ExportingExportStarExport {
				exportStarDeclaration: ts.ExportDeclaration;
				exportedNodeSymbol: ts.Symbol;
			}

			/**
			 * This function returns an export-star object that exports given {@link nodeSymbol} symbol.
			 * If an exporting export declaration object is not from an importable module then `null` is returned.
			 * Also if the symbol is exported explicitly (i.e. via `export { Name }` or specifying `export` keyword next to the node) then `null` is returned as well.
			 */
			function findExportingExportStarExportFromImportableModule(referencedModuleSymbol: ts.Symbol, nodeSymbol: ts.Symbol): ExportingExportStarExport | null {
				function findResultRecursively(referencedModuleSym: ts.Symbol, exportedNodeSym: ts.Symbol, visitedSymbols: Set<ts.Symbol>): ExportingExportStarExport | null {
					// prevent infinite recursion
					if (visitedSymbols.has(referencedModuleSym)) {
						return null;
					}

					visitedSymbols.add(referencedModuleSym);

					// `export * from` exports always have less priority over explicit exports so it should go last
					const exportStarExport = referencedModuleSym.exports?.get(ts.InternalSymbolName.ExportStar);
					if (exportStarExport === undefined) {
						return null;
					}

					for (const exportStarDeclaration of getDeclarationsForSymbol(exportStarExport).filter(ts.isExportDeclaration)) {
						if (exportStarDeclaration.moduleSpecifier === undefined) {
							// this seems impossible, but to make the compiler/types happy
							continue;
						}

						const exportStarModuleSymbol = getNodeOwnSymbol(exportStarDeclaration.moduleSpecifier, typeChecker);
						if (exportStarModuleSymbol.exports === undefined) {
							continue;
						}

						if (isReferencedModuleImportable(exportStarDeclaration)) {
							// for "importable" modules we don't need to go deeper or even check "explicit" exports
							// as it doesn't matter how its done internally and we care about "public" interface only
							// so we can just check whether it exports a symbol or not (irregardless of how it is exported exactly internally)
							const referencedModuleExports = typeChecker.getExportsOfModule(exportStarModuleSymbol);
							const exportedNodeSymbol = referencedModuleExports.find((exp: ts.Symbol) => getActualSymbol(exp, typeChecker) === nodeSymbol);
							if (exportedNodeSymbol !== undefined) {
								return { exportStarDeclaration, exportedNodeSymbol };
							}

							continue;
						}

						const result = findResultRecursively(exportStarModuleSymbol, exportedNodeSym, visitedSymbols);
						if (result !== null) {
							return result;
						}
					}

					return null;
				}

				if (referencedModuleSymbol.exports === undefined) {
					throw new Error(`No exports found for "${referencedModuleSymbol.getName()}" symbol`);
				}

				const hasExplicitExportOfSymbol = Array.from(referencedModuleSymbol.exports.values()).some((exp: ts.Symbol) => {
					if (exp.escapedName === ts.InternalSymbolName.ExportStar) {
						return false;
					}

					return getActualSymbol(exp, typeChecker) === nodeSymbol;
				});

				if (hasExplicitExportOfSymbol) {
					// symbol is exported explicitly ¯\_(ツ)_/¯
					return null;
				}

				return findResultRecursively(referencedModuleSymbol, nodeSymbol, new Set());
			}

			// `export * from 'module'`
			if (exportDeclaration.exportClause === undefined) {
				handleExportStarStatement(exportDeclaration);
				return;
			}

			if (exportDeclaration.exportClause !== undefined && ts.isNamedExports(exportDeclaration.exportClause)) {
				// `export { val, val2 }`
				if (exportDeclaration.moduleSpecifier === undefined) {
					for (const exportElement of exportDeclaration.exportClause.elements) {
						const exportElementSymbol = getImportExportReferencedSymbol(exportElement, typeChecker);

						const namespaceImportFromImportableModule = getDeclarationsForSymbol(exportElementSymbol).find((importDecl: ts.Declaration): importDecl is ts.NamespaceImport => {
							// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
							return ts.isNamespaceImport(importDecl) && isReferencedModuleImportable(importDecl.parent.parent as ts.ImportDeclaration);
						});

						if (namespaceImportFromImportableModule !== undefined) {
							// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
							const importModuleSpecifier = getImportModuleName(namespaceImportFromImportableModule.parent.parent as ts.ImportDeclaration);
							if (importModuleSpecifier === null) {
								throw new Error(`Cannot get import module name from '${namespaceImportFromImportableModule.parent.parent.getText()}'`);
							}

							addNsImport(
								getImportItem(importModuleSpecifier),
								namespaceImportFromImportableModule.name
							);
						}
					}

					return;
				}

				// `export { val, val2 } from 'module'`
				if (exportDeclaration.moduleSpecifier !== undefined) {
					const referencedModuleSymbol = getNodeOwnSymbol(exportDeclaration.moduleSpecifier, typeChecker);

					// in this case we want to find all elements that we re-exported via `export * from` exports as they aren't handled elsewhere
					for (const exportElement of exportDeclaration.exportClause.elements) {
						const exportedNodeSymbol = getActualSymbol(getImportExportReferencedSymbol(exportElement, typeChecker), typeChecker);
						const exportingExportStarResult = findExportingExportStarExportFromImportableModule(
							referencedModuleSymbol,
							exportedNodeSymbol
						);

						if (exportingExportStarResult === null) {
							continue;
						}

						const importModuleSpecifier = getImportModuleName(exportingExportStarResult.exportStarDeclaration);
						if (importModuleSpecifier === null) {
							throw new Error(`Cannot get import module name from '${exportingExportStarResult.exportStarDeclaration.getText()}'`);
						}

						// technically we could use named imports and then add re-exports
						// but this solution affects names scope (re-exports don't affect it)
						// and also it is slightly complicated to find a name declaration (identifier) that needs to be imported
						// so it feels better to go this way, but happy to change in the future if there would be any issues
						addReExport(
							getImportItem(importModuleSpecifier),
							exportingExportStarResult.exportedNodeSymbol.getName(),
							exportElement.name.text
						);
					}

					return;
				}
			}
		}

		function updateResultForRootModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void {
			updateResultForAnyModule(statements, currentModule);

			// add skipped by `updateResult` exports
			for (const statement of statements) {
				if (ts.isExportDeclaration(statement)) {
					handleExportDeclarationFromRootModule(statement);
					continue;
				}

				if (ts.isExportAssignment(statement)) {
					// `"export ="` or `export default 123` or `export default "str"`
					if (statement.isExportEquals || !ts.isIdentifier(statement.expression)) {
						collectionResult.statements.push(statement);
					}

					continue;
				}
			}
		}

		function updateResultForModuleDeclaration(moduleDecl: ts.ModuleDeclaration, currentModule: ModuleInfo): void {
			if (moduleDecl.body === undefined || !ts.isModuleBlock(moduleDecl.body)) {
				return;
			}

			const referencedModuleInfo = getModuleLikeModuleInfo(moduleDecl, criteria, typeChecker);
			if (referencedModuleInfo === null) {
				return;
			}

			// if we have declaration of external module inside internal one
			if (!currentModule.isExternal && referencedModuleInfo.isExternal) {
				// if it's allowed - we need to just add it to result without any processing
				if (inlineDeclareExternals) {
					collectionResult.statements.push(moduleDecl);
				}

				return;
			}

			updateResultForAnyModule(moduleDecl.body.statements, referencedModuleInfo);
		}

		function addTypesReference(library: string): void {
			if (!collectionResult.typesReferences.has(library)) {
				normalLog(`Library "${library}" will be added via reference directive`);
				collectionResult.typesReferences.add(library);
			}
		}

		function forEachNodeThatShouldBeImported(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier, callback: (st: ts.DeclarationStatement) => void): void {
			const statementsToImport = ts.isVariableStatement(statement)
				? statement.declarationList.declarations
				: ts.isExportDeclaration(statement) && statement.exportClause !== undefined
					? ts.isNamespaceExport(statement.exportClause)
						? [statement.exportClause]
						: statement.exportClause.elements
					: [statement];

			for (const statementToImport of statementsToImport) {
				if (shouldNodeBeImported(statementToImport as ts.DeclarationStatement)) {
					callback(statementToImport as ts.DeclarationStatement);
				}
			}
		}

		function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier): void {
			forEachNodeThatShouldBeImported(statement, (statementToImport: ts.DeclarationStatement) => {
				addImport(statementToImport);

				// if we're going to add import of any statement in the bundle
				// we should check whether the library of that statement
				// could be referenced via triple-slash reference-types directive
				// because the project which will use bundled declaration file
				// can have `types: []` in the tsconfig and it'll fail
				// this is especially related to the types packages
				// which declares different modules in their declarations
				// e.g. @types/node has declaration for "packages" events, fs, path and so on
				const sourceFile = statementToImport.getSourceFile();
				const moduleInfo = getFileModuleInfo(sourceFile.fileName, criteria);
				if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) {
					addTypesReference(moduleInfo.typesLibraryName);
				}
			});
		}

		function getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration> {
			return new Set(
				getExportedSymbolsUsingStatement(declaration)
					.map((symbol: ts.Symbol) => getDeclarationsForSymbol(symbol))
					.reduce((acc: ts.Declaration[], val: ts.Declaration[]) => acc.concat(val), [])
					.map(getClosestModuleLikeNode)
			);
		}

		function getImportItem(importModuleSpecifier: string): ModuleImportsSet {
			let importItem = collectionResult.imports.get(importModuleSpecifier);
			if (importItem === undefined) {
				importItem = {
					defaultImports: new Set(),
					namedImports: new Map(),
					nsImport: null,
					requireImports: new Set(),
					reExports: new Map(),
				};

				collectionResult.imports.set(importModuleSpecifier, importItem);
			}

			return importItem;
		}

		function addRequireImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier): void {
			importItem.requireImports.add(collisionsResolver.addTopLevelIdentifier(preferredLocalName));
		}

		function addNamedImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier, importedIdentifier: ts.Identifier): void {
			const newLocalName = collisionsResolver.addTopLevelIdentifier(preferredLocalName);
			const importedName = importedIdentifier.text;
			importItem.namedImports.set(newLocalName, importedName);
		}

		function addReExport(importItem: ModuleImportsSet, moduleExportedName: string, reExportedName: string): void {
			// re-exports don't affect local names scope so we don't need to register them in collisions resolver
			importItem.reExports.set(reExportedName, moduleExportedName);
		}

		function addNsImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier): void {
			if (importItem.nsImport === null) {
				importItem.nsImport = collisionsResolver.addTopLevelIdentifier(preferredLocalName);
			}
		}

		function addDefaultImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier): void {
			importItem.defaultImports.add(collisionsResolver.addTopLevelIdentifier(preferredLocalName));
		}

		function addImport(statement: ts.DeclarationStatement | ts.SourceFile): void {
			forEachImportOfStatement(statement, (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => {
				// if a referenced module should be inlined we can just ignore it
				if (referencedModuleInfo.type !== ModuleType.ShouldBeImported) {
					return;
				}

				const importItem = getImportItem(importModuleSpecifier);

				if (ts.isImportEqualsDeclaration(imp)) {
					// import x = require("mod");
					addRequireImport(importItem, imp.name);
					return;
				}

				if (ts.isExportSpecifier(imp)) {
					// export { El1, El2 as ExportedName } from 'module';
					addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
					return;
				}

				if (ts.isNamespaceExport(imp)) {
					// export * as name from 'module';
					addNsImport(importItem, imp.name);
					return;
				}

				if (ts.isImportClause(imp) && imp.name !== undefined) {
					// import name from 'module';
					addDefaultImport(importItem, imp.name);
					return;
				}

				if (ts.isImportSpecifier(imp)) {
					// import { El1, El2 as ImportedName } from 'module';
					addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
					return;
				}

				if (ts.isNamespaceImport(imp)) {
					// import * as name from 'module';
					addNsImport(importItem, imp.name);
					return;
				}
			});
		}

		type ImportOfStatement = ts.ImportEqualsDeclaration | ts.ExportSpecifier | ts.NamespaceExport | ts.ImportClause | ts.ImportSpecifier | ts.NamespaceImport;

		function forEachImportOfStatement(statement: ts.DeclarationStatement | ts.SourceFile, callback: (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => void): void {
			if (!ts.isSourceFile(statement) && statement.name === undefined) {
				throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`);
			}

			getDeclarationUsagesSourceFiles(statement).forEach((sourceFile: ts.SourceFile | ts.ModuleDeclaration) => {
				if (getModuleLikeModuleInfo(sourceFile, criteria, typeChecker).type !== ModuleType.ShouldBeInlined) {
					// we should ignore source files that aren't inlined
					return;
				}

				const sourceFileStatements: readonly ts.Statement[] = ts.isSourceFile(sourceFile)
					? sourceFile.statements
					: sourceFile.body !== undefined && ts.isModuleBlock(sourceFile.body)
						? sourceFile.body.statements
						: []
					;

				// eslint-disable-next-line complexity
				sourceFileStatements.forEach((st: ts.Statement) => {
					if (!ts.isImportEqualsDeclaration(st) && !ts.isImportDeclaration(st) && !ts.isExportDeclaration(st)) {
						return;
					}

					const importModuleSpecifier = getImportModuleName(st);
					if (importModuleSpecifier === null) {
						return;
					}

					const referencedModuleInfo = getReferencedModuleInfo(st, criteria, typeChecker);
					// if a referenced module should be inlined we can just ignore it
					if (referencedModuleInfo === null) {
						return;
					}

					if (ts.isImportEqualsDeclaration(st)) {
						if (areDeclarationSame(statement, st)) {
							callback(st, referencedModuleInfo, importModuleSpecifier);
						}

						return;
					}

					if (ts.isExportDeclaration(st) && st.exportClause !== undefined) {
						if (ts.isNamedExports(st.exportClause)) {
							// export { El1, El2 as ExportedName } from 'module';
							st.exportClause.elements
								.filter(areDeclarationSame.bind(null, statement))
								.forEach((specifier: ts.ExportSpecifier) => {
									callback(specifier, referencedModuleInfo, importModuleSpecifier);
								});
						} else {
							// export * as name from 'module';
							if (isNodeUsed(st.exportClause)) {
								callback(st.exportClause, referencedModuleInfo, importModuleSpecifier);
							}
						}
					} else if (ts.isImportDeclaration(st) && st.importClause !== undefined) {
						if (st.importClause.name !== undefined && areDeclarationSame(statement, st.importClause)) {
							// import name from 'module';
							callback(st.importClause, referencedModuleInfo, importModuleSpecifier);
						}

						if (st.importClause.namedBindings !== undefined) {
							if (ts.isNamedImports(st.importClause.namedBindings)) {
								// import { El1, El2 as ImportedName } from 'module';
								st.importClause.namedBindings.elements
									.filter(areDeclarationSame.bind(null, statement))
									.forEach((specifier: ts.ImportSpecifier) => {
										callback(specifier, referencedModuleInfo, importModuleSpecifier);
									});
							} else {
								// import * as name from 'module';
								if (isNodeUsed(st.importClause)) {
									callback(st.importClause.namedBindings, referencedModuleInfo, importModuleSpecifier);
								}
							}
						}
					}
				});
			});
		}

		function getInlinedSymbolsUsingSymbol(symbol: ts.Symbol, predicate: (usedInSymbol: ts.Symbol) => boolean): ts.Symbol[] {
			return Array.from(typesUsageEvaluator.getSymbolsUsingSymbol(symbol) ?? []).filter((usedInSymbol: ts.Symbol) => {
				if (!predicate(usedInSymbol)) {
					return false;
				}

				return getDeclarationsForSymbol(usedInSymbol).some((decl: ts.Declaration) => {
					const closestModuleLike = getClosestSourceFileLikeNode(decl);
					const moduleInfo = getModuleLikeModuleInfo(closestModuleLike, criteria, typeChecker);
					return moduleInfo.type === ModuleType.ShouldBeInlined;
				});
			});
		}

		function isSymbolUsedByInlinedSymbols(symbol: ts.Symbol, predicate: (usedInSymbol: ts.Symbol) => boolean, visitedSymbols: Set<ts.Symbol> = new Set()): boolean {
			if (visitedSymbols.has(symbol)) {
				return false;
			}

			visitedSymbols.add(symbol);

			return Array.from(typesUsageEvaluator.getSymbolsUsingSymbol(symbol) ?? []).some((usedInSymbol: ts.Symbol) => {
				if (!predicate(usedInSymbol)) {
					return isSymbolUsedByInlinedSymbols(usedInSymbol, predicate, visitedSymbols);
				}

				const usedByThisSymbol = getDeclarationsForSymbol(usedInSymbol).some((decl: ts.Declaration) => {
					const closestModuleLike = getClosestSourceFileLikeNode(decl);
					const moduleInfo = getModuleLikeModuleInfo(closestModuleLike, criteria, typeChecker);
					return moduleInfo.type === ModuleType.ShouldBeInlined;
				});

				if (usedByThisSymbol) {
					return true;
				}

				return isSymbolUsedByInlinedSymbols(usedInSymbol, predicate, visitedSymbols);
			});
		}

		function isSymbolUsedByRootFileExports(symbol: ts.Symbol): boolean {
			return rootFileExportSymbols.some((rootSymbol: ts.Symbol) => typesUsageEvaluator.isSymbolUsedBySymbol(symbol, rootSymbol));
		}

		function isSymbolForGlobalDeclaration(symbol: ts.Symbol): boolean {
			return symbol.escapedName === ts.InternalSymbolName.Global;
		}

		function isSymbolForDeclareModuleDeclaration(symbol: ts.Symbol): boolean {
			return getDeclarationsForSymbol(symbol).some(isDeclareModule);
		}

		// eslint-disable-next-line complexity
		function isNodeUsed(node: ts.Node): boolean {
			if (isNodeNamedDeclaration(node) || ts.isSourceFile(node)) {
				const nodeSymbol = getNodeSymbol(node, typeChecker);
				if (nodeSymbol === null) {
					return false;
				}

				// note we don't need a function similar to `isSymbolUsedByGlobalSymbols` or `isSymbolUsedByGlobalSymbols`
				// because `TypesUsageEvaluator.isSymbolUsedBySymbol` already handles recursive checks
				const nodeUsedByDirectExports = isSymbolUsedByRootFileExports(nodeSymbol);
				if (nodeUsedByDirectExports) {
					return true;
				}

				if (inlineDeclareGlobals && isSymbolUsedByInlinedSymbols(nodeSymbol, isSymbolForGlobalDeclaration)) {
					return true;
				}

				if (inlineDeclareExternals && isSymbolUsedByInlinedSymbols(nodeSymbol, isSymbolForDeclareModuleDeclaration)) {
					return true;
				}

				return false;
			}

			if (ts.isVariableStatement(node)) {
				return node.declarationList.declarations.some((declaration: ts.VariableDeclaration) => {
					if (ts.isObjectBindingPattern(declaration.name) || ts.isArrayBindingPattern(declaration.name)) {
						return declaration.name.elements.some(isNodeUsed);
					}

					return isNodeUsed(declaration);
				});
			}

			if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamespaceExport(node.exportClause)) {
				return isNodeUsed(node.exportClause);
			}

			if (ts.isImportClause(node) && node.namedBindings !== undefined) {
				return isNodeUsed(node.namedBindings);
			}

			return false;
		}

		function shouldNodeBeImported(node: ts.NamedDeclaration): boolean {
			const nodeSymbol = getNodeSymbol(node, typeChecker);
			if (nodeSymbol === null) {
				return false;
			}

			return shouldSymbolBeImported(nodeSymbol);
		}

		function shouldSymbolBeImported(nodeSymbol: ts.Symbol): boolean {
			const isSymbolDeclaredInDefaultLibrary = getDeclarationsForSymbol(nodeSymbol).some(
				(declaration: ts.Declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())
			);
			if (isSymbolDeclaredInDefaultLibrary) {
				// we shouldn't import a node declared in the default library (such dom, es2015)
				// yeah, actually we should check that node is declared only in the default lib
				// but it seems we can check that at least one declaration is from default lib
				// to treat the node as un-importable
				// because we can't re-export declared somewhere else node with declaration merging

				// also, if some lib file will not be added to the project
				// for example like it is described in the react declaration file (e.g. React Native)
				// then here we still have a bug with "importing global declaration from a package"
				// (see https://github.com/timocov/dts-bundle-generator/issues/71)
				// but I don't think it is a big problem for now
				// and it's possible that it will be fixed in https://github.com/timocov/dts-bundle-generator/issues/59
				return false;
			}

			const symbolsDeclarations = getDeclarationsForSymbol(nodeSymbol);

			// if all declarations of the symbol are in modules that should be inlined then this symbol must be inlined, not imported
			const shouldSymbolBeInlined = symbolsDeclarations.every(
				(decl: ts.Declaration) => getModuleLikeModuleInfo(
					getClosestSourceFileLikeNode(decl),
					criteria,
					typeChecker
				).type === ModuleType.ShouldBeInlined
			);
			if (shouldSymbolBeInlined) {
				return false;
			}

			return getExportedSymbolsUsingSymbol(nodeSymbol).length !== 0;
		}

		function getExportedSymbolsUsingStatement(node: ts.NamedDeclaration): readonly ts.Symbol[] {
			const nodeSymbol = getNodeSymbol(node, typeChecker);
			if (nodeSymbol === null) {
				return [];
			}

			return getExportedSymbolsUsingSymbol(nodeSymbol);
		}

		function getExportedSymbolsUsingSymbol(nodeSymbol: ts.Symbol): readonly ts.Symbol[] {
			const symbolsUsingNode = typesUsageEvaluator.getSymbolsUsingSymbol(nodeSymbol);
			if (symbolsUsingNode === null) {
				throw new Error(`Something went wrong - getSymbolsUsingSymbol returned null but expected to be a set of symbols (symbol=${nodeSymbol.name})`);
			}

			return [
				...(rootFileExportSymbols.includes(nodeSymbol) ? [nodeSymbol] : []),

				// symbols which are used in types directly
				...getInlinedSymbolsUsingSymbol(nodeSymbol, isSymbolUsedByRootFileExports),

				// symbols which are used in global types i.e. in `declare global`s
				...(inlineDeclareGlobals ? getInlinedSymbolsUsingSymbol(nodeSymbol, isSymbolForGlobalDeclaration) : []),

				// symbols which are used in "declare module" types
				...(inlineDeclareExternals ? getInlinedSymbolsUsingSymbol(nodeSymbol, isSymbolForDeclareModuleDeclaration) : []),
			];
		}

		function areDeclarationSame(left: ts.NamedDeclaration, right: ts.NamedDeclaration): boolean {
			const leftSymbols = splitTransientSymbol(getNodeSymbol(left, typeChecker) as ts.Symbol, typeChecker);
			const rightSymbols = splitTransientSymbol(getNodeSymbol(right, typeChecker) as ts.Symbol, typeChecker);

			for (const leftSymbol of leftSymbols) {
				if (rightSymbols.has(leftSymbol)) {
					return true;
				}
			}

			return false;
		}

		function createNamespaceForExports(exports: ts.SymbolTable, namespaceSymbol: ts.Symbol): string | null {
			function addSymbolToNamespaceExports(namespaceExports: Map<string, string>, symbol: ts.Symbol): void {
				// if a symbol isn't used by the namespace symbol it might mean that it shouldn't be included into the bundle because of tree-shaking
				// in this case we shouldn't even try to add such symbol to the namespace
				if (!typesUsageEvaluator.isSymbolUsedBySymbol(symbol, namespaceSymbol)) {
					return;
				}

				const symbolKnownNames = collisionsResolver.namesForSymbol(symbol);
				if (symbolKnownNames.size === 0) {
					throw new Error(`Cannot get local names for symbol '${symbol.getName()}' while generating namespaced export`);
				}

				namespaceExports.set(symbol.getName(), Array.from(symbolKnownNames)[0]);
			}

			function handleNamespacedImportOrExport(namespacedImportOrExport: ts.ExportDeclaration | ts.ImportDeclaration, namespaceExports: Map<string, string>, symbol: ts.Symbol): void {
				if (namespacedImportOrExport.moduleSpecifier === undefined) {
					return;
				}

				if (isReferencedModuleImportable(namespacedImportOrExport)) {
					// in case of an external export statement we should copy it as is
					// here we assume that a namespace import will be added in other places
					// so here we can just add re-export
					addSymbolToNamespaceExports(namespaceExports, symbol);
					return;
				}

				const referencedSourceFileSymbol = getNodeOwnSymbol(namespacedImportOrExport.moduleSpecifier, typeChecker);

				if (referencedSourceFileSymbol.exports === undefined) {
					return;
				}

				if (ts.isImportDeclaration(namespacedImportOrExport) && referencedSourceFileSymbol.exports.has(ts.InternalSymbolName.ExportEquals)) {
					// in case of handling `import * as Ns` statements with `export =` export in a module we need to ignore it
					// as that import will be renamed later
					return;
				}

				const localNamespaceName = createNamespaceForExports(referencedSourceFileSymbol.exports, symbol);
				if (localNamespaceName !== null) {
					namespaceExports.set(symbol.getName(), localNamespaceName);
				}
			}

			function processExportSymbol(namespaceExports: Map<string, string>, symbol: ts.Symbol): void {
				if (symbol.escapedName === ts.InternalSymbolName.ExportStar) {
					// this means that an export contains `export * from 'module'` statement
					for (const exportStarDeclaration of getSymbolExportStarDeclarations(symbol)) {
						if (exportStarDeclaration.moduleSpecifier === undefined) {
							throw new Error(`Export star declaration does not have a module specifier '${exportStarDeclaration.getText()}'`);
						}

						if (isReferencedModuleImportable(exportStarDeclaration)) {
							// in case of re-exporting from other modules directly we should import everything and re-export manually
							// but it is not supported yet so lets just fail for now
							throw new Error(`Having a re-export from an importable module as a part of namespaced export is not supported yet.`);
						}

						const referencedSourceFileSymbol = getNodeOwnSymbol(exportStarDeclaration.moduleSpecifier, typeChecker);
						referencedSourceFileSymbol.exports?.forEach(
							processExportSymbol.bind(null, namespaceExports)
						);
					}

					return;
				}

				symbol.declarations?.forEach((decl: ts.Declaration) => {
					if (ts.isNamespaceExport(decl) && decl.parent.moduleSpecifier !== undefined) {
						handleNamespacedImportOrExport(decl.parent, namespaceExports, symbol);
						return;
					}

					if (ts.isExportSpecifier(decl)) {
						const exportElementSymbol = getImportExportReferencedSymbol(decl, typeChecker);
						const namespaceImport = getDeclarationsForSymbol(exportElementSymbol).find(ts.isNamespaceImport);
						if (namespaceImport !== undefined) {
							// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
							handleNamespacedImportOrExport(namespaceImport.parent.parent as ts.ImportDeclaration, namespaceExports, symbol);
						}

						return;
					}
				});

				addSymbolToNamespaceExports(namespaceExports, symbol);
			}

			// eslint-disable-next-line complexity
			function getIdentifierOfNamespaceImportFromInlinedModule(nsSymbol: ts.Symbol): ts.Identifier | null {
				// handling namespaced re-exports/imports
				// e.g. `export * as NS from './local-module';` or `import * as NS from './local-module'; export { NS }`
				for (const decl of getDeclarationsForSymbol(nsSymbol)) {
					if (!ts.isNamespaceExport(decl) && !ts.isExportSpecifier(decl) && !ts.isNamespaceImport(decl)) {
						continue;
					}

					// if it is namespace export then it should be from a inlined module (e.g. `export * as NS from './local-module';`)
					if (ts.isNamespaceExport(decl) && !isReferencedModuleImportable(decl.parent)) {
						return decl.name;
					}

					// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
					if (ts.isNamespaceImport(decl) && !isReferencedModuleImportable(decl.parent.parent as ts.ImportDeclaration)) {
						return decl.name;
					}

					if (ts.isExportSpecifier(decl)) {
						// if it is export specifier then it should exporting a local symbol i.e. without a module specifier (e.g. `export { NS };` or `export { NS as NewNsName };`)
						if (decl.parent.parent.moduleSpecifier !== undefined) {
							// this means that namespace symbol is created somewhere else in the import/export chain
							if (isReferencedModuleImportable(decl.parent.parent)) {
								continue;
							}

							// in case of a chain of imports/exports we need to keep searching recursively
							if (getIdentifierOfNamespaceImportFromInlinedModule(getImportExportReferencedSymbol(decl, typeChecker))) {
								return decl.name;
							}
						}

						// but also that local symbol should be a namespace imported from inlined module
						// i.e. `import * as NS from './local-module'`
						const result = getDeclarationsForSymbol(getImportExportReferencedSymbol(decl, typeChecker)).some((importDecl: ts.Declaration) => {
							if (ts.isNamespaceImport(importDecl)) {
								// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
								return !isReferencedModuleImportable(importDecl.parent.parent as ts.ImportDeclaration);
							}

							if (ts.isImportSpecifier(importDecl)) {
								// this means that namespace symbol is created somewhere else in the import/export chain
								return getIdentifierOfNamespaceImportFromInlinedModule(getImportExportReferencedSymbol(importDecl, typeChecker));
							}

							return false;
						});

						if (result) {
							return decl.name;
						}
					}
				}

				return null;
			}

			const namespaceNameIdentifier = getIdentifierOfNamespaceImportFromInlinedModule(namespaceSymbol);
			if (namespaceNameIdentifier === null) {
				return null;
			}

			const namespaceExports = new Map<string, string>();
			exports.forEach(
				processExportSymbol.bind(null, namespaceExports)
			);

			if (namespaceExports.size !== 0) {
				const namespaceLocalName = collisionsResolver.addTopLevelIdentifier(namespaceNameIdentifier);
				collectionResult.wrappedNamespaces.set(namespaceLocalName, namespaceExports);
				return namespaceLocalName;
			}

			return null;
		}

		function syncExports(): void {
			for (const exp of rootFileExports) {
				if (exp.type === ExportType.CommonJS) {
					// commonjs will be handled separately where we handle root source files
					// as the only way of adding it is to add an explicit export statement in the root source file
					continue;
				}

				// if resolved symbol is a "synthetic" symbol already (i.e. not namespace module)
				// then we should create a namespace for its exports
				// otherwise most likely it will be inlined as is anyway so we don't need to do anything
				const namespaceLocalName = exp.symbol.flags & ts.SymbolFlags.ValueModule && exp.symbol.exports !== undefined
					? createNamespaceForExports(exp.symbol.exports, exp.originalSymbol)
					: null
					;

				if (namespaceLocalName !== null) {
					collectionResult.renamedExports.set(exp.exportedName, namespaceLocalName);
				}

				const symbolKnownNames = collisionsResolver.namesForSymbol(exp.symbol);
				if (symbolKnownNames.size === 0) {
					// that's fine if a symbol doesn't exists in collisions resolver because it operates on top-level symbols only
					// in some cases a symbol can be exported but not added to the top-level scope
					// for instance in case of re-export from a library `export { Foo } from 'bar'`
					// in this case we'll add this re-export differently
					continue;
				}

				if (symbolKnownNames.has(exp.exportedName)) {
					// an exported symbol is already known with its "exported" name so nothing to do at this point
					// but if it is re-exported from imported library then we assume it was previously imported so we should re-export it anyway
					if (shouldSymbolBeImported(exp.symbol)) {
						collectionResult.renamedExports.set(exp.exportedName, exp.exportedName);
					}

					continue;
				}

				// in case if this symbol isn't known yet we need to add it via renamed export
				// we assume that all known names are equal so we can use any (first)
				// usually all "local" names should have only one known name
				// but having multiple names is possible with imports - you can import the same node with different names
				// and we want to preserve the source input as much as we can that's why we re-use them
				collectionResult.renamedExports.set(exp.exportedName, Array.from(symbolKnownNames)[0]);
			}
		}

		for (const sourceFile of sourceFiles) {
			verboseLog(`======= Processing ${sourceFile.fileName} =======`);

			const updateFn = sourceFile === rootSourceFile ? updateResultForRootModule : updateResultForAnyModule;
			const currentModule = getFileModuleInfo(sourceFile.fileName, criteria);

			updateFn(sourceFile.statements, currentModule);

			// handle `import * as module` usage if it's used as whole module
			if (isNodeUsed(sourceFile)) {
				switch (currentModule.type) {
					case ModuleType.ShouldBeImported: {
						updateImportsForStatement(sourceFile);
						break;
					}

					case ModuleType.ShouldBeInlined: {
						const sourceFileSymbol = getNodeSymbol(sourceFile, typeChecker);
						if (sourceFileSymbol === null || sourceFileSymbol.exports === undefined) {
							throw new Error(`Cannot find symbol or exports for source file ${sourceFile.fileName}`);
						}

						let namespaceIdentifier: ts.Identifier | null = null;

						forEachImportOfStatement(sourceFile, (imp: ImportOfStatement) => {
							// here we want to handle creation of artificial namespace for a inlined module
							// so we don't care about other type of imports/exports - only these that create a "namespace"
							if (ts.isNamespaceExport(imp) || ts.isNamespaceImport(imp)) {
								namespaceIdentifier = imp.name;
							}
						});

						if (namespaceIdentifier === null) {
							break;
						}

						createNamespaceForExports(sourceFileSymbol.exports, getNodeOwnSymbol(namespaceIdentifier, typeChecker));
						break;
					}
				}
			}
		}

		if (entryConfig.failOnClass) {
			const classes = collectionResult.statements.filter(ts.isClassDeclaration);
			if (classes.length !== 0) {
				const classesNames = classes.map((c: ts.ClassDeclaration) => c.name === undefined ? 'anonymous class' : c.name.text);
				throw new Error(`${classes.length} class statement(s) are found in generated dts: ${classesNames.join(', ')}`);
			}
		}

		syncExports();

		// by default this option should be enabled
		const exportReferencedTypes = outputOptions.exportReferencedTypes !== false;

		function isExportedWithLocalName(namedDeclaration: ts.NamedDeclaration, exportedName: string): boolean {
			const nodeName = getNodeName(namedDeclaration);
			if (nodeName === undefined) {
				throw new Error(`Cannot find node name ${namedDeclaration.getText()}`);
			}

			return collisionsResolver.resolveReferencedIdentifier(nodeName as ts.Identifier) === exportedName;
		}

		const renamedAndNotExplicitlyExportedTypes: ts.NamedDeclaration[] = [];

		const output = generateOutput(
			{
				...collectionResult,
				resolveIdentifierName: (identifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null => {
					if (ts.isPropertyAccessOrQualifiedName(identifier)) {
						return collisionsResolver.resolveReferencedQualifiedName(identifier);
					} else {
						return collisionsResolver.resolveReferencedIdentifier(identifier);
					}
				},
				// eslint-disable-next-line complexity
				getStatementSettings: (statement: ts.Statement): StatementSettings => {
					if (isAmbientModule(statement) || ts.isExportDeclaration(statement)) {
						return { shouldHaveExportKeyword: false, shouldHaveJSDoc: true };
					}

					const statementExports = getExportsForStatement(rootFileExports, typeChecker, statement);

					// If true, then no direct export was found. That means that node might have
					// an export keyword (like interface, type, etc) otherwise, if there are
					// only re-exports with renaming (like export { foo as bar }) we don't need
					// to put export keyword for this statement because we'll re-export it in the way
					const isExplicitlyExportedWithOriginalName = statementExports.find((exp: SourceFileExport) => {
						if (ts.isVariableStatement(statement)) {
							for (const variableDeclaration of statement.declarationList.declarations) {
								if (ts.isIdentifier(variableDeclaration.name)) {
									const resolvedName = collisionsResolver.resolveReferencedIdentifier(variableDeclaration.name);
									if (exp.exportedName === resolvedName) {
										return true;
									}

									continue;
								}

								// it seems that the compiler doesn't produce anything else (e.g. binding elements) in declaration files
								// but it is still possible to write such code manually
								// this feels like quite rare case so no support for now
								warnLog(`Unhandled variable identifier type detected (${ts.SyntaxKind[variableDeclaration.name.kind]}). Please report this issue to https://github.com/timocov/dts-bundle-generator`);
							}

							return false;
						}

						return isNodeNamedDeclaration(statement) && isExportedWithLocalName(statement, exp.exportedName);
					}) !== undefined;

					// "direct export" means export from the root source file
					// e.g. classes/functions/etc must be exported from the root source file to have an "export" keyword
					// by default interfaces/types are exported even if they aren't directly exported (e.g. when they are referenced by other types)
					// but if `exportReferencedTypes` option is disabled we have to check direct export for them either
					const onlyExplicitlyExportedShouldBeExported = !exportReferencedTypes
						|| ts.isClassDeclaration(statement)
						|| (ts.isEnumDeclaration(statement) && !hasNodeModifier(statement, ts.SyntaxKind.ConstKeyword))
						|| ts.isFunctionDeclaration(statement)
						|| ts.isVariableStatement(statement)
						|| ts.isModuleDeclaration(statement);

					if (onlyExplicitlyExportedShouldBeExported) {
						// "valuable" statements must be re-exported from root source file
						// to having export keyword in declaration file
						return { shouldHaveExportKeyword: isExplicitlyExportedWithOriginalName, shouldHaveJSDoc: statementExports.length !== 0 };
					}

					// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
					if (isNodeNamedDeclaration(statement) && !isExportedWithLocalName(statement, getNodeName(statement)!.getText())) {
						// if a type node was renamed because of name collisions it shouldn't be exported with its new name
						renamedAndNotExplicitlyExportedTypes.push(statement);
						return { shouldHaveExportKeyword: false, shouldHaveJSDoc: statementExports.length !== 0 };
					}

					// at this point a statement of a type (interface, const enum, etc) will be exported 100% (it's just a matter of a name)
					// so it must have jsdoc comment
					return { shouldHaveExportKeyword: isExplicitlyExportedWithOriginalName || statementExports.length === 0, shouldHaveJSDoc: true };
				},
				needStripConstFromConstEnum: (constEnum: ts.EnumDeclaration) => {
					if (!program.getCompilerOptions().preserveConstEnums || !outputOptions.respectPreserveConstEnum) {
						return false;
					}

					const enumSymbol = getNodeSymbol(constEnum, typeChecker);
					if (enumSymbol === null) {
						return false;
					}

					return rootFileExportSymbols.includes(enumSymbol);
				},
				needStripImportFromImportTypeNode: (node: ts.ImportTypeNode) => {
					if (node.qualifier === undefined) {
						return false;
					}

					if (!ts.isLiteralTypeNode(node.argument) || !ts.isStringLiteral(node.argument.literal)) {
						return false;
					}

					return getReferencedModuleInfo(node, criteria, typeChecker)?.type === ModuleType.ShouldBeInlined;
				},
			},
			{
				sortStatements: outputOptions.sortNodes,
				umdModuleName: outputOptions.umdModuleName,
				noBanner: outputOptions.noBanner,
			}
		);

		if (renamedAndNotExplicitlyExportedTypes.length !== 0) {
			warnLog(`The following type nodes were renamed because of the name collisions and will not be exported from the generated bundle:\n- ${
				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
				renamedAndNotExplicitlyExportedTypes.map(node => `${getNodeName(node)!.getText()} (from ${node.getSourceFile().fileName})`).join('\n- ')
			}${
				'\n'
			}This might lead to unpredictable and unexpected output, and possible breaking changes to your API.${
				'\n'
			}Consider either (re-)exporting them explicitly from the entry point, or disable --export-referenced-types option ('output.exportReferencedTypes' in the config).`);
		}

		return output;
	});
}
