import json
import contextlib

import execjs._exceptions as exceptions
from execjs._abstract_runtime import AbstractRuntime
from execjs._abstract_runtime_context import AbstractRuntimeContext
from execjs._misc import encode_unicode_codepoints

try:
    import PyV8
except ImportError:
    _pyv8_available = False
else:
    _pyv8_available = True


class PyV8Runtime(AbstractRuntime):
    '''Runtime to execute codes with PyV8.'''
    def __init__(self):
        pass

    @property
    def name(self):
        return "PyV8"

    def _compile(self, source, cwd=None):
        return self.Context(source)

    def is_available(self):
        return _pyv8_available

    class Context(AbstractRuntimeContext):
        def __init__(self, source=""):
            self._source = source

        def is_available(self):
            return _pyv8_available

        def _exec_(self, source):
            source = '''\
            (function() {{
                {0};
                {1};
            }})()'''.format(
                encode_unicode_codepoints(self._source),
                encode_unicode_codepoints(source)
            )
            source = str(source)

            # backward compatibility
            with contextlib.nested(PyV8.JSContext(), PyV8.JSEngine()) as (ctxt, engine):
                js_errors = (PyV8.JSError, IndexError, ReferenceError, SyntaxError, TypeError)
                try:
                    script = engine.compile(source)
                except js_errors as e:
                    raise exceptions.RuntimeError(e)
                try:
                    value = script.run()
                except js_errors as e:
                    raise exceptions.ProgramError(e)
                return self.convert(value)

        def _eval(self, source):
            return self.exec_('return ' + encode_unicode_codepoints(source))

        def _call(self, identifier, *args):
            args = json.dumps(args)
            return self.eval("{identifier}.apply(this, {args})".format(identifier=identifier, args=args))

        @classmethod
        def convert(cls, obj):
            from PyV8 import _PyV8
            if isinstance(obj, bytes):
                return obj.decode('utf8')
            if isinstance(obj, _PyV8.JSArray):
                return [cls.convert(v) for v in obj]
            elif isinstance(obj, _PyV8.JSFunction):
                return None
            elif isinstance(obj, _PyV8.JSObject):
                ret = {}
                for k in obj.keys():
                    v = cls.convert(obj[k])
                    if v is not None:
                        ret[cls.convert(k)] = v
                return ret
            else:
                return obj
