#
# Copyright (C) 2009 Oracle Corporation
#
# This file is part of VirtualBox Open Source Edition (OSE), as
# available from http://www.virtualbox.org. This file is free software;
# you can redistribute it and/or modify it under the terms of the GNU
# General Public License (GPL) as published by the Free Software
# Foundation, in version 2 as it comes in the "COPYING" file of the
# VirtualBox OSE distribution. VirtualBox OSE is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
#
import sys,os
import traceback

# To set Python bitness on OSX use 'export VERSIONER_PYTHON_PREFER_32_BIT=yes'

VboxBinDir = os.environ.get("VBOX_PROGRAM_PATH", None)
VboxSdkDir = os.environ.get("VBOX_SDK_PATH", None)

if VboxBinDir is None:
    # Will be set by the installer
    VboxBinDir = "%VBOX_INSTALL_PATH%"

if VboxSdkDir is None:
    # Will be set by the installer
    VboxSdkDir = "%VBOX_SDK_PATH%"

os.environ["VBOX_PROGRAM_PATH"] = VboxBinDir
os.environ["VBOX_SDK_PATH"] = VboxSdkDir
sys.path.append(VboxBinDir)

from VirtualBox_constants import VirtualBoxReflectionInfo

class PerfCollector:
    """ This class provides a wrapper over IPerformanceCollector in order to
    get more 'pythonic' interface.

    To begin collection of metrics use setup() method.

    To get collected data use query() method.

    It is possible to disable metric collection without changing collection
    parameters with disable() method. The enable() method resumes metric
    collection.
    """

    def __init__(self, mgr, vbox):
        """ Initializes the instance.

        """
        self.mgr = mgr
        self.isMscom = (mgr.type == 'MSCOM')
        self.collector = vbox.performanceCollector

    def setup(self, names, objects, period, nsamples):
        """ Discards all previously collected values for the specified
        metrics, sets the period of collection and the number of retained
        samples, enables collection.
        """
        self.collector.setupMetrics(names, objects, period, nsamples)

    def enable(self, names, objects):
        """ Resumes metric collection for the specified metrics.
        """
        self.collector.enableMetrics(names, objects)

    def disable(self, names, objects):
        """ Suspends metric collection for the specified metrics.
        """
        self.collector.disableMetrics(names, objects)

    def query(self, names, objects):
        """ Retrieves collected metric values as well as some auxiliary
        information. Returns an array of dictionaries, one dictionary per
        metric. Each dictionary contains the following entries:
        'name': metric name
        'object': managed object this metric associated with
        'unit': unit of measurement
        'scale': divide 'values' by this number to get float numbers
        'values': collected data
        'values_as_string': pre-processed values ready for 'print' statement
        """
        # Get around the problem with input arrays returned in output
        # parameters (see #3953) for MSCOM.
        if self.isMscom:
            (values, names, objects, names_out, objects_out, units, scales, sequence_numbers,
                indices, lengths) = self.collector.queryMetricsData(names, objects)
        else:
            (values, names_out, objects_out, units, scales, sequence_numbers,
                indices, lengths) = self.collector.queryMetricsData(names, objects)
        out = []
        for i in xrange(0, len(names_out)):
            scale = int(scales[i])
            if scale != 1:
                fmt = '%.2f%s'
            else:
                fmt = '%d %s'
            out.append({
                'name':str(names_out[i]),
                'object':str(objects_out[i]),
                'unit':str(units[i]),
                'scale':scale,
                'values':[int(values[j]) for j in xrange(int(indices[i]), int(indices[i])+int(lengths[i]))],
                'values_as_string':'['+', '.join([fmt % (int(values[j])/scale, units[i]) for j in xrange(int(indices[i]), int(indices[i])+int(lengths[i]))])+']'
            })
        return out

def ComifyName(name):
    return name[0].capitalize()+name[1:]

_COMForward = { 'getattr' : None,
                'setattr' : None}

def CustomGetAttr(self, attr):
    # fastpath
    if self.__class__.__dict__.get(attr) != None:
        return self.__class__.__dict__.get(attr)

    # try case-insensitivity workaround for class attributes (COM methods)
    for k in self.__class__.__dict__.keys():
        if k.lower() == attr.lower():
            self.__class__.__dict__[attr] = self.__class__.__dict__[k]
            return getattr(self, k)
    try:
        return _COMForward['getattr'](self,ComifyName(attr))
    except AttributeError:
        return _COMForward['getattr'](self,attr)

def CustomSetAttr(self, attr, value):
    try:
        return _COMForward['setattr'](self, ComifyName(attr), value)
    except AttributeError:
        return _COMForward['setattr'](self, attr, value)

class PlatformMSCOM:
    # Class to fake access to constants in style of foo.bar.boo
    class ConstantFake:
        def __init__(self, parent, name):
            self.__dict__['_parent'] = parent
            self.__dict__['_name'] = name
            self.__dict__['_consts'] = {}
            try:
                self.__dict__['_depth']=parent.__dict__['_depth']+1
            except:
                self.__dict__['_depth']=0
                if self.__dict__['_depth'] > 4:
                    raise AttributeError

        def __getattr__(self, attr):
            import win32com
            from win32com.client import constants

            if attr.startswith("__"):
                raise AttributeError

            consts = self.__dict__['_consts']

            fake = consts.get(attr, None)
            if fake != None:
               return fake
            try:
               name = self.__dict__['_name']
               parent = self.__dict__['_parent']
               while parent != None:
                  if parent._name is not None:
                    name = parent._name+'_'+name
                  parent = parent._parent

               if name is not None:
                  name += "_" + attr
               else:
                  name = attr
               return win32com.client.constants.__getattr__(name)
            except AttributeError,e:
               fake = PlatformMSCOM.ConstantFake(self, attr)
               consts[attr] = fake
               return fake


    class InterfacesWrapper:
            def __init__(self):
                self.__dict__['_rootFake'] = PlatformMSCOM.ConstantFake(None, None)

            def __getattr__(self, a):
                import win32com
                from win32com.client import constants
                if a.startswith("__"):
                    raise AttributeError
                try:
                    return win32com.client.constants.__getattr__(a)
                except AttributeError,e:
                    return self.__dict__['_rootFake'].__getattr__(a)

    VBOX_TLB_GUID  = '{46137EEC-703B-4FE5-AFD4-7C9BBBBA0259}'
    VBOX_TLB_LCID  = 0
    VBOX_TLB_MAJOR = 1
    VBOX_TLB_MINOR = 0

    def __init__(self, params):
            from win32com import universal
            from win32com.client import gencache, DispatchBaseClass
            from win32com.client import constants, getevents
            import win32com
            import pythoncom
            import win32api
            from win32con import DUPLICATE_SAME_ACCESS
            from win32api import GetCurrentThread,GetCurrentThreadId,DuplicateHandle,GetCurrentProcess
            import threading
            pid = GetCurrentProcess()
            self.tid = GetCurrentThreadId()
            handle = DuplicateHandle(pid, GetCurrentThread(), pid, 0, 0, DUPLICATE_SAME_ACCESS)
            self.handles = []
            self.handles.append(handle)
            _COMForward['getattr'] = DispatchBaseClass.__dict__['__getattr__']
            DispatchBaseClass.__dict__['__getattr__'] = CustomGetAttr
            _COMForward['setattr'] = DispatchBaseClass.__dict__['__setattr__']
            DispatchBaseClass.__dict__['__setattr__'] = CustomSetAttr
            win32com.client.gencache.EnsureDispatch('VirtualBox.Session')
            win32com.client.gencache.EnsureDispatch('VirtualBox.VirtualBox')
            win32com.client.gencache.EnsureDispatch('VirtualBox.CallbackWrapper')
            self.oIntCv = threading.Condition()
            self.fInterrupted = False;

    def getSessionObject(self, vbox):
        import win32com
        from win32com.client import Dispatch
        return win32com.client.Dispatch("VirtualBox.Session")

    def getVirtualBox(self):
        import win32com
        from win32com.client import Dispatch
        return win32com.client.Dispatch("VirtualBox.VirtualBox")

    def getType(self):
        return 'MSCOM'

    def getRemote(self):
        return False

    def getArray(self, obj, field):
        return obj.__getattr__(field)

    def initPerThread(self):
        import pythoncom
        pythoncom.CoInitializeEx(0)

    def deinitPerThread(self):
        import pythoncom
        pythoncom.CoUninitialize()

    def createCallback(self, iface, impl, arg):
        d = {}
        d['BaseClass'] = impl
        d['arg'] = arg
        d['tlb_guid'] = PlatformMSCOM.VBOX_TLB_GUID
        str = ""
        str += "import win32com.server.util\n"
        str += "import pythoncom\n"

        str += "class "+iface+"Impl(BaseClass):\n"
        str += "   _com_interfaces_ = ['"+iface+"']\n"
        str += "   _typelib_guid_ = tlb_guid\n"
        str += "   _typelib_version_ = 1, 0\n"
        str += "   _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER\n"
        # Maybe we'd better implement Dynamic invoke policy, to be more flexible here
        str += "   _reg_policy_spec_ = 'win32com.server.policy.EventHandlerPolicy'\n"

        # generate capitalized version of callback methods -
        # that's how Python COM looks them up
        for m in dir(impl):
           if m.startswith("on"):
             str += "   "+ComifyName(m)+"=BaseClass."+m+"\n"

        str += "   def __init__(self): BaseClass.__init__(self, arg)\n"
        str += "result = win32com.client.Dispatch('VirtualBox.CallbackWrapper')\n"
        str += "result.SetLocalObject(win32com.server.util.wrap("+iface+"Impl()))\n"
        exec (str,d,d)
        return d['result']

    def waitForEvents(self, timeout):
        from win32api import GetCurrentThreadId
        from win32event import INFINITE
        from win32event import MsgWaitForMultipleObjects, \
                               QS_ALLINPUT, WAIT_TIMEOUT, WAIT_OBJECT_0
        from pythoncom import PumpWaitingMessages
        import types

        if not isinstance(timeout, types.IntType):
            raise TypeError("The timeout argument is not an integer")
        if (self.tid != GetCurrentThreadId()):
            raise Exception("wait for events from the same thread you inited!")

        if timeout < 0:     cMsTimeout = INFINITE
        else:               cMsTimeout = timeout
        rc = MsgWaitForMultipleObjects(self.handles, 0, cMsTimeout, QS_ALLINPUT)
        if rc >= WAIT_OBJECT_0 and rc < WAIT_OBJECT_0+len(self.handles):
            # is it possible?
            rc = 2;
        elif rc==WAIT_OBJECT_0 + len(self.handles):
            # Waiting messages
            PumpWaitingMessages()
            rc = 0;
        else:
            # Timeout
            rc = 1;

        # check for interruption
        self.oIntCv.acquire()
        if self.fInterrupted:
            self.fInterrupted = False
            rc = 1;
        self.oIntCv.release()

        return rc;

    def interruptWaitEvents(self):
        """
        Basically a python implementation of EventQueue::postEvent().

        The magic value must be in sync with the C++ implementation or this
        won't work.

        Note that because of this method we cannot easily make use of a
        non-visible Window to handle the message like we would like to do.
        """
        from win32api import PostThreadMessage
        from win32con import WM_USER
        self.oIntCv.acquire()
        self.fInterrupted = True
        self.oIntCv.release()
        try:
            PostThreadMessage(self.tid, WM_USER, None, 0xf241b819)
        except:
            return False;
        return True;

    def deinit(self):
        import pythoncom
        from win32file import CloseHandle

        for h in self.handles:
           if h is not None:
              CloseHandle(h)
        self.handles = None
        pythoncom.CoUninitialize()
        pass


class PlatformXPCOM:
    def __init__(self, params):
        sys.path.append(VboxSdkDir+'/bindings/xpcom/python/')
        import xpcom.vboxxpcom
        import xpcom
        import xpcom.components

    def getSessionObject(self, vbox):
        import xpcom.components
        return xpcom.components.classes["@virtualbox.org/Session;1"].createInstance()

    def getVirtualBox(self):
        import xpcom.components
        return xpcom.components.classes["@virtualbox.org/VirtualBox;1"].createInstance()

    def getType(self):
        return 'XPCOM'

    def getRemote(self):
        return False

    def getArray(self, obj, field):
        return obj.__getattr__('get'+ComifyName(field))()

    def initPerThread(self):
        import xpcom
        xpcom._xpcom.AttachThread()

    def deinitPerThread(self):
        import xpcom
        xpcom._xpcom.DetachThread()

    def createCallback(self, iface, impl, arg):
        d = {}
        d['BaseClass'] = impl
        d['arg'] = arg
        str = ""
        str += "import xpcom.components\n"
        str += "class "+iface+"Impl(BaseClass):\n"
        str += "   _com_interfaces_ = xpcom.components.interfaces."+iface+"\n"
        str += "   def __init__(self): BaseClass.__init__(self, arg)\n"
        str += "result = xpcom.components.classes['@virtualbox.org/CallbackWrapper;1'].createInstance()\n"
        str += "result.setLocalObject("+iface+"Impl())\n"
        exec (str,d,d)
        return d['result']

    def waitForEvents(self, timeout):
        import xpcom
        return xpcom._xpcom.WaitForEvents(timeout)

    def interruptWaitEvents(self):
        import xpcom
        return xpcom._xpcom.InterruptWait()

    def deinit(self):
        import xpcom
        xpcom._xpcom.DeinitCOM()

class PlatformWEBSERVICE:
    def __init__(self, params):
        sys.path.append(os.path.join(VboxSdkDir,'bindings', 'webservice', 'python', 'lib'))
        # not really needed, but just fail early if misconfigured
        import VirtualBox_services
        import VirtualBox_wrappers
        from VirtualBox_wrappers import IWebsessionManager2

        if params is not None:
            self.user = params.get("user", "")
            self.password = params.get("password", "")
            self.url = params.get("url", "")
        else:
            self.user = ""
            self.password = ""
            self.url = None
        self.vbox = None

    def getSessionObject(self, vbox):
        return self.wsmgr.getSessionObject(vbox)

    def getVirtualBox(self):
        return self.connect(self.url, self.user, self.password)

    def connect(self, url, user, passwd):
        if self.vbox is not None:
             self.disconnect()
        from VirtualBox_wrappers import IWebsessionManager2
        if url is None:
            url = ""
        self.url = url
        if user is None:
            user = ""
        self.user = user
        if passwd is None:
            passwd = ""
        self.password = passwd
        self.wsmgr = IWebsessionManager2(self.url)
        self.vbox = self.wsmgr.logon(self.user, self.password)
        if not self.vbox.handle:
            raise Exception("cannot connect to '"+self.url+"' as '"+self.user+"'")
        return self.vbox

    def disconnect(self):
        if self.vbox is not None and self.wsmgr is not None:
                self.wsmgr.logoff(self.vbox)
                self.vbox = None
                self.wsmgr = None

    def getType(self):
        return 'WEBSERVICE'

    def getRemote(self):
        return True

    def getArray(self, obj, field):
        return obj.__getattr__(field)

    def initPerThread(self):
        pass

    def deinitPerThread(self):
        pass

    def createCallback(self, iface, impl, arg):
        raise Exception("no callbacks for webservices")

    def waitForEvents(self, timeout):
        # Webservices cannot do that yet
        return 2;

    def interruptWaitEvents(self, timeout):
        # Webservices cannot do that yet
        return False;

    def deinit(self):
        try:
           disconnect()
        except:
           pass

class SessionManager:
    def __init__(self, mgr):
        self.mgr = mgr

    def getSessionObject(self, vbox):
        return self.mgr.platform.getSessionObject(vbox)

class VirtualBoxManager:
    def __init__(self, style, platparams):
        if style is None:
            if sys.platform == 'win32':
                style = "MSCOM"
            else:
                style = "XPCOM"


        exec "self.platform = Platform"+style+"(platparams)"
        # for webservices, enums are symbolic
        self.constants = VirtualBoxReflectionInfo(style == "WEBSERVICE")
        self.type = self.platform.getType()
        self.remote = self.platform.getRemote()
        self.style = style
        self.mgr = SessionManager(self)

        try:
            self.vbox = self.platform.getVirtualBox()
        except NameError,ne:
            print "Installation problem: check that appropriate libs in place"
            traceback.print_exc()
            raise ne
        except Exception,e:
            print "init exception: ",e
            traceback.print_exc()
            if self.remote:
                self.vbox = None
            else:
                raise e

    def getArray(self, obj, field):
        return self.platform.getArray(obj, field)

    def getVirtualBox(self):
        return  self.platform.getVirtualBox()

    def __del__(self):
        self.deinit()

    def deinit(self):
        if hasattr(self, "vbox"):
            del self.vbox
            self.vbox = None
        if hasattr(self, "platform"):
            self.platform.deinit()
            self.platform = None

    def initPerThread(self):
        self.platform.initPerThread()

    def openMachineSession(self, machineId):
         session = self.mgr.getSessionObject(self.vbox)
         try:
             self.vbox.openExistingSession(session, machineId)
         except:
             self.vbox.openSession(session, machineId)
         return session

    def closeMachineSession(self, session):
        if session is not None:
            session.close()

    def deinitPerThread(self):
        self.platform.deinitPerThread()

    def createCallback(self, iface, impl, arg):
        return self.platform.createCallback(iface, impl, arg)

    def waitForEvents(self, timeout):
        """
        Wait for events to arrive and process them.

        The timeout is in milliseconds.  A negative value means waiting for
        ever, while 0 does not wait at all.

        Returns 0 if events was processed.
        Returns 1 if timed out or interrupted in some way.
        Returns 2 on error (like not supported for web services).

        Raises an exception if the calling thread is not the main thread (the one
        that initialized VirtualBoxManager) or if the time isn't an integer.
        """
        return self.platform.waitForEvents(timeout)

    def interruptWaitEvents(self):
        """
        Interrupt a waitForEvents call.
        This is normally called from a worker thread.

        Returns True on success, False on failure.
        """
        return self.platform.interruptWaitEvents()

    def getPerfCollector(self, vbox):
        return PerfCollector(self, vbox)

    def getBinDir(self):
        global VboxBinDir
        return VboxBinDir

    def getSdkDir(self):
        global VboxSdkDir
        return VboxSdkDir
