#!/usr/bin/env node

var chalk = require('chalk'),
    parseExpression = require('assetgraph/lib/parseExpression'),
    optimist = require('optimist')
        .usage('$0 --root <inputRootDirectory> --outroot <dir> [options] <htmlFile(s)>')
        .wrap(72)
        .options('h', {
            alias: 'help',
            describe: 'Show this help',
            type: 'boolean',
            default: false
        })
        .options('root', {
            describe: 'Path to your web root (will be deduced from your input files if not specified)',
            type: 'string',
            demand: false
        })
        .options('outroot', {
            describe: 'Path to the output folder. Will be generated if non-existing',
            type: 'string',
            demand: true
        })
        .options('cdnroot', {
            describe: 'URI root where the static assets will be deployed. Must be either an absolute or a protocol-relative url',
            type: 'string',
            demand: false
        })
        .options('canonicalurl', {
            describe: 'URI root where the project being built will be deployed (experimental)',
            type: 'string',
            demand: false
        })
        .options('optimizeimages', {
            describe: 'Perform automatic lossless optimization of all images using pngcrush, pngquant, optipng, and jpegtran',
            type: 'boolean',
            default: false
        })
        .options('browsers', {
            alias: 'b',
            describe: 'Specify which browsers to support. Configures autoprefixer and controls which IE hacks to apply. Syntax: https://github.com/ai/browserslist',
            type: 'string',
            demand: false
        })
        .options('debug', {
            describe: 'Keep statement level console.*() calls and debugger statements in JavaScript assets',
            type: 'boolean',
            default: false
        })
        .options('version', {
            describe: 'Adds or updates <html data-version="..."> to the specified value. Use {0} to refer to the current value, eg. --version {0}/production or --version `git describe --long --tags --always --dirty 2>/dev/null || echo unknown`',
            type: 'string'
        })
        .options('gzip', {
            describe: 'Include a gzipped copy of text-based assets > 860 bytes for which it yields a saving',
            default: false
        })
        .options('deferscripts', {
            describe: 'Sets the "defer" attribute on all script tags',
            type: 'boolean',
            default: false
        })
        .options('asyncscripts', {
            describe: 'Sets the "async" attribute on all script tags',
            type: 'boolean',
            default: false
        })
        .options('reservednames', {
            describe: 'Exclude certain variable names from mangling (equivalent to uglifyjs --reserved-names ...)'
        })
        .options('stoponwarning', {
            describe: 'Whether to stop with a non-zero exit code when a warning is encountered',
            type: 'boolean',
            default: false
        })
        .options('recursive', {
            describe: 'Follow local HTML anchors when populating the graph (use --no-recursive to turn off this behavior)',
            type: 'boolean',
            default: true
        })
        .options('minify', {
            describe: 'Minifies HTML, CSS, SVG and Javascript.',
            type: 'boolean',
            default: true
        })
        .options('nocompress', {
            describe: 'Prettifies HTML, CSS and Javascript for easier debugging',
            alias: 'no-compress',
            type: 'boolean',
            default: false
        })
        .options('removedeadifs', {
            describe: 'Optimize away certain if (...) {...} constructs. Only makes sense with --nocompress as UglifyJS includes the same optimization',
            type: 'boolean',
            default: false
        })
        .options('noless', {
            describe: 'Keep .less files as they are instead of automatically compiling them to CSS',
            type: 'boolean',
            default: false
        })
        .options('noscss', {
            describe: 'Keep .scss files as they are instead of automatically compiling them to CSS',
            type: 'boolean',
            default: false
        })
        .options('nofilerev', {
            describe: 'Revision files with a hash of their content in their file name for optimal far future caching',
            type: 'boolean',
            default: false
        })
        .options('nocdnflash', {
            describe: 'Avoid putting flash files on the cdnroot. Use this if you have problems setting up CORS',
            type: 'boolean',
            default: false
        })
        .options('define', {
            alias: 'd',
            describe: '--define SYMBOL[=value] will be passed to UglifyJS as is (see the docs at https://github.com/mishoo/UglifyJS#usage). Remember to protect quotes from the shell, eg. --define foo=\\"bar\\"',
            type: 'string'
        })
        .options('inline', {
            describe: 'Set size threshold for inlining. Supported values: false (never inline), true (always inline, except when found inside @media queries), number (inline if target is smaller than this number of bytes). Also supported: --inlinehtmlscript true, --inlinecssimage 8192 etc.',
            default: false
        })
        .options('cdnhtml', {
            describe: 'Put non-initial HTML files on the cdnroot as well. Some CDN packages (such as Akamai\'s cheapest one) don\'t allow this',
            type: 'boolean',
            default: false
        })
        .options('sharedbundles', {
            describe: 'Try to create shared bundles including commin files across multiple pages',
            type: 'boolean',
            default: false
        })
        .options('manifest', {
            describe: 'Generates an appcache manifest file with all static assets included',
            type: 'boolean',
            default: false
        })
        .options('negotiatemanifest', {
            describe: 'Removes the locale id from the <html manifest="..."> references so all manifests are assumed to be accessible from the same url. Useful if you want the browser to pick up the right cache manifest and HTML after a locale change (your static file server needs to support content negotiation). Only makes sense when both  --manifest and --locale have been specified',
            type: 'boolean',
            default: false
        })
        .options('locales', {
            describe: 'Comma-separated list of locales to build seperate versions for',
            type: 'string',
            demand: false
        })
        .options('defaultlocale', {
            describe: 'The locale of the default value in TR statements and tags with a data-i18n attribute',
            type: 'string',
            default: 'en'
        })
        .options('localecookiename', {
            describe: 'The name of your locale cookie (exposed as LOCALECOOKIENAME)',
            type: 'string',
            default: 'en'
        })
        .options('repl', {
            describe: 'Start the REPL after a particular transform (or "error")',
            type: 'string'
        })
        .options('sweep', {
            describe: 'Experimental: Free up memory by cleaning up relations and assets after they have been removed from the graph',
            type: 'boolean'
        })
        .options('angular', {
            describe: 'Enable Angular.js specific annotations and template inlining',
            type: 'boolean'
        });
        /*
        // These are internal One.com options. No reason to expose them to the world.
        // Might be deprecated in the near future. If in doubt, don't use!
        .options('label', {
            describe: 'Registers labels as custom protocols for path resolving. You can create multiple of these: --label <labelName>=<dir> --label <otherLabelName>=<otherDir>',
            type: 'string',
            demand: false
        })
        .options('parentdir', {
            describe: 'If an unknown label (scheme) is found, look for at parent dir of that name before failing (breaks custom protocols)',
            type: 'boolean',
            demand: false
        })
        */

var javaScriptSerializationOptions = {};

// These names originally come from UglifyJS' code generator, but are now mapped to their escodegen equivalents
var javaScriptSerializationOptionNames = [ 'indent_level', 'ascii_only' ];

javaScriptSerializationOptionNames.forEach(function (javaScriptSerializationOptionName) {
    var type = (javaScriptSerializationOptionName === 'indent_level') ? 'number' : 'boolean';
    // Also accept the option without underscores, or with the underscores replaced with dashes:
    optimist.options(javaScriptSerializationOptionName.replace(/_/g, ''), {
        alias: [javaScriptSerializationOptionName, javaScriptSerializationOptionName.replace(/_/g, '-')],
        type: type,
        description: 'UglifyJS serialization option, see http://lisperator.net/uglifyjs/codegen'
    });
});

var commandLineOptions = optimist.argv,
    browsers = commandLineOptions.browsers;

[ 'preserve_line', 'preamble', 'quote_char', 'indent_start', 'quote_keys', 'space_colon', 'unescape_regexps', 'width', 'max_line_len', 'beautify', 'bracketize' ].forEach(function (deprecatedUglifyJsOption) {
    [deprecatedUglifyJsOption, deprecatedUglifyJsOption.replace(/_/g, ''), deprecatedUglifyJsOption.replace(/_/g, '-')].forEach(function (alias) {
        if (typeof commandLineOptions[alias] !== 'undefined') {
            throw new Error('--' + alias + ' is no longer supported after UglifyJS was replaced with esprima/escodegen');
        }
    });
});

if (Array.isArray(browsers)) {
    browsers = browsers.join(',');
}

javaScriptSerializationOptionNames.forEach(function (javaScriptSerializationOptionName) {
    var value = commandLineOptions[javaScriptSerializationOptionName.replace(/_/g, '')];
    if (typeof value !== 'undefined') {
        javaScriptSerializationOptions[javaScriptSerializationOptionName] = value;
    }
});

if (commandLineOptions.h) {
    optimist.showHelp();
    process.exit(1);
}

// Temporary deprecation message
if (commandLineOptions.less) {
    console.warn(chalk.yellow('INFO: The --less command line option is deprecated. This is now default behavior. Use --noless to keep .less files in build output'));
}

// Temporary deprecation message
if (commandLineOptions.stripdebug) {
    console.warn(chalk.yellow('INFO: the --stripdebug switch is deprecated. This behavior is now default. Use --debug to keep debugging in build output'));
}

// Temporary deprecation message
if (commandLineOptions.cdnflash) {
    console.warn(chalk.yellow('INFO: the --cdnflash switch is deprecated. This is now default functionality. Use --nocdnflash to get the old default behavior.'));
}

// Temporary deprecation message
if (commandLineOptions.cdnoutroot) {
    console.warn(chalk.yellow('INFO: the --cdnoutroot switch is deprecated. Default location for your cdn assets is now <outroot>/static/cdn'));
}

var _ = require('lodash'),
    AssetGraph = require('../lib/AssetGraph'),
    i18nTools = require('../lib/i18nTools'),
    query = AssetGraph.query,
    urlTools = require('urltools'),
    outRoot = urlTools.fsDirToFileUrl(commandLineOptions.outroot),
    cdnRoot = commandLineOptions.cdnroot && urlTools.ensureTrailingSlash(commandLineOptions.cdnroot),
    fullCdnRoot = (/^\/\//.test(cdnRoot) ? 'http:' : '') + cdnRoot,
    canonicalUrl = commandLineOptions.canonicalurl && urlTools.ensureTrailingSlash(commandLineOptions.canonicalurl),
    rootUrl = commandLineOptions.root && urlTools.urlOrFsPathToUrl(commandLineOptions.root, true),
    localeIds = commandLineOptions.locales && _.flatten(_.flatten([commandLineOptions.locales]).map(function (localeId) {
        return localeId.split(',');
    })).map(i18nTools.normalizeLocaleId),
    reservedNames = commandLineOptions.reservednames && _.flatten(_.flatten([commandLineOptions.reservednames]).map(function (reservedName) {
        return reservedName.split(',');
    })),
    defaultLocaleId = commandLineOptions.defaultlocale && i18nTools.normalizeLocaleId(commandLineOptions.defaultlocale),
    defines = {},
    inputUrls,
    inlineByRelationType = {};

if (commandLineOptions.inline) {
   inlineByRelationType['*'] = true;
}

// Doesn't touch non-string values or values that don't look like something boolean:
function convertStringToBoolean(str) {
    if (typeof str === 'string') {
        if (/^(?:on|true|yes|)$/.test(str)) {
            return true;
        } else if (/^(?:off|false|no)$/.test(str)) {
            return false;
        }
    }
    return str;
}

// Look for --inline<relationType> command line arguments:
Object.keys(AssetGraph).forEach(function (propertyName) {
    var inlineThreshold = convertStringToBoolean(commandLineOptions['inline' + propertyName.toLowerCase()]);
    if (typeof inlineThreshold !== 'undefined') {
        inlineByRelationType[propertyName] = inlineThreshold;
    }
});

// Use a default inline threshold of 8192 bytes for HtmlStyle and HtmlScript, unless --inlinehtmlscript/--inlinehtmlstyle (or --inline) was given
if (typeof inlineByRelationType['*'] === 'undefined') {
    ['HtmlScript', 'HtmlStyle'].forEach(function (relationType) {
        if (typeof inlineByRelationType[relationType] === 'undefined') {
            inlineByRelationType[relationType] = 8192;
        }
    });
}

if (commandLineOptions.inlinesize) {
    console.warn(chalk.yellow('INFO: the --inlinesize switch is deprecated. Please use --inlinecssimage <number> instead'));
    inlineByRelationType.CssImage = commandLineOptions.inlinesize;
} else if (!('CssImage' in inlineByRelationType)) {
    inlineByRelationType.CssImage = 8192;
}

(commandLineOptions.define ? _.flatten(_.flatten([commandLineOptions.define])) : []).forEach(function (define) {
    var matchDefine = define.match(/^(\w+)(?:=(.*))?$/);
    if (matchDefine) {
        var valueAst;
        if (matchDefine[2]) {
            try {
                valueAst = parseExpression(matchDefine[2]);
            } catch (e) {
                console.error('Invalid --define ' + matchDefine[1] + ': Could not parse ' + matchDefine[2] + ' as a JavaScript expression. Missing shell escapes?');
                console.error(e.message + ' (line ' + e.line + ', column ' + e.col + ')');
                process.exit(1);
            }
        } else {
            valueAst = { type: 'Literal', value: true };
        }
        defines[matchDefine[1]] = valueAst;
    }
});


if (commandLineOptions._.length > 0) {
    inputUrls = commandLineOptions._.map(function (urlOrFsPath) {
        return urlTools.urlOrFsPathToUrl(String(urlOrFsPath), false);
    });
    if (!rootUrl) {
        rootUrl = urlTools.findCommonUrlPrefix(inputUrls.filter(function (inputUrl) {
            return /^file:/.test(inputUrl);
        }));
        if (rootUrl) {
            console.warn('Guessing --root from input files: ' + rootUrl);
        }
    }
} else if (rootUrl && /^file:/.test(rootUrl)) {
    inputUrls = [rootUrl + '*.html'];
    console.warn('No input files specified, defaulting to ' + inputUrls[0]);
} else {
    throw new Error('No input files and no --root specified (or it isn\'t file:), cannot proceed');
}

new AssetGraph({root: rootUrl})
    .queue(function (assetGraph) {
        if (commandLineOptions.sweep) {
            assetGraph.on('removeAsset', function (asset) {
                setImmediate(function () {
                    if (!asset.assetGraph) {
                        asset.unload();
                    }
                });
            }).on('removeRelation', function (relation) {
                setImmediate(function () {
                    if (!relation.assetGraph) {
                        Object.keys(relation).forEach(function (key) {
                            relation[key] = null;
                        });
                    }
                });
            });
        }
    })
    .logEvents({repl: commandLineOptions.repl, stopOnWarning: commandLineOptions.stoponwarning, suppressJavaScriptCommonJsRequireWarnings: true})
    .registerRequireJsConfig({preventPopulationOfJavaScriptAssetsUntilConfigHasBeenFound: true})
    .registerLabelsAsCustomProtocols(commandLineOptions.label, {installFindParentDirectoryAsDefault: commandLineOptions.parentdir})
    .loadAssets(inputUrls)
    .buildProduction({
        version: commandLineOptions.version,
        browsers: browsers,
        less: !commandLineOptions.noless,
        scss: !commandLineOptions.noscss,
        optimizeImages: commandLineOptions.optimizeimages,
        inlineByRelationType: inlineByRelationType,
        gzip: commandLineOptions.gzip,
        noFileRev: commandLineOptions.nofilerev,
        defines: defines,
        reservedNames: reservedNames,
        localeIds: localeIds,
        localeCookieName: commandLineOptions.localecookiename,
        defaultLocaleId: defaultLocaleId,
        manifest: commandLineOptions.manifest,
        negotiateManifest: commandLineOptions.negotiatemanifest,
        asyncScripts: commandLineOptions.asyncscripts,
        deferScripts: commandLineOptions.deferscripts,
        cdnRoot: cdnRoot,
        recursive: commandLineOptions.recursive,
        cdnFlash: !commandLineOptions.nocdnflash,
        cdnHtml: commandLineOptions.cdnhtml,
        minify: commandLineOptions.minify,
        noCompress: commandLineOptions.nocompress,
        removeDeadIfs: commandLineOptions.removedeadifs,
        sharedBundles: commandLineOptions.sharedbundles,
        stripDebug: !commandLineOptions.debug,
        addInitialHtmlExtension: commandLineOptions.addinitialhtmlextension,
        canonicalUrl: canonicalUrl,
        javaScriptSerializationOptions: javaScriptSerializationOptions,
        angular: commandLineOptions.angular
    })
    .writeAssetsToDisc({url: canonicalUrl ? query.createPrefixMatcher(canonicalUrl) : /^file:/, isLoaded: true}, outRoot, canonicalUrl)
    .if(cdnRoot)
        .writeAssetsToDisc({url: query.createPrefixMatcher(fullCdnRoot), isLoaded: true}, outRoot + 'static/cdn/', fullCdnRoot)
    .endif()
    .writeStatsToStderr()
    .run();
