#! /usr/bin/env python

"""
Create a Makefile dependency file that includes all of the names of the source
files that a given doxygen configuration file specifies should be parsed.

Usage:

    makedeps.py target [doxyfile]

where

    target      is the name of the Makefile target that depends on the doxygen
                configuration file
    doxyfile    is the name of the doxygen configuration file (default Doxyfile)

Output is to standard output.
"""

########################################################################

import fnmatch
from   glob import glob
import os
import re
import sys

########################################################################

class Doxyfile:

    contRe    = re.compile(r"\\\s*")
    commentRe = re.compile(r"#(.*)")
    tagRe     = re.compile(r"(\w+)\s*(\+?=)(.*)")

    defaultFilePatterns = ["*.c", "*.cc", "*.cxx", "*.cpp", "*.c++", "*.java",
                           "*.ii", "*.ixx", "*.ipp", "*.i++", "*.inl", "*.h",
                           "*.hh", "*.hxx", "*.hpp", "*.h++", "*.idl", "*.odl",
                           "*.cs", "*.php", "*.php3", "*.inc", "*.m", "*.mm",
                           "*.py"]

    def __init__(self, arg):
        """
        __init__(self, str filename)
        __init__(self, file f)

        Initialize a Doxyfile object. Take a string filename or file object that
        points to a doxygen configuration file.
        """
        # Obtain the text of the doxygen configuration file
        if hasattr(arg, "read"):
            fileObj = arg
        else:
            fileObj = open(arg)
        self.__text = fileObj.read()
        # Extract the tags from the Doxyfile
        self.__tags = { }
        self.extractTags()

    ####################################################################

    def removeContinuations(self):
        """
        removeContinuations(self) -> None

        Take the text of the Doxyfile and merge all continuation lins into
        single lines.
        """
        start = 0
        match = True
        while match:
            match = self.contRe.search(self.__text[start:])
            if match:
                self.__text = self.__text[:start+match.start()] + \
                               self.__text[start+match.end():]
                start       += match.start()

    ####################################################################

    def removeComments(self):
        """
        removeComments(self) -> None

        Take the text of the Doxyfile and remove all comments.
        """
        start = 0
        match = True
        while match:
            match = self.commentRe.search(self.__text[start:])
            if match:
                self.__text = self.__text[:start+match.start()] + \
                               self.__text[start+match.end():]
                start       += match.start()

    ####################################################################

    def extractTags(self):
        """
        extractTags(self) -> None

        Search through the Doxyfile text and extract all "TAG = VALUE"
        specifications, storing the results in an internal dictionary.
        """
        self.removeContinuations()
        self.removeComments()
        start = 0
        match = True
        while match:
            match = self.tagRe.search(self.__text[start:])
            if match:
                if match.group(2) == "+=":
                    self.__tags[match.group(1).strip()] += " " + \
                                                        match.group(3).strip()
                else:
                    self.__tags[match.group(1).strip()] = \
                                                        match.group(3).strip()
                start += match.end()

    ####################################################################

    def __getitem__(self, tag):
        """
        __getitem__(self, str tag) -> str

        Str argument tag is matched against the tags within the Doxyfile, and
        the corresponding value is returned as a single string.  If the tag is
        not defined, then the empty string is returned.
        """
        return self.__tags.get(tag, "")

    ####################################################################

    def tags(self):
        """
        tags(self) -> [str, ...]

        Return a list of the Doxyfile's tags.
        """
        return self.__tags.keys()

    ####################################################################

    def sources(self):
        """
        sources(self) -> [str, ...]

        Return a list of all the files that this Doxyfile will parse in order to
        generate documentation.
        """
        # Generate the initial directories and sources based on INPUT
        sources = [ ]
        dirs    = [ ]
        for item in self["INPUT"].split():
            if os.path.isdir(item):
                dirs.append(os.path.abspath(item))
            elif os.path.isfile(item):
                sources.append(os.path.abspath(item))

        # Add recursive directories, if requested
        if self["RECURSIVE"] == "YES":
            for dir in dirs:
                for (dirpath, dirnames, filenames) in os.walk(dir):
                    dirs.extend([os.path.abspath(os.path.join(dirpath,dir))
                                 for dir in dirnames])

        # Exclude directories and generate the initial list of exclude sources
        # based on EXCLUDE
        exclude_sources = [ ]
        for item in self["EXCLUDE"].split():
            if os.path.isdir(item):
                try:
                    dirs.remove(os.path.abspath(item))
                except ValueError:
                    pass
            elif os.path.isfile(item):
                exclude_sources.append(os.path.abspath(item))

        # Add sources based on FILE_PATTERNS
        file_patterns = self["FILE_PATTERNS"].split()
        if len(file_patterns) == 0:
            file_patterns = self.defaultFilePatterns
        for dir in dirs:
            for pattern in file_patterns:
                full_pattern = os.path.join(dir,pattern)
                sources.extend(glob(full_pattern))

        # Add to the list of exclude sources based on EXCLUDE_PATTERNS
        exclude_patterns = self["EXCLUDE_PATTERNS"].split()
        for dir in dirs:
            for pattern in exclude_patterns:
                full_pattern = os.path.join(dir,pattern)
                exclude_sources.extend(fnmatch.filter(
                    sources, os.path.abspath(full_pattern)))

        # Exclude sources
        for src in exclude_sources:
            try:
                sources.remove(src)
            except ValueError:
                pass

        # Sort and return the list of sources
        sources.sort()
        return sources

########################################################################

if __name__ == "__main__":

    # Check the command line
    numArgs = len(sys.argv)
    if numArgs < 2 or numArgs > 3:
        print __doc__
        sys.exit(-1)

    # Obtain the target and doxyfile names
    target = sys.argv[1]
    if numArgs == 3:
        filename = sys.argv[2]
    else:
        filename = "Doxyfile"

    # Parse the doxyfile and output the dependencies
    doxyfile = Doxyfile(filename)
    print "%s: \\" % target
    sources = doxyfile.sources()
    if len(sources): lastSrc = sources[-1]
    for src in sources:
        print "   ", src,
        if src is not lastSrc: print "\\",
        print
