import * as fs from 'fs-extra';
import * as _ from '../utils/collection.util';
import * as path from 'path';

import { SyntaxKind } from 'ts-morph';

const chokidar = require('chokidar');

const traverse = require('neotraverse/legacy');
const crypto = require('crypto');

import { logger } from '../utils/logger';

import Configuration from './configuration';

import DependenciesEngine from './engines/dependencies.engine';
import ExportEngine from './engines/export.engine';
import ExportJsonEngine from './engines/export-json.engine';
import FileEngine from './engines/file.engine';
import HtmlEngine from './engines/html.engine';
import I18nEngine from './engines/i18n.engine';
import MarkdownEngine, { markdownReadedDatas } from './engines/markdown.engine';
import NgdEngine from './engines/ngd.engine';
import SearchEngine from './engines/search.engine';

import { AngularDependencies } from './compiler/angular-dependencies';
import { AngularJSDependencies } from './compiler/angularjs-dependencies';
import { ProjectTypeDetector } from './application/project-type-detector';
import { DocumentationCoverageUseCase } from './application/documentation-coverage-use-case';
import {
    applyPackageDocumentationMetadata,
    copyDocumentedPackageProperties
} from './application/package-json-metadata';
import { RouteTreeMetrics } from './application/route-tree-metrics';
import { WatchFileClassifier } from './application/watch-file-classifier';

import { COMPODOC_CONSTANTS } from '../utils/constants';
import { COMPODOC_DEFAULTS } from '../utils/defaults';
import { promiseSequential } from '../utils/promise-sequential';
import RouterParserUtil from '../utils/router-parser.util';

import {
    cleanNameWithoutSpaceAndToLowerCase,
    cleanSourcesForWatch,
    findMainSourceFolder,
    normalizeWatchFilePath
} from '../utils/utils';

import { AdditionalNode } from './interfaces/additional-node.interface';
import { CoverageData } from './interfaces/coverageData.interface';
import { markedAcl } from '../utils/marked.acl';
import { ComponentHelper } from './compiler/angular/deps/helpers/component-helper';
import { renderLlmMd } from '../llm-md';
import {
    DocumentationServerConfiguration,
    startDocumentationServer
} from '../utils/documentation-server';

const cwd = process.cwd();
let startTime = new Date();
type AnyRecord = Record<string, any>;
type GenerationPromiseResolve = (value: boolean | PromiseLike<boolean>) => void;
type GenerationPromiseReject = (reason?: any) => void;

let generationPromiseResolve: GenerationPromiseResolve = () => {};
let generationPromiseReject: GenerationPromiseReject = () => {};
const generationPromise = new Promise<boolean>((resolve, reject) => {
    generationPromiseResolve = resolve;
    generationPromiseReject = reject;
});

type DependenciesCrawler = AngularDependencies | AngularJSDependencies;
type DependenciesCrawlerConstructor = new (
    files: string[],
    options: any,
    ...legacyDependencies: any[]
) => DependenciesCrawler;

export class Application {
    private readonly projectTypeDetector = new ProjectTypeDetector();
    private readonly routeTreeMetrics = new RouteTreeMetrics();
    private readonly watchFileClassifier = new WatchFileClassifier();

    /**
     * Files processed during initial scanning
     */
    public files: Array<string> = [];
    /**
     * Files processed during watch scanning
     */
    public updatedFiles: Array<string> = [];
    /**
     * Files changed during watch scanning
     */
    public watchChangedFiles: Array<string> = [];
    /**
     * Boolean for watching status
     * @type {boolean}
     */
    public isWatching: boolean = false;

    /**
     * Store package.json data
     */
    private packageJsonData: AnyRecord = {};

    /**
     * Create a new compodoc application instance.
     *
     * @param options An object containing the options that should be used.
     */
    constructor(options?: AnyRecord) {
        const mainData = Configuration.mainData as AnyRecord;
        for (let option in options) {
            if (typeof mainData[option] !== 'undefined') {
                mainData[option] = options[option];
            }
            // For documentationMainName, process it outside the loop, for handling conflict with pages name
            if (option === 'name') {
                Configuration.mainData.documentationMainName = options[option];
            }
            // For documentationMainName, process it outside the loop, for handling conflict with pages name
            if (option === 'silent') {
                logger.silent = false;
            }
        }
    }

    /**
     * Start compodoc process
     */
    protected generate(): Promise<boolean> {
        process.on('unhandledRejection', this.unhandledRejectionListener);
        process.on('uncaughtException', this.uncaughtExceptionListener);

        I18nEngine.init(Configuration.mainData.language);

        if (
            Configuration.mainData.output.charAt(Configuration.mainData.output.length - 1) !== '/'
        ) {
            Configuration.mainData.output += '/';
        }

        if (Configuration.mainData.exportFormat !== COMPODOC_DEFAULTS.exportFormat) {
            this.processPackageJson();
        } else {
            HtmlEngine.init(Configuration.mainData.templates).then(() => this.processPackageJson());
        }
        return generationPromise;
    }

    private endCallback() {
        process.removeListener('unhandledRejection', this.unhandledRejectionListener);
        process.removeListener('uncaughtException', this.uncaughtExceptionListener);
    }

    private unhandledRejectionListener(err: any, p: any) {
        console.log('Unhandled Rejection at:', p, 'reason:', err);
        logger.error(
            'Sorry, but there was a problem during parsing or generation of the documentation. Please fill an issue on github. (https://github.com/compodoc/compodoc/issues/new)'
        ); // tslint:disable-line
        process.exit(1);
    }

    private uncaughtExceptionListener(err: any) {
        logger.error(err);
        logger.error(
            'Sorry, but there was a problem during parsing or generation of the documentation. Please fill an issue on github. (https://github.com/compodoc/compodoc/issues/new)'
        ); // tslint:disable-line
        process.exit(1);
    }

    /**
     * Start compodoc documentation coverage
     */
    protected testCoverage() {
        this.getDependenciesData();
    }

    /**
     * Store files for initial processing
     * @param  {Array<string>} files Files found during source folder and tsconfig scan
     */
    public setFiles(files: Array<string>) {
        this.files = files;
    }

    /**
     * Store files for watch processing
     * @param  {Array<string>} files Files found during source folder and tsconfig scan
     */
    public setUpdatedFiles(files: Array<string>) {
        this.updatedFiles = files;
    }

    /**
     * Return a boolean indicating presence of one TypeScript file in updatedFiles list
     * @return {boolean} Result of scan
     */
    public hasWatchedFilesTSFiles(): boolean {
        return this.watchFileClassifier.hasTypeScriptFile(this.updatedFiles);
    }

    /**
     * Return a boolean indicating presence of one root markdown files in updatedFiles list
     * @return {boolean} Result of scan
     */
    public hasWatchedFilesRootMarkdownFiles(): boolean {
        return this.watchFileClassifier.hasRootMarkdownFile(this.updatedFiles, cwd);
    }

    /**
     * Clear files for watch processing
     */
    public clearUpdatedFiles(): void {
        this.updatedFiles = [];
        this.watchChangedFiles = [];
    }

    private processPackageJson(): void {
        logger.info('Searching package.json file');
        FileEngine.get(cwd + path.sep + 'package.json').then(
            packageData => {
                let parsedData = JSON.parse(packageData);
                this.packageJsonData = parsedData;
                applyPackageDocumentationMetadata(parsedData);
                logger.info('package.json file found');

                if (!Configuration.mainData.disableDependencies) {
                    if (typeof parsedData.dependencies !== 'undefined') {
                        this.processPackageDependencies(parsedData.dependencies);
                    }
                    if (typeof parsedData.peerDependencies !== 'undefined') {
                        this.processPackagePeerDependencies(parsedData.peerDependencies);
                    }
                }

                if (!Configuration.mainData.disableProperties) {
                    if (copyDocumentedPackageProperties(parsedData)) {
                        Configuration.addPage({
                            name: 'properties',
                            id: 'packageProperties',
                            context: 'package-properties',
                            depth: 0,
                            pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                        });
                    }
                }

                this.processMarkdowns().then(
                    () => {
                        this.getDependenciesData();
                    },
                    errorMessage => {
                        logger.error(errorMessage);
                        process.exit(1);
                    }
                );
            },
            errorMessage => {
                logger.error(errorMessage);
                logger.error('Continuing without package.json file');
                this.processMarkdowns().then(
                    () => {
                        this.getDependenciesData();
                    },
                    errorMessage1 => {
                        logger.error(errorMessage1);
                        process.exit(1);
                    }
                );
            }
        );
    }

    private processPackagePeerDependencies(dependencies: any): void {
        logger.info('Processing package.json peerDependencies');
        Configuration.mainData.packagePeerDependencies = dependencies;
        if (!Configuration.hasPage('dependencies')) {
            Configuration.addPage({
                name: 'dependencies',
                id: 'packageDependencies',
                context: 'package-dependencies',
                depth: 0,
                pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
            });
        }
    }

    private processPackageDependencies(dependencies: any): void {
        logger.info('Processing package.json dependencies');
        Configuration.mainData.packageDependencies = dependencies;
        Configuration.addPage({
            name: 'dependencies',
            id: 'packageDependencies',
            context: 'package-dependencies',
            depth: 0,
            pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
        });
    }

    private processMarkdowns(): Promise<any> {
        logger.info(
            'Searching README.md, CHANGELOG.md, CONTRIBUTING.md, LICENSE.md, TODO.md files'
        );

        return new Promise((resolve, reject) => {
            let i = 0;
            const markdowns = ['readme', 'changelog', 'contributing', 'license', 'todo'];
            const numberOfMarkdowns = 5;
            const loop = () => {
                if (i < numberOfMarkdowns) {
                    MarkdownEngine.getTraditionalMarkdown(markdowns[i].toUpperCase())
                        .then((readmeData: markdownReadedDatas) => {
                            logger.info(`${markdowns[i].toUpperCase()}.md file found`);
                            if (markdowns[i] === 'readme') {
                                Configuration.mainData.readme = true;
                                // Always create index.html as main page with README content
                                Configuration.addPage({
                                    name: 'index',
                                    context: 'readme',
                                    id: 'index',
                                    markdown: readmeData.markdown,
                                    data: readmeData.rawData,
                                    depth: 0,
                                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                });

                                // If overview is not disabled, also create separate overview page
                                if (!Configuration.mainData.disableOverview) {
                                    Configuration.addPage({
                                        name: 'overview',
                                        context: 'overview',
                                        id: 'overview',
                                        depth: 0,
                                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                    });
                                }
                            } else {
                                // For other markdown files (changelog, contributing, etc.)
                                Configuration.addPage({
                                    name: markdowns[i],
                                    context: markdowns[i],
                                    id: markdowns[i],
                                    markdown: readmeData.markdown,
                                    data: readmeData.rawData,
                                    depth: 0,
                                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                });
                                Configuration.mainData.markdowns.push({
                                    name: markdowns[i],
                                    uppername: markdowns[i].toUpperCase(),
                                    depth: 0,
                                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                });
                            }
                            i++;
                            loop();
                        })
                        .catch(errorMessage => {
                            logger.warn(errorMessage);
                            logger.warn(`Continuing without ${markdowns[i].toUpperCase()}.md file`);
                            if (markdowns[i] === 'readme') {
                                if (!Configuration.mainData.disableOverview) {
                                    Configuration.addPage({
                                        name: 'index',
                                        id: 'index',
                                        context: 'overview',
                                        depth: 0,
                                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                    });
                                } else {
                                    // When README doesn't exist and overview is disabled,
                                    // generate overview page anyway but show warning
                                    logger.warn(
                                        'No README.md found and --disableOverview is enabled.'
                                    );
                                    logger.warn(
                                        'Generating overview page as landing page. Consider adding a README.md file.'
                                    );
                                    Configuration.addPage({
                                        name: 'index',
                                        id: 'index',
                                        context: 'overview',
                                        depth: 0,
                                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                                    });
                                }
                            }
                            i++;
                            loop();
                        });
                } else {
                    if (!Configuration.hasPage('architecture')) {
                        Configuration.addPage({
                            name: 'architecture',
                            id: 'architecture',
                            context: 'architecture',
                            depth: 0,
                            pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
                        });
                    }
                    resolve(true);
                }
            };
            loop();
        });
    }

    private rebuildRootMarkdowns(): void {
        logger.info(
            'Regenerating README.md, CHANGELOG.md, CONTRIBUTING.md, LICENSE.md, TODO.md pages'
        );

        let actions = [];

        Configuration.resetRootMarkdownPages();

        actions.push(() => {
            return this.processMarkdowns();
        });

        promiseSequential(actions)
            .then(res => {
                this.processPages();
                this.clearUpdatedFiles();
            })
            .catch(errorMessage => {
                logger.error(errorMessage);
            });
    }

    /**
     * Get dependency data for small group of updated files during watch process
     */
    private getMicroDependenciesData(): void {
        logger.info('Get diff dependencies data');

        let dependenciesClass: DependenciesCrawlerConstructor = AngularDependencies;
        Configuration.mainData.angularProject = true;

        if (this.detectAngularJSProjects()) {
            logger.info('AngularJS project detected');
            Configuration.mainData.angularProject = false;
            Configuration.mainData.angularJSProject = true;
            dependenciesClass = AngularJSDependencies;
        }

        let crawler = new dependenciesClass(
            this.updatedFiles,
            {
                tsconfigDirectory: path.dirname(Configuration.mainData.tsconfig)
            },
            Configuration,
            RouterParserUtil
        );

        let dependenciesData = crawler.getDependencies();

        DependenciesEngine.update(dependenciesData);

        this.prepareJustAFewThings(dependenciesData);
    }

    /**
     * Rebuild external documentation during watch process
     */
    private rebuildExternalDocumentation(): void {
        logger.info('Rebuild external documentation');

        let actions = [];

        Configuration.resetAdditionalPages();

        if (Configuration.mainData.includes !== '') {
            actions.push(() => {
                return this.prepareExternalIncludes();
            });
        }

        promiseSequential(actions)
            .then(res => {
                this.processPages();
                this.clearUpdatedFiles();
            })
            .catch(errorMessage => {
                logger.error(errorMessage);
            });
    }

    private detectAngularJSProjects() {
        return this.projectTypeDetector.isAngularJSProject(this.packageJsonData, this.files);
    }

    private getDependenciesData(): void {
        logger.info('Get dependencies data');

        /**
         * AngularJS detection strategy :
         * - if in package.json
         * - if 75% of scanned files are *.js files
         */
        let dependenciesClass: DependenciesCrawlerConstructor = AngularDependencies;
        Configuration.mainData.angularProject = true;

        if (this.detectAngularJSProjects()) {
            logger.info('AngularJS project detected');
            Configuration.mainData.angularProject = false;
            Configuration.mainData.angularJSProject = true;
            dependenciesClass = AngularJSDependencies;
        }

        let crawler = new dependenciesClass(
            this.files,
            {
                tsconfigDirectory: path.dirname(Configuration.mainData.tsconfig)
            },
            Configuration,
            RouterParserUtil
        );

        let dependenciesData = crawler.getDependencies();

        DependenciesEngine.init(dependenciesData as any);

        if (dependenciesData.routesTree) {
            Configuration.mainData.routesLength = this.routesLengthFromTree(
                dependenciesData.routesTree
            );
        } else {
            Configuration.mainData.routesLength = RouterParserUtil.routesLength();
        }

        this.printStatistics();

        this.prepareEverything();
    }

    private routesLengthFromTree(routesTree: any): number {
        return this.routeTreeMetrics.countRoutes(routesTree);
    }

    private prepareJustAFewThings(diffCrawledData: any): void {
        let actions: Array<() => any> = [];

        Configuration.resetPages();

        if (!Configuration.mainData.disableRoutesGraph) {
            actions.push(() => this.prepareRoutes());
        }

        if (diffCrawledData.components.length > 0) {
            actions.push(() => this.prepareComponents());
        }
        if (diffCrawledData.controllers.length > 0) {
            actions.push(() => this.prepareControllers());
        }
        if (diffCrawledData.entities.length > 0) {
            actions.push(() => this.prepareEntities());
        }
        if (diffCrawledData.modules.length > 0) {
            actions.push(() => this.prepareModules());
        }

        if (diffCrawledData.directives.length > 0) {
            actions.push(() => this.prepareDirectives());
        }

        if (diffCrawledData.injectables.length > 0) {
            actions.push(() => this.prepareInjectables());
        }

        if (diffCrawledData.interceptors.length > 0) {
            actions.push(() => this.prepareInterceptors());
        }

        if (diffCrawledData.guards.length > 0) {
            actions.push(() => this.prepareGuards());
        }

        if (diffCrawledData.pipes.length > 0) {
            actions.push(() => this.preparePipes());
        }

        if (diffCrawledData.classes.length > 0) {
            actions.push(() => this.prepareClasses());
        }

        if (diffCrawledData.interfaces.length > 0) {
            actions.push(() => this.prepareInterfaces());
        }

        if (
            diffCrawledData.miscellaneous.variables.length > 0 ||
            diffCrawledData.miscellaneous.functions.length > 0 ||
            diffCrawledData.miscellaneous.typealiases.length > 0 ||
            diffCrawledData.miscellaneous.enumerations.length > 0
        ) {
            actions.push(() => this.prepareMiscellaneous());
        }

        if (!Configuration.mainData.disableCoverage) {
            actions.push(() => this.prepareCoverage());
        }

        promiseSequential(actions)
            .then(res => {
                if (Configuration.mainData.exportFormat !== COMPODOC_DEFAULTS.exportFormat) {
                    if (
                        COMPODOC_DEFAULTS.exportFormatsSupported.indexOf(
                            Configuration.mainData.exportFormat
                        ) > -1
                    ) {
                        logger.info(
                            `Generating documentation in export format ${Configuration.mainData.exportFormat}`
                        );
                        Promise.resolve(
                            ExportEngine.export(
                                Configuration.mainData.output,
                                Configuration.mainData
                            )
                        ).then(() => {
                            generationPromiseResolve(true);
                            this.endCallback();
                            logger.info(
                                'Documentation generated in ' +
                                    Configuration.mainData.output +
                                    ' in ' +
                                    this.getElapsedTime() +
                                    ' seconds'
                            );
                            if (Configuration.mainData.serve) {
                                logger.info(
                                    `Serving documentation from ${Configuration.mainData.output} at http://${Configuration.mainData.hostname}:${Configuration.mainData.port}`
                                );
                                this.runWebServer(Configuration.mainData.output);
                            }
                        });
                    } else {
                        logger.warn(`Exported format not supported`);
                    }
                } else {
                    this.processGraphs();
                    this.clearUpdatedFiles();
                }
            })
            .catch(errorMessage => {
                logger.error(errorMessage);
            });
    }

    private printStatistics() {
        logger.info('-------------------');
        logger.info('Project statistics ');
        if (DependenciesEngine.modules.length > 0) {
            logger.info(`- files        : ${this.files.length}`);
        }
        if (DependenciesEngine.modules.length > 0) {
            logger.info(`- module       : ${DependenciesEngine.modules.length}`);
        }
        if (DependenciesEngine.components.length > 0) {
            logger.info(`- component    : ${DependenciesEngine.components.length}`);
        }
        if (DependenciesEngine.controllers.length > 0) {
            logger.info(`- controller   : ${DependenciesEngine.controllers.length}`);
        }
        if (DependenciesEngine.entities.length > 0) {
            logger.info(`- entity       : ${DependenciesEngine.entities.length}`);
        }
        if (DependenciesEngine.directives.length > 0) {
            logger.info(`- directive    : ${DependenciesEngine.directives.length}`);
        }
        if (DependenciesEngine.injectables.length > 0) {
            logger.info(`- injectable   : ${DependenciesEngine.injectables.length}`);
        }
        if (DependenciesEngine.interceptors.length > 0) {
            logger.info(`- injector     : ${DependenciesEngine.interceptors.length}`);
        }
        if (DependenciesEngine.guards.length > 0) {
            logger.info(`- guard        : ${DependenciesEngine.guards.length}`);
        }
        if (DependenciesEngine.pipes.length > 0) {
            logger.info(`- pipe         : ${DependenciesEngine.pipes.length}`);
        }
        if (DependenciesEngine.classes.length > 0) {
            logger.info(`- class        : ${DependenciesEngine.classes.length}`);
        }
        if (DependenciesEngine.interfaces.length > 0) {
            logger.info(`- interface    : ${DependenciesEngine.interfaces.length}`);
        }
        if (Configuration.mainData.routesLength > 0) {
            logger.info(`- route        : ${Configuration.mainData.routesLength}`);
        }
        if (DependenciesEngine.miscellaneous.typealiases.length > 0) {
            logger.info(`- type aliases : ${DependenciesEngine.miscellaneous.typealiases.length}`);
        }
        logger.info('-------------------');
    }

    private prepareEverything() {
        let actions = [];

        actions.push(() => {
            return this.prepareComponents();
        });
        actions.push(() => {
            return this.prepareModules();
        });

        if (DependenciesEngine.directives.length > 0) {
            actions.push(() => {
                return this.prepareDirectives();
            });
        }

        if (DependenciesEngine.controllers.length > 0) {
            actions.push(() => {
                return this.prepareControllers();
            });
        }

        if (DependenciesEngine.entities.length > 0) {
            actions.push(() => {
                return this.prepareEntities();
            });
        }

        if (DependenciesEngine.injectables.length > 0) {
            actions.push(() => {
                return this.prepareInjectables();
            });
        }

        if (DependenciesEngine.interceptors.length > 0) {
            actions.push(() => {
                return this.prepareInterceptors();
            });
        }

        if (DependenciesEngine.guards.length > 0) {
            actions.push(() => {
                return this.prepareGuards();
            });
        }

        if (DependenciesEngine.routes && !Configuration.mainData.disableRoutesGraph) {
            actions.push(() => {
                return this.prepareRoutes();
            });
        }

        if (DependenciesEngine.pipes.length > 0) {
            actions.push(() => {
                return this.preparePipes();
            });
        }

        if (DependenciesEngine.classes.length > 0) {
            actions.push(() => {
                return this.prepareClasses();
            });
        }

        if (DependenciesEngine.interfaces.length > 0) {
            actions.push(() => {
                return this.prepareInterfaces();
            });
        }

        if (
            DependenciesEngine.miscellaneous.variables.length > 0 ||
            DependenciesEngine.miscellaneous.functions.length > 0 ||
            DependenciesEngine.miscellaneous.typealiases.length > 0 ||
            DependenciesEngine.miscellaneous.enumerations.length > 0
        ) {
            actions.push(() => {
                return this.prepareMiscellaneous();
            });
        }

        if (!Configuration.mainData.disableCoverage) {
            actions.push(() => {
                return this.prepareCoverage();
            });
        }

        if (Configuration.mainData.unitTestCoverage !== '') {
            actions.push(() => {
                return this.prepareUnitTestCoverage();
            });
        }

        if (Configuration.mainData.includes !== '') {
            actions.push(() => {
                return this.prepareExternalIncludes();
            });
        }

        promiseSequential(actions)
            .then(res => {
                if (Configuration.mainData.exportFormat !== COMPODOC_DEFAULTS.exportFormat) {
                    if (
                        COMPODOC_DEFAULTS.exportFormatsSupported.indexOf(
                            Configuration.mainData.exportFormat
                        ) > -1
                    ) {
                        logger.info(
                            `Generating documentation in export format ${Configuration.mainData.exportFormat}`
                        );
                        Promise.resolve(
                            ExportEngine.export(
                                Configuration.mainData.output,
                                Configuration.mainData
                            )
                        ).then(() => {
                            generationPromiseResolve(true);
                            this.endCallback();
                            logger.info(
                                'Documentation generated in ' +
                                    Configuration.mainData.output +
                                    ' in ' +
                                    this.getElapsedTime() +
                                    ' seconds'
                            );
                            if (Configuration.mainData.serve) {
                                logger.info(
                                    `Serving documentation from ${Configuration.mainData.output} at http://${Configuration.mainData.hostname}:${Configuration.mainData.port}`
                                );
                                this.runWebServer(Configuration.mainData.output);
                            }
                        });
                    } else {
                        logger.warn(`Exported format not supported`);
                    }
                } else {
                    this.processGraphs();
                }
            })
            .catch(errorMessage => {
                logger.error(errorMessage);
                process.exit(1);
            });
    }

    private getIncludedPathForFile(file: string) {
        return path.join(Configuration.mainData.includes, file);
    }

    private prepareExternalIncludes() {
        logger.info('Adding external markdown files');
        // Scan include folder for files detailed in summary.json
        // For each file, add to Configuration.mainData.additionalPages
        // Each file will be converted to html page, inside COMPODOC_DEFAULTS.additionalEntryPath
        return new Promise((resolve, reject) => {
            FileEngine.get(this.getIncludedPathForFile('summary.json')).then(
                summaryData => {
                    logger.info('Additional documentation: summary.json file found');

                    const parsedSummaryData = JSON.parse(summaryData);

                    let that = this;
                    let lastLevelOnePage: any = undefined;

                    traverse(parsedSummaryData).forEach(function (this: any) {
                        // tslint:disable-next-line:no-invalid-this
                        if (this.notRoot && typeof this.node === 'object') {
                            // tslint:disable-next-line:no-invalid-this
                            let rawPath = this.path;
                            // tslint:disable-next-line:no-invalid-this
                            let additionalNode: AdditionalNode = this.node;
                            let file = additionalNode.file;
                            let title = additionalNode.title;
                            let finalPath = Configuration.mainData.includesFolder;

                            let finalDepth = rawPath.filter((el: any) => {
                                return !isNaN(parseInt(el, 10));
                            });

                            if (typeof file !== 'undefined' && typeof title !== 'undefined') {
                                const url = cleanNameWithoutSpaceAndToLowerCase(title);

                                /**
                                 * Id created with title + file path hash, seems to be hypothetically unique here
                                 */
                                const id = crypto
                                    .createHash('sha512')
                                    .update(title + file)
                                    .digest('hex');

                                // tslint:disable-next-line:no-invalid-this
                                this.node.id = id;

                                let lastElementRootTree: any = undefined;
                                finalDepth.forEach((el: any) => {
                                    let elementTree =
                                        typeof lastElementRootTree === 'undefined'
                                            ? parsedSummaryData
                                            : lastElementRootTree;
                                    if (typeof elementTree.children !== 'undefined') {
                                        elementTree = elementTree.children[el];
                                    } else {
                                        elementTree = elementTree[el];
                                    }
                                    finalPath +=
                                        '/' +
                                        cleanNameWithoutSpaceAndToLowerCase(elementTree.title);
                                    lastElementRootTree = elementTree;
                                });

                                finalPath = finalPath.replace('/' + url, '');
                                let markdownFile = MarkdownEngine.getTraditionalMarkdownSync(
                                    that.getIncludedPathForFile(file)
                                );

                                if (finalDepth.length > 5) {
                                    logger.error('Only 5 levels of depth are supported');
                                } else {
                                    let _page: any = {
                                        name: title,
                                        id: id,
                                        filename: url,
                                        context: 'additional-page',
                                        path: finalPath,
                                        additionalPage: markdownFile,
                                        depth: finalDepth.length,
                                        childrenLength: additionalNode.children
                                            ? additionalNode.children.length
                                            : 0,
                                        children: [],
                                        lastChild: false,
                                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                                    };
                                    if (finalDepth.length === 1) {
                                        lastLevelOnePage = _page;
                                    }
                                    if (finalDepth.length > 1) {
                                        // store all child pages of the last root level 1 page inside it
                                        lastLevelOnePage.children.push(_page);
                                    } else {
                                        Configuration.addAdditionalPage(_page);
                                    }
                                }
                            }
                        }
                    });

                    resolve(true);
                },
                errorMessage => {
                    logger.error(errorMessage);
                    reject('Error during Additional documentation generation');
                }
            );
        });
    }

    public prepareModules(someModules?: any): Promise<any> {
        logger.info('Prepare modules');
        let i = 0;
        let _modules = someModules ? someModules : DependenciesEngine.getModules();

        return new Promise((resolve, reject) => {
            Configuration.mainData.modules = _modules.map((ngModule: any) => {
                ngModule.compodocLinks = {
                    components: [],
                    controllers: [],
                    directives: [],
                    injectables: [],
                    pipes: []
                };
                ['declarations', 'bootstrap', 'imports', 'exports', 'controllers'].forEach(
                    metadataType => {
                        ngModule[metadataType] = ngModule[metadataType].filter(
                            (metaDataItem: any) => {
                                switch (metaDataItem.type) {
                                    case 'directive':
                                        return DependenciesEngine.getDirectives().some(
                                            directive => {
                                                let selectedDirective;
                                                if (typeof metaDataItem.id !== 'undefined') {
                                                    selectedDirective =
                                                        (directive as any).id === metaDataItem.id;
                                                } else {
                                                    selectedDirective =
                                                        (directive as any).name ===
                                                        metaDataItem.name;
                                                }
                                                if (
                                                    selectedDirective &&
                                                    !ngModule.compodocLinks.directives.includes(
                                                        directive
                                                    )
                                                ) {
                                                    ngModule.compodocLinks.directives.push(
                                                        directive
                                                    );
                                                }
                                                return selectedDirective;
                                            }
                                        );

                                    case 'component':
                                        return DependenciesEngine.getComponents().some(
                                            (component: any) => {
                                                let selectedComponent;
                                                if (typeof metaDataItem.id !== 'undefined') {
                                                    selectedComponent =
                                                        (component as any).id === metaDataItem.id;
                                                } else {
                                                    selectedComponent =
                                                        (component as any).name ===
                                                        metaDataItem.name;
                                                }
                                                if (
                                                    selectedComponent &&
                                                    !ngModule.compodocLinks.components.includes(
                                                        component
                                                    )
                                                ) {
                                                    if (!component.standalone) {
                                                        ngModule.compodocLinks.components.push(
                                                            component
                                                        );
                                                    }
                                                }
                                                return selectedComponent;
                                            }
                                        );

                                    case 'controller':
                                        return DependenciesEngine.getControllers().some(
                                            controller => {
                                                let selectedController;
                                                if (typeof metaDataItem.id !== 'undefined') {
                                                    selectedController =
                                                        (controller as any).id === metaDataItem.id;
                                                } else {
                                                    selectedController =
                                                        (controller as any).name ===
                                                        metaDataItem.name;
                                                }
                                                if (
                                                    selectedController &&
                                                    !ngModule.compodocLinks.controllers.includes(
                                                        controller
                                                    )
                                                ) {
                                                    ngModule.compodocLinks.controllers.push(
                                                        controller
                                                    );
                                                }
                                                return selectedController;
                                            }
                                        );

                                    case 'module':
                                        return DependenciesEngine.getModules().some(
                                            module => (module as any).name === metaDataItem.name
                                        );

                                    case 'pipe':
                                        return DependenciesEngine.getPipes().some(pipe => {
                                            let selectedPipe;
                                            if (typeof metaDataItem.id !== 'undefined') {
                                                selectedPipe = (pipe as any).id === metaDataItem.id;
                                            } else {
                                                selectedPipe =
                                                    (pipe as any).name === metaDataItem.name;
                                            }
                                            if (
                                                selectedPipe &&
                                                !ngModule.compodocLinks.pipes.includes(pipe)
                                            ) {
                                                ngModule.compodocLinks.pipes.push(pipe);
                                            }
                                            return selectedPipe;
                                        });

                                    default:
                                        return true;
                                }
                            }
                        );
                    }
                );
                ngModule.providers = ngModule.providers.filter((provider: any) => {
                    return (
                        DependenciesEngine.getInjectables().some(injectable => {
                            let selectedInjectable = (injectable as any).name === provider.name;
                            if (
                                selectedInjectable &&
                                !ngModule.compodocLinks.injectables.includes(injectable)
                            ) {
                                ngModule.compodocLinks.injectables.push(injectable);
                            }
                            return selectedInjectable;
                        }) ||
                        DependenciesEngine.getInterceptors().some(
                            interceptor => (interceptor as any).name === provider.name
                        )
                    );
                });
                // Try fixing type undefined for each providers
                _.forEach(ngModule.providers, provider => {
                    if (
                        DependenciesEngine.getInjectables().find(
                            injectable => (injectable as any).name === provider.name
                        )
                    ) {
                        provider.type = 'injectable';
                    }
                    if (
                        DependenciesEngine.getInterceptors().find(
                            interceptor => (interceptor as any).name === provider.name
                        )
                    ) {
                        provider.type = 'interceptor';
                    }
                });
                // Order things
                ngModule.compodocLinks.components = _.sortBy(ngModule.compodocLinks.components, [
                    'name'
                ]);
                ngModule.compodocLinks.controllers = _.sortBy(ngModule.compodocLinks.controllers, [
                    'name'
                ]);
                ngModule.compodocLinks.directives = _.sortBy(ngModule.compodocLinks.directives, [
                    'name'
                ]);
                ngModule.compodocLinks.injectables = _.sortBy(ngModule.compodocLinks.injectables, [
                    'name'
                ]);
                ngModule.compodocLinks.pipes = _.sortBy(ngModule.compodocLinks.pipes, ['name']);

                ngModule.declarations = _.sortBy(ngModule.declarations, ['name']);
                ngModule.entryComponents = _.sortBy(ngModule.entryComponents, ['name']);
                ngModule.providers = _.sortBy(ngModule.providers, ['name']);
                ngModule.imports = _.sortBy(ngModule.imports, ['name']);
                ngModule.exports = _.sortBy(ngModule.exports, ['name']);

                return ngModule;
            });

            Configuration.addPage({
                name: 'modules',
                id: 'modules',
                context: 'modules',
                depth: 0,
                pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
            });

            const len = Configuration.mainData.modules.length;
            const loop = () => {
                if (i < len) {
                    if (
                        MarkdownEngine.hasNeighbourReadmeFile(
                            Configuration.mainData.modules[i].file
                        )
                    ) {
                        logger.info(
                            ` ${Configuration.mainData.modules[i].name} has a README file, include it`
                        );
                        const readme = MarkdownEngine.readNeighbourReadmeFile(
                            Configuration.mainData.modules[i].file
                        );
                        Configuration.mainData.modules[i].readme = markedAcl(readme);
                    }
                    Configuration.mainData.modules[i].relationships = this.getRelationships(
                        Configuration.mainData.modules[i]
                    );
                    Configuration.addPage({
                        path: 'modules',
                        name: Configuration.mainData.modules[i].name,
                        id: Configuration.mainData.modules[i].id,
                        navTabs: this.getNavTabs(Configuration.mainData.modules[i]),
                        context: 'module',
                        module: Configuration.mainData.modules[i],
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    });
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    }

    public preparePipes = (somePipes?: any) => {
        logger.info('Prepare pipes');
        Configuration.mainData.pipes = somePipes ? somePipes : DependenciesEngine.getPipes();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.pipes.length;
            const loop = () => {
                if (i < len) {
                    const pipe = Configuration.mainData.pipes[i];
                    if (MarkdownEngine.hasNeighbourReadmeFile(pipe.file)) {
                        logger.info(` ${pipe.name} has a README file, include it`);
                        const readme = MarkdownEngine.readNeighbourReadmeFile(pipe.file);
                        pipe.readme = markedAcl(readme);
                    }
                    pipe.relationships = this.getRelationships(pipe);
                    const page = {
                        path: 'pipes',
                        name: pipe.name,
                        id: pipe.id,
                        navTabs: this.getNavTabs(pipe),
                        context: 'pipe',
                        pipe: pipe,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (pipe.isDuplicate) {
                        page.name += '-' + pipe.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    };

    public prepareClasses = (someClasses?: any) => {
        logger.info('Prepare classes');
        Configuration.mainData.classes = someClasses
            ? someClasses
            : DependenciesEngine.getClasses();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.classes.length;
            const loop = () => {
                if (i < len) {
                    const classe = Configuration.mainData.classes[i];
                    if (MarkdownEngine.hasNeighbourReadmeFile(classe.file)) {
                        logger.info(` ${classe.name} has a README file, include it`);
                        const readme = MarkdownEngine.readNeighbourReadmeFile(classe.file);
                        classe.readme = markedAcl(readme);
                    }
                    classe.relationships = this.getRelationships(classe);
                    const page = {
                        path: 'classes',
                        name: classe.name,
                        id: classe.id,
                        navTabs: this.getNavTabs(classe),
                        context: 'class',
                        class: classe,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (classe.isDuplicate) {
                        page.name += '-' + classe.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    };

    public prepareInterfaces(someInterfaces?: any) {
        logger.info('Prepare interfaces');
        Configuration.mainData.interfaces = someInterfaces
            ? someInterfaces
            : DependenciesEngine.getInterfaces();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.interfaces.length;
            const loop = () => {
                if (i < len) {
                    const interf = Configuration.mainData.interfaces[i];
                    if (MarkdownEngine.hasNeighbourReadmeFile(interf.file)) {
                        logger.info(` ${interf.name} has a README file, include it`);
                        const readme = MarkdownEngine.readNeighbourReadmeFile(interf.file);
                        interf.readme = markedAcl(readme);
                    }
                    interf.relationships = this.getRelationships(interf);
                    const page = {
                        path: 'interfaces',
                        name: interf.name,
                        id: interf.id,
                        navTabs: this.getNavTabs(interf),
                        context: 'interface',
                        interface: interf,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (interf.isDuplicate) {
                        page.name += '-' + interf.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    }

    public prepareMiscellaneous(someMisc?: any) {
        logger.info('Prepare miscellaneous');
        Configuration.mainData.miscellaneous = someMisc
            ? someMisc
            : DependenciesEngine.getMiscellaneous();

        return new Promise((resolve, reject) => {
            if (Configuration.mainData.miscellaneous.functions.length > 0) {
                Configuration.addPage({
                    path: 'miscellaneous',
                    name: 'functions',
                    id: 'miscellaneous-functions',
                    context: 'miscellaneous-functions',
                    depth: 1,
                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                });
            }
            if (Configuration.mainData.miscellaneous.variables.length > 0) {
                Configuration.addPage({
                    path: 'miscellaneous',
                    name: 'variables',
                    id: 'miscellaneous-variables',
                    context: 'miscellaneous-variables',
                    depth: 1,
                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                });
            }
            if (Configuration.mainData.miscellaneous.typealiases.length > 0) {
                Configuration.addPage({
                    path: 'miscellaneous',
                    name: 'typealiases',
                    id: 'miscellaneous-typealiases',
                    context: 'miscellaneous-typealiases',
                    depth: 1,
                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                });
            }
            if (Configuration.mainData.miscellaneous.enumerations.length > 0) {
                Configuration.addPage({
                    path: 'miscellaneous',
                    name: 'enumerations',
                    id: 'miscellaneous-enumerations',
                    context: 'miscellaneous-enumerations',
                    depth: 1,
                    pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                });
            }

            resolve(true);
        });
    }

    private handleTemplateurl(component: any): Promise<any> {
        const dirname = path.dirname(component.file);
        const templatePath = path.resolve(dirname + path.sep + component.templateUrl);

        if (!FileEngine.existsSync(templatePath)) {
            const err = `Cannot read template for ${component.name}`;
            logger.error(err);
            return new Promise((resolve, reject) => {});
        }

        return FileEngine.get(templatePath).then(
            data => {
                component.templateData = data;
                component.templateVariables =
                    ComponentHelper.getTemplateVariablesFromTemplate(data);
            },
            err => {
                logger.error(err);
                return Promise.reject('');
            }
        );
    }

    private handleStyles(component: any): Promise<any> {
        const styles = component.styles;
        component.stylesData = '';
        return new Promise((resolveStyles, rejectStyles) => {
            styles.forEach((style: any) => {
                component.stylesData = component.stylesData + style + '\n';
            });
            resolveStyles(true);
        });
    }

    private handleStyleurls(component: any): Promise<any> {
        const dirname = path.dirname(component.file);

        const styleDataPromise = component.styleUrls.map((styleUrl: any) => {
            const stylePath = path.resolve(dirname + path.sep + styleUrl);

            if (!FileEngine.existsSync(stylePath)) {
                const err = `Cannot read style url ${stylePath} for ${component.name}`;
                logger.error(err);
                return Promise.resolve(null);
            }

            return new Promise((resolve, reject) => {
                FileEngine.get(stylePath).then(data => {
                    resolve({
                        data,
                        styleUrl
                    });
                });
            });
        });

        return Promise.all(styleDataPromise).then(
            data => (component.styleUrlsData = data.filter(item => item !== null)),
            err => {
                logger.error(err);
                return Promise.reject('');
            }
        );
    }

    private getRelationships(dependency: any): {
        incoming: Array<{ name: string; type: string; description?: string }>;
        outgoing: Array<{ name: string; type: string; description?: string }>;
    } {
        if (!dependency?.name) {
            return { incoming: [], outgoing: [] };
        }
        return DependenciesEngine.getRelationships(dependency);
    }

    private hasApiSections(dependency: any): boolean {
        if (!dependency) {
            return false;
        }

        const candidates = [
            dependency.constructorObj,
            dependency.inputsClass,
            dependency.outputsClass,
            dependency.hostBindings,
            dependency.hostListeners,
            dependency.methodsClass,
            dependency.propertiesClass,
            dependency.accessors,
            dependency.properties,
            dependency.methods,
            dependency.indexSignatures
        ];

        return candidates.some(entry => {
            if (!entry) {
                return false;
            }
            if (Array.isArray(entry)) {
                return entry.length > 0;
            }
            return true;
        });
    }

    private getNavTabs(dependency: any): Array<any> {
        let navTabConfig = Configuration.mainData.navTabConfig;
        const hasCustomNavTabConfig = navTabConfig.length !== 0;
        navTabConfig =
            navTabConfig.length === 0
                ? _.cloneDeep(COMPODOC_CONSTANTS.navTabDefinitions)
                : navTabConfig;
        const matchDepType = (depType: string) => {
            return depType === 'all' || depType === dependency.type;
        };

        let navTabs: any[] = [];
        _.forEach(navTabConfig, customTab => {
            const navTab = _.find(COMPODOC_CONSTANTS.navTabDefinitions, {
                id: customTab.id
            });
            if (!navTab) {
                throw new Error(`Invalid tab ID '${customTab.id}' specified in tab configuration`);
            }

            navTab.label = customTab.label;

            if (hasCustomNavTabConfig) {
                (navTab as typeof navTab & { custom?: boolean }).custom = true;
            }

            // is tab applicable to target dependency?
            if (-1 === _.findIndex(navTab.depTypes, matchDepType)) {
                return;
            }

            // global config
            if (customTab.id === 'tree' && Configuration.mainData.disableDomTree) {
                return;
            }
            if (customTab.id === 'source' && Configuration.mainData.disableSourceCode) {
                return;
            }
            if (customTab.id === 'templateData' && Configuration.mainData.disableTemplateTab) {
                return;
            }
            if (customTab.id === 'styleData' && Configuration.mainData.disableStyleTab) {
                return;
            }

            // per dependency config
            if (customTab.id === 'readme' && !dependency.readme) {
                return;
            }
            if (customTab.id === 'example' && !dependency.exampleUrls) {
                return;
            }
            if (
                customTab.id === 'templateData' &&
                (!dependency.templateUrl || dependency.templateUrl.length === 0)
            ) {
                return;
            }
            if (
                customTab.id === 'styleData' &&
                (!dependency.styleUrls || dependency.styleUrls.length === 0) &&
                (!dependency.styles || dependency.styles.length === 0)
            ) {
                return;
            }
            if (customTab.id === 'api' && !this.hasApiSections(dependency)) {
                return;
            }

            navTabs.push(navTab);
        });

        if (navTabs.length === 0) {
            throw new Error(`No valid navigation tabs have been defined for dependency type '${dependency.type}'. Specify \
at least one config for the 'info' or 'source' tab in --navTabConfig.`);
        }

        return navTabs;
    }

    public prepareControllers(someControllers?: any) {
        logger.info('Prepare controllers');
        Configuration.mainData.controllers = someControllers
            ? someControllers
            : DependenciesEngine.getControllers();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.controllers.length;
            const loop = () => {
                if (i < len) {
                    const controller = Configuration.mainData.controllers[i];
                    controller.relationships = this.getRelationships(controller);
                    const page = {
                        path: 'controllers',
                        name: controller.name,
                        id: controller.id,
                        navTabs: this.getNavTabs(controller),
                        context: 'controller',
                        controller: controller,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (controller.isDuplicate) {
                        page.name += '-' + controller.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    }

    public prepareEntities(someEntities?: any) {
        logger.info('Prepare entities');
        Configuration.mainData.entities = someEntities
            ? someEntities
            : DependenciesEngine.getEntities();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.entities.length;
            const loop = () => {
                if (i < len) {
                    let entity = Configuration.mainData.entities[i];
                    entity.relationships = this.getRelationships(entity);
                    let page = {
                        path: 'entities',
                        name: entity.name,
                        id: entity.id,
                        navTabs: this.getNavTabs(entity),
                        context: 'entity',
                        entity: entity,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (entity.isDuplicate) {
                        page.name += '-' + entity.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    }

    public prepareComponents(someComponents?: any) {
        logger.info('Prepare components');
        Configuration.mainData.components = someComponents
            ? someComponents
            : DependenciesEngine.getComponents();

        return new Promise((mainPrepareComponentResolve, mainPrepareComponentReject) => {
            let i = 0;
            const len = Configuration.mainData.components.length;
            const loop = () => {
                if (i <= len - 1) {
                    const component = Configuration.mainData.components[i];
                    if (MarkdownEngine.hasNeighbourReadmeFile(component.file)) {
                        logger.info(` ${component.name} has a README file, include it`);
                        const readmeFile = MarkdownEngine.readNeighbourReadmeFile(component.file);
                        component.readme = markedAcl(readmeFile);
                    }
                    component.relationships = this.getRelationships(component);
                    const page = {
                        path: 'components',
                        name: component.name,
                        id: component.id,
                        navTabs: this.getNavTabs(component),
                        context: 'component',
                        component: component,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };

                    if (component.isDuplicate) {
                        page.name += '-' + component.duplicateId;
                    }
                    Configuration.addPage(page);

                    const componentTemplateUrlPromise = new Promise(
                        (componentTemplateUrlResolve, componentTemplateUrlReject) => {
                            if (component.templateUrl.length > 0) {
                                logger.info(` ${component.name} has a templateUrl, include it`);
                                this.handleTemplateurl(component).then(
                                    () => {
                                        componentTemplateUrlResolve(true);
                                    },
                                    e => {
                                        logger.error(e);
                                        componentTemplateUrlReject();
                                    }
                                );
                            } else {
                                componentTemplateUrlResolve(true);
                            }
                        }
                    );
                    const componentStyleUrlsPromise = new Promise(
                        (componentStyleUrlsResolve, componentStyleUrlsReject) => {
                            if (component.styleUrls.length > 0) {
                                logger.info(` ${component.name} has styleUrls, include them`);
                                this.handleStyleurls(component).then(
                                    () => {
                                        componentStyleUrlsResolve(true);
                                    },
                                    e => {
                                        logger.error(e);
                                        componentStyleUrlsReject();
                                    }
                                );
                            } else {
                                componentStyleUrlsResolve(true);
                            }
                        }
                    );
                    const componentStylesPromise = new Promise(
                        (componentStylesResolve, componentStylesReject) => {
                            if (component.styles.length > 0) {
                                logger.info(` ${component.name} has styles, include them`);
                                this.handleStyles(component).then(
                                    () => {
                                        componentStylesResolve(true);
                                    },
                                    e => {
                                        logger.error(e);
                                        componentStylesReject();
                                    }
                                );
                            } else {
                                componentStylesResolve(true);
                            }
                        }
                    );

                    Promise.all([
                        componentTemplateUrlPromise,
                        componentStyleUrlsPromise,
                        componentStylesPromise
                    ]).then(() => {
                        i++;
                        loop();
                    });
                } else {
                    mainPrepareComponentResolve(true);
                }
            };
            loop();
        });
    }

    public prepareDirectives(someDirectives?: any) {
        logger.info('Prepare directives');

        Configuration.mainData.directives = someDirectives
            ? someDirectives
            : DependenciesEngine.getDirectives();

        return new Promise((resolve, reject) => {
            let i = 0;
            let len = Configuration.mainData.directives.length;
            let loop = () => {
                if (i < len) {
                    let directive = Configuration.mainData.directives[i];
                    directive.relationships = this.getRelationships(directive);
                    if (MarkdownEngine.hasNeighbourReadmeFile(directive.file)) {
                        logger.info(` ${directive.name} has a README file, include it`);
                        let readme = MarkdownEngine.readNeighbourReadmeFile(directive.file);
                        directive.readme = markedAcl(readme);
                    }
                    let page = {
                        path: 'directives',
                        name: directive.name,
                        id: directive.id,
                        navTabs: this.getNavTabs(directive),
                        context: 'directive',
                        directive: directive,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (directive.isDuplicate) {
                        page.name += '-' + directive.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve(true);
                }
            };
            loop();
        });
    }

    public prepareInjectables(someInjectables?: any): Promise<void> {
        logger.info('Prepare injectables');

        Configuration.mainData.injectables = someInjectables
            ? someInjectables
            : DependenciesEngine.getInjectables();

        return new Promise((resolve, reject) => {
            let i = 0;
            let len = Configuration.mainData.injectables.length;
            let loop = () => {
                if (i < len) {
                    let injec = Configuration.mainData.injectables[i];
                    injec.relationships = this.getRelationships(injec);
                    if (MarkdownEngine.hasNeighbourReadmeFile(injec.file)) {
                        logger.info(` ${injec.name} has a README file, include it`);
                        let readme = MarkdownEngine.readNeighbourReadmeFile(injec.file);
                        injec.readme = markedAcl(readme);
                    }
                    let page = {
                        path: 'injectables',
                        name: injec.name,
                        id: injec.id,
                        navTabs: this.getNavTabs(injec),
                        context: 'injectable',
                        injectable: injec,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (injec.isDuplicate) {
                        page.name += '-' + injec.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve();
                }
            };
            loop();
        });
    }

    public prepareInterceptors(someInterceptors?: any): Promise<void> {
        logger.info('Prepare interceptors');

        Configuration.mainData.interceptors = someInterceptors
            ? someInterceptors
            : DependenciesEngine.getInterceptors();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.interceptors.length;
            const loop = () => {
                if (i < len) {
                    const interceptor = Configuration.mainData.interceptors[i];
                    interceptor.relationships = this.getRelationships(interceptor);
                    if (MarkdownEngine.hasNeighbourReadmeFile(interceptor.file)) {
                        logger.info(` ${interceptor.name} has a README file, include it`);
                        const readme = MarkdownEngine.readNeighbourReadmeFile(interceptor.file);
                        interceptor.readme = markedAcl(readme);
                    }
                    const page = {
                        path: 'interceptors',
                        name: interceptor.name,
                        id: interceptor.id,
                        navTabs: this.getNavTabs(interceptor),
                        context: 'interceptor',
                        injectable: interceptor,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (interceptor.isDuplicate) {
                        page.name += '-' + interceptor.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve();
                }
            };
            loop();
        });
    }

    public prepareGuards(someGuards?: any): Promise<void> {
        logger.info('Prepare guards');

        Configuration.mainData.guards = someGuards ? someGuards : DependenciesEngine.getGuards();

        return new Promise((resolve, reject) => {
            let i = 0;
            const len = Configuration.mainData.guards.length;
            const loop = () => {
                if (i < len) {
                    const guard = Configuration.mainData.guards[i];
                    guard.relationships = this.getRelationships(guard);
                    if (MarkdownEngine.hasNeighbourReadmeFile(guard.file)) {
                        logger.info(` ${guard.name} has a README file, include it`);
                        const readme = MarkdownEngine.readNeighbourReadmeFile(guard.file);
                        guard.readme = markedAcl(readme);
                    }
                    const page = {
                        path: 'guards',
                        name: guard.name,
                        id: guard.id,
                        navTabs: this.getNavTabs(guard),
                        context: 'guard',
                        injectable: guard,
                        depth: 1,
                        pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
                    };
                    if (guard.isDuplicate) {
                        page.name += '-' + guard.duplicateId;
                    }
                    Configuration.addPage(page);
                    i++;
                    loop();
                } else {
                    resolve();
                }
            };
            loop();
        });
    }

    public prepareRoutes(): Promise<void> {
        logger.info('Process routes');
        Configuration.mainData.routes = DependenciesEngine.getRoutes();

        return new Promise((resolve, reject) => {
            Configuration.addPage({
                name: 'routes',
                id: 'routes',
                context: 'routes',
                depth: 0,
                pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
            });

            if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) {
                RouterParserUtil.generateRoutesIndex(
                    Configuration.mainData.output,
                    Configuration.mainData.routes
                ).then(
                    () => {
                        logger.info(' Routes index generated');
                        resolve();
                    },
                    e => {
                        logger.error(e);
                        reject();
                    }
                );
            } else {
                resolve();
            }
        });
    }

    public prepareCoverage() {
        const coverageUseCase = new DocumentationCoverageUseCase(
            generationPromiseResolve,
            generationPromiseReject
        );

        return coverageUseCase.execute();
    }

    public prepareUnitTestCoverage() {
        logger.info('Process unit test coverage report');
        return new Promise((resolve, reject) => {
            let covDat: AnyRecord | undefined;
            let covFileNames: string[] | undefined;
            const normalizeCoveragePath = (filePath: string): string => {
                return path
                    .normalize(filePath)
                    .replace(/\\/g, '/')
                    .replace(/^[A-Za-z]:\//, '/')
                    .replace(/\/+/g, '/')
                    .toLowerCase();
            };
            const getSourceRelativePath = (normalizedFilePath: string): string => {
                const sourceRootIndex = normalizedFilePath.lastIndexOf('/src/');
                if (sourceRootIndex === -1) {
                    return normalizedFilePath;
                }
                return normalizedFilePath.slice(sourceRootIndex + 1);
            };
            const isCoveragePathMatch = (
                sourceFilePath: string,
                coverageFilePath: string
            ): boolean => {
                const normalizedSourcePath = normalizeCoveragePath(sourceFilePath);
                const normalizedCoveragePath = normalizeCoveragePath(coverageFilePath);

                if (
                    normalizedSourcePath === normalizedCoveragePath ||
                    normalizedSourcePath.endsWith(normalizedCoveragePath) ||
                    normalizedCoveragePath.endsWith(normalizedSourcePath)
                ) {
                    return true;
                }

                const sourceRelativePath = getSourceRelativePath(normalizedSourcePath);
                const coverageRelativePath = getSourceRelativePath(normalizedCoveragePath);

                return (
                    sourceRelativePath === coverageRelativePath ||
                    sourceRelativePath.endsWith(coverageRelativePath) ||
                    coverageRelativePath.endsWith(sourceRelativePath)
                );
            };

            let coverageData: CoverageData = Configuration.mainData.coverageData;

            if (!coverageData.files) {
                logger.warn('Missing documentation coverage data');
            } else {
                covDat = {};
                covFileNames = _.map(coverageData.files, (el: any) => {
                    let fileName = path.normalize(el.filePath);
                    covDat![fileName] = {
                        type: el.type,
                        linktype: el.linktype,
                        linksubtype: el.linksubtype,
                        name: el.name
                    };
                    return fileName;
                });
            }
            // read coverage summary file and data
            let unitTestSummary: AnyRecord = {};
            let fileDat = FileEngine.getSync(Configuration.mainData.unitTestCoverage);
            if (fileDat) {
                unitTestSummary = JSON.parse(fileDat);
            } else {
                return Promise.reject('Error reading unit test coverage file');
            }
            let getCovStatus = function (percent: number, totalLines: number) {
                let status;
                if (totalLines === 0) {
                    status = 'uncovered';
                } else if (percent <= 25) {
                    status = 'low';
                } else if (percent > 25 && percent <= 50) {
                    status = 'medium';
                } else if (percent > 50 && percent <= 75) {
                    status = 'good';
                } else {
                    status = 'very-good';
                }
                return status;
            };
            let getCoverageData = function (data: any, fileName: string) {
                let out: AnyRecord = {};
                if (fileName !== 'total') {
                    if (covDat === undefined) {
                        // need a name to include in output but this isn't visible
                        out = { name: fileName, filePath: fileName };
                    } else {
                        const findMatch = _.filter(covFileNames, (el: string) => {
                            return isCoveragePathMatch(el, fileName);
                        });
                        if (findMatch.length > 0) {
                            out = _.clone(covDat[findMatch[0]]);
                            out['filePath'] = fileName;
                        }
                    }
                }
                let keysToGet = ['statements', 'branches', 'functions', 'lines'];
                _.forEach(keysToGet, (key: string) => {
                    if (data[key]) {
                        let t = data[key];
                        out[key] = {
                            coveragePercent: Math.round(t.pct),
                            coverageCount: '' + t.covered + '/' + t.total,
                            status: getCovStatus(t.pct, t.total)
                        };
                    }
                });
                return out;
            };

            let unitTestData: AnyRecord = {};
            let files: any[] = [];
            for (let file in unitTestSummary) {
                let dat = getCoverageData(unitTestSummary[file], file);
                if (file === 'total') {
                    unitTestData['total'] = dat;
                } else {
                    files.push(dat);
                }
            }
            unitTestData['files'] = files;
            unitTestData['idColumn'] = covDat !== undefined; // should we include the id column
            Configuration.mainData.unitTestData = unitTestData;
            Configuration.addPage({
                name: 'unit-test',
                id: 'unit-test',
                context: 'unit-test',
                files: files,
                data: unitTestData,
                depth: 0,
                pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
            });

            if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) {
                let keysToGet = ['statements', 'branches', 'functions', 'lines'];
                _.forEach(keysToGet, (key: string) => {
                    if (unitTestData['total'][key]) {
                        HtmlEngine.generateCoverageBadge(Configuration.mainData.output, key, {
                            count: unitTestData['total'][key]['coveragePercent'],
                            status: unitTestData['total'][key]['status']
                        });
                    }
                });
            }
            resolve(true);
        });
    }

    private processPage(page: any): Promise<void> {
        logger.info('Process page', page.name);

        let htmlData = HtmlEngine.render(Configuration.mainData, page);
        let finalPath = Configuration.mainData.output;

        if (Configuration.mainData.output.lastIndexOf('/') === -1) {
            finalPath += '/';
        }
        if (page.path) {
            finalPath += page.path + '/';
        }

        if (page.filename) {
            finalPath += page.filename + '.html';
        } else {
            finalPath += page.name + '.html';
        }

        if (!Configuration.mainData.disableSearch) {
            SearchEngine.indexPage({
                infos: page,
                rawData: htmlData,
                url: finalPath
            });
        }

        FileEngine.writeSync(finalPath, htmlData);
        return Promise.resolve();
    }

    private processTemplatePlayground(): void {
        logger.info('Process template playground');

        // Create template playground page
        const templatePlaygroundPage = {
            name: 'template-playground',
            filename: 'template-playground',
            context: 'template-playground',
            depth: 0,
            pageType: 'template-playground'
        };

        // Generate a comprehensive template playground page with all required dependencies
        const htmlContent = `<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Template Playground - ${Configuration.mainData.documentationMainName || 'Documentation'}</title>
    <meta name="description" content="Template playground for customizing Compodoc templates">
    <link rel="stylesheet" href="styles/template-playground.css">
    <script src="js/libs/monaco-editor/min/vs/loader.js"></script>
    <script src="js/libs/handlebars.min.js"></script>
    <script src="js/libs/jszip.min.js"></script>
</head>
<body>
    <div id="template-playground-root">
        <div class="template-playground-container">
            <h1>Template Playground</h1>
            <p>Welcome to the Template Playground! This tool allows you to customize and preview Compodoc templates.</p>
            
            <div class="features">
                <h2>Features</h2>
                <ul>
                    <li>Live template editing with monaco-editor</li>
                    <li>Real-time preview using handlebars.min.js</li>
                    <li>Template export and download with jszip.min.js</li>
                    <li>Session-based customization</li>
                </ul>
            </div>
            
            <div class="usage">
                <h2>How to Use</h2>
                <ol>
                    <li>Start the template playground server</li>
                    <li>Edit templates in the Monaco editor</li>
                    <li>Preview changes in real-time</li>
                    <li>Download customized template package</li>
                </ol>
            </div>
        </div>
    </div>
    
    <script src="js/template-playground.js"></script>
</body>
</html>`;

        let finalPath = Configuration.mainData.output;

        if (Configuration.mainData.output.lastIndexOf('/') === -1) {
            finalPath += '/';
        }
        finalPath += 'template-playground.html';

        FileEngine.writeSync(finalPath, htmlContent);
        logger.info('Template playground page generated');

        // Generate required JavaScript file
        const jsPath = path.join(Configuration.mainData.output, 'js', 'template-playground.js');
        const jsContent = `// Template Playground JavaScript
(function() {
    'use strict';
    
    // Initialize template playground
    document.addEventListener('DOMContentLoaded', function() {
        console.log('Template Playground initialized');
        
        // Initialize Monaco Editor when available
        if (typeof require !== 'undefined') {
            require.config({ paths: { 'vs': 'js/libs/monaco-editor/min/vs' }});
            require(['vs/editor/editor.main'], function() {
                console.log('Monaco Editor loaded');
            });
        }
        
        // Initialize Handlebars when available
        if (typeof Handlebars !== 'undefined') {
            console.log('Handlebars loaded');
        }
        
        // Initialize JSZip when available
        if (typeof JSZip !== 'undefined') {
            console.log('JSZip loaded');
        }
    });
})();`;

        // Ensure js directory exists
        const jsDir = path.join(Configuration.mainData.output, 'js');
        if (!fs.existsSync(jsDir)) {
            fs.mkdirSync(jsDir, { recursive: true });
        }
        FileEngine.writeSync(jsPath, jsContent);
        logger.info('Template playground JavaScript generated');

        // Generate required CSS file
        const cssPath = path.join(
            Configuration.mainData.output,
            'styles',
            'template-playground.css'
        );
        const cssContent = `/* Template Playground Styles */
.template-playground-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

#template-playground-root {
    min-height: 100vh;
    background: #f8f9fa;
}

.template-playground-container h1 {
    color: #2c3e50;
    border-bottom: 2px solid #3498db;
    padding-bottom: 10px;
}

.features, .usage {
    background: white;
    padding: 20px;
    margin: 20px 0;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.features h2, .usage h2 {
    color: #34495e;
    margin-top: 0;
}

.features ul, .usage ol {
    line-height: 1.6;
}

.features li, .usage li {
    margin: 8px 0;
}`;

        // Ensure styles directory exists
        const stylesDir = path.join(Configuration.mainData.output, 'styles');
        if (!fs.existsSync(stylesDir)) {
            fs.mkdirSync(stylesDir, { recursive: true });
        }
        FileEngine.writeSync(cssPath, cssContent);
        logger.info('Template playground CSS generated');
    }

    public processPages() {
        let pages = _.sortBy(Configuration.pages, ['name']);

        logger.info('Process pages');
        Promise.all(pages.map(page => this.processPage(page)))
            .then(() => {
                let callbacksAfterGenerateSearchIndexJson = () => {
                    // Process template playground if enabled
                    if (Configuration.mainData.templatePlayground) {
                        this.processTemplatePlayground();
                    }

                    if (Configuration.mainData.additionalPages.length > 0) {
                        this.processAdditionalPages();
                    } else {
                        if (Configuration.mainData.assetsFolder !== '') {
                            this.processAssetsFolder();
                        }
                        this.processResources();
                    }
                };
                if (!Configuration.mainData.disableSearch) {
                    SearchEngine.generateSearchIndexJson(Configuration.mainData.output).then(
                        () => {
                            callbacksAfterGenerateSearchIndexJson();
                        },
                        (e: any) => {
                            logger.error(e);
                        }
                    );
                } else {
                    callbacksAfterGenerateSearchIndexJson();
                }
            })
            .then(() => {
                return this.processMenu(Configuration.mainData);
            })
            .catch((e: any) => {
                logger.error(e);
            });
    }

    private processMenu(mainData: any): Promise<void> {
        logger.info('Process menu...');

        return new Promise((resolveProcessMenu, rejectProcessMenu) => {
            let output = mainData.output.slice();
            const outputLastCharacter = output.lastIndexOf('/');
            if (outputLastCharacter !== -1) {
                output = output.slice(0, -1);
            }
            const finalPathES6 = `${output}/js/menu-wc.js`;

            HtmlEngine.renderMenu(Configuration.mainData.templates, mainData)
                .then(htmlData => {
                    FileEngine.write(finalPathES6, htmlData)
                        .then(() => {
                            resolveProcessMenu();
                        })
                        .catch(err => {
                            logger.error('Error during ' + finalPathES6 + ' page generation');
                            logger.error(err);
                            return rejectProcessMenu('');
                        });
                })
                .catch(err => {
                    logger.error('Error during ' + finalPathES6 + ' page generation');
                    logger.error(err);
                    return rejectProcessMenu('');
                });
        });
    }

    public processAdditionalPages() {
        logger.info('Process additional pages');
        let pages = Configuration.mainData.additionalPages;
        Promise.all(
            pages.map((page: any) => {
                if (page.children.length > 0) {
                    return Promise.all([
                        this.processPage(page),
                        ...page.children.map((childPage: any) => this.processPage(childPage))
                    ]);
                } else {
                    return this.processPage(page);
                }
            })
        )
            .then(() => {
                SearchEngine.generateSearchIndexJson(Configuration.mainData.output).then(() => {
                    if (Configuration.mainData.assetsFolder !== '') {
                        this.processAssetsFolder();
                    }
                    this.processResources();
                });
            })
            .catch(e => {
                logger.error(e);
                return Promise.reject(e);
            });
    }

    public processAssetsFolder(): void {
        logger.info('Copy assets folder');

        if (!FileEngine.existsSync(Configuration.mainData.assetsFolder)) {
            logger.error(
                `Provided assets folder ${Configuration.mainData.assetsFolder} did not exist`
            );
        } else {
            let finalOutput = Configuration.mainData.output;

            let testOutputDir = Configuration.mainData.output.match(cwd);

            if (testOutputDir && testOutputDir.length > 0) {
                finalOutput = Configuration.mainData.output.replace(cwd + path.sep, '');
            }

            const destination = path.join(
                finalOutput,
                path.basename(Configuration.mainData.assetsFolder)
            );
            fs.copy(
                path.resolve(Configuration.mainData.assetsFolder),
                path.resolve(destination),
                err => {
                    if (err) {
                        logger.error('Error during resources copy ', err);
                    }
                }
            );
        }
    }

    public processResources() {
        logger.info('Copy main resources');

        const onComplete = () => {
            logger.info(
                'Documentation generated in ' +
                    Configuration.mainData.output +
                    ' in ' +
                    this.getElapsedTime() +
                    ' seconds using ' +
                    Configuration.mainData.theme +
                    ' theme'
            );
            if (Configuration.mainData.serve) {
                logger.info(
                    `Serving documentation from ${Configuration.mainData.output} at http://${Configuration.mainData.hostname}:${Configuration.mainData.port}`
                );
                this.runWebServer(Configuration.mainData.output);
            } else {
                generationPromiseResolve(true);
                this.endCallback();
            }
        };

        let finalOutput = Configuration.mainData.output;

        let testOutputDir = Configuration.mainData.output.match(cwd);

        if (testOutputDir && testOutputDir.length > 0) {
            finalOutput = Configuration.mainData.output.replace(cwd + path.sep, '');
        }

        fs.copy(
            path.resolve(__dirname + '/../src/resources/'),
            path.resolve(finalOutput),
            errorCopy => {
                if (errorCopy) {
                    logger.error('Error during resources copy ', errorCopy);
                } else {
                    const extThemePromise = new Promise((extThemeResolve, extThemeReject) => {
                        if (Configuration.mainData.extTheme) {
                            fs.copy(
                                path.resolve(cwd + path.sep + Configuration.mainData.extTheme),
                                path.resolve(finalOutput + '/styles/'),
                                function (errorCopyTheme) {
                                    if (errorCopyTheme) {
                                        logger.error(
                                            'Error during external styling theme copy ',
                                            errorCopyTheme
                                        );
                                        extThemeReject();
                                    } else {
                                        logger.info('External styling theme copy succeeded');
                                        extThemeResolve(true);
                                    }
                                }
                            );
                        } else {
                            extThemeResolve(true);
                        }
                    });

                    const customFaviconPromise = new Promise(
                        (customFaviconResolve, customFaviconReject) => {
                            if (Configuration.mainData.customFavicon !== '') {
                                logger.info(`Custom favicon supplied`);
                                fs.copy(
                                    path.resolve(
                                        cwd + path.sep + Configuration.mainData.customFavicon
                                    ),
                                    path.resolve(finalOutput + '/images/favicon.ico'),
                                    errorCopyFavicon => {
                                        // tslint:disable-line
                                        if (errorCopyFavicon) {
                                            logger.error(
                                                'Error during resources copy of favicon',
                                                errorCopyFavicon
                                            );
                                            customFaviconReject();
                                        } else {
                                            logger.info('External custom favicon copy succeeded');
                                            customFaviconResolve(true);
                                        }
                                    }
                                );
                            } else {
                                customFaviconResolve(true);
                            }
                        }
                    );

                    const customLogoPromise = new Promise((customLogoResolve, customLogoReject) => {
                        if (Configuration.mainData.customLogo !== '') {
                            logger.info(`Custom logo supplied`);
                            fs.copy(
                                path.resolve(cwd + path.sep + Configuration.mainData.customLogo),
                                path.resolve(
                                    finalOutput +
                                        '/images/' +
                                        Configuration.mainData.customLogo.split('/').pop()
                                ),
                                errorCopyLogo => {
                                    // tslint:disable-line
                                    if (errorCopyLogo) {
                                        logger.error(
                                            'Error during resources copy of logo',
                                            errorCopyLogo
                                        );
                                        customLogoReject();
                                    } else {
                                        logger.info('External custom logo copy succeeded');
                                        customLogoResolve(true);
                                    }
                                }
                            );
                        } else {
                            customLogoResolve(true);
                        }
                    });

                    Promise.all([extThemePromise, customFaviconPromise, customLogoPromise]).then(
                        () => {
                            this.writeLlmsTxt(path.resolve(finalOutput)).then(
                                onComplete,
                                onComplete
                            );
                        }
                    );
                }
            }
        );
    }

    /**
     * Calculates the elapsed time since the program was started.
     *
     * @returns {number}
     */
    private getElapsedTime() {
        return (new Date().valueOf() - startTime.valueOf()) / 1000;
    }

    private writeLlmsTxt(outputFolder: string): Promise<void> {
        const pkg = require('../../package.json');
        const data = Configuration.mainData;
        const exportData: any = {
            pipes: data.pipes,
            interfaces: data.interfaces,
            injectables: data.injectables,
            guards: data.guards,
            interceptors: data.interceptors,
            classes: data.classes,
            directives: data.directives,
            components: data.components,
            modules: ExportJsonEngine.processModules(),
            miscellaneous: data.miscellaneous
        };
        const markdown = renderLlmMd(
            exportData,
            {
                projectName: data.documentationMainName || 'Application documentation',
                projectDescription: data.documentationMainDescription
            },
            pkg.version
        );
        return FileEngine.write(path.join(outputFolder, 'llms.txt'), markdown).then(() => {
            logger.info(`llms.txt written to ${outputFolder}`);
        });
    }

    private extractDepName(dep: any): string | undefined {
        if (!dep) {
            return undefined;
        }
        if (typeof dep === 'string') {
            return dep;
        }
        if (typeof dep.name === 'string') {
            return dep.name;
        }
        return undefined;
    }

    private buildArchitectureGraph() {
        const components = (Configuration.mainData.components as any[]) || [];
        const directives = (Configuration.mainData.directives as any[]) || [];
        const pipes = (Configuration.mainData.pipes as any[]) || [];
        const modules = (Configuration.mainData.modules as any[]) || [];
        const injectables = (Configuration.mainData.injectables as any[]) || [];

        const entityMap = new Map<
            string,
            { id: string; name: string; type: string; url?: string }
        >();
        const addEntity = (items: any[], type: string, routePrefix: string) => {
            for (const item of items) {
                if (!item?.name || entityMap.has(item.name)) {
                    continue;
                }
                const slug = item.duplicateName || item.name;
                entityMap.set(item.name, {
                    id: `${type}:${item.name}`,
                    name: item.name,
                    type,
                    url: `${routePrefix}${slug}.html`
                });
            }
        };

        addEntity(components, 'component', './components/');
        addEntity(directives, 'directive', './directives/');
        addEntity(pipes, 'pipe', './pipes/');
        addEntity(modules, 'module', './modules/');
        addEntity(injectables, 'injectable', './injectables/');

        const usedNodeNames = new Set<string>();
        const edgeKeys = new Set<string>();
        const edges: Array<{ source: string; target: string }> = [];

        const addEdge = (sourceName?: string, targetName?: string) => {
            if (!sourceName || !targetName || sourceName === targetName) {
                return;
            }
            const source = entityMap.get(sourceName);
            const target = entityMap.get(targetName);
            if (!source || !target) {
                return;
            }
            const edgeKey = `${source.id}->${target.id}`;
            if (edgeKeys.has(edgeKey)) {
                return;
            }
            edgeKeys.add(edgeKey);
            usedNodeNames.add(sourceName);
            usedNodeNames.add(targetName);
            edges.push({ source: source.id, target: target.id });
        };

        for (const module of modules) {
            for (const dependency of module.imports || []) {
                addEdge(module.name, this.extractDepName(dependency));
            }
            for (const declaration of module.declarations || []) {
                addEdge(module.name, this.extractDepName(declaration));
            }
            for (const exported of module.exports || []) {
                addEdge(module.name, this.extractDepName(exported));
            }
            for (const bootstrap of module.bootstrap || []) {
                addEdge(module.name, this.extractDepName(bootstrap));
            }
            for (const provider of module.providers || []) {
                addEdge(module.name, this.extractDepName(provider));
            }
        }

        for (const component of components) {
            if (component.standalone) {
                for (const dependency of component.imports || []) {
                    addEdge(component.name, this.extractDepName(dependency));
                }
            }
            for (const provider of component.providers || []) {
                addEdge(component.name, this.extractDepName(provider));
            }
        }

        for (const directive of directives) {
            for (const provider of directive.providers || []) {
                addEdge(directive.name, this.extractDepName(provider));
            }
        }

        const nodes = Array.from(usedNodeNames)
            .map(name => entityMap.get(name))
            .filter(
                (
                    node
                ): node is {
                    id: string;
                    name: string;
                    type: string;
                    url?: string;
                } => !!node
            );

        Configuration.mainData.dependencyGraph = { nodes, edges };
        Configuration.mainData.dependencyGraphSerialized = JSON.stringify({
            nodes,
            edges
        });
    }

    public processGraphs() {
        this.buildArchitectureGraph();
        if (Configuration.mainData.disableGraph) {
            logger.info('Graph generation disabled');
            this.processPages();
        } else {
            logger.info('Process main graph');
            let modules = Configuration.mainData.modules;
            let i = 0;
            let len = modules.length;
            let loop = () => {
                if (i <= len - 1) {
                    logger.info('Process module graph ', modules[i].name);
                    let finalPath = Configuration.mainData.output;
                    if (Configuration.mainData.output.lastIndexOf('/') === -1) {
                        finalPath += '/';
                    }
                    finalPath += 'modules/' + modules[i].name;
                    let _rawModule = DependenciesEngine.getRawModule(modules[i].name);
                    if (
                        _rawModule.declarations.length > 0 ||
                        _rawModule.bootstrap.length > 0 ||
                        _rawModule.imports.length > 0 ||
                        _rawModule.exports.length > 0 ||
                        _rawModule.providers.length > 0
                    ) {
                        NgdEngine.renderGraph(
                            modules[i].file,
                            finalPath,
                            'f',
                            modules[i].name
                        ).then(
                            () => {
                                NgdEngine.readGraph(
                                    path.resolve(finalPath + path.sep + 'dependencies.svg'),
                                    modules[i].name,
                                    'module'
                                ).then(
                                    data => {
                                        modules[i].graph = data;
                                        i++;
                                        loop();
                                    },
                                    err => {
                                        logger.error('Error during graph read: ', err);
                                    }
                                );
                            },
                            (errorMessage: any) => {
                                logger.error(errorMessage);
                            }
                        );
                    } else {
                        i++;
                        loop();
                    }
                } else {
                    this.processPages();
                }
            };
            let finalMainGraphPath = Configuration.mainData.output;
            if (finalMainGraphPath.lastIndexOf('/') === -1) {
                finalMainGraphPath += '/';
            }
            finalMainGraphPath += 'graph';
            NgdEngine.init(path.resolve(finalMainGraphPath));

            NgdEngine.renderGraph(
                Configuration.mainData.tsconfig,
                path.resolve(finalMainGraphPath),
                'p'
            ).then(
                () => {
                    NgdEngine.readGraph(
                        path.resolve(finalMainGraphPath + path.sep + 'dependencies.svg'),
                        'Main graph',
                        'overview'
                    ).then(
                        data => {
                            Configuration.mainData.mainGraph = data;
                            loop();
                        },
                        err => {
                            logger.error('Error during main graph reading : ', err);
                            Configuration.mainData.disableMainGraph = true;
                            loop();
                        }
                    );
                },
                (err: any) => {
                    logger.error(
                        'Ooops error during main graph generation, moving on next part with main graph disabled : ',
                        err
                    );
                    Configuration.mainData.disableMainGraph = true;
                    loop();
                }
            );
        }
    }

    public runWebServer(folder: string) {
        if (!this.isWatching) {
            let documentationServerConfiguration: DocumentationServerConfiguration = {
                root: folder,
                open: Configuration.mainData.open,
                port: Configuration.mainData.port
            };
            if (Configuration.mainData.host !== '') {
                documentationServerConfiguration.host = Configuration.mainData.host;
            }
            startDocumentationServer(documentationServerConfiguration);
        }
        if (Configuration.mainData.watch && !this.isWatching) {
            if (typeof this.files === 'undefined') {
                logger.error('No sources files available, please use -p flag');
                generationPromiseReject();
                process.exit(1);
            } else {
                this.runWatch();
            }
        } else if (Configuration.mainData.watch && this.isWatching) {
            let srcFolder = findMainSourceFolder(this.files);
            logger.info(`Already watching sources in ${srcFolder} folder`);
        }
    }

    public runWatch() {
        let sources = [findMainSourceFolder(this.files)];
        let watcherReady = false;

        this.isWatching = true;

        logger.info(`Watching sources in ${findMainSourceFolder(this.files)} folder`);

        if (MarkdownEngine.hasRootMarkdowns()) {
            sources = sources.concat(MarkdownEngine.listRootMarkdowns());
        }

        if (Configuration.mainData.includes !== '') {
            sources = sources.concat(Configuration.mainData.includes);
        }

        // Check all elements of sources list exist
        sources = cleanSourcesForWatch(sources);

        let watcher = chokidar.watch(sources, {
            awaitWriteFinish: true,
            ignoreInitial: true,
            ignored: /(spec|\.d)\.ts/
        });
        let timerAddAndRemoveRef: ReturnType<typeof setTimeout>;
        let timerChangeRef: ReturnType<typeof setTimeout>;
        let runnerAddAndRemove = () => {
            startTime = new Date();
            this.generate();
        };
        let waiterAddAndRemove = () => {
            clearTimeout(timerAddAndRemoveRef);
            timerAddAndRemoveRef = setTimeout(runnerAddAndRemove, 1000);
        };
        let runnerChange = () => {
            startTime = new Date();
            this.setUpdatedFiles(this.watchChangedFiles);
            if (this.hasWatchedFilesTSFiles()) {
                this.getMicroDependenciesData();
            } else if (this.hasWatchedFilesRootMarkdownFiles()) {
                this.rebuildRootMarkdowns();
            } else {
                this.rebuildExternalDocumentation();
            }
        };
        let waiterChange = () => {
            clearTimeout(timerChangeRef);
            timerChangeRef = setTimeout(runnerChange, 1000);
        };

        watcher.on('ready', () => {
            if (!watcherReady) {
                watcherReady = true;
                watcher
                    .on('add', (file: string) => {
                        logger.debug(`File ${file} has been added`);
                        // Test extension, if ts
                        // rescan everything
                        if (path.extname(file) === '.ts') {
                            waiterAddAndRemove();
                        }
                    })
                    .on('change', (file: string) => {
                        logger.debug(`File ${file} has been changed`);
                        // Test extension, if ts
                        // rescan only file
                        if (
                            path.extname(file) === '.ts' ||
                            path.extname(file) === '.md' ||
                            path.extname(file) === '.json'
                        ) {
                            this.watchChangedFiles.push(normalizeWatchFilePath(file, cwd));
                            waiterChange();
                        }
                    })
                    .on('unlink', (file: string) => {
                        logger.debug(`File ${file} has been removed`);
                        // Test extension, if ts
                        // rescan everything
                        if (path.extname(file) === '.ts') {
                            waiterAddAndRemove();
                        }
                    });
            }
        });
    }

    /**
     * Return the application / root component instance.
     */
    get application(): Application {
        return this;
    }

    get isCLI(): boolean {
        return false;
    }
}
