# This is vaguely related to an nsIScriptContext.  The pydom C code
# just calls back into this class - but that C code still retains some of the
# functionality.

import sys, types, new, logging

import domcompile

from xpcom.client import Component
from xpcom import components, primitives, COMException, nsError, _xpcom

import _nsdom

GlobalWindowIIDs = [_nsdom.IID_nsIScriptGlobalObject,
                    components.interfaces.nsIDOMWindow]

IID_nsIDOMXULElement = components.interfaces.nsIDOMXULElement

# Borrow the xpcom logger (but use a new sub-logger)
logger = logging.getLogger("xpcom.nsdom")
# Note that many calls to logger.debug are prefixed with if __debug__.
# This is to prevent *any* penalty in non-debug mode for logging in perf
# critical areas.

# Also note that assert statements have no cost in non-debug builds.

# XUL Elements don't expose the XBL implemented interfaces via classinfo.
# So we cheat :)
# Most support nsIAccessibleProvider - don't list that!
# Although this sucks for now, if you strike something that isn't listed,
# then simply QI the object you have for the interface you need to use. To
# avoid the explicit QI, you could also have your code do:
#    import nsdom.context
#    xei = nsdom.context.xul_element_interfaces
#    # flag for manual inspection should a future version include it
#    if 'somewidget' in xei: raise RuntimeError, "already here!?"
#    xei['somewidget'] = [...]

xul_element_interfaces = {
    # tagName:      [IID, ...]
    'textbox':      [components.interfaces.nsIDOMXULTextBoxElement],
    'button':       [components.interfaces.nsIDOMXULButtonElement],
    'checkbox':     [components.interfaces.nsIDOMXULCheckboxElement],
    'image':        [components.interfaces.nsIDOMXULImageElement],
    'tree':         [components.interfaces.nsIDOMXULTreeElement,
                     components.interfaces.nsIDOMXULMultiSelectControlElement],
    'listbox':      [components.interfaces.nsIDOMXULMultiSelectControlElement],
    'menu':         [components.interfaces.nsIDOMXULSelectControlItemElement],
    'popup':        [components.interfaces.nsIDOMXULPopupElement],
    'radiogroup':   [components.interfaces.nsIDOMXULSelectControlElement],
}

def dump(fmt, *args):
    """A global 'dump' function, available in global namespaces.  Similar to
    The JS version, but this supports % message formatting.

    Main advantage over 'print' is that it catches the possible IO error
    when there is no available console.
    """
    try:
        sys.stderr.write(fmt % args)
        sys.stderr.write("\n")
    except IOError:
        pass

# The event listener class we attach to an object for addEventListener
class EventListener:
    _com_interfaces_ = components.interfaces.nsIDOMEventListener
    def __init__(self, context, evt_name, handler, globs):
        # 'globs' are the globals the event handler will use, or None
        # if the handler should use the globals it was compiled with.
        self.context = context
        if callable(handler):
            self.func = handler
        else:
            # event name for addEventListener has no 'on' prefix
            func_name = "on" + evt_name
            # What to do about arg names?  It looks like onerror
            # still only gets one arg here anyway - handleEvent only takes
            # one!
            arg_names = ('event',)
            co = domcompile.compile_function(handler,
                                             "inline event '%s'" % evt_name,
                                             func_name,
                                             arg_names)
            # Could use globs here, but should not be necessary - all we
            # are doing is getting the function object from it.
            g = {}
            exec co in g
            self.func = g[func_name]
        self.globals = globs

    def handleEvent(self, event):
        # Although handler is already a function object, we must re-bind to
        # new globals if necessary.
        if self.globals is not None:
            f = new.function(self.func.func_code, self.globals, 
                            self.func.func_name, self.func.func_closure)
        else:
            f = self.func
        # Convert the raw pyxpcom object to a magic _nsdom one, that
        # knows how to remember expandos etc based on context (ie, winds
        # up calling back into MakeInterfaceResult().
        event = _nsdom.MakeDOMObject(self.context, event)
        
        args = (event,)
        # We support having less args declared than supplied, a-la JS.
        # (This can only happen when passed a function object - we always
        # compile a string handler into a function with 1 arg)
        args = args[:f.func_code.co_argcount]
        return f(*args)

class WrappedNative(Component):
    """Implements the xpconnect concept of 'wrapped natives' and 'expandos'.

    DOM objects can have arbitrary values set on them.  Once this is done for
    the first time, it gets stored in a map in the context.  This leads to
    cycles, which must be cleaned up when the context is closed.
    """
    def __init__(self, context, obj, iid):
        # Store our context - but our context doesn't keep a reference
        # to us until someone calls self._remember_object() on the context -
        # which sets up all kinds of cycles!
        self.__dict__['_context_'] = context
        # We store expandos in a separate dict rather than directly in our
        # __dict__.  No real need for this other than to prevent these
        # attributes clobbering ones we need to work!
        self.__dict__['_expandos_'] = {}
        Component.__init__(self, obj, iid)

    def __repr__(self):
        iface_desc = self._get_classinfo_repr_()
        return "<DOM object '%s' (%s)>" % (self._object_name_,iface_desc)
        
    def __getattr__(self, attr):
        # If it exists in expandos, always return it.
        if attr.startswith("__"):
            raise AttributeError, attr
        # expandos come before interface attributes (which may be wrong.  If
        # we do this, why not just store it in __dict__?)
        expandos = self._expandos_
        if expandos.has_key(attr):
            return expandos[attr]
        return Component.__getattr__(self, attr)

    def __setattr__(self, attr, value):
        try:
            Component.__setattr__(self, attr, value)
        except AttributeError:
            # Set it as an 'expando'.  It looks like we should delegate *all*
            # to the outside object.
            logger.debug("setting expando %s.%r=%r for object %r",
                         self, attr, value, self._comobj_)
            # and register if an event.
            if attr.startswith("on"):
                # I'm quite confused by this :(
                target = self._comobj_
                if _nsdom.IsOuterWindow(target):
                    target = _nsdom.GetCurrentInnerWindow(target)
                go = self._context_.globalObject
                scope = self._context_.GetNativeGlobal()
                if callable(value):
                    # no idea if this is right - set the compiled object ourselves.
                    self._expandos_[attr] = value
                    _nsdom.RegisterScriptEventListener(go, scope, target, attr)
                else:
                    _nsdom.AddScriptEventListener(target, attr, value, False, False)
                    _nsdom.CompileScriptEventListener(go, scope, target, attr)
            else:
                self._expandos_[attr] = value
            self._context_._remember_object(self)

    def _build_all_supported_interfaces_(self):
        # Generally called as pyxpcom is finding an attribute, overridden to
        # work around lack of class info for xbl bindings.
        Component._build_all_supported_interfaces_(self)
        # Now hard-code certain element names we know about, as the XBL
        # implemented interfaces are not exposed by this.
        ii = self.__dict__['_interface_infos_']
        if ii.has_key(IID_nsIDOMXULElement):
            # Is a DOM element - see if in our map.
            tagName = self.tagName
            interfaces = xul_element_interfaces.get(tagName, [])
            for interface in interfaces:
                if not ii.has_key(interface):
                        self._remember_interface_info(interface)
        else:
            logger.info("Not a DOM element - not hooking extra interfaces")

    def addEventListener(self, event, handler, useCapture=False, globs=None):
        # We need to transform string or function objects into
        # nsIDOMEventListener interfaces.
        logger.debug("addEventListener for %r, event=%r, handler=%r, cap=%s",
                     self, event, handler, useCapture)

        if not hasattr(handler, "handleEvent"): # may already be a handler instance.
            # Wrap it in our instance, which knows how to convert strings and
            # function objects.

            # Handle semantics for event handler globals:
            # * If a function, then function globals are used, as per normal.
            # * If a string object and no explicit globals param, use the
            #   caller's globals
            if globs is None and not callable(handler):
                globs = sys._getframe().f_back.f_globals
            handler = EventListener(self._context_, event, handler, globs)
        else:
            assert not globs, "can't specify a handler instance and globals"

        base = self.__getattr__('addEventListener')
        base(event, handler, useCapture)

class WrappedNativeGlobal(WrappedNative):
    # Special support for our global object.  Certain methods exposed by
    # IDL are JS specific - generally ones that take a variable number of args,
    # a concept that doesn't exist in xpcom.
    def __repr__(self):
        iface_desc = self._get_classinfo_repr_()
        outer = _nsdom.IsOuterWindow(self._comobj_)
        return "<GlobalWindow outer=%s %s>" % (outer, iface_desc)

    # Open window/dialog
    def openDialog(self, url, name, features="", *args):
        svc = components.classes['@mozilla.org/embedcomp/window-watcher;1'] \
                .getService(components.interfaces.nsIWindowWatcher)
        # Wrap in an nsIArray with special support for Python being able to
        # access the raw objects if the call-site is smart enough
        args = _nsdom.MakeArray(args)
        ret = svc.openWindow(self._comobj_, url, name, features, args)
        # and re-wrap in one of our "dom" objects
        return _nsdom.MakeDOMObject(self._context_, ret)

    # window.open and window.openDialog seem identical except for *args???
    open = openDialog

    # Timeout related functions.
    def setTimeout(self, interval, handler, *args):
        return _nsdom.SetTimeoutOrInterval(self._comobj_, interval, handler,
                                           args, False)

    # setInterval appears to have reversed args???
    def setInterval(self, handler, interval, *args):
        return _nsdom.SetTimeoutOrInterval(self._comobj_, interval, handler,
                                           args, True)

    def clearTimeout(self, tid):
        return _nsdom.ClearTimeoutOrInterval(self._comobj_, tid)

    def clearInterval(self, tid):
        return _nsdom.ClearTimeoutOrInterval(self._comobj_, tid)

# Our "script context" - morally an nsIScriptContext, although that lives
# in our C++ code and delegates to this.
class ScriptContext:
    def __init__(self):
        self.globalNamespace = {} # must not change identity!
        self._remembered_objects_ = {} # could, but doesn't
        self._reset()

    def _reset(self):
        # Explicitly wipe all 'expandos' for our remembered objects.
        # Its not clear this is necessary, but it is easy to envisage someone
        # setting up a cycle via expandos.
        for ro in self._remembered_objects_.itervalues():
            ro._expandos_.clear()
        self._remembered_objects_.clear()
        # self.globalObject is the "outer window" global, whereas the
        # inner window global tends to be passed in to each function as the
        # 'scope' object.
        self.globalObject = None
        self.globalNamespace.clear()

    def __repr__(self):
        return "<ScriptContext at %d>" % id(self)

    # Called by the _nsdom C++ support to wrap the DOM objects.
    def MakeInterfaceResult(self, object, iid):
        if 0 and __debug__: # this is a little noisy...
            logger.debug("MakeInterfaceResult for %r (remembered=%s)",
                         object,
                         self._remembered_objects_.has_key(object))
        assert not hasattr(object, "_comobj_"), "should not be wrapped!"
        # If it is remembered, just return that object.
        try:
            return self._remembered_objects_[object]
        except KeyError:
            # Make a new wrapper - but don't remember the wrapper until
            # we need to, when a property is set on it.

            # We should probably QI for nsIClassInfo, and only do this special
            # wrapping for objects with the DOM flag set.
        
            if iid in GlobalWindowIIDs:
                klass = WrappedNativeGlobal
            else:
                klass = WrappedNative
            return klass(self, object, iid)

    # Called whenever this object must be "permanently" attached to the
    # context.  Typically this means an attribute or event handler
    # has been set on the object, which should "persist" while the context
    # is alive (ie, future requests to getElementById(), for example, must
    # return the same underlying object, with event handlers and properties
    # still in-place.
    def _remember_object(self, object):
        # You must only try and remember a wrapped object.
        # Once an object has been wrapped once, all further requests must
        # be identical
        assert self._remembered_objects_.get(object._comobj_, object)==object, \
               "Previously remembered object is not this object!"
        self._remembered_objects_[object._comobj_] = object
        if __debug__:
            logger.debug("%s remembering object %r - now %d items", self,
                         object, len(self._remembered_objects_))

    def _fixArg(self, arg):
        if arg is None:
            return []
        # Already a tuple?  This means the original Python args have been
        # found and passed directly.
        if type(arg) == types.TupleType:
            return arg
        try:
            argv = arg.QueryInterface(components.interfaces.nsIArray)
        except COMException, why:
            if why.errno != nsError.NS_NOINTERFACE:
                raise
            # This is not an array - see if it is a variant or primitive.
            try:
                var = arg.queryInterface(components.interfaces.nsIVariant)
                parent = None
                if self.globalObject is not None:
                    parent = self.globalObject._comobj_
                if parent is None:
                    logger.warning("_fixArg for context with no global??")
                return _xpcom.GetVariantValue(var, parent)
            except COMException, why:
                if why.errno != nsError.NS_NOINTERFACE:
                    raise
                try:
                    return primitives.GetPrimitive(arg)
                except COMException, why:
                    if why.errno != nsError.NS_NOINTERFACE:
                        raise
                return arg
        # Its an array - do each item
        ret = []
        for i in range(argv.length):
            val = argv.queryElementAt(i, components.interfaces.nsISupports)
            ret.append(self._fixArg(val))
        return ret

    def GetNativeGlobal(self):
        return self.globalNamespace

    def CreateNativeGlobalForInner(self, globalObject, isChrome):
        ret = dict()
        if __debug__:
            logger.debug("%r CreateNativeGlobalForInner returning %d",
                         self, id(ret))
        return ret

    def ConnectToInner(self, newInner, globalScope, innerScope):
        if __debug__:
            logger.debug("%r ConnectToInner(%r, %d, %d)",
                         self, newInner, id(globalScope), id(innerScope))
        globalScope['_inner_'] = innerScope # will do for now.

        self._prepareGlobalNamespace(newInner, innerScope)

    def WillInitializeContext(self):
        if __debug__:
            logger.debug("%r WillInitializeContent", self)

    def DidInitializeContext(self):
        if __debug__:
            logger.debug("%r DidInitializeContent", self)

    def DidSetDocument(self, doc, scope):
        if __debug__:
            logger.debug("%r DidSetDocument doc=%r scope=%d",
                         self, doc, id(scope))
        scope['document'] = doc

    def _prepareGlobalNamespace(self, globalObject, globalNamespace):
        assert isinstance(globalObject, WrappedNativeGlobal), \
               "Our global should have been wrapped in WrappedNativeGlobal"
        if __debug__:
            logger.debug("%r initializing (outer=%s), ns=%d", self,
                         _nsdom.IsOuterWindow(globalObject),
                         id(globalNamespace))
        assert len(globalObject._expandos_)==0, \
               "already initialized this global?"
        ns = globalObject.__dict__['_expandos_'] = globalNamespace
        self._remember_object(globalObject)
        ns['window'] = globalObject
        # we can't fetch 'document' and stash it now - there may not be one
        # at this instant (ie, it may be in the process of being attached)
        # Poke some other utilities etc into the namespace.
        ns['dump'] = dump

    def InitContext(self, globalObject):
        self._reset()
        self.globalObject = globalObject
        if globalObject is None:
            logger.debug("%r initializing with NULL global, ns=%d", self,
                         id(self.globalNamespace))
        else:
            self._prepareGlobalNamespace(globalObject, self.globalNamespace)

    def FinalizeClasses(self, globalObject):
        self._reset()

    def ClearScope(self, globalObject):
        if __debug__:
            logger.debug("%s.ClearScope (%d)", self, id(globalObject))
        globalObject.clear()

    def FinalizeContext(self):
        if __debug__:
            logger.debug("%s.FinalizeContext", self)
        self._reset()

    def EvaluateString(self, script, glob, principal, url, lineno, ver):
        # This appears misnamed - return value it not used.  Therefore we can
        # just do a simple 'exec'.  If we needed a return value, we would have
        # to treat this like a string event-handler, and compile as a temp
        # function.
        assert type(glob) == types.DictType
        # compile then exec to make use of lineno
        co = domcompile.compile(script, url, lineno=lineno-1)
        exec co in glob

    def ExecuteScript(self, scriptObject, scopeObject):
        if __debug__:
            logger.debug("%s.ExecuteScript %r in scope %s",
                         self, scriptObject, id(scopeObject))
        if scopeObject is None:
            scopeObject = self.GetNativeGlobal()
        assert type(scriptObject) == types.CodeType, \
               "Script object should be a code object (got %r)" % (scriptObject,)
        exec scriptObject in scopeObject

    def CompileScript(self, text, scopeObject, principal, url, lineno, version):
        # The line number passed is the first; offset is -1
        return domcompile.compile(text, url, lineno=lineno-1)

    def CompileEventHandler(self, name, argNames, body, url, lineno, version):
        if __debug__:
            logger.debug("%s.CompileEventHandler %s %s:%s ('%s')",
                         self, name, url, lineno, body[:100])
        co = domcompile.compile_function(body, url, name, argNames,
                                         lineno=lineno-1)
        g = {}
        exec co in g
        handler = g[name]
        # Flag that this function started life as inline script code,
        # so we later ensure the correct globals are used when the event fires
        handler._nsdom_compiled = True
        return g[name]

    def CallEventHandler(self, target, scope, handler, argv):
        if __debug__:
            logger.debug("CallEventHandler %r on target %s in scope %s",
                         handler, target, id(scope))
        # Event handlers must fire in their real globals (not a copy) so
        # it can set global vars!
        # If this was an inline event handler, re-bind to the window's globals.
        if hasattr(handler, '_nsdom_compiled'):
            globs = scope
            # re-bind to new globals
            f = new.function(handler.func_code, globs, handler.func_name,
                             handler.func_defaults)
            handler = f
        args = tuple(self._fixArg(argv))
        # We support having less args declared than supplied, a-la JS.
        args = args[:handler.func_code.co_argcount]
        return handler(*args)

    def BindCompiledEventHandler(self, target, scope, name, handler):
        if __debug__:
            logger.debug("%s.BindCompiledEventHandler (%s=%r) on target %s in scope %s",
                         self, name, handler, target, id(scope))
        # Keeps a ref to both the target and handler.
        self._remember_object(target)
        ns = target._expandos_
        ns[name] = handler

    def GetBoundEventHandler(self, target, scope, name):
        if __debug__:
            logger.debug("%s.GetBoundEventHandler for '%s' on target %s in scope %s",
                         self, name, target, id(scope))
        ns = target._expandos_
        return ns[name]

    def SetProperty(self, target, name, value):
        if __debug__:
            logger.debug("%s.SetProperty for %s=%r", self, name, value)
        target[name] = self._fixArg(value)
