# (c) 2008 Criterion Software Limited and its licensors. All Rights Reserved
import os
import re
import string
import sys
import getopt

##############################################################################
# Configurable constants
##############################################################################

# Define the eabase define that must be set for each platform.  Note: a VPL
# define may map to multiple platforms, just choose one that can be used for
# parsing purposes.
platformDefine = { 'SSE' : ['EA_PLATFORM_WINDOWS', 'RW_MATH_TARGET_SUPPORTS_SSE'], 'SSE2' : ['EA_PLATFORM_WINDOWS', '_M_IX86_FP=2', 'RW_MATH_TARGET_SUPPORTS_SSE'], 'PPU' : ['EA_PLATFORM_PS3'], 'SPU' : ['EA_PLATFORM_PS3_SPU'], 'Xenon' : ['EA_PLATFORM_XENON'], 'Broadway' : ['EA_PLATFORM_REVOLUTION'], 'Neon' : ['EA_PROCESSOR_ARM', 'RWMATH_USE_NEON']}
platforms = platformDefine.keys()

# These strings are not significant (as far as the documentation is concerned)
# and should be eliminated from the function signatures.
functionDeclNullStrings = [ 'RW_MATH_FORCE_INLINE' ]

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

APIDefinitionREStr = '^[^/]*VPL_API\\s*([^;]*)$'
APIDefinitionRE = re.compile(APIDefinitionREStr)

APIDeclarationREStr = '^[^/]*VPL_API\\s*(.*);$'
APIDeclarationRE = re.compile(APIDeclarationREStr)

# Break a function prototype up into the <return value + function name> and <argument list>.
FunctionPrototypeREStr = '\\s*([^(].*)\((.*)\)\\s*'
FunctionPrototypeRE = re.compile(FunctionPrototypeREStr)

splitReturnTypeAndNameREStr = '(.*\\s)([^\\s]*)'
splitReturnTypeAndNameRE = re.compile(splitReturnTypeAndNameREStr)

# Match C++ style comments on a line without any other characters on the line (except whitespace)
commentLineREStr = '^\\s*//(.*)'
commentLineRE = re.compile(commentLineREStr)

# Match preprocessor line info directives that are generated by the CPP.
preprocLineInfoREStr = '^#.*'
preprocLineInfoRE = re.compile(preprocLineInfoREStr)

# Match a platform rating tag
ratingTokenREStr = 'VPL_PLATFORM_RATING_(\\d*)'
ratingTokenRE = re.compile(ratingTokenREStr)

# Match a platform equivalent tag
equivalentTokenREStr = 'VPL_PLATFORM_EQUIVALENT (.*)'
equivalentTokenRE = re.compile(equivalentTokenREStr)

anyPlatformREStr = '|'.join(map(string.upper,platforms))

# Match a rating for a specific platform.
specificPlatformRatingTokenREStr = 'VPL_(%s)_RATING_(\\d*)' % (anyPlatformREStr, )
specificPlatformRatingTokenRE = re.compile(specificPlatformRatingTokenREStr)

# Match an equivalent instruction tag that is platform specific
specificPlatformEquivalentTokenREStr = 'VPL_(%s)_EQUIVALENT (.*)' % (anyPlatformREStr, )
specificPlatformEquivalentTokenRE = re.compile(specificPlatformEquivalentTokenREStr)

# Match a platform specific documentation tag.
platformDocTokenREStr = '\\s*VPL_PLATFORM_DOC(.*)'
platformDocTokenRE = re.compile(platformDocTokenREStr)

class Function(object):
    """
    This class is used to encapsulate the data for a function.
    """
    def __init__(self, returnType, name, arguments):
        self.returnType = returnType
        self.name = name
        self.arguments = arguments
    def FromDeclaration(str):
        """
           Constructs a 'Function' instance from a function declaration string.
           For example:
             Function.FromDeclaration('int Foo(int baz, int baz);')
        """
        for strToEliminate in functionDeclNullStrings:
            str = str.replace( strToEliminate, '' )
        str = re.sub('([(),;])', ' \\1 ', str );
        str = re.sub('\\s\\s*', ' ', str );
        mo = FunctionPrototypeRE.match(str)
        if not mo:
            raise Exception('unable to parse \'%s\'' % (str, ))
        returnTypeAndName = mo.group(1).strip()
        argumentList = mo.group(2).strip()
        mo = splitReturnTypeAndNameRE.match(returnTypeAndName)
        if not mo:
            raise Exception('unable to parse \'%s\'' % (str, ))
        returnType = mo.group(1).strip()
        functionName = mo.group(2).strip()
        argumentList = argumentList.strip()
        argumentListTokens = argumentList.split(' ')
        argumentListTokens.append(',')  # To avoid special case handling at end of argument list (this way all arguments are followed by a comma)
        arguments = []
        curArgument = ''
        for token in argumentListTokens:
            if token == ',':
                arguments.append(curArgument.strip())
                curArgument = ''
            else:
                curArgument = '%s %s' % (curArgument, token)
        curArgument = curArgument.strip()
        return Function(returnType, functionName, arguments)

    def __eq__(self, other):
        return self.returnType == other.returnType and self.name == other.name and self.arguments == other.arguments

    def __hash__(self):
        result = self.returnType.__hash__() ^ self.name.__hash__()
        for arg in self.arguments:
            result = result ^ arg.__hash__()
        return result

    def GetArgumentString(self):
        def combine(x,y):
            if x == '':
                return y
            return '%s, %s' % (x,y)
        return reduce(combine, self.arguments, '')

    def __str__(self):
        result = '%s %s(%s)' % ( self.returnType, self.name, self.GetArgumentString() )
        return result

    # Static methods
    FromDeclaration = staticmethod(FromDeclaration)

class PlatformInfo(object):
    def __init__(self, platformList, platformInfoMap ):
        self.platformList = platformList
        self.platformList.sort()
        self.platformInfoMap = platformInfoMap
        # create list of all functions
        self.functionList = []
        for platform, info in platformInfoMap.iteritems():
            self.functionList = self.functionList + info.keys()
        # Ensure the list only has unique entries
        self.functionList = list(set(self.functionList))

class FunctionMetaInfo(object):
    """This object currently stores a platform rating (i.e. how efficient the functions implementation is) and
       any platform specific comments for the function."""
    def __init__(self, platformRating, platformEquivalent, platformSpecificComments):
        self.platformRating = platformRating
        self.platformEquivalent = platformEquivalent
        self.platformSpecificComments = platformSpecificComments

def ParseCommentBlock(commentBlock, platform):
    """This function parses a block of comments for information that is tagged using VPL tags.
       For example, platform ratings and platform specific documentation is parsed."""
    PARSE_STATE_PLATFORM_DEFAULT = 0
    PARSE_STATE_PLATFORM_DOC = 1

    # Initialize state
    platformRating = None
    platformEquivalent = None
    platformDocumentation = None
    parseState = PARSE_STATE_PLATFORM_DEFAULT

    for line in commentBlock:
        mo = ratingTokenRE.search(line) 
        if mo:
            if platformRating:
                Exception('Platform rating specified twice within a comment block')
            platformRating = string.atoi(mo.group(1))
            parseState = PARSE_STATE_PLATFORM_DEFAULT
            continue
        mo = equivalentTokenRE.search(line)
        if mo:
            if platformEquivalent:
                Exception('Platform equivalent specified twice within a comment block')
            platformEquivalent = mo.group(1)
            parseState = PARSE_STATE_PLATFORM_DEFAULT
            continue
        mo = specificPlatformRatingTokenRE.search(line) 
        if mo:
            # Check to see if the rating's platform matches the platform currently being parsed.
            # If the platforms match, use the rating specified.  If the platform does not match,
            # then simply ignore the rating.
            platformTemp = platform
            if platform == 'SSE2':
                platformTemp = 'SSE'
            if string.upper(platformTemp) == mo.group(1):
                if platformRating:
                    Exception('Platform rating specified twice within a comment block')
                platformRating = string.atoi(mo.group(2))
                parseState = PARSE_STATE_PLATFORM_DEFAULT
            continue
        mo = specificPlatformEquivalentTokenRE.search(line) 
        if mo:
            # Check to see if the tag's platform matches the platform currently being parsed.
            # If the platforms match, use the equivalent specified.  If the platform does not match,
            # then simply ignore the equivalent.
            platformTemp = platform
            if platform == 'SSE2':
                platformTemp = 'SSE'
            if string.upper(platformTemp) == mo.group(1):
                if platformEquivalent:
                    Exception('Platform equivalent specified twice within a comment block')
                platformEquivalent = mo.group(2)
                parseState = PARSE_STATE_PLATFORM_DEFAULT
            continue
        mo = platformDocTokenRE.match(line)
        if mo:
            platformDocumentation = [mo.group(1)]
            parseState = PARSE_STATE_PLATFORM_DOC
            continue
        if parseState == PARSE_STATE_PLATFORM_DOC:
            platformDocumentation.append(line)

    return FunctionMetaInfo(platformRating, platformEquivalent, platformDocumentation)

def ExtractAPIFromHeaders(preprocessorCommand, headerFiles, includeDirs, platformList ):
    missingPlatformRating = False
    platformInfoMap = {}
    for headerFile in headerFiles:
        for platform in platformList:
            includeString = ''
            for dir in includeDirs:
                includeString = '%s -I%s' % ( includeString, dir )
            defineString = ''
            for define in platformDefine[platform]:
                defineString = '%s -D%s' % (defineString, define)
            parseCmd = r'%s %s -C -DRW_MATH_TAG_API_DURING_PREPROCESSING %s %s' % ( preprocessorCommand, includeString, defineString, headerFile )
            if not platformInfoMap.has_key(platform):
                platformInfoMap[platform] = {}
            info = platformInfoMap[platform]
            if verbose:
                print 'Executing preprocessor using the following command:\n    %s\n' % parseCmd
            outputStream = os.popen(parseCmd)
            COMMENT_BLOCK_STARTED = 0
            COMMENT_BLOCK_ENDED = 1
            currentComment = None
            commentState = COMMENT_BLOCK_ENDED
            for line in outputStream.readlines():
                # This loop is essentially a simple finite state machine.
                #
                # If this is a preprocessor conditional, ignore the line.
                # else if a comment is seen it is either added to the current comment block, or it begins a new block.
                # else a non comment is seen:
                #   The current comment block is ended.
                #   If this is an API declaration/definition, the current comment block is associated with the decl/def.
                preprocLineMO = preprocLineInfoRE.match(line) 
                commentMO = commentLineRE.match(line)
                defMO = APIDefinitionRE.match(line)
                declMO = APIDeclarationRE.match(line)
                if preprocLineMO:
                    # Preprocessor line information directive.  Ignore the line and continue
                    continue
                elif commentMO:
                    # Accumulate comments in a block
                    if commentState == COMMENT_BLOCK_ENDED:
                        commentState = COMMENT_BLOCK_STARTED
                        currentComment = [commentMO.group(1)]
                    else:
                        currentComment.append(commentMO.group(1))
                else:
                    commentState = COMMENT_BLOCK_ENDED
                    if declMO:
                        apiString = declMO.group(1)
                        # Create a Function object from the api string
                        function = Function.FromDeclaration(apiString)
                        # Store the function object in a map to record what functions are implemented for this platform
                        # Set the platform information to None since it will be parsed from the function definition.
                        if info.has_key(function):
                            raise Exception('Header parse error: Function \'%s\' was redeclared unnecessarily.' % (str(function), ))
                        info[function] = None
                    elif defMO:
                        apiString = defMO.group(1)
                        # Create a Function object from the api string
                        function = Function.FromDeclaration(apiString)
                        # Store the function object in a map to record what functions are implemented for this platform
                        if not info.has_key(function):
                            raise Exception('Header parse error: Function \'%s\' was defined, but never declared.' % (str(function), ))
                        if currentComment:
                            info[function] = ParseCommentBlock(currentComment, platform)
                        else:
                            # This should be an error in the future.
                            missingPlatformRating = True
                            info[function] = FunctionMetaInfo(None, None, None)
                    # Clear the current comment so it doesn't get associated with the next function definition/declaration
                    currentComment = None
            if outputStream.close():
                # the child process returned an error code
                raise Exception('Error: preprocessing file \'%s\' using command \'%s\'' % (headerFile, parseCmd) )
    return PlatformInfo(platformList, platformInfoMap), missingPlatformRating

def CompareFunctionNames(func1, func2):
    """Compare's functions based on their names."""
    return cmp(func1.name, func2.name)

def GenerateSupportedFunctionList(platformInfo):
    result = """\\section s_supported_vpl_functions Supported Functions\n
These functions are implemented using the native vector instruction set of the
platform.  In most cases, these functions map to a single vector instruction.
However, in some cases these functions may need to be implemented using several
instructions depending on what instructions are available on the platform.
Functions that are not supported may only be used by enabling "simulated"
functions.  However, simulated functions should \\em not be used for any
performance critical code.  For this reason simulated functions are disabled by
default.\n
"""
    for platform in platformInfo.platformList:
        result = result + '\\par Platform : %s\\n\n' % (platform, )
        info = platformInfo.platformInfoMap[platform]
        functionList = platformInfo.functionList[:]
        functionList.sort(CompareFunctionNames)
        for func in functionList:
            if info.has_key(func):
                result = result + '    %s \\ref %s(%s)\\n\n' % (func.returnType, func.name, func.GetArgumentString())
        result = result + '\n'
    return result

def GenerateUnsupportedFunctionList(platformInfo):
    result = """\\section s_simulated_vpl_functions Simulated Functions\n
These functions are not supported using the native vector instruction set of
the platform.  However, they are \\em optionally simulated using portable
scalar code.  The simulated code is intended to allow VPL code to run on any
platform.  However, the performance of this simulation is very low, and its
intent is to prevent broken builds, or allow VPL code to run in a PC tool where
performance may not be a priority.\n
"""
    for platform in platformInfo.platformList:
        result = result + '\\par Platform : %s\n\\n' % (platform, )
        info = platformInfo.platformInfoMap[platform]
        functionList = platformInfo.functionList[:]
        functionList.sort(CompareFunctionNames)
        for func in functionList:
            if not info.has_key(func):
                result = result + '    %s \\ref %s(%s)\n\\n' % (func.returnType, func.name, func.GetArgumentString())
        result = result + '\n'
    return result


def GeneratePlatformSupportMatrix(platformInfo, missingPlatformRating):
    ratingInfoTable = {
            None : ('native_unknown',      'Unknown efficiency'),
               1 : ('native_green',        'Native Single'),
               2 : ('native_dark_green',   'Native Multi'),
               3 : ('native_yellow',       'Native Large'),
            }

    tableStr = '\\section s_supported_vpl_function_platform_matrix Supported Function/Platform Matrix\n\n'
    tableStr = tableStr + '\\image html native_green.png - indicates functions implemented using a single native instruction.\n'
    tableStr = tableStr + '\\image html native_dark_green.png - indicates functions implemented using a small number of instructions, and should still be very efficient.\n'
    tableStr = tableStr + '\\image html native_yellow.png - indicates functions implemented using a modest number of instructions.  You may want to investigate ways of avoiding this function.\n'
    if missingPlatformRating:
        tableStr = tableStr + '\\image html native_unknown.png - This function is missing a rating.  \\b The corresponding function definition should have rating information added!\n'
    tableStr = tableStr + '\\image html simulated.png - indicates functions simulated using scalar code.  Avoid using this function if possible, it could be very inefficient.  You must enable simulated operations to use this operation.\n'
    tableStr = tableStr + '(See \\ref p_vpl_simulated_operations for information on how to enable simulated operations)\n'
    tableStr = tableStr + '\\htmlonly\n'
    tableStr = tableStr + '<table class="table" border=1 cellpadding=3 cellspacing=0>\n'
    tableStr = tableStr + '<tr>\n'
    tableStr = tableStr + '<td class="tableHeader">Function vs Platform</td>\n'
    for platform in platformInfo.platformList:
        tableStr = tableStr + '<td class="tableHeader"> %s </td>\n' % platform
    tableStr = tableStr + '</tr>\n'
    functionList = platformInfo.functionList[:]
    functionList.sort(CompareFunctionNames)
    for func in functionList:
        tableStr = tableStr + '<tr>\n'
        tableStr = tableStr + '<td> \\endhtmlonly %s  \\ref %s(%s) \\htmlonly \n </td>' % (func.returnType, func.name, func.GetArgumentString())
        for platform in platformInfo.platformList:
            info = platformInfo.platformInfoMap[platform]
            if info.has_key(func):
                if not info[func]:
                    raise Exception('Header parse error: No function definition for \'%s\' on %s.' % (str(func), platform))
                ratingDesc = ratingInfoTable[info[func].platformRating]
            else:
                ratingDesc = ('simulated', 'Simulated')
            tableStr = tableStr + '<td align="center"> <img src="%s.png" alt="%s - %s"> </td>\n' % ( ratingDesc[0], platform, ratingDesc[1] )
        tableStr = tableStr + '</tr>\n'
    tableStr = tableStr + '</table>\n'
    tableStr = tableStr + '\\endhtmlonly\n'
    return tableStr


def ProcessDoxygenHeader(filename, outputDir, platformInfo):
    """
    This function reads in a header file and replaces occurences of VPL_AUTODOC
    with a list of supported platforms.  The modified file is written to outputDir.
    """
    AUTO_DOC_STRING = 'VPL_AUTODOC'
    dummy, fileComponent = os.path.split(filename)
    outFilename = os.path.join(outputDir, fileComponent)
    inFile = open(filename, 'r')
    lines = inFile.readlines()
    inFile.close()
    outFile = open(outFilename, 'w')
    lineIndex = 0
    for lineIndex in range(len(lines)):
        line = lines[lineIndex]
        if line.find(AUTO_DOC_STRING) != -1:
            # AUTO_DOC_STRING must be replaced with the platforms that are supported

            # Find the function this documentation is for
            for searchLine in lines[lineIndex:]:
                mo = APIDeclarationRE.search(searchLine)
                if mo:
                    apiString = mo.group(1)
                    function = Function.FromDeclaration(apiString)
                    break

            # Build up the string to replace AUTO_DOC_STRING with.
            replacementStr = None
            equivalentPairs = []    # store tuples of platforms and their matching equivalent instructions
            for platform in platformInfo.platformList:
                info = platformInfo.platformInfoMap[platform]
                if info.has_key(function):
                    if not replacementStr:
                        replacementStr = '\\bold Supported Platforms \\endbold : %s' % (platform, )
                    else:
                        replacementStr = '%s, %s' % (replacementStr, platform)

                    if not info[function]:
                        raise Exception('Header parse error: No function definition for \'%s\'.' % (str(function), ))
                    comments = info[function].platformSpecificComments
                    if comments:
                        # Write out extra comments
                        outFile.write( '* \\par %s Notes:\n' % (platform, ) )
                        for commentLine in comments:
                            outFile.write( '* ' + commentLine + '\n' )
                        outFile.write( '*\n' )
                    if info[function].platformEquivalent:
                        equivalentPairs.append( (platform, info[function].platformEquivalent ) )

            # If there are equivalent instructions specified for this function on some platform generate
            # a table listing the platform and its equivalent instructions.
            if len(equivalentPairs):
                platformTableHeadings = ''
                equivalentTableData = ''
                for platform, eqv in equivalentPairs:
                    platformTableHeadings = '%s \\th %s \\endth' % (platformTableHeadings, platform)
                    equivalentTableData = '%s \\td %s \\endtd' % (equivalentTableData, eqv)
                replacementStr = '\n\\bold Platform Equivalent(s) \\endbold :\n\\table \\tr %s \\endtr \\tr %s \\endtr \\endtable\n\n%s' % (platformTableHeadings, equivalentTableData, replacementStr)

            if not replacementStr:
                print function
            line = line.replace( AUTO_DOC_STRING, replacementStr )

        outFile.write(line)
    outFile.close()


def PrintUsage():
  print r"""
Usage:
--cpp=
    (Required) Specify the location of a C pre-processor executable to be used
    when filtering source code.
--help, -h
    Print this usage message
--include-dir=
    Specify an include directory to be used when running source code through
    the C pre-processor.
--input-file=
    (Required) Read this input file, and output a preprocessed version of the
    file to the output directory.  This file is also used to generate function
    support documentation for the various platforms.
--output-dir=
    Output the generated and preprocessed files to this directory
--verbose
    Output verbose information during the doc build.
"""

verbose = False

def main():
    global verbose

    commandLineArgs = sys.argv[1:]
    # Parse command line arguments using getopt
    try:
        options, args = getopt.getopt(commandLineArgs, 'h', ['help', 'output-dir=', 'input-file=', 'verbose', 'include-dir=', 'cpp='])
    except getopt.GetoptError:
        print 'Error parsing command line parameters.\n'
        PrintUsage()
        sys.exit(-1)

    # Make sure there are not any arguments that are "left over".
    if len(args) > 0:
        print args
        print 'Please specify command line paramaters.\n'
        PrintUsage()
        sys.exit()

    cpp = ''
    outputDir = '.'
    inputFiles = []
    includeDirs = []

    for option, arg in options:
        if option in ('-h', '--help'):
            PrintUsage()
            sys.exit()
        if option == '--output-dir':
            outputDir = arg
        if option == '--input-file':
            inputFiles.append(arg)
        if option == '--verbose':
            verbose = True
        if option == '--include-dir':
            includeDirs.append(arg)
        if option == '--cpp':
            cpp = arg

    if len(inputFiles) == 0:
        print 'Please specify at least one input file.\n'
        sys.exit()

    if cpp == '':
        print 'Please specify a C pre-processor.\n'
        sys.exit()

    platformInfo, missingPlatformRating = ExtractAPIFromHeaders( cpp, inputFiles, includeDirs, platforms )
    apiSupportDocs = """
        /*!
        \page p_vpl_api_support_docs VPL API Support Documentation

        """
    apiSupportDocs = apiSupportDocs + GeneratePlatformSupportMatrix( platformInfo, missingPlatformRating )
    apiSupportDocs = apiSupportDocs + GenerateSupportedFunctionList( platformInfo )
    apiSupportDocs = apiSupportDocs + GenerateUnsupportedFunctionList( platformInfo )
    apiSupportDocs = apiSupportDocs + """
        */
        """

    supportDocsFile = open( os.path.join( outputDir, 'apisupport.dox' ), 'w' )
    supportDocsFile.write(apiSupportDocs)
    supportDocsFile.close()
    for file in inputFiles:
        ProcessDoxygenHeader(file, outputDir, platformInfo)
            
if __name__ == '__main__':
    main()

