# An example showing how to use the python doxmlparser module to extract some metrics from
# the XML output generated by doxygen for a project.

import sys

import doxmlparser

from doxmlparser.compound import DoxCompoundKind, DoxMemberKind, DoxSectionKind, MixedContainer

class Metrics:
    def __init__(self):
        self.numClasses = 0
        self.numDocClasses = 0
        self.numStructs = 0
        self.numUnions = 0
        self.numInterfaces = 0
        self.numExceptions = 0
        self.numNamespaces = 0
        self.numFiles = 0
        self.numDocFiles = 0
        self.numGroups = 0
        self.numPages = 0
        self.numPubMethods = 0
        self.numDocPubMethods = 0
        self.numProMethods = 0
        self.numDocProMethods = 0
        self.numPriMethods = 0
        self.numDocPriMethods = 0
        self.numAttributes = 0
        self.numDocAttributes = 0
        self.numFunctions = 0
        self.numDocFunctions = 0
        self.numVariables = 0
        self.numDocVariables = 0
        self.numParams = 0
    def print(self):
        numMethods    = self.numPubMethods    + self.numProMethods    + self.numPriMethods
        numDocMethods = self.numDocPubMethods + self.numDocProMethods + self.numDocPriMethods
        print("Metrics:")
        print("-----------------------------------")
        if self.numClasses>0:
            print("Classes:     {:=10} ({} documented)".format(self.numClasses,self.numDocClasses))
        if self.numStructs>0:
            print("Structs:     {:=10}".format(self.numStructs))
        if self.numUnions>0:
            print("Unions:      {:=10}".format(self.numUnions))
        if self.numExceptions>0:
            print("Exceptions:  {:=10}".format(self.numExceptions))
        if self.numNamespaces>0:
            print("Namespaces:  {:=10}".format(self.numNamespaces))
        if self.numFiles>0:
            print("Files:       {:=10} ({} documented)".format(self.numFiles,self.numDocFiles))
        if self.numGroups>0:
            print("Groups:      {:=10}".format(self.numGroups))
        if self.numPages>0:
            print("Pages:       {:=10}".format(self.numPages))
        if numMethods>0:
            print("Methods:     {:=10} ({} documented)".format(numMethods,numDocMethods))
        if self.numPubMethods>0:
            print("  Public:    {:=10} ({} documented)".format(self.numPubMethods,self.numDocPubMethods))
        if self.numProMethods>0:
            print("  Protected: {:=10} ({} documented)".format(self.numProMethods,self.numDocProMethods))
        if self.numPriMethods>0:
            print("  Private:   {:=10} ({} documented)".format(self.numPriMethods,self.numDocPriMethods))
        if self.numFunctions>0:
            print("Functions:   {:=10} ({} documented)".format(self.numFunctions,self.numDocFunctions))
        if self.numAttributes>0:
            print("Attributes:  {:=10} ({} documented)".format(self.numAttributes,self.numDocAttributes))
        if self.numVariables>0:
            print("Variables:   {:=10} ({} documented)".format(self.numVariables,self.numDocVariables))
        if self.numParams>0:
            print("Params:      {:=10}".format(self.numParams))
        print("-----------------------------------")
        if self.numClasses>0:
            print("Avg. #methods/compound:  {:=10}".format(float(numMethods)/float(self.numClasses)))
        if numMethods>0:
            print("Avg. #params/method:     {:=10}".format(float(self.numParams)/float(numMethods)))
        print("-----------------------------------")


def description_is_empty(description):
    for content in description.content_:
        if content.getCategory()==MixedContainer.CategoryText:
            if not content.getValue().isspace():
                return False # non space-only text
        elif not content.getCategory()==MixedContainer.CategoryNone:
            return False # some internal object like a paragraph
    return True

def is_documented(definition):
    return not description_is_empty(definition.get_briefdescription()) or \
           not description_is_empty(definition.get_detaileddescription())

def section_is_protected(sectionkind):
    return sectionkind in [DoxSectionKind.PROTECTEDTYPE,
                           DoxSectionKind.PROTECTEDFUNC,
                           DoxSectionKind.PROTECTEDATTRIB,
                           DoxSectionKind.PROTECTEDSLOT,
                           DoxSectionKind.PROTECTEDSTATICFUNC,
                           DoxSectionKind.PROTECTEDSTATICATTRIB]

def section_is_private(sectionkind):
    return sectionkind in [DoxSectionKind.PRIVATETYPE,
                           DoxSectionKind.PRIVATEFUNC,
                           DoxSectionKind.PRIVATEATTRIB,
                           DoxSectionKind.PRIVATESLOT,
                           DoxSectionKind.PRIVATESTATICFUNC,
                           DoxSectionKind.PRIVATESTATICATTRIB]

def section_is_public(sectionkind):
    return not section_is_protected(sectionkind) and not section_is_private(sectionkind)

def linked_text_to_string(linkedtext):
    str=''
    if linkedtext:
        for text_or_ref in linkedtext.content_:
            if text_or_ref.getCategory()==MixedContainer.CategoryText:
                str+=text_or_ref.getValue()
            else:
                str+=text_or_ref.getValue().get_valueOf_()
    return str

def parse_members(compounddef,sectiondef,metrics):
    functionLikeKind = [DoxMemberKind.FUNCTION,
                        DoxMemberKind.PROTOTYPE,
                        DoxMemberKind.SIGNAL,
                        DoxMemberKind.SLOT,
                        DoxMemberKind.DCOP]
    variableLikeKind = [DoxMemberKind.VARIABLE, DoxMemberKind.PROPERTY]
    for memberdef in sectiondef.get_memberdef():
        if compounddef.get_kind() in [DoxCompoundKind.CLASS, DoxCompoundKind.STRUCT, DoxCompoundKind.INTERFACE]:
            if memberdef.get_kind() in functionLikeKind:
                if section_is_public(sectiondef.get_kind()):
                    metrics.numPubMethods+=1
                    if is_documented(memberdef):
                        metrics.numDocPubMethods+=1
                elif section_is_protected(sectiondef.get_kind()):
                    metrics.numProMethods+=1
                    if is_documented(memberdef):
                        metrics.numDocProMethods+=1
                elif section_is_private(sectiondef.get_kind()):
                    metrics.numPriMethods+=1
                    if is_documented(memberdef):
                        metrics.numDocPriMethods+=1
            elif memberdef.get_kind() in variableLikeKind:
                metrics.numAttributes+=1
                if is_documented(memberdef):
                    metrics.numDocAttributes+=1
        elif compounddef.get_kind() in [DoxCompoundKind.FILE, DoxCompoundKind.NAMESPACE]:
            if memberdef.get_kind() in functionLikeKind:
                metrics.numFunctions+=1
                if is_documented(memberdef):
                    metrics.numDocFunctions+=1
            elif memberdef.get_kind() in variableLikeKind:
                metrics.numVariables+=1
                if is_documented(memberdef):
                    metrics.numDocVariables+=1
        #for param in memberdef.get_param():
        #    name = ''
        #    if param.get_defname():
        #        name = param.get_defname()
        #    if param.get_declname():
        #        name = param.get_declname()
        #    print("param '{}':'{}'".format(linked_text_to_string(param.get_type()),name))
        metrics.numParams+=len(memberdef.get_param())
        if memberdef.get_type() and memberdef.get_type()!="void" and linked_text_to_string(memberdef.get_type()):
            metrics.numParams+=1  # count non-void return types as well
            #print("returns '{}'".format(linked_text_to_string(memberdef.get_type())))

def parse_sections(compounddef,metrics):
    for sectiondef in compounddef.get_sectiondef():
        parse_members(compounddef,sectiondef,metrics)

def parse_compound(inDirName,baseName,metrics):
    rootObj = doxmlparser.compound.parse(inDirName+"/"+baseName+".xml",True)
    for compounddef in rootObj.get_compounddef():
        kind = compounddef.get_kind()
        if kind==DoxCompoundKind.CLASS:
            metrics.numClasses+=1
            if is_documented(compounddef):
                metrics.numDocClasses+=1
        elif kind==DoxCompoundKind.STRUCT:
            metrics.numStructs+=1
        elif kind==DoxCompoundKind.UNION:
            metrics.numUnions+=1
        elif kind==DoxCompoundKind.INTERFACE:
            metrics.numInterfaces+=1
        elif kind==DoxCompoundKind.EXCEPTION:
            metrics.numExceptions+=1
        elif kind==DoxCompoundKind.NAMESPACE:
            metrics.numNamespaces+=1
        elif kind==DoxCompoundKind.FILE:
            metrics.numFiles+=1
            if is_documented(compounddef):
                metrics.numDocFiles+=1
        elif kind==DoxCompoundKind.GROUP:
            metrics.numGroups+=1
        elif kind==DoxCompoundKind.PAGE:
            metrics.numPages+=1
        else:
            continue
        parse_sections(compounddef,metrics)

def parse_index(inDirName):
    metrics = Metrics()
    rootObj = doxmlparser.index.parse(inDirName+"/index.xml",True)
    for compound in rootObj.get_compound(): # for each compound defined in the index
        print("Processing {0}...".format(compound.get_name()))
        parse_compound(inDirName,compound.get_refid(),metrics)
    metrics.print()

def usage():
    print("Usage {0} <xml_output_dir>".format(sys.argv[0]))
    sys.exit(1)

def main():
    args = sys.argv[1:]
    if len(args)==1:
        parse_index(args[0])
    else:
        usage()

if __name__ == '__main__':
    main()

