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

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

"""
Module implementing a dialog showing a UML like class diagram of a package.
"""

import glob
import os.path

from qt import *
from qtcanvas import *

from KdeQt.KQProgressDialog import KQProgressDialog

from UMLCanvasView import UMLCanvasView
from Utilities.ModuleParser import readModule
from ClassWidget import ClassWidget, ClassModel
from AssociationWidget import AssociationWidget, Generalisation
import GraphicsUtilities
import Utilities

class PackageDiagram(QDialog):
    """
    Class implementing a dialog showing a UML like class diagram of a package.
    """
    def __init__(self,package,parent = None,name = None,modal = 0,fl = 0, noAttrs=0):
        """
        Constructor
        
        @param package name of a python package to be shown (string)
        @param parent parent widget of the view (QWidget)
        @param name name of the view widget (QString or string)
        @param flags the window flags to be passed to the view widget
        @keyparam noAttrs flag indicating, that no attributes should be shown (boolean)
        """
        QDialog.__init__(self,parent,name,modal,fl)

        if not name:
            self.setName("PackageDiagram")

        UMLFormLayout = QVBoxLayout(self,6,6,"UMLFormLayout")

        self.canvas = QCanvas(800, 600)
        self.umlCanvas = UMLCanvasView(self.canvas,self,"umlCanvas")
        self.umlCanvas.setGeometry(QRect(6,6,788,555))
        UMLFormLayout.addWidget(self.umlCanvas)

        layout1 = QHBoxLayout(None,0,6,"layout1")
        spacer = QSpacerItem(40,20,QSizePolicy.Expanding,QSizePolicy.Minimum)
        layout1.addItem(spacer)

        self.closeButton = QPushButton(self,"closeButton")
        layout1.addWidget(self.closeButton)
        spacer_2 = QSpacerItem(40,20,QSizePolicy.Expanding,QSizePolicy.Minimum)
        layout1.addItem(spacer_2)
        UMLFormLayout.addLayout(layout1)

        self.languageChange()

        self.resize(QSize(800,604).expandedTo(self.minimumSizeHint()))
        self.clearWState(Qt.WState_Polished)

        self.connect(self.closeButton,SIGNAL("clicked()"),self,SLOT("close()"))
        
        self.package = Utilities.normabspath(package)
        self.allClasses = {}
        self.noAttrs = noAttrs

    def languageChange(self):
        """
        Private method used to show the localized strings for this dialog.
        """
        self.setCaption(self.__tr("Package-Diagram"))
        self.closeButton.setText(self.__tr("&Close"))
        self.closeButton.setAccel(self.__tr("Alt+C"))


    def __tr(self,s,c = None):
        """
        Private method to translate the display strings.
        """
        return qApp.translate("PackageDiagram",s,c)
        
    
    def getDiagramName(self):
        """
        Method to retrieve a name for the diagram.
        
        @return name for the diagram
        """
        return self.package
        
    def getCurrentShape(self, name):
        """
        Private method to get the named shape.
        
        @param name name of the shape (string)
        @return shape (QCanvasItem)
        """
        return self.allClasses.get(name)
        
    def buildModulesDict(self):
        """
        Private method to build a dictionary of modules contained in the package.
        
        @return dictionary of modules contained in the package.
        """
        supportedExt = ['*.py', '*.ptl', '*.rb']
        
        moduleDict = {}
        modules = []
        for ext in supportedExt:
            modules.extend(glob.glob(Utilities.normjoinpath(self.package, ext)))
        tot = len(modules)
        try:
            prog = 0
            progress = KQProgressDialog(self.trUtf8("Parsing modules..."),
                QString.null, tot, None, None, 1)
            progress.show()
            qApp.processEvents()
            for module in modules:
                progress.setProgress(prog)
                qApp.processEvents()
                prog = prog + 1
                try: 
                    mod = readModule(module)
                except ImportError:
                    continue
                else:
                    name = mod.name
                    if name.startswith(self.package):
                        name = name[len(self.package) + 1:]
                    moduleDict[name] = mod
        finally:
            progress.setProgress(tot)
        return moduleDict
        
    def buildClasses(self):
        """
        Private method to build the class shapes of the class diagram.
        
        The algorithm is borrowed from Boa Constructor.
        """
        initlist = glob.glob(os.path.join(self.package, '__init__.*'))
        if len(initlist) == 0:
            ct = QCanvasText(self.trUtf8("The directory '%1' is not a package.")
                                 .arg(self.package), self.canvas)
            ct.setX(10)
            ct.setY(10)
            ct.show()
            return
        
        modules = self.buildModulesDict()
        if not modules:
            ct = QCanvasText(self.trUtf8("The package '%1' does not contain any modules.")
                                 .arg(self.package), self.canvas)
            ct.setX(10)
            ct.setY(10)
            ct.show()
            return
            
        # step 1: build all classes found in the modules
        classesFound = 0
        
        for modName in modules.keys():
            module = modules[modName]
            for cls in module.classes.keys():
                classesFound = 1
                self.addLocalClass(cls, module.classes[cls], 10, 10)
        if not classesFound:
            ct = QCanvasText(self.trUtf8("The package '%1' does not contain any classes.")
                                 .arg(self.package), self.canvas)
            ct.setX(10)
            ct.setY(10)
            ct.show()
            return
        
        # step 2: build the class hierarchies
        routes = []
        nodes = []
        
        for modName in modules.keys():
            module = modules[modName]
            todo = [module.createHierarchy()]
            while todo:
                hierarchy = todo[0]
                for className in hierarchy.keys():
                    cw = self.getCurrentShape(className)
                    if not cw and className.find('.') >= 0:
                        cw = self.getCurrentShape(className.split('.')[-1])
                        if cw:
                            self.allClasses[className] = cw
                    if cw and cw.noAttrs != self.noAttrs:
                        cw = None
                    if cw and not (cw.external and \
                                   (module.classes.has_key(className) or
                                    module.modules.has_key(className))
                                  ):
                        if className not in nodes:
                            nodes.append(className)
                    else:
                        if module.classes.has_key(className):
                            # this is a local class (defined in this module)
                            self.addLocalClass(className, module.classes[className], 10, 10)
                        elif module.modules.has_key(className):
                            # this is a local module (defined in this module)
                            self.addLocalClass(className, module.modules[className], 10, 10, 1)
                        else:
                            self.addExternalClass(className, 10, 10)
                        nodes.append(className)
                        
                    if hierarchy.get(className):
                        todo.append(hierarchy.get(className))
                        children = hierarchy.get(className).keys()
                        for child in children:
                            if (className, child) not in routes:
                                routes.append((className, child))
                
                del todo[0]
            
        self.arrangeClasses(nodes, routes[:])
        self.createAssociations(routes)
        
        # resize the canvas to accomodate the widgets
        rect = self.umlCanvas.getDiagramRect(10) # 10 pixel border
        newSize = self.canvas.size()
        if rect.width() > newSize.width():
            newSize.setWidth(rect.width())
        if rect.height() > newSize.height():
            newSize.setHeight(rect.height())
        self.canvas.resize(newSize.width(), newSize.height())
        
    def arrangeClasses(self, nodes, routes, whiteSpaceFactor = 1.2):
        """
        Private method to arrange the shapes on the canvas.
        
        The algorithm is borrowed from Boa Constructor.
        """
        generations = GraphicsUtilities.sort(nodes, routes)
        
        # calculate width and height of all elements
        sizes = []
        for generation in generations:
            sizes.append([])
            for child in generation:
                sizes[-1].append(self.getCurrentShape(child).rect())
                
        # calculate total width and total height
        width = 0
        height = 0
        widths = []
        heights = []
        for generation in sizes:
            currentWidth = 0
            currentHeight = 0
            
            for rect in generation:
                if rect.bottom() > currentHeight:
                    currentHeight = rect.bottom()
                currentWidth = currentWidth + rect.right()
                
            # update totals
            if currentWidth > width:
                width = currentWidth
            height = height + currentHeight
            
            # store generation info
            widths.append(currentWidth)
            heights.append(currentHeight)
            
        # add in some whitespace
        width = width * whiteSpaceFactor
        rawHeight = height
        height = height * whiteSpaceFactor - 20
        verticalWhiteSpace = (height - rawHeight) / (len(generations) - 1.0 or 2.0)
        
        width += 50
        height += 50
        cwidth = width < self.canvas.width() and self.canvas.width() or width
        cheight = height < self.canvas.height() and self.canvas.height() or height
        self.canvas.resize(cwidth, cheight)
        
        # distribute each generation across the width and the
        # generations across height
        y = 10
        for currentWidth, currentHeight, generation in map(None, widths, heights, generations):
            x = 10
            # whiteSpace is the space between any two elements
            whiteSpace = (width - currentWidth - 20) / (len(generation) - 1.0 or 2.0)
            for className in generation:
                cw = self.getCurrentShape(className)
                cw.move(x, y)
                rect = cw.rect()
                x = x + rect.width() + whiteSpace
            y = y + currentHeight + verticalWhiteSpace
            
    def addLocalClass(self, className, _class, x, y, isRbModule=0):
        """
        Private method to add a class defined in the module.
        
        If the canvas is too small to take the shape, it
        is enlarged.
        
        @param className name of the class to be as a dictionary key (string)
        @param _class class to be shown (ModuleParser.Class)
        @param x x-coordinate (integer)
        @param y y-coordinate (integer)
        @param isRbModule flag indicating a Ruby module (boolean)
        """
        meths = _class.methods.keys()
        meths.sort()
        attrs = _class.attributes[:]
        attrs.sort()
        name = _class.name
        if isRbModule:
            name = "%s (Module)" % name
        cl = ClassModel(name, meths, attrs)
        cw = ClassWidget(self.canvas, cl, 0, x, y, noAttrs=self.noAttrs)
        cw.show()
        self.allClasses[className] = cw
        rect = cw.rect()
        newSize = self.canvas.size()
        if rect.right() > newSize.width():
            newSize.setWidth(rect.right()+10)
        if rect.bottom() > newSize.height():
            newSize.setHeight(rect.bottom()+10)
        self.canvas.resize(newSize.width(), newSize.height())
        
    def addExternalClass(self, _class, x, y):
        """
        Private method to add a class defined outside the module.
        
        If the canvas is too small to take the shape, it
        is enlarged.
        
        @param _class class to be shown (string)
        @param x x-coordinate (integer)
        @param y y-coordinate (integer)
        """
        cl = ClassModel(_class)
        cw = ClassWidget(self.canvas, cl, 1, x, y, noAttrs=self.noAttrs)
        cw.show()
        self.allClasses[_class] = cw
        rect = cw.rect()
        newSize = self.canvas.size()
        if rect.right() > newSize.width():
            newSize.setWidth(rect.right()+10)
        if rect.bottom() > newSize.height():
            newSize.setHeight(rect.bottom()+10)
        self.canvas.resize(newSize.width(), newSize.height())
        
    def createAssociations(self, routes):
        """
        Private method to generate the associations between the class shapes.
        
        @param routes list of relationsships
        """
        for route in routes:
            if len(route) > 1:
                assoc = AssociationWidget(self.canvas, 
                        self.getCurrentShape(route[1]),
                        self.getCurrentShape(route[0]),
                        Generalisation)
                assoc.show()
                
    def show(self):
        """
        Overriden method to show the dialog.
        """
        self.buildClasses()
        QDialog.show(self)
        
    def relayout(self):
        """
        Method to relayout the diagram.
        """
        self.allClasses.clear()
        self.buildClasses()
