# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2006 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the variables viewer widget.
"""

import types
from math import log10
import sys

from qt import QListView, QListViewItem, QWhatsThis, SIGNAL, QString, QRegExp, qApp

from Config import ConfigVarTypeDispStrings, ConfigVarTypeStrings
from VariableDetailDialog import VariableDetailDialog
from Utilities import toUnicode


class SpecialVarNode(QListViewItem):
    """
    Class implementing a QListViewItem that represents a special variable node.
    
    These special variable nodes are generated for classes, lists, tuples and dictionaries.
    """
    def __init__(self, parent, dvar, dvalue, dtype, frmnr, scope):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string or QString)
        @param dvalue value string (string or QString)
        @param dtype type string (string or QString)
        @param frmnr frame number (0 is the current frame) (int)
        @param scope flag indicating global (1) or local (0) variables
        """
        QListViewItem.__init__(self,parent,dvar,dvalue,dtype)
        
        self.setExpandable(1)
        self.framenr = frmnr
        self.scope = scope

    def setOpen(self,o):
        """
        Public slot to set/reset the open state.
        
        @param o flag indicating the open state
        """
        if o:
            pathlist = [unicode(self.text(0))]
            par = self.parent()
            
            # step 1: get a pathlist up to the requested variable
            while par is not None:
                pathlist.insert(0, unicode(par.text(0)))
                par = par.parent()
                
            dbg = qApp.mainWidget().getDebugger()
            dbs = qApp.mainWidget().getDebugServer()
            if self.scope == 1:
                filter = dbg.globalsVarFilter
            else:
                filter = dbg.localsVarFilter
            dbs.remoteClientVariable(self.scope, filter, pathlist, self.framenr)
        else:
            children = []
            itm = self.firstChild()
            while itm is not None:
                children.append(itm)
                itm = itm.nextSibling()
                
            for child in children:
                self.takeItem(child)
                del child

        QListViewItem.setOpen(self,o)

class ArrayElementVarNode(QListViewItem):
    """
    Class implementing a QListViewItem that represents an array element.
    """
    def __init__(self, parent, dvar, dvalue, dtype):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string or QString)
        @param dvalue value string (string or QString)
        @param dtype type string (string or QString)
        """
        QListViewItem.__init__(self, parent, dvar, dvalue, dtype)
        """
        Array elements have numbers as names, but the key must be
        right justified and zero filled to 6 decimal places. Then 
        element 2 will have a key of '000002' and appear before 
        element 10 with a key of '000010'
        """
        keyStr = str(self.text(0))
        self.arrayElementKey = QString("%.6d" % int(keyStr))

    def key(self, column, ascending):
        """
        Overridden method generating the key for this item.
        
        @param column the colomn to sort on (integer)
        @param ascending sort order (default)
        """
        if column == 0:
            return self.arrayElementKey
        else:
            return self.text(column)

class SpecialArrayElementVarNode(SpecialVarNode):
    """
    Class implementing a QListViewItem that represents a special array variable node.
    """
    def __init__(self, parent, dvar, dvalue, dtype, frmnr, scope):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string or QString)
        @param dvalue value string (string or QString)
        @param dtype type string (string or QString)
        @param frmnr frame number (0 is the current frame) (int)
        @param scope flag indicating global (1) or local (0) variables
        """
        SpecialVarNode.__init__(self, parent, dvar, dvalue, dtype, frmnr, scope)
        
        keyStr = str(self.text(0))[:-2] # strip off [], () or {}
        self.arrayElementKey = QString("%.6d" % int(keyStr))

    def key(self, column, ascending):
        """
        Overridden method generating the key for this item.
        
        @param column the colomn to sort on (integer)
        @param ascending sort order (default)
        """
        if column == 0:
            return self.arrayElementKey
        else:
            return self.text(column)

class VariablesViewer(QListView):
    """
    Class implementing the variables viewer widget.
    
    This widget is used to display the variables of the program being
    debugged in a tree. Compound types will be shown with
    their main entry first. Once the subtree has been expanded, the 
    individual entries will be shown. Double clicking an entry will
    popup a dialog showing the variables parameters in a more readable
    form. This is especially useful for lengthy strings.
    
    This widget has two modes for displaying the global and the local
    variables.
    """
    def __init__(self,parent=None,scope=1):
        """
        Constructor
        
        @param parent the parent (QWidget)
        @param scope flag indicating global (1) or local (0) variables
        """
        QListView.__init__(self,parent)
        
        self.indicators = {'list' : '[]', 'tuple' : '()', 'dict' : '{}', # Python types
                           'Array' : '[]', 'Hash' : '{}'}                # Ruby types
        self.rx_class = QRegExp('<.*(instance|object) at 0x.*>')
        self.rx_class2 = QRegExp('class .*')
        self.rx_class3 = QRegExp('<class .* at 0x.*>')
        self.dvar_rx_class1 = QRegExp(r'<.*(instance|object) at 0x.*>(\[\]|\{\}|\(\))')
        self.dvar_rx_class2 = QRegExp(r'<class .* at 0x.*>(\[\]|\{\}|\(\))')
        self.dvar_rx_array_element = QRegExp(r'^\d+$')
        self.dvar_rx_special_array_element = QRegExp(r'^\d+(\[\]|\{\}|\(\))$')
        self.rx_nonprintable = QRegExp(r"""(\\x\d\d)+""")
        self.framenr = 0

        self.loc = sys.getdefaultencoding()
            
        self.openItems = []
        
        self.setRootIsDecorated(1)
        self.setAllColumnsShowFocus(1)
        self.scope = scope
        if scope:
            self.setCaption(self.trUtf8("Global Variables"))
            self.addColumn(self.trUtf8("Globals"), 100)
            QWhatsThis.add(self,self.trUtf8(
                """<b>The Global Variables Viewer Window</b>"""
                """<p>This window displays the global variables"""
                """ of the debugged program.</p>"""
            ))
        else:
            self.setCaption(self.trUtf8("Local Variables"))
            self.addColumn(self.trUtf8("Locals"), 100)
            QWhatsThis.add(self,self.trUtf8(
                """<b>The Local Variables Viewer Window</b>"""
                """<p>This window displays the local variables"""
                """ of the debugged program.</p>"""
            ))
        
        self.addColumn(self.trUtf8("Value"), 150)
        self.addColumn(self.trUtf8("Type"))
        self.setSorting(0)
        
        self.connect(self, SIGNAL('expanded(QListViewItem *)'), self.handleExpanded)
        self.connect(self, SIGNAL('collapsed(QListViewItem *)'), self.handleCollapsed)
        
    def findItem(self, slist, column, node=None):
        """
        Reimplemented method
        
        It is used to find a specific item in column,
        that is a child of node. If node is None, a child of the
        QListView is searched.
        
        @param slist searchlist (list of strings or QStrings)
        @param column index of column to search in (int)
        @param node start point of the search
        @return the found item or None
        """
        if node is None:
            node = self
            
        itm = node.firstChild()
        while itm is not None:
            if QString.compare(itm.text(column), slist[0]) == 0:
                if len(slist) > 1:
                    itm = self.findItem(slist[1:], column, itm)
                break
            itm = itm.nextSibling()
        return itm
        
    def showVariables(self, vlist, frmnr):
        """
        Public method to show variables in a listview.
        
        @param vlist the list of variables to be displayed. Each
                listentry is a tuple of three values.
                <ul>
                <li>the variable name (string)</li>
                <li>the variables type (string)</li>
                <li>the variables value (string)</li>
                </ul>
        @param frmnr frame number (0 is the current frame) (int)
        """
        self.current = self.selectedItem()
        if self.current:
            self.curpathlist = self.__buildTreePath(self.current)
        self.clear()
        self.framenr = frmnr
        
        if len(vlist):
            for (var, vtype, value) in vlist:
                self.addItem(None, vtype, var, value)
        
            # reexpand tree
            openItems = self.openItems[:]
            openItems.sort()
            self.openItems = []
            for itemPath in openItems:
                itm = self.findItem(itemPath, 0)
                if itm is not None:
                    itm.setOpen(1)
                else:
                    self.openItems.append(itemPath)
            
            if self.current:
                citm = self.findItem(self.curpathlist, 0)
                if citm:
                    self.setCurrentItem(citm)
                    self.setSelected(citm, 1)
                    self.ensureItemVisible(citm)

    def showVariable(self, vlist):
        """
        Public method to show variables in a listview.
        
        @param vlist the list of variables to be displayed. Each
                listentry is a tuple of three values.
                <ul>
                <li>the variable name (string)</li>
                <li>the variables type (string)</li>
                <li>the variables value (string)</li>
                </ul>
        """
        self.current = self.selectedItem()
        if self.current:
            self.curpathlist = self.__buildTreePath(self.current)
        
        if len(vlist):
            itm = self.findItem(vlist[0], 0)
            for (var, vtype, value) in vlist[1:]:
                self.addItem(itm, vtype, var, value)

        # reexpand tree
        openItems = self.openItems[:]
        openItems.sort()
        self.openItems = []
        for itemPath in openItems:
            itm = self.findItem(itemPath, 0)
            if itm is not None and not itm.isOpen():
                itm.setOpen(1)
        self.openItems = openItems[:]
            
        if self.current:
            citm = self.findItem(self.curpathlist, 0)
            if citm:
                self.setCurrentItem(citm)
                self.setSelected(citm, 1)
                self.ensureItemVisible(citm)

    def generateItem(self, parent, dvar, dvalue, dtype, isSpecial = 0):
        """
        Private method used to generate a QListViewItem representing a variable.
        
        @param parent parent of the item to be generated
        @param dvar variable name (string or QString)
        @param dvalue value string (string or QString)
        @param dtype type string (string or QString)
        @param isSpecial flag indicating that a special node should be generated (boolean)
        @return The item that was generated (QListViewItem or SpecialVarNode).
        """
        if isSpecial and \
           (self.dvar_rx_class1.exactMatch(dvar) or \
            self.dvar_rx_class2.exactMatch(dvar)):
            isSpecial = 0
        
        if self.rx_class2.exactMatch(dtype):
            return SpecialVarNode(parent, dvar, dvalue, dtype[7:-1], self.framenr, self.scope)
        elif self.rx_class.exactMatch(dvalue) or \
             self.rx_class3.exactMatch(dvalue) or \
             isSpecial:
            if self.dvar_rx_special_array_element.exactMatch(dvar):
                return SpecialArrayElementVarNode(parent, dvar, dvalue, dtype, self.framenr, self.scope)
            else:
                return SpecialVarNode(parent, dvar, dvalue, dtype, self.framenr, self.scope)
        else:
            if isinstance(dvalue, str):
                dvalue = QString.fromLocal8Bit( dvalue )
            if self.dvar_rx_array_element.exactMatch(dvar):
                return ArrayElementVarNode(parent, dvar, dvalue, dtype)
            else:
                return QListViewItem(parent, dvar, dvalue, dtype)
        
    def __unicode(self, s):
        """
        Private method to convert a string to unicode.
        
        @param s the string to be converted (string)
        @return unicode representation of s (unicode object)
        """
        if type(s) is type(u""):
            return s
        try:
            u = unicode(s, self.loc)
        except TypeError:
            u = str(s)
        except UnicodeError:
            u = toUnicode(s)
        return u
        
    def addItem(self, parent, vtype, var, value):
        """
        Private method used to add an item to the listview.
        
        If the item is of a type with subelements (i.e. list, dictionary, 
        tuple), these subelements are added by calling this method recursively.
        
        @param parent the parent of the item to be added
            (QListViewItem or None)
        @param vtype the type of the item to be added
            (string)
        @param var the variable name (string)
        @param value the value string (string)
        @return The item that was added to the listview (QListViewItem).
        """
        if parent is None:
            parent = self
        try:
            dvar = '%s%s' % (var, self.indicators[vtype])
        except:
            dvar = var
        dvtype = self.getDispType(vtype)
        
        if vtype in ['list', 'Array', 'tuple', 'dict', 'Hash']:
            itm = self.generateItem(parent, dvar, self.trUtf8("%1 items").arg(value), dvtype, 1)
        elif vtype in ['unicode', 'str']:
            if self.rx_nonprintable.search(value) != -1:
                sval = value
            else:
                try:
                    sval = eval(value)
                except:
                    sval = value
            itm = self.generateItem(parent, dvar, self.__unicode(sval), dvtype)
        
        else:
            itm = self.generateItem(parent, dvar, value, dvtype)
            
        return itm

    def getDispType(self, vtype):
        """
        Private method used to get the display string for type vtype.
        
        @param vtype the type, the display string should be looked up for
              (string)
        @return displaystring (string or QString)
        """
        try:
            i = ConfigVarTypeStrings.index(vtype)
            dvtype = self.trUtf8(ConfigVarTypeDispStrings[i])
        except ValueError:
            if vtype == 'classobj':
                dvtype = self.trUtf8(\
                    ConfigVarTypeDispStrings[ConfigVarTypeStrings.index('instance')]\
                )
            else:
                dvtype = vtype
        return dvtype

    def contentsMouseDoubleClickEvent(self, mouseEvent):
        """
        Protected method of QListView. 
        
        Reimplemented to disable expanding/collapsing
        of items when double-clicking. Instead the double-clicked entry is opened.
        
        @param mouseEvent The mouse event object. (QMouseEvent)
        """
        itm = self.currentItem()
        
        val = itm.text(1)
        
        if val.isEmpty():
            return  # do not display anything, if the variable has no value
            
        vtype = itm.text(2)
        name = unicode(itm.text(0))
        if name[-2:] in ['[]', '{}', '()']:
            name = name[:-2]
        
        par = itm.parent()
        nlist = [name]
        # build up the fully qualified name
        while par is not None:
            pname = unicode(par.text(0))
            if pname[-2:] in ['[]', '{}', '()']:
                if nlist[0].endswith("."):
                    nlist[0] = '[%s].' % nlist[0][:-1]
                else:
                    nlist[0] = '[%s]' % nlist[0]
                nlist.insert(0, pname[:-2])
            else:
                nlist.insert(0, '%s.' % pname)
            par = par.parent()
            
        name = ''.join(nlist)
        # now show the dialog
        dlg = VariableDetailDialog(name, vtype, val)
        dlg.exec_loop()
    
    def __buildTreePath(self, itm):
        """
        Private method to build up a path from the top to an item.
        
        @param itm item to build the path for (QListViewItem)
        @return list of names denoting the path from the top (list of strings)
        """
        name = unicode(itm.text(0))
        pathlist = [name]
        
        par = itm.parent()
        # build up a path from the top to the item
        while par is not None:
            pname = unicode(par.text(0))
            pathlist.insert(0, pname)
            par = par.parent()
        
        return pathlist
    
    def handleExpanded(self, itm):
        """
        Private slot to handle the expanded signal.
        
        @param itm item being expanded (QListViewItem)
        """
        pathlist = self.__buildTreePath(itm)
        self.openItems.append(pathlist)
    
    def handleCollapsed(self, itm):
        """
        Private slot to handle the collapsed signal.
        
        @param itm item being expanded (QListViewItem)
        """
        pathlist = self.__buildTreePath(itm)
        self.openItems.remove(pathlist)
    
    def handleResetUI(self):
        """
        Public method to reset the VariablesViewer.
        """
        self.clear()
        self.openItems = []
