#!/usr/bin/env node

// entry point for node-based CLI that produces snapshots (e.g. Pebble SDK)

var yargs = require('yargs');
var jsCompiler = require('./js_tooling');
var fs = require('fs');
var zip = require('node-zip');

yargs
    .command('compile', 'compiles JavaScript to byte code', function(yargs) {
        return defaultOptions(yargs);
    }, compile)
    .command('patch', 'patches an existing PBPack or PBW file', function(yargs) {
        return defaultOptions(yargs)
            .option('pbpack', {
                describe: 'PBPack file to patch',
                type: 'string',
            })
            .option('pbw', {
                describe: 'PBW file to patch',
                type: 'string',
            })
            .check(function(argv, arr) {
                if (typeof (argv.pbw) == typeof (argv.pbpack)) {
                    throw 'Need to specifiy either --pbw or --pbpack'
                }
                return true;
            });
    }, patch)
    .help()
    .detectLocale(false) // force english
    .check(function(argv, arr) {
        throw "No command provided."
    })
    .argv;

function defaultOptions(yargs) {
    return yargs
        .option('js', {
            describe: 'JavaScript input file (or - for stdin)',
            demand: true,
            type: 'string'
        })
        .option('prefix', {
            describe: 'Non-standard sequence of bytes to prepend snapshot with (passed through decodeURIComponent())',
            type: 'string'
        })
        .option('maxsize', {
            describe: 'Maximum number of bytes the resulting snapshot (including padding) can be',
            type: 'number',
            default: jsCompiler.defaultSnapshotMaxSize
        })
        .option('padding', {
            describe: 'Number of bytes to add to the snapshot for padding',
            type: 'number',
            default: 0
        })
        .option('output', {
            describe: 'output file (or - for stdout)',
            demand: true,
            type: 'string'
        })
        .strict()
        .check(function(argv, arr) {
            if (argv._.length != 1) {
                throw 'Ambiguous command provided: "' + argv._.join(' ') + '"'
            }
            return true;
        });
}

function snapshotOptions(argv) {
    return {
        prefix: argv.prefix ? decodeURIComponent(argv.prefix) : undefined,
        maxsize: argv.maxsize,
        padding: argv.padding
    };
}

function compile(argv) {
    var js = readJS(argv);
    var result = jsCompiler.createSnapshot(js, snapshotOptions(argv));
    bailOnError(result);
    writeOutput(argv, result.snapshot)
}

function patchPBW(js, inputBytes, argv) {
    var pbw = zip(inputBytes);

    // add or replace the JS in the PBW
    pbw.file('rocky-app.js', js);

    // this does the actual compiling (multiple times, yes, there's room for improvement)
    Object.getOwnPropertyNames(pbw.files).forEach(function(prop) {
        if (prop.endsWith('.pbpack')) {
            var pbpack = pbw.files[prop].asUint8Array();
            var result = jsCompiler.patchPBPack(js, pbpack, snapshotOptions(argv));
            if (result.result != 'success') {
                return result;
            }
            pbw.file(prop, new Buffer(result.pbpack));
        }
    });

    return {
        result: 'success',
        pbw: pbw.generate({type:'string', compression:'DEFLATE'})
    };
}

function patch(argv) {
    var js = readJS(argv);
    var inputBytes = strToByteArray(fs.readFileSync(argv.pbpack || argv.pbw, 'binary'));
    var result = argv.pbpack ? jsCompiler.patchPBPack(js, inputBytes, snapshotOptions(argv)) : patchPBW(js, inputBytes, argv);
    bailOnError(result);
    writeOutput(argv, result.pbpack || result.pbw);
}

function strToByteArray(str) {
    var result = new Array(str.length);
    for (var i = 0; i < str.length; i++) {
        result[i] = str[i].charCodeAt(0);
    }
    return result;
}

function byteArrayToStr(arr) {
    var result = '';
    for (i = 0; i < arr.length; i++) {
        result += String.fromCharCode(arr[i]);
    }
    return result;
}

function bailOnError(returnValue, prop) {
    if (returnValue.result != 'success') {
        console.error(returnValue.result + ':', returnValue.reason);
        process.exit(1);
    }
}

function writeOutput(argv, bytesOrString) {
    var data = typeof bytesOrString === 'string' ? bytesOrString : byteArrayToStr(bytesOrString);
    var isStdOut = argv.output == '/dev/stdout';
    if (isStdOut) {
        fs.writeSync(1, data, 'binary');
    } else {
        fs.writeFileSync(argv.output, data, 'binary');
    }
}

function readJS(argv) {
    return fs.readFileSync(argv.js, 'utf8');
}