'use strict';

// dependencies
var path = require('path');
var fs = require('fs');
var fse = require('fs-extra');
var child_process = require('child_process');

var gulp = require('gulp');
var gutil = require('gulp-util');
var less = require('gulp-less');
var sass = require('gulp-sass');
var replace = require('gulp-replace');
var header = require('gulp-header');
var footer = require('gulp-footer');
var rename = require('gulp-rename');
var browsersync = require('browser-sync');
var vstream = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');

var browserify = require('browserify');
var babelify = require('babelify');
var uglify = require('gulp-uglify');
var envify = require('envify');
var htmllint = require('gulp-htmllint');
var crawler = require('simplecrawler');
var ncp = require('ncp');

var nextversion = require('./tools/bin/nextversion');
var util = require('./tools/bin/util');

// constants
var ROOT_DIR = '.';
var CONFIG_DIR = 'conf';
var SOURCE_DIR = path.join(ROOT_DIR, 'www');
var DEV_DIR = path.join(ROOT_DIR, 'build-dev');
var PROD_DIR = path.join(ROOT_DIR, 'build-prod');

var DATA_DIR = path.join(SOURCE_DIR, '_data');
var TOC_DIR = path.join(DATA_DIR, 'toc');
var DOCS_DIR = path.join(SOURCE_DIR, 'docs');
var FETCH_DIR = path.join(DOCS_DIR, 'en', 'dev', 'reference');
var CSS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'css-src');
var CSS_OUT_DIR = path.join(SOURCE_DIR, 'static', 'css');
var PLUGINS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'plugins');
var JS_DIR = path.join(SOURCE_DIR, 'static', 'js');
var BIN_DIR = path.join(ROOT_DIR, 'tools', 'bin');

var CONFIG_FILE = path.join(CONFIG_DIR, '_config.yml');
var DEFAULTS_CONFIG_FILE = path.join(CONFIG_DIR, '_defaults.yml');
var VERSION_CONFIG_FILE = path.join(CONFIG_DIR, '_version.yml');
var PROD_CONFIG_FILE = path.join(CONFIG_DIR, '_prod.yml');
var DEV_CONFIG_FILE = path.join(CONFIG_DIR, '_dev.yml');
var NODOCS_CONFIG_FILE = path.join(CONFIG_DIR, '_nodocs.yml');

var VERSION_FILE = 'VERSION';
var DOCS_VERSION_FILE = path.join(DATA_DIR, 'docs-versions.yml');
var ALL_PAGES_FILE = path.join(DATA_DIR, 'all-pages.yml');
var FETCH_CONFIG = path.join(DATA_DIR, 'fetched-files.yml');
var REDIRECTS_FILE = path.join(DATA_DIR, 'redirects.yml');
var PLUGINS_FILE_NAME = 'plugins.js';
var PLUGINS_FILE = path.join(JS_DIR, PLUGINS_FILE_NAME);
var PLUGINS_SRC_FILE = path.join(PLUGINS_SRC_DIR, 'app.js');

var BASE_CONFIGS = [CONFIG_FILE, DEFAULTS_CONFIG_FILE, VERSION_CONFIG_FILE];
var DEV_CONFIGS = [DEV_CONFIG_FILE];
var PROD_CONFIGS = [PROD_CONFIG_FILE];
var DEV_FLAGS = ['--trace'];
var PROD_FLAGS = [];

var BASE_URL = '';
var YAML_FRONT_MATTER = '---\n---\n';
var WATCH_INTERVAL = 1000; // in milliseconds
var VERSION_VAR_NAME = 'latest_docs_version';
var LATEST_DOCS_VERSION = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
var NEXT_DOCS_VERSION = nextversion.getNextVersion(LATEST_DOCS_VERSION);
var LANGUAGES = util.listdirsSync(DOCS_DIR);

var PROD_BY_DEFAULT = false;

// compute/get/set/adjust passed options
gutil.env.prod = gutil.env.prod || PROD_BY_DEFAULT;
gutil.env.dev = !gutil.env.prod;
gutil.env.outDir = gutil.env.prod ? PROD_DIR : DEV_DIR;

// check for errors
if (gutil.env.prod && gutil.env.nodocs) {
    fatal("can't ignore docs when doing a production build");
}

// helpers
function fatal (message) {
    gutil.log(gutil.colors.red('ERROR') + ': ' + message);
    process.exit(1);
}

function execPiped (command, args, fileName) {
    console.log(command + ' ' + args.join(' '));
    var task = child_process.spawn(command, args);
    return task.stdout.pipe(vstream(fileName)).pipe(buffer());
}

function exec (command, args, cb) {
    console.log(command + ' ' + args.join(' '));
    var task = child_process.spawn(command, args, { stdio: 'inherit' });
    task.on('exit', cb);
}

function bin (name) {
    return path.join(BIN_DIR, name);
}

function remove (path) {
    console.log('removing ' + path);
    fse.removeSync(path);
}

function getBundleExecutable () {
    if (process.platform === 'win32') {
        return 'bundle.bat';
    } else {
        return 'bundle';
    }
}

function getJekyllConfigs () {
    var configs = BASE_CONFIGS;

    // add build-specific config files
    if (gutil.env.prod) {
        configs = configs.concat(PROD_CONFIGS);
    } else {
        configs = configs.concat(DEV_CONFIGS);
    }

    // add a special exclude file if "nodocs" was specified
    if (gutil.env.nodocs) {
        configs = configs.concat(NODOCS_CONFIG_FILE);
    }

    return configs;
}

function jekyllBuild (done) {
    var bundle = getBundleExecutable();
    var configs = getJekyllConfigs();
    var flags = gutil.env.prod ? PROD_FLAGS : DEV_FLAGS;

    flags = flags.concat(['--config', configs.join(',')]);

    exec(bundle, ['exec', 'jekyll', 'build'].concat(flags), done);
}

function copyDocsVersion (oldVersion, newVersion, cb) {

    // copying a folder and a ToC file for each language
    var numCopyOperations = LANGUAGES.length * 2;

    // pseudo-CV (condition variable)
    var numCopied = 0;
    function doneCopying (error) {

        if (error) {
            cb(error);
            return;
        }

        // call callback if all folders have finished copying
        numCopied += 1;
        if (numCopied === numCopyOperations) {
            cb();
        }
    }

    // create a new version for each language
    LANGUAGES.forEach(function (languageName) {

        // get files to copy
        var oldVersionDocs = path.join(DOCS_DIR, languageName, oldVersion);
        var oldVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, oldVersion));
        var newVersionDocs = path.join(DOCS_DIR, languageName, newVersion);
        var newVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, newVersion));

        var copyOptions = {
            stopOnErr: true
        };

        // copy docs
        console.log(oldVersionDocs + ' -> ' + newVersionDocs);
        ncp.ncp(oldVersionDocs, newVersionDocs, copyOptions, doneCopying);

        // copy ToC
        console.log(oldVersionToc + ' -> ' + newVersionToc);
        ncp.ncp(oldVersionToc, newVersionToc, copyOptions, doneCopying);
    });
}

// tasks
gulp.task('default', ['help']);

gulp.task('help', function () {
    gutil.log('');
    gutil.log('Tasks:');
    gutil.log('');
    gutil.log('    build         same as configs + data + styles + plugins + jekyll');
    gutil.log('    jekyll        build with jekyll');
    gutil.log('    regen         same as jekyll + reload');
    gutil.log('    serve         build the site and open it in a browser');
    gutil.log('    reload        refresh the browser');
    gutil.log('');
    gutil.log('    newversion    create ' + NEXT_DOCS_VERSION + ' docs from dev docs');
    gutil.log('    snapshot      copy dev docs to ' + LATEST_DOCS_VERSION + ' docs');
    gutil.log('');
    gutil.log('    plugins       build ' + PLUGINS_FILE);
    gutil.log('');
    gutil.log('    configs       run all the below tasks');
    gutil.log('    defaults      create ' + DEFAULTS_CONFIG_FILE);
    gutil.log('    version       create ' + VERSION_CONFIG_FILE);
    gutil.log('');
    gutil.log('    data          run all the below tasks');
    gutil.log('    docs-versions create ' + DOCS_VERSION_FILE);
    gutil.log('    pages-dict    create ' + ALL_PAGES_FILE);
    gutil.log('    toc           create all generated ToC files in ' + TOC_DIR);
    gutil.log('    fetch         download docs specified in ' + FETCH_CONFIG);
    gutil.log('');
    gutil.log('    styles        run all the below tasks');
    gutil.log('    less          compile all .less files');
    gutil.log('    sass          compile all .scss files');
    gutil.log('    css           copy over all .css files');
    gutil.log('');
    gutil.log('    watch         serve + then watch all source files and regenerate as necessary');
    gutil.log('    link-bugs     replace CB-XXXX references with nice links');
    gutil.log('');
    gutil.log('    help          show this text');
    gutil.log('    clean         remove all generated files and folders');
    gutil.log('');
    gutil.log('Arguments:');
    gutil.log("    --nodocs      don't generate docs");
    gutil.log('    --prod        build for production; without it, will build dev instead');
    gutil.log('');
});

gulp.task('data', ['toc', 'docs-versions', 'pages-dict']);
gulp.task('configs', ['defaults', 'version']);
gulp.task('styles', ['less', 'css', 'sass']);

gulp.task('watch', ['serve'], function () {
    gulp.watch(
        [
            path.join(CSS_SRC_DIR, '**', '*')
        ],
        { interval: WATCH_INTERVAL },
        ['styles']
    );
    gulp.watch(
        [
            path.join(PLUGINS_SRC_DIR, '**', '*.js'),
            path.join(PLUGINS_SRC_DIR, '**', '*.jsx'),
            path.join(PLUGINS_SRC_DIR, '**', '*.json')
        ],
        { interval: WATCH_INTERVAL },
        ['plugins']
    );
    gulp.watch(
        [
            path.join(ROOT_DIR, '**', '*.yml'),
            path.join(JS_DIR, '**', '*.js'),
            path.join(CSS_OUT_DIR, '**', '*.css'),

            // NOTE:
            //      watch all non-docs HTML, and only docs/en/dev HTML because
            //      versions other than dev usually don't change much; this is
            //      an optimization
            path.join(SOURCE_DIR, '_layouts', '*.html'),
            path.join(SOURCE_DIR, '_includes', '*.html'),
            path.join(SOURCE_DIR, '**', '*.html') + '!' + path.join(DOCS_DIR, '**'),
            path.join(SOURCE_DIR, '**', '*.md') + '!' + path.join(DOCS_DIR, '**'),
            path.join(DOCS_DIR, 'en', 'dev', '**', '*.md'),
            path.join(DOCS_DIR, 'en', 'dev', '**', '*.html')
        ],
        { interval: WATCH_INTERVAL },
        ['regen']
    );
});

gulp.task('serve', ['build'], function () {
    var route = {};

    // set site root for browsersync
    if (gutil.env.prod) {
        route[BASE_URL] = gutil.env.outDir;
    }

    browsersync({
        notify: true,
        server: {
            baseDir: gutil.env.outDir,
            routes: route
        }
    });
});

gulp.task('build', ['configs', 'data', 'styles', 'plugins'], function (done) {
    jekyllBuild(done);
});

gulp.task('jekyll', function (done) {
    jekyllBuild(done);
});

gulp.task('regen', ['jekyll'], function () {
    browsersync.reload();
});

gulp.task('fetch', function (done) {

    // skip fetching if --nofetch was passed
    if (gutil.env.nofetch) {
        gutil.log(gutil.colors.yellow(
            'Skipping fetching external docs.'));
        done();
        return;
    }

    exec('node', [bin('fetch_docs.js'), '--config', FETCH_CONFIG, '--docsRoot', DOCS_DIR], done);
});

gulp.task('reload', function () {
    browsersync.reload();
});

gulp.task('docs-versions', function () {
    return execPiped('node', [bin('gen_versions.js'), DOCS_DIR], DOCS_VERSION_FILE)
        .pipe(gulp.dest(ROOT_DIR));
});

gulp.task('pages-dict', function () {
    var args = [
        bin('gen_pages_dict.js'),
        '--siteRoot', SOURCE_DIR,
        '--redirectsFile', REDIRECTS_FILE,
        '--latestVersion', LATEST_DOCS_VERSION,
        '--languages', LANGUAGES.join(',')
    ];

    return execPiped('node', args, ALL_PAGES_FILE).pipe(gulp.dest(ROOT_DIR));
});

gulp.task('version', function () {
    // this code is stupid; it's basically the line:
    //      cat VERSION | sed -e 's/^/VERSION_VAR_NAME: /' > _version.yml
    // however we're in Gulp, and we need to support Windows...
    // so we contort it into a monster
    return gulp
        .src(VERSION_FILE)
        .pipe(header(VERSION_VAR_NAME + ': '))
        .pipe(footer('\n'))
        .pipe(rename(VERSION_CONFIG_FILE))
        .pipe(gulp.dest('.'));
});

gulp.task('defaults', function () {
    return execPiped('node', [bin('gen_defaults.js'), DOCS_DIR, LATEST_DOCS_VERSION], DEFAULTS_CONFIG_FILE)
        .pipe(gulp.dest(ROOT_DIR));
});

gulp.task('toc', ['fetch'], function (done) {
    exec('node', [bin('toc.js'), DOCS_DIR, TOC_DIR], done);
});

gulp.task('less', function () {
    return gulp
        .src(path.join(CSS_SRC_DIR, '**', '*.less'))
        .pipe(less())
        .pipe(header(YAML_FRONT_MATTER))
        .pipe(gulp.dest(CSS_OUT_DIR))
        .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
        .pipe(browsersync.reload({ stream: true }));
});

gulp.task('css', function () {
    return gulp
        .src(path.join(CSS_SRC_DIR, '**', '*.css'))
        .pipe(header(YAML_FRONT_MATTER))
        .pipe(gulp.dest(CSS_OUT_DIR))
        .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
        .pipe(browsersync.reload({ stream: true }));
});

gulp.task('sass', function () {
    return gulp
        .src(path.join(CSS_SRC_DIR, '**', '*.scss'))
        .pipe(sass().on('error', sass.logError))
        .pipe(header(YAML_FRONT_MATTER))
        .pipe(gulp.dest(CSS_OUT_DIR))
        .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
        .pipe(browsersync.reload({ stream: true }));
});

gulp.task('plugins', function () {
    if (gutil.env.prod) {
        process.env.NODE_ENV = 'production';
    }

    var stream = browserify(PLUGINS_SRC_FILE, { debug: !gutil.env.prod })
        .transform(babelify, {
            presets: ['react'],
            plugins: [
                ['transform-react-jsx', { 'pragma': 'h' }]
            ]
        })
        .transform(envify)
        .bundle()
        .on('error', gutil.log)
        .pipe(vstream(PLUGINS_FILE_NAME))
        .pipe(buffer());

    if (gutil.env.prod) {
        stream = stream
            .pipe(uglify())
            .on('error', gutil.log);
    }

    return stream
        .pipe(gulp.dest(JS_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
        .pipe(browsersync.reload({ stream: true }))

        // NOTE:
        //      adding YAML front matter after doing everything
        //      else so that uglify doesn't screw it up
        .pipe(header(YAML_FRONT_MATTER))

        // WORKAROUND:
        //           minified JS has some things that look like
        //           Liquid tags, so we replace them manually
        .pipe(replace('){{', '){ {'))
        .pipe(gulp.dest(JS_DIR));
});

// convenience tasks
gulp.task('link-bugs', function (done) {
    exec(bin('linkify-bugs.sh'), [path.join(SOURCE_DIR, '_posts')], done);
});

gulp.task('lint', function () {
    return gulp.src(path.join('./', '**', '*.html'))
        .pipe(htmllint());
});

gulp.task('newversion', ['fetch'], function (done) {

    copyDocsVersion('dev', NEXT_DOCS_VERSION, function (error) {

        if (error) {
            console.error(error);
            done();
            return;
        }

        // finally update the version file with the new version
        fs.writeFile(VERSION_FILE, NEXT_DOCS_VERSION + '\n', done);
    });
});

gulp.task('snapshot', ['fetch'], function (done) {

    // remove current version first
    LANGUAGES.forEach(function (languageName) {
        var languageLatestDocs = path.join(DOCS_DIR, languageName, LATEST_DOCS_VERSION);
        remove(languageLatestDocs);
    });

    copyDocsVersion('dev', LATEST_DOCS_VERSION, done);
});

gulp.task('checklinks', function (done) {
    crawler
        .crawl('http://localhost:3000/')
        .on('fetch404', function (queueItem, response) {
            gutil.log(
                'Resource not found linked from ' +
                queueItem.referrer + ' to', queueItem.url
            );
            gutil.log('Status code: ' + response.statusCode);
        })
        .on('complete', function (queueItem) {
            done();
        });
});

gulp.task('clean', function () {
    remove(DEV_DIR);
    remove(PROD_DIR);
    remove(FETCH_DIR);
    remove(path.join(DATA_DIR, 'toc', '*-gen.yml'));
    remove(CSS_OUT_DIR);
    remove(PLUGINS_FILE);
    remove(DOCS_VERSION_FILE);
    remove(ALL_PAGES_FILE);
    remove(DEFAULTS_CONFIG_FILE);
    remove(VERSION_CONFIG_FILE);
});