#!/usr/bin/nodejs
/* Process an input file, trying to recreate what webpack would have done. */

var fs = require('fs');
var recast = require('recast');
var types = require('ast-types');
var pathModule = require('path');

var astTrue = { type: 'Literal', value: true, raw: 'true' };
var astFalse = { type: 'Literal', value: false, raw: 'false' };
var astExports = { type: 'Identifier', name: 'exports' };
var astWebpackPublicPath = recast.parse("__webpack_require__.p").program.body[0].expression;
var astGlobalObject = recast.parse("(function() { return this; }())").program.body[0];

var webpackAmdDefine = function(DEPS, BODY) {
    var newHeader = (BODY.type == "Identifier") ?
        "var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;" :
        "var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;";
    var ast = (BODY.type == "Identifier") ?
        recast.parse("!(__WEBPACK_AMD_DEFINE_ARRAY__ = DEPS, \
__WEBPACK_AMD_DEFINE_FACTORY__ = (BODY), \
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? \
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : \
__WEBPACK_AMD_DEFINE_FACTORY__), \
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\
").program.body[0].expression :
        recast.parse("!(__WEBPACK_AMD_DEFINE_ARRAY__ = DEPS, \
__WEBPACK_AMD_DEFINE_RESULT__ = BODY.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), \
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\
").program.body[0].expression;
    types.visit(ast, {
        visitIdentifier: function(path) {
            this.traverse(path);
            if (path.node.name === 'DEPS') {
                path.replace(DEPS);
            } else if (path.node.name === 'BODY') {
                path.replace(BODY);
            }
        }
    });
    return { ast: ast, newHeader: recast.parse(newHeader).program.body[0] };
}
var webpackVarInject = function(names, BODY, REPLACE) {
    var nameStr = names.join(", ");
    var body = recast.print(BODY).code;
    var replaceStr = REPLACE.map(function(r) { return recast.print(r).code; }).join(", ");
    var ast = recast.parse("/* WEBPACK VAR INJECTION */(function(" + nameStr + ") {\
" + body + "\n\
/* WEBPACK VAR INJECTION */}.call(exports, " + replaceStr + "))\
").program;
    return ast;
}
var webpackRequire = function(i) {
    return recast.parse("__webpack_require__(" + i + ")").program.body[0].expression;
}

var isIndirectRequire = function(path) {
    var binding = path.scope.getBindings()[path.node.callee.name];
    return (binding
      && binding.length > 0
      && binding[0].parentPath.value.init
      && binding[0].parentPath.value.init.name === 'require');
}

var transform = function transform(ast, fn, moduleList) {

    var dirname = pathModule.dirname(fn);
    var basename = pathModule.basename(fn);

    var getModuleIndex = function(name) {
        var key = name;
        var fkey = fn + ":" + key;
        var wkey = dirname + "/*:" + key;
        if (moduleList.hasOwnProperty(fkey)) {
            return moduleList[fkey];
        } else if (moduleList.hasOwnProperty(wkey)) {
            return moduleList[wkey];
        } else if (moduleList.hasOwnProperty(key)) {
            return moduleList[key];
        } else {
            throw new Error("can't find module index for: " + key);
        }
    };

    /**************************************************************************/
    /* Set up injection stuff. This probably needs to be updated with ipywidgets */
    /**************************************************************************/
    var astProcessModule = webpackRequire(getModuleIndex("process"));
    var astSetImmediateModule = recast.parse(
        "__webpack_require__(" + getModuleIndex("timers-browserify") + ").setImmediate"
    ).program.body[0].expression;

    var injectGlobals = [];
    var injectGlobalsReplace = {
        "global": astGlobalObject,
        "process": astProcessModule,
        "setImmediate": astSetImmediateModule,
    };
    // don't inject setImmediate into itself
    if (fn == "./node_modules/timers-browserify/main.js" ||
        fn == "./node_modules/setimmediate/setImmediate.js") {
        delete injectGlobalsReplace["setImmediate"];
    }
    // never inject anything for jquery, our injection-detection goes wrong due to
    // https://github.com/benjamn/recast/issues/397
    if (basename == "jquery.js") {
        injectGlobalsReplace = {};
    }
    /**************************************************************************/
    /* End injection stuff. */
    /**************************************************************************/

    var changed = false;
    var amdChanged = 0;

    types.visit(ast, {
        visitIdentifier: function(path) {
            this.traverse(path);
            // replace __webpack_public_path__ with __webpack_require__.p
            if (path.node.name === '__webpack_public_path__') {
                path.replace(astWebpackPublicPath);
                changed = true;
            }
            // if we see a dangling reference to a "global" object then add the VAR INJECTION stuff
            var parent = path.parent.node;
            if (injectGlobals.indexOf(path.node.name) < 0
              && injectGlobalsReplace.hasOwnProperty(path.node.name)
              && (parent.type !== 'MemberExpression' || parent.object === path.node)
              && (parent.type !== 'Property' || parent.value === path.node)) {
                if (!path.scope.getBindings().hasOwnProperty(path.node.name)) {
                    injectGlobals.push(path.node.name);
                }
            }
        },
        visitCallExpression: function(path) {
            this.traverse(path);
            // replace require() with __webpack_require__
            if (path.node.arguments.length === 1 &&
              (path.node.callee.name === 'require' || isIndirectRequire(path))) {
                var parent = path.parent.node;
                /*if (parent.type === 'IfStatement' && recast.prettyPrint(parent.test).code === 'typeof exports !== "undefined"') {
                    return; // webpack skips these for some reason, we follow
                    // TODO: doesn't quite work, it rewrites too much.
                    // we need to analyze what's further up the tree and decide based on that.
                    // do we want to bother fixing it? probably not...
                }*/
                var idx = getModuleIndex(path.node.arguments[0].value);
                path.replace(webpackRequire(idx));
                changed = true;
            }
        },
        visitIfStatement: function(path) {
            var node = path.node;
            var iftest = recast.prettyPrint(node.test).code;
            // if we see 'define.amd' stuff then add the AMD_DEFINE stuff
            if (iftest === 'typeof DEBUG !== "undefined" && DEBUG' || iftest === 'module.hot') {
                path.get("test").replace(astFalse);
                changed = true;
            } else if (iftest === 'typeof exports !== "undefined"') {
                // pass
            } else if (iftest === 'typeof define === "function" && define.amd') {
                var c = node.consequent;
                if (c.type === 'BlockStatement' &&
                  c.body[0].type === 'ExpressionStatement' &&
                  c.body[0].expression.type === 'CallExpression' &&
                  c.body[0].expression.callee.name === 'define') {
                    var def = c.body[0].expression;
                    var args = def.arguments[def.arguments[0].type === 'ArrayExpression' ? 0 : 1];
                    args.elements = args.elements.map(function(el) {
                        if (el.value === 'exports') {
                            return astExports;
                        } else {
                            return webpackRequire(getModuleIndex(el.value));
                        }
                    });
                    path.get("test").replace(astTrue);
                    var body = def.arguments[def.arguments[0].type === 'ArrayExpression' ? 1 : 2];
                    var amdBody = webpackAmdDefine(args, body);
                    path.get("consequent", "body", 0, "expression").replace(amdBody.ast);
                    amdChanged = amdBody.newHeader;
                }
            }
            this.traverse(path);
        },
        visitProgram: function(path) {
            this.traverse(path);
            // wrap the file with various things based on what got added
            if (injectGlobals.length > 0) {
                var main = path.node;
                path.replace(webpackVarInject(injectGlobals, main, injectGlobals.map(function(n) {
                    return injectGlobalsReplace[n];
                })));
                changed = true;
            }
            if (amdChanged) {
                path.get("body").unshift(amdChanged);
                changed = true;
            }
        },
    });
    return [changed, ast];
}

var parseModules = function(s) {
    var map = {};
    s.split("\n").map(function(x, i) {
        x.split(" ").forEach(function(n) { map[n] = i; });
    });
    return map;
}

var moduleList = parseModules(fs.readFileSync(process.argv[3], 'utf8'));

function readStdin(callback) {
  var stdin = '';

  process.stdin.setEncoding('utf8');

  process.stdin.on('readable', function() {
    var chunk = process.stdin.read();
    if (chunk !== null) {
      stdin += chunk;
    }
  });

  process.stdin.on('end', function() {
    callback(stdin);
  });
}


var fn = process.argv[2];
var contents = fs.readFileSync(fn, 'utf8');
var res = transform(recast.parse(contents), fn, moduleList);
if (res[0]) {
    // only write something if we made changes
    process.stdout.write(recast.print(res[1]).code);
}
