/*
 Copyright 2012-2015, Yahoo Inc.
 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
 */
var path = require('path'),
    fs = require('fs'),
    mkdirp = require('mkdirp'),
    compareVersions = require('compare-versions'),
    matcherFor = require('./file-matcher').matcherFor,
    libInstrument = require('istanbul-lib-instrument'),
    libCoverage = require('istanbul-lib-coverage'),
    libSourceMaps = require('istanbul-lib-source-maps'),
    hook = require('istanbul-lib-hook'),
    Reporter = require('./reporter');

function getCoverFunctions(config, includes, callback) {

    if (!callback && typeof includes === 'function') {
        callback = includes;
        includes = null;
    }

    var includePid = config.instrumentation.includePid(),
        reportingDir = path.resolve(config.reporting.dir()),
        reporter = new Reporter(config),
        excludes = config.instrumentation.excludes(true),
        // The coverage variable below should have different value than
        // that of the coverage variable actually used by the instrumenter (in this case: __coverage__).
        // Otherwise if you run nyc to provide coverage on these files,
        // both the actual instrumenter and this file will write to the global coverage variable,
        // and provide unexpected coverage result.
        coverageVar = '$$coverage$$',
        instOpts = config.instrumentation.getInstrumenterOpts(),
        sourceMapStore = libSourceMaps.createSourceMapStore({}),
        instrumenter,
        transformer,
        runInContextTransformer,
        runInThisContextTransformer,
        fakeRequire,
        requireTransformer,
        reportInitFn,
        hookFn,
        unhookFn,
        coverageFinderFn,
        coverageSetterFn,
        beforeReportFn,
        exitFn;

    instOpts.coverageVariable = coverageVar;
    instOpts.sourceMapUrlCallback = function (file, url) {
        sourceMapStore.registerURL(file, url);
    };
    coverageFinderFn = function () {
        return global[coverageVar];
    };
    instrumenter = libInstrument.createInstrumenter(instOpts);
    transformer = function (code, options) {
      var filename = typeof options === 'string' ? options : options.filename;
      return instrumenter.instrumentSync(code, filename);
    };
    runInContextTransformer = function (code, options) {
      return transformer(code, options);
    };
    runInThisContextTransformer = function (code, options) {
      return transformer(code, options);
    };
    requireTransformer = function (code, options) {
      var cov,
        ret = transformer(code, options),
        filename = typeof options === 'string' ? options : options.filename;
      if (fakeRequire) {
        cov = coverageFinderFn();
        cov[filename] = instrumenter.lastFileCoverage();
        return 'function x() {}';
      }
      return ret;
    };

    coverageSetterFn = function (cov) {
        global[coverageVar] = cov;
    };

    reportInitFn = function () {
        // set up reporter
        mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
        reporter.addAll(config.reporting.reports());
        if (config.reporting.print() !== 'none') {
            switch (config.reporting.print()) {
                case 'detail':
                    reporter.add('text');
                    break;
                case 'both':
                    reporter.add('text');
                    reporter.add('text-summary');
                    break;
                default:
                    reporter.add('text-summary');
                    break;
            }
        }
    };

    var disabler;
    hookFn = function (matchFn) {
        var hookOpts = {
            verbose: config.verbose,
            extensions: config.instrumentation.extensions(),
            coverageVariable: coverageVar
        };

        //initialize the global variable
        coverageSetterFn({});
        reportInitFn();

        if (config.hooks.hookRunInContext()) {
            hook.hookRunInContext(matchFn, runInContextTransformer, hookOpts);
        }
        if (config.hooks.hookRunInThisContext()) {
            hook.hookRunInThisContext(matchFn, runInThisContextTransformer, hookOpts);
            if(compareVersions(process.versions.node, "6.0.0") === -1) {
              disabler = hook.hookRequire(matchFn, requireTransformer, hookOpts);
            }
        } else {
            disabler = hook.hookRequire(matchFn, requireTransformer, hookOpts);
        }
    };

    unhookFn = function (matchFn) {
        if (disabler) {
            disabler();
        }
        hook.unhookRunInThisContext();
        hook.unhookRunInContext();
        hook.unloadRequireCache(matchFn);
    };

    beforeReportFn = function (matchFn, cov) {
        var pidExt = includePid ? ('-' + process.pid) : '',
            file = path.resolve(reportingDir, 'coverage' + pidExt + '.raw.json'),
            missingFiles,
            finalCoverage = cov;

        if (config.instrumentation.includeAllSources()) {
            if (config.verbose) {
                console.error("Including all sources not require'd by tests");
            }
            missingFiles = [];
            // Files that are not touched by code ran by the test runner is manually instrumented, to
            // illustrate the missing coverage.
            matchFn.files.forEach(function (file) {
                if (!cov[file]) {
                    missingFiles.push(file);
                }
            });

            fakeRequire = true;
            missingFiles.forEach(function (file) {
                try {
                    require(file);
                } catch (ex) {
                    console.error('Unable to post-instrument: ' + file);
                }
            });
        }
        if (Object.keys(finalCoverage).length >0) {
            if (config.verbose) {
                console.error('=============================================================================');
                console.error('Writing coverage object [' + file + ']');
                console.error('Writing coverage reports at [' + reportingDir + ']');
                console.error('=============================================================================');
            }
            fs.writeFileSync(file, JSON.stringify(finalCoverage), 'utf8');
        }
        return finalCoverage;
    };

    exitFn = function (matchFn, reporterOpts) {
        var cov,
            coverageMap,
            transformed;

        cov = coverageFinderFn() || {};
        cov = beforeReportFn(matchFn, cov);
        coverageSetterFn(cov);

        if (!(cov && typeof cov === 'object') || Object.keys(cov).length === 0) {
            console.error('No coverage information was collected, exit without writing coverage information');
            return;
        }

        coverageMap = libCoverage.createCoverageMap(cov);
        transformed = sourceMapStore.transformCoverage(coverageMap);
        reporterOpts.sourceFinder = transformed.sourceFinder;
        reporter.write(transformed.map, reporterOpts);
        sourceMapStore.dispose();
    };

    excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
    includes = includes || config.instrumentation.extensions().map(function (ext) {
            return '**/*' + ext;
        });
    var matchConfig = {
        root: config.instrumentation.root() || /* istanbul ignore next: untestable */ process.cwd(),
        includes: includes,
        excludes: excludes
    };
    matcherFor(matchConfig, function (err, matchFn) {
        /* istanbul ignore if: untestable */
        if (err) {
            return callback(err);
        }
        return callback(null, {
            coverageFn: coverageFinderFn,
            hookFn: hookFn.bind(null, matchFn),
            exitFn: exitFn.bind(null, matchFn, {}), // XXX: reporter opts
            unhookFn: unhookFn.bind(null, matchFn)
        });
    });
}

module.exports = {
    getCoverFunctions: getCoverFunctions
};

