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

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

"""
Module implementing a Python code coverage dialog.
"""

import sys
import os
import types

from qt import *

from KdeQt import KQInputDialog
from KdeQt.KQProgressDialog import KQProgressDialog

from PyCoverageForm import PyCoverageForm
import CodeMetrics
import Utilities
from DebugClients.Python.PyCoverage import coverage

class PyCoverageDialog(PyCoverageForm):
    """
    Class implementing a dialog to display the collected code coverage data.
    """
    def __init__(self, parent = None):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        """
        PyCoverageForm.__init__(self, parent)
        
        self.resultList.setSorting(-1)
        self.summaryList.setSorting(-1)
        
        for col in range(1,4):
            self.resultList.setColumnAlignment(col, Qt.AlignRight)
        for col in range(0, 3):
            self.summaryList.setColumnAlignment(col, Qt.AlignRight)
        
        self.cancelled = 0
        self.path = '.'
        self.reload = 0
        
        self.excludeList = QStringList()
        
        self.menu = QPopupMenu(self)
        self.annotate = self.menu.insertItem(self.trUtf8('Annotate'), self.handleAnnotate)
        self.menu.insertItem(self.trUtf8('Annotate all'), self.handleAnnotateAll)
        self.menu.insertItem(self.trUtf8('Delete annotated files'), self.deleteAnnotated)
        self.menu.insertSeparator()
        self.menu.insertItem(self.trUtf8('Erase Coverage Info'), self.handleErase)
    
    def __format_lines(self, lines):
        """
        Private method to format a list of integers into string by coalescing groups.
        
        @param lines list of integers
        @return string representing the list
        """
        pairs = []
        lines.sort()
        maxValue = lines[-1]
        start = None
        
        i = lines[0]
        while i <= maxValue:
            try:
                if start is None:
                    start = i
                ind = lines.index(i)
                end = i
                i += 1
            except ValueError:
                pairs.append((start, end))
                start = None
                if ind + 1 >= len(lines):
                    break
                i = lines[ind+1]
        if start:
            pairs.append((start, end))
        
        def stringify(pair):
            start, end = pair
            if start == end:
                return "%d" % start
            else:
                return "%d-%d" % (start, end)
        
        import string
        return string.join(map(stringify, pairs), ", ")
    
    def start(self, cfn, fn):
        """
        Public slot to start the coverage data evaluation.
        
        @param cfn basename of the coverage file (string)
        @param fn file or list of files or directory to be checked
                (string or list of strings)
        """
        self.__cfn = cfn
        self.__fn = fn
        
        self.basename = os.path.splitext(cfn)[0]
        
        self.cfn = "%s.coverage" % self.basename
        
        if type(fn) is types.ListType:
            files = fn
            self.path = os.path.dirname(cfn)
        elif os.path.isdir(fn):
            files = Utilities.direntries(fn, 1, '*.py', 0)
            self.path = fn
        else:
            files = [fn]
            self.path = os.path.dirname(cfn)
        files.sort()
        files.reverse()
        
        cover = coverage(self.cfn)
        
        # set the exclude pattern
        if not self.reload:
            self.excludeList.prepend(cover.getExclude())
        self.excludeCombo.clear()
        self.excludeCombo.insertStringList(self.excludeList)
        
        self.checkProgress.setTotalSteps(len(files))
        qApp.processEvents()
        
        total_statements = 0
        total_executed = 0
        
        # now go through all the files
        cover.exclude(unicode(self.excludeList[0]), 1)
        progress = 0
        for file in files:
            if self.cancelled:
                return
            
            _, statements, excluded, missing, readable  = cover.analysis2(file)
            readableEx = excluded and self.__format_lines(excluded) or ''
            n = len(statements)
            m = n - len(missing)
            if n > 0:
                pc = 100.0 * m / n
            else:
                pc = 100.0
            itm = QListViewItem(self.resultList, file, str(n), str(m), "%d%%" % pc, 
                                readableEx, readable)
            
            total_statements = total_statements + n
            total_executed = total_executed + m
            
            progress += 1
            self.checkProgress.setProgress(progress)
            qApp.processEvents()
        
        # show summary info
        if len(files) > 1:
            if total_statements > 0:
                pc = 100.0 * total_executed / total_statements
            else:
                pc = 100.0
            itm = QListViewItem(self.summaryList, 
                str(total_statements), str(total_executed), "%d%%" % pc)
                
        self.finish()
        
    def finish(self):
        """
        Private slot called when the action finished or the user pressed the button.
        """
        self.cancelled = 1
        self.cancelButton.setText(self.trUtf8('&OK'))
        self.cancelButton.setDefault(1)
        
    def buttonPressed(self):
        """
        Private slot connected to the button clicked signal.
        """
        if self.cancelled:
            self.close()
        else:
            self.finish()
            
    def handleContextMenu(self, itm, pos, col):
        """
        Private slot to show the context menu of the listview.
        
        @param itm the item under the mouse cursor (QListViewItem)
        @param pos global position of the context menu (QPoint)
        @param col column of the mouse cursor (int)
        """
        if itm:
            self.menu.setItemEnabled(self.annotate, 1)
        else:
            self.menu.setItemEnabled(self.annotate, 0)
        self.menu.popup(pos)
        
    def handleAnnotate(self):
        """
        Private slot to handle the annotate context menu action.
        
        This method produce an annotated coverage file of the
        selected file.
        """
        itm = self.resultList.currentItem()
        fn = unicode(itm.text(0))
        
        cover = coverage(self.cfn)
        cover.exclude(unicode(self.excludeList[0]), 1)
        cover.annotate([fn], None, 1)
        
    def handleAnnotateAll(self):
        """
        Private slot to handle the annotate all context menu action.
        
        This method produce an annotated coverage file of every
        file listed in the listview.
        """
        n = self.resultList.childCount()
        if n == 0:
            return
        
        # get list of all filenames
        files = []
        itm = self.resultList.firstChild()
        while itm:
            files.append(unicode(itm.text(0)))
            itm = itm.itemBelow()
            
        cover = coverage(self.cfn)
        cover.exclude(unicode(self.excludeList[0]), 1)
        
        # now process them
        progress = KQProgressDialog(self.trUtf8("Annotating files..."), 
            self.trUtf8("Abort"), len(files), self, "progress", 1)
        progress.setMinimumDuration(0)
        i = 0
        
        for file in files:
            progress.setProgress(i)
            if progress.wasCancelled():
                break
            cover.annotate([file], None, 1)
            i += 1
            
        progress.setProgress(len(files))
        
    def handleErase(self):
        """
        Private slot to handle the erase context menu action.
        
        This method erases the collected coverage data that is
        stored in the .coverage file.
        """
        cover = coverage(self.cfn)
        cover.erase()
        
    def deleteAnnotated(self):
        """
        Private slot to handle the delete annotated context menu action.
        
        This method deletes all annotated files. These are files
        ending with ',cover'.
        """
        files = Utilities.direntries(self.path, 1, '*,cover', 0)
        for file in files:
            try:
                os.remove(file)
            except:
                pass

    def handleReload(self):
        """
        Privale slot to reload the coverage info.
        """
        self.resultList.clear()
        self.summaryList.clear()
        self.reload = 1
        excludePattern = self.excludeCombo.currentText()
        self.excludeList.remove(excludePattern)
        self.excludeList.prepend(excludePattern)
        self.cancelled = 0
        self.cancelButton.setText(self.trUtf8('&Cancel'))
        self.cancelButton.setDefault(1)
        self.start(self.__cfn, self.__fn)
