'use strict';

import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import * as assert from 'assert';
import * as glob from 'glob';
import * as mkdirp from 'mkdirp';
import * as detectIndent from 'detect-indent';

let pkg = require('../package');

const dtsExp = /\.d\.ts$/;
const bomOptExp = /^\uFEFF?/;

const externalExp = /^([ \t]*declare module )(['"])(.+?)(\2[ \t]*{?.*)$/;
const importExp = /^([ \t]*(?:export )?(?:import .+? )= require\()(['"])(.+?)(\2\);.*)$/;
const importEs6Exp = /^([ \t]*(?:export|import) ?(?:(?:\* (?:as [^ ,]+)?)|.*)?,? ?(?:[^ ,]+ ?,?)(?:\{(?:[^ ,]+ ?,?)*\})? ?from )(['"])([^ ,]+)(\2;.*)$/;
const referenceTagExp = /^[ \t]*\/\/\/[ \t]*<reference[ \t]+path=(["'])(.*?)\1?[ \t]*\/>.*$/;
const identifierExp = /^\w+(?:[\.-]\w+)*$/;
const fileExp = /^([\./].*|.:.*)$/;
const privateExp = /^[ \t]*(?:static )?private (?:static )?/;
const publicExp = /^([ \t]*)(static |)(public |)(static |)(.*)/;

export interface Options {
    main: string;
    name: string;
    baseDir?: string;
    out?: string;
    newline?: string;
    indent?: string;
    outputAsModuleFolder?: boolean;
    prefix?: string;
    separator?: string;
    externals?: boolean;
    exclude?: { (file: string): boolean; } | RegExp;
    removeSource?: boolean;
    verbose?: boolean;
    referenceExternals?: boolean;
    emitOnIncludedFileNotFound?: boolean;
    emitOnNoIncludedFileNotFound?: boolean;
    headerPath: string;
    headerText: string;
}

export interface ModLine {
    original: string;
    modified?: string;
    skip?: boolean;
}

export interface Result {
    file: string;
    name: string;
    indent: string;
    exp: string;
    refs: string[];
    externalImports: string[];
    relativeImports: string[];
    exports: string[];
    lines: ModLine[];
    importLineRef: ModLine[];
    relativeRef: ModLine[];
    fileExists: boolean;
}

export interface BundleResult {
    fileMap: { [name: string]: Result; };
    includeFilesNotFound: string[];
    noIncludeFilesNotFound: string[];
    emitted?: boolean;
    options: Options;
}

export function bundle(options: Options): BundleResult {
    assert(typeof options === 'object' && options, 'options must be an object');

    // if main ends with **/*.d.ts all .d.ts files will be loaded
    const allFiles = stringEndsWith(options.main, "**/*.d.ts");

    // option parsing & validation
    const main = allFiles ? "*.d.ts" : options.main;
    const exportName = options.name;
    const _baseDir = (() => {
        let baseDir = optValue(options.baseDir, path.dirname(options.main));
        if (allFiles) {
            baseDir = baseDir.substr(0, baseDir.length - 2);
        }
        return baseDir;
    })();
    const out = optValue(options.out, exportName + '.d.ts').replace(/\//g, path.sep);

    const newline = optValue(options.newline, os.EOL);
    const indent = optValue(options.indent, '    ');
    const outputAsModuleFolder = optValue(options.outputAsModuleFolder, false);
    const prefix = optValue(options.prefix, '');
    const separator = optValue(options.separator, '/');

    const externals = optValue(options.externals, false);
    const exclude = optValue(options.exclude, null);
    const removeSource = optValue(options.removeSource, false);
    const referenceExternals = optValue(options.referenceExternals, false);
    const emitOnIncludedFileNotFound = optValue(options.emitOnIncludedFileNotFound, false);
    const emitOnNoIncludedFileNotFound = optValue(options.emitOnNoIncludedFileNotFound, false);
    const _headerPath = optValue(options.headerPath, null);
    const headerText = optValue(options.headerText, '');

    // regular (non-jsdoc) comments are not actually supported by declaration compiler
    const comments = false;

    const verbose = optValue(options.verbose, false);

    assert.ok(main, 'option "main" must be defined');
    assert.ok(exportName, 'option "name" must be defined');

    assert(typeof newline === 'string', 'option "newline" must be a string');
    assert(typeof indent === 'string', 'option "indent" must be a string');
    assert(typeof prefix === 'string', 'option "prefix" must be a string');
    assert(separator.length > 0, 'option "separator" must have non-zero length');

    // turn relative paths into absolute paths
    const baseDir = path.resolve(_baseDir);
    let mainFile = allFiles ? path.resolve(baseDir, "**/*.d.ts") : path.resolve(main.replace(/\//g, path.sep));
    const outFile = calcOutFilePath(out, baseDir);
    let headerData = '// Generated by dts-bundle v' + pkg.version + newline;
    const headerPath = _headerPath && _headerPath !== "none" ? path.resolve(_headerPath.replace(/\//g, path.sep)) : _headerPath;

    trace('### settings object passed ###');
    traceObject(options);

    trace('### settings ###');
    trace('main:         %s', main);
    trace('name:         %s', exportName);
    trace('out:          %s', out);
    trace('baseDir:      %s', baseDir);
    trace('mainFile:     %s', mainFile);
    trace('outFile:      %s', outFile);
    trace('externals:    %s', externals ? 'yes' : 'no');
    trace('exclude:      %s', exclude);
    trace('removeSource: %s', removeSource ? 'yes' : 'no');
    trace('comments:     %s', comments ? 'yes' : 'no');
    trace('emitOnIncludedFileNotFound:   %s', emitOnIncludedFileNotFound ? "yes" : "no");
    trace('emitOnNoIncludedFileNotFound: %s', emitOnNoIncludedFileNotFound ? "yes" : "no");
    trace("headerPath    %s", headerPath);
    trace("headerText    %s", headerText);

    if (!allFiles) {
        assert(fs.existsSync(mainFile), 'main does not exist: ' + mainFile);
    }

    if (headerPath) {
        if (headerPath === "none") {
            headerData = "";
        } else {
            assert(fs.existsSync(headerPath), 'header does not exist: ' + headerPath);
            headerData = fs.readFileSync(headerPath, 'utf8') + headerData;
        }
    } else if (headerText) {
        headerData = '/*' + headerText + '*/\n';
    }

    let isExclude: (file: string, arg?: boolean) => boolean;
    if (typeof exclude === 'function') {
        isExclude = <any>exclude;
    }
    else if (exclude instanceof RegExp) {
        isExclude = file => exclude.test(file);
    }
    else {
        isExclude = () => false;
    }

    const sourceTypings = glob.sync('**/*.d.ts', { cwd: baseDir }).map(file => path.resolve(baseDir, file));

    // if all files, generate temporally main file
    if (allFiles) {
        let mainFileContent = "";
        trace("## temporally main file ##");
        sourceTypings.forEach(file => {
            let generatedLine = "export * from './" + path.relative(baseDir, file.substr(0, file.length - 5)).replace(path.sep, "/") + "';";
            trace(generatedLine);
            mainFileContent += generatedLine + "\n";
        });
        mainFile = path.resolve(baseDir, "dts-bundle.tmp." + exportName + ".d.ts");
        fs.writeFileSync(mainFile, mainFileContent, 'utf8');
    }

    trace('\n### find typings ###');

    const inSourceTypings = (file: string) => {
        return sourceTypings.indexOf(file) !== -1 || sourceTypings.indexOf(path.join(file, 'index.d.ts')) !== -1;
    }; // if file reference is a directory assume commonjs index.d.ts

    trace('source typings (will be included in output if actually used)');

    sourceTypings.forEach(file => trace(' - %s ', file));

    trace('excluded typings (will always be excluded from output)');

    let fileMap: { [name: string]: Result; } = Object.create(null);
    let globalExternalImports: string[] = [];
    let mainParse: Result; // will be parsed result of first parsed file
    let externalTypings: string[] = [];
    let inExternalTypings = (file: string) => externalTypings.indexOf(file) !== -1;
    {
        // recursively parse files, starting from main file,
        // following all references and imports
        trace('\n### parse files ###');

        let queue: string[] = [mainFile];
        let queueSeen: { [name: string]: boolean; } = Object.create(null);

        while (queue.length > 0) {
            let target = queue.shift();
            if (queueSeen[target]) {
                continue;
            }
            queueSeen[target] = true;

            // parse the file
            let parse = parseFile(target);
            if (!mainParse) {
                mainParse = parse;
            }
            fileMap[parse.file] = parse;
            pushUniqueArr(queue, parse.refs, parse.relativeImports);
        }
    }

    // map all exports to their file
    trace('\n### map exports ###');

    let exportMap = Object.create(null);
    Object.keys(fileMap).forEach(file => {
        let parse = fileMap[file];
        parse.exports.forEach(name => {
            assert(!(name in exportMap), 'already got export for: ' + name);
            exportMap[name] = parse;
            trace('- %s -> %s', name, parse.file);
        });
    });

    // build list of typings to include in output later
    trace('\n### determine typings to include ###');

    let excludedTypings: string[] = [];
    let usedTypings: Result[] = [];
    let externalDependencies: string[] = []; // lists all source files that we omit due to !externals
    {
        let queue = [mainParse];
        let queueSeen: { [name: string]: boolean; } = Object.create(null);

        trace('queue');
        trace(queue);

        while (queue.length > 0) {
            let parse = queue.shift();
            if (queueSeen[parse.file]) {
                continue;
            }
            queueSeen[parse.file] = true;

            trace('%s (%s)', parse.name, parse.file);

            usedTypings.push(parse);

            parse.externalImports.forEach(name => {
                let p = exportMap[name];
                if (!externals) {
                    trace(' - exclude external %s', name);
                    pushUnique(externalDependencies, !p ? name : p.file);
                    return;
                }
                if (isExclude(path.relative(baseDir, p.file), true)) {
                    trace(' - exclude external filter %s', name);
                    pushUnique(excludedTypings, p.file);
                    return;
                }
                trace(' - include external %s', name);
                assert(p, name);
                queue.push(p);
            });
            parse.relativeImports.forEach(file => {
                let p = fileMap[file];
                if (isExclude(path.relative(baseDir, p.file), false)) {
                    trace(' - exclude internal filter %s', file);
                    pushUnique(excludedTypings, p.file);
                    return;
                }
                trace(' - import relative %s', file);
                assert(p, file);
                queue.push(p);
            });
        }
    }

    // rewrite global external modules to a unique name
    trace('\n### rewrite global external modules ###');

    usedTypings.forEach(parse => {
        trace(parse.name);

        parse.relativeRef.forEach((line, i) => {
            line.modified = replaceExternal(line.original, getLibName);
            trace(' - %s  ==>  %s', line.original, line.modified);
        });

        parse.importLineRef.forEach((line, i) => {
            if (outputAsModuleFolder) {
                trace(' - %s was skipped.', line.original);
                line.skip = true;
                return;
            }

            if (importExp.test(line.original)) {
                line.modified = replaceImportExport(line.original, getLibName);
            } else {
                line.modified = replaceImportExportEs6(line.original, getLibName);
            }
            trace(' - %s  ==>  %s', line.original, line.modified);
        });
    });

    // build collected content
    trace('\n### build output ###');

    let content = headerData;
    if (externalDependencies.length > 0) {
        content += '// Dependencies for this module:' + newline;
        externalDependencies.forEach(file => {
            if (referenceExternals) {
                content += formatReference(path.relative(baseDir, file).replace(/\\/g, '/')) + newline;
            }
            else {
                content += '//   ' + path.relative(baseDir, file).replace(/\\/g, '/') + newline;
            }
        });
    }

    if ( globalExternalImports.length > 0 ) {
        content += newline;
        content += globalExternalImports.join(newline) + newline;
    }

    content += newline;

    // content += header.stringify(header.importer.packageJSON(pkg)).join(lb) + lb;
    // content += lb;

    // add wrapped modules to output
    content += usedTypings.filter((parse: Result) => {
        // Eliminate all the skipped lines
        parse.lines = parse.lines.filter((line: ModLine) => {
            return (true !== line.skip);
        });

        // filters empty parse objects.
        return ( parse.lines.length > 0 );
    }).map((parse: Result) => {
        if (inSourceTypings(parse.file)) {
            return formatModule(parse.file, parse.lines.map(line => {
                return getIndenter(parse.indent, indent)(line);
            }));
        }
        else {
            return parse.lines.map(line => {
                return getIndenter(parse.indent, indent)(line);
            }).join(newline) + newline;
        }
    }).join(newline) + newline;

    // remove internal typings, except the 'regenerated' main typing
    if (removeSource) {
        trace('\n### remove source typings ###');

        sourceTypings.forEach(p => {
            // safety check, only delete .d.ts files, leave our outFile intact for now
            if (p !== outFile && dtsExp.test(p) && fs.statSync(p).isFile()) {
                trace(' - %s', p);
                fs.unlinkSync(p);
            }
        });
    }

    let inUsed = (file: string): boolean => {
        return usedTypings.filter(parse => parse.file === file).length !== 0;
    };

    let bundleResult: BundleResult = {
        fileMap,
        includeFilesNotFound: [],
        noIncludeFilesNotFound: [],
        options
    };

    trace('## files not found ##');
    for (let p in fileMap) {
        let parse = fileMap[p];
        if (!parse.fileExists) {
            if (inUsed(parse.file)) {
                bundleResult.includeFilesNotFound.push(parse.file);
                warning(' X Included file NOT FOUND %s ', parse.file)
            } else {
                bundleResult.noIncludeFilesNotFound.push(parse.file);
                trace(' X Not used file not found %s', parse.file);
            }
        }
    }

    // write main file
    trace('\n### write output ###');
    // write only if there aren't not found files or there are and option "emit file not found" is true.
    if ((bundleResult.includeFilesNotFound.length == 0
        || (bundleResult.includeFilesNotFound.length > 0 && emitOnIncludedFileNotFound))
        && (bundleResult.noIncludeFilesNotFound.length == 0
            || (bundleResult.noIncludeFilesNotFound.length > 0 && emitOnNoIncludedFileNotFound))) {

        trace(outFile);
        {
            let outDir = path.dirname(outFile);
            if (!fs.existsSync(outDir)) {
                mkdirp.sync(outDir);
            }
        }

        fs.writeFileSync(outFile, content, 'utf8');
        bundleResult.emitted = true;
    } else {
        warning(" XXX Not emit due to exist files not found.")
        trace("See documentation for emitOnIncludedFileNotFound and emitOnNoIncludedFileNotFound options.")
        bundleResult.emitted = false;
    }

    // print some debug info
    if (verbose) {
        trace('\n### statistics ###');

        trace('used sourceTypings');
        sourceTypings.forEach(p => {
            if (inUsed(p)) {
                trace(' - %s', p);
            }
        });

        trace('unused sourceTypings');
        sourceTypings.forEach(p => {
            if (!inUsed(p)) {
                trace(' - %s', p);
            }
        });

        trace('excludedTypings');
        excludedTypings.forEach(p => {
            trace(' - %s', p);
        });

        trace('used external typings');
        externalTypings.forEach(p => {
            if (inUsed(p)) {
                trace(' - %s', p);
            }
        });

        trace('unused external typings');
        externalTypings.forEach(p => {
            if (!inUsed(p)) {
                trace(' - %s', p);
            }
        });

        trace('external dependencies');
        externalDependencies.forEach(p => {
            trace(' - %s', p);
        });
    }

    trace('\n### done ###\n');
    // remove temporally file.
    if (allFiles) {
        fs.unlinkSync(mainFile);
    }
    return bundleResult;

    function stringEndsWith(str: string, suffix: string) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    }

    function stringStartsWith(str: string, prefix: string) {
        return str.slice(0, prefix.length) == prefix;
    }

    // Calculate out file path (see #26 https://github.com/TypeStrong/dts-bundle/issues/26)
    function calcOutFilePath(out: any, baseDir: any) {
        var result = path.resolve(baseDir, out);
        // if path start with ~, out parameter is relative from current dir
        if (stringStartsWith(out, "~" + path.sep)) {
            result = path.resolve(".", out.substr(2));
        }
        return result;
    }

    function traceObject(obj: any) {
        if (verbose) {
            console.log(obj);
        }
    }

    function trace(...args: any[]) {
        if (verbose) {
            console.log(util.format.apply(null, args));
        }
    }

    function warning(...args: any[]) {
        console.log(util.format.apply(null, args));
    }

    function getModName(file: string) {
        return path.relative(baseDir, path.dirname(file) + path.sep + path.basename(file).replace(/\.d\.ts$/, ''));
    }

    function getExpName(file: string) {
        if (file === mainFile) {
            return exportName;
        }
        return getExpNameRaw(file);
    }

    function getExpNameRaw(file: string) {
        return prefix + exportName + separator + cleanupName(getModName(file));
    }

    function getLibName(ref: string) {
        return getExpNameRaw(mainFile) + separator + prefix + separator + ref;
    }

    function cleanupName(name: string) {
        return name.replace(/\.\./g, '--').replace(/[\\\/]/g, separator);
    }

    function mergeModulesLines(lines: any) {
        var i = (outputAsModuleFolder ? '' : indent);
        return (lines.length === 0 ? '' : i + lines.join(newline + i)) + newline;
    }

    function formatModule(file: string, lines: string[]) {
        let out = '';
        if (outputAsModuleFolder) {
            return mergeModulesLines(lines);
        }

        out += 'declare module \'' + getExpName(file) + '\' {' + newline;
        out += mergeModulesLines(lines);
        out += '}' + newline;
        return out;
    }

    // main info extractor
    function parseFile(file: string): Result {
        const name = getModName(file);

        trace('%s (%s)', name, file);

        const res: Result = {
            file: file,
            name: name,
            indent: indent,
            exp: getExpName(file),
            refs: [], // triple-slash references
            externalImports: [], // import()'s like "events"
            relativeImports: [], // import()'s like "./foo"
            exports: [],
            lines: [],
            fileExists: true,
            // the next two properties contain single-element arrays, which reference the same single-element in .lines,
            // in order to be able to replace their contents later in the bundling process.
            importLineRef: [],
            relativeRef: []
        };

        if (!fs.existsSync(file)) {
            trace(' X - File not found: %s', file);
            res.fileExists = false;
            return res;
        }
        if (fs.lstatSync(file).isDirectory()) { // if file is a directory then lets assume commonjs convention of an index file in the given folder
            file = path.join(file, 'index.d.ts');
        }
        const code = fs.readFileSync(file, 'utf8').replace(bomOptExp, '').replace(/\s*$/, '');
        res.indent = detectIndent(code) || indent;

        // buffer multi-line comments, handle JSDoc
        let multiComment: string[] = [];
        let queuedJSDoc: string[];
        let inBlockComment = false;
        const popBlock = () => {
            if (multiComment.length > 0) {
                // jsdoc
                if (/^[ \t]*\/\*\*/.test(multiComment[0])) {
                    // flush but hold
                    queuedJSDoc = multiComment;
                }
                else if (comments) {
                    // flush it
                    multiComment.forEach(line => res.lines.push({ original: line }));
                }
                multiComment = [];
            }
            inBlockComment = false;
        };
        const popJSDoc = () => {
            if (queuedJSDoc) {
                queuedJSDoc.forEach(line => {
                    // fix shabby TS JSDoc output
                    let match = line.match(/^([ \t]*)(\*.*)/);
                    if (match) {
                        res.lines.push({ original: match[1] + ' ' + match[2] });
                    }
                    else {
                        res.lines.push({ original: line });
                    }
                });
                queuedJSDoc = null;
            }
        };

        code.split(/\r?\n/g).forEach((line: any) => {
            let match: string[];

            // block comment end
            if (/^[((=====)(=*)) \t]*\*+\//.test(line)) {
                multiComment.push(line);
                popBlock();
                return;
            }

            // block comment start
            if (/^[ \t]*\/\*/.test(line)) {
                multiComment.push(line);
                inBlockComment = true;

                // single line block comment
                if (/\*+\/[ \t]*$/.test(line)) {
                    popBlock();
                }
                return;
            }

            if (inBlockComment) {
                multiComment.push(line);
                return;
            }

            // blankline
            if (/^\s*$/.test(line)) {
                res.lines.push({ original: '' });
                return;
            }

            // reference tag
            if (/^\/\/\//.test(line)) {
                let ref = extractReference(line);
                if (ref) {
                    let refPath = path.resolve(path.dirname(file), ref);
                    if (inSourceTypings(refPath)) {
                        trace(' - reference source typing %s (%s)', ref, refPath);
                    } else {
                        let relPath = path.relative(baseDir, refPath).replace(/\\/g, '/');

                        trace(' - reference external typing %s (%s) (relative: %s)', ref, refPath, relPath);

                        if (!inExternalTypings(refPath)) {
                            externalTypings.push(refPath);
                        }
                    }
                    pushUnique(res.refs, refPath);
                    return;
                }
            }

            // line comments
            if (/^\/\//.test(line)) {
                if (comments) {
                    res.lines.push({ original: line });
                }
                return;
            }

            // private member
            if (privateExp.test(line)) {
                queuedJSDoc = null;
                return;
            }
            popJSDoc();

            // import() statement or es6 import
            if ((line.indexOf("from") >= 0 && (match = line.match(importEs6Exp))) ||
                (line.indexOf("require") >= 0 && (match = line.match(importExp)))) {
                const [_, lead, quote, moduleName, trail] = match;
                assert(moduleName);

                const impPath = path.resolve(path.dirname(file), moduleName);

                // filename (i.e. starts with a dot, slash or windows drive letter)
                if (fileExp.test(moduleName)) {
                    // TODO: some module replacing is handled here, whereas the rest is
                    // done in the "rewrite global external modules" step. It may be
                    // more clear to do all of it in that step.
                    let modLine: ModLine = {
                        original: lead + quote + getExpName(impPath) + trail
                    };
                    res.lines.push(modLine);

                    let full = path.resolve(path.dirname(file), impPath);
                    // If full is not an existing file, then let's assume the extension .d.ts
                    if(!fs.existsSync(full) || fs.existsSync(full + '.d.ts')) {
                        full += '.d.ts';
                    }
                    trace(' - import relative %s (%s)', moduleName, full);

                    pushUnique(res.relativeImports, full);
                    res.importLineRef.push(modLine);
                }
                // identifier
                else {
                    let modLine: ModLine = {
                        original: line
                    };
                    trace(' - import external %s', moduleName);

                    pushUnique(res.externalImports, moduleName);
                    if (externals) {
                        res.importLineRef.push(modLine);
                    }
                    if (!outputAsModuleFolder) {
                        res.lines.push(modLine);
                    } else {
                        pushUnique(globalExternalImports, line);
                    }
                }
            }

            // declaring an external module
            // this triggers when we're e.g. parsing external module declarations, such as node.d.ts
            else if ((match = line.match(externalExp))) {
                let [_, declareModule, lead, moduleName, trail] = match;
                assert(moduleName);

                trace(' - declare %s', moduleName);
                pushUnique(res.exports, moduleName);
                let modLine: ModLine = {
                    original: line
                };
                res.relativeRef.push(modLine); // TODO
                res.lines.push(modLine);
            }
            // clean regular lines
            else {
                // remove public keyword
                if ((match = line.match(publicExp))) {
                    let [_, sp, static1, pub, static2, ident] = match;
                    line = sp + static1 + static2 + ident;
                }
                if (inSourceTypings(file)) {
                    // for internal typings, remove the 'declare' keyword (but leave 'export' intact)
                    res.lines.push({ original: line.replace(/^(export )?declare /g, '$1') });
                }
                else {
                    res.lines.push({ original: line });
                }
            }
        });

        return res;
    }
}

function pushUnique<T>(arr: T[], value: T) {
    if (arr.indexOf(value) < 0) {
        arr.push(value);
    }
    return arr;
}

function pushUniqueArr<T>(arr: T[], ...values: T[][]) {
    values.forEach(vs => vs.forEach(v => pushUnique(arr, v)));
    return arr;
}

function formatReference(file: string) {
    return '/// <reference path="' + file.replace(/\\/g, '/') + '" />';
}

function extractReference(tag: string) {
    let match = tag.match(referenceTagExp);
    if (match) {
        return match[2];
    }
    return null;
}

function replaceImportExport(line: string, replacer: (str: string) => string) {
    let match = line.match(importExp);
    if (match) {
        assert(match[4]);
        if (identifierExp.test(match[3])) {
            return match[1] + match[2] + replacer(match[3]) + match[4];
        }
    }
    return line;
}

function replaceImportExportEs6(line: string, replacer: (str: string) => string) {
    if (line.indexOf("from") < 0) {
        return line;
    }
    let match = line.match(importEs6Exp);
    if (match) {
        assert(match[4]);
        if (identifierExp.test(match[3])) {
            return match[1] + match[2] + replacer(match[3]) + match[4];
        }
    }
    return line;
}

function replaceExternal(line: string, replacer: (str: string) => string) {
    let match = line.match(externalExp);
    if (match) {
        let [_, declareModule, beforeIndent, moduleName, afterIdent] = match;
        assert(afterIdent);
        if (identifierExp.test(moduleName)) {
            return declareModule + beforeIndent + replacer(moduleName) + afterIdent;
        }
    }
    return line;
}

function getIndenter(actual: string, use: string): (line: ModLine) => string {
    if (actual === use || !actual) {
        return line => line.modified || line.original;
    }
    return line => (line.modified || line.original).replace(new RegExp('^' + actual + '+', 'g'), match => match.split(actual).join(use));
}

function optValue<T>(passed: T, def: T): T {
    if (typeof passed === 'undefined') {
        return def;
    }
    return passed;
}

function regexEscape(s: string) {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
