__all__ = ['require']

import subprocess, os, codecs, glob
from .evaljs import translate_js, DEFAULT_HEADER
from .translators.friendly_nodes import is_valid_py_name
import six
import tempfile
import hashlib
import random

DID_INIT = False
DIRNAME = tempfile.mkdtemp()
PY_NODE_MODULES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'py_node_modules')


def _init():
    global DID_INIT
    if DID_INIT:
        return
    assert subprocess.call(
        'node -v', shell=True, cwd=DIRNAME
    ) == 0, 'You must have node installed! run: brew install node'
    assert subprocess.call(
        'cd %s;npm install babel-core babel-cli babel-preset-es2015 babel-polyfill babelify browserify browserify-shim'
        % repr(DIRNAME),
        shell=True,
        cwd=DIRNAME) == 0, 'Could not link required node_modules'
    DID_INIT = True


ADD_TO_GLOBALS_FUNC = '''
;function addToGlobals(name, obj) {
    if (!Object.prototype.hasOwnProperty('_fake_exports')) {
        Object.prototype._fake_exports = {};
    }
    Object.prototype._fake_exports[name] = obj;
};

'''
# subprocess.call("""node -e 'require("browserify")'""", shell=True)
GET_FROM_GLOBALS_FUNC = '''
;function getFromGlobals(name) {
    if (!Object.prototype.hasOwnProperty('_fake_exports')) {
        throw Error("Could not find any value named "+name);
    }
    if (Object.prototype._fake_exports.hasOwnProperty(name)) {
        return Object.prototype._fake_exports[name];
    } else {
        throw Error("Could not find any value named "+name);
    }
};

'''


def _get_module_py_name(module_name):
    return module_name.replace('-', '_')


def _get_module_var_name(module_name):
    cand =  _get_module_py_name(module_name).rpartition('/')[-1]
    if not is_valid_py_name(cand):
        raise ValueError(
            "Invalid Python module name %s (generated from %s). Unsupported/invalid npm module specification?" % (
                repr(cand), repr(module_name)))
    return cand


def _get_and_translate_npm_module(module_name, include_polyfill=False, update=False, maybe_version_str=""):
    assert isinstance(module_name, str), 'module_name must be a string!'

    py_name = _get_module_py_name(module_name)
    module_filename = '%s.py' % py_name
    var_name = _get_module_var_name(module_name)
    if not os.path.exists(os.path.join(PY_NODE_MODULES_PATH,
                                       module_filename)) or update:
        _init()
        module_hash = hashlib.sha1(module_name.encode("utf-8")).hexdigest()[:15]
        version = random.randrange(10000000000000)
        in_file_name = 'in_%s_%d.js' % (module_hash, version)
        out_file_name = 'out_%s_%d.js' % (module_hash, version)
        code = ADD_TO_GLOBALS_FUNC
        if include_polyfill:
            code += "\n;require('babel-polyfill');\n"
        code += """
        var module_temp_love_python = require(%s);
        addToGlobals(%s, module_temp_love_python);
        """ % (repr(module_name), repr(module_name))
        with open(os.path.join(DIRNAME, in_file_name), 'wb') as f:
            f.write(code.encode('utf-8') if six.PY3 else code)

        pkg_name = module_name.partition('/')[0]
        if maybe_version_str:
            pkg_name += '@' + maybe_version_str
        # make sure the module is installed
        assert subprocess.call(
            'cd %s;npm install %s' % (repr(DIRNAME), pkg_name),
            shell=True,
            cwd=DIRNAME
        ) == 0, 'Could not install the required module: ' + pkg_name

        # convert the module
        assert subprocess.call(
            '''node -e "(require('browserify')('./%s').bundle(function (err,data) {if (err) {console.log(err);throw new Error(err);};fs.writeFile('%s', require('babel-core').transform(data, {'presets': require('babel-preset-es2015')}).code, ()=>{});}))"'''
            % (in_file_name, out_file_name),
            shell=True,
            cwd=DIRNAME,
        ) == 0, 'Error when converting module to the js bundle'

        os.remove(os.path.join(DIRNAME, in_file_name))
        with codecs.open(os.path.join(DIRNAME, out_file_name), "r",
                         "utf-8") as f:
            js_code = f.read()
        print("Bundled JS library dumped at: %s" % os.path.join(DIRNAME, out_file_name))
        if len(js_code) < 50:
            raise RuntimeError("Candidate JS bundle too short - likely browserify issue.")
        js_code += GET_FROM_GLOBALS_FUNC
        js_code += ';var %s = getFromGlobals(%s);%s' % (
            var_name, repr(module_name), var_name)
        print('Please wait, translating...')
        py_code = translate_js(js_code)

        dirname = os.path.dirname(
            os.path.join(PY_NODE_MODULES_PATH, module_filename))
        if not os.path.isdir(dirname):
            os.makedirs(dirname)
        with open(os.path.join(PY_NODE_MODULES_PATH, module_filename),
                  'wb') as f:
            f.write(py_code.encode('utf-8') if six.PY3 else py_code)
    else:
        with codecs.open(
                os.path.join(PY_NODE_MODULES_PATH, module_filename), "r",
                "utf-8") as f:
            py_code = f.read()
    return py_code


def require(module_name, include_polyfill=True, update=False, context=None):
    """
    Installs the provided npm module, exports a js bundle via browserify, converts to ECMA 5.1 via babel and
    finally translates the generated JS bundle to Python via Js2Py.
    Returns a pure python object that behaves like the installed module. Nice!

    :param module_name: Name of the npm module to require. For example 'esprima'. Supports specific versions via @
        specification. Eg: 'crypto-js@3.3'.
    :param include_polyfill: Whether the babel-polyfill should be included as part of the translation. May be needed
    for some modules that use unsupported features of JS6 such as Map or typed arrays.
    :param update: Whether to force update the translation. Otherwise uses a cached version if exists.
    :param context: Optional context in which the translated module should be executed in. If provided, the
        header (js2py imports) will be skipped as it is assumed that the context already has all the necessary imports.
    :return: The JsObjectWrapper containing the translated module object. Can be used like a standard python object.
    """
    module_name, maybe_version = (module_name+"@@@").split('@')[:2]

    py_code = _get_and_translate_npm_module(module_name, include_polyfill=include_polyfill, update=update,
                                            maybe_version_str=maybe_version)
    # this is a bit hacky but we need to strip the default header from the generated code...
    if context is not None:
        if not py_code.startswith(DEFAULT_HEADER):
            # new header version? retranslate...
            assert not update, "Unexpected header."
            py_code = _get_and_translate_npm_module(module_name, include_polyfill=include_polyfill, update=True)
            assert py_code.startswith(DEFAULT_HEADER), "Unexpected header."
        py_code = py_code[len(DEFAULT_HEADER):]
    context = {} if context is None else context
    exec(py_code, context)
    return context['var'][_get_module_var_name(module_name)].to_py()