#!/usr/bin/env python

# (C) Copyright 2015, NVIDIA CORPORATION.
# All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# on the rights to use, copy, modify, merge, publish, distribute, sub
# license, and/or sell copies of the Software, and to permit persons to whom
# the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.  IN NO EVENT SHALL
# IBM AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# Authors:
#    Kyle Brenneman <kbrenneman@nvidia.com>

import sys
import collections
import re
import xml.etree.ElementTree as etree

MAPI_TABLE_NUM_DYNAMIC = 4096

_LIBRARY_FEATURE_NAMES = {
    # libGL and libGLdiapatch both include every function.
    "gldispatch" : None,
    "opengl" : frozenset(( "GL_VERSION_1_0", "GL_VERSION_1_1",
        "GL_VERSION_1_2", "GL_VERSION_1_3", "GL_VERSION_1_4", "GL_VERSION_1_5",
        "GL_VERSION_2_0", "GL_VERSION_2_1", "GL_VERSION_3_0", "GL_VERSION_3_1",
        "GL_VERSION_3_2", "GL_VERSION_3_3", "GL_VERSION_4_0", "GL_VERSION_4_1",
        "GL_VERSION_4_2", "GL_VERSION_4_3", "GL_VERSION_4_4", "GL_VERSION_4_5",
    )),
    "glesv1" : frozenset(("GL_VERSION_ES_CM_1_0", "GL_OES_point_size_array")),
    "glesv2" : frozenset(("GL_ES_VERSION_2_0", "GL_ES_VERSION_3_0",
            "GL_ES_VERSION_3_1", "GL_ES_VERSION_3_2",
    )),
}

def getFunctions(xmlFiles):
    """
    Reads an XML file and returns all of the functions defined in it.

    xmlFile should be the path to Khronos's gl.xml file. The return value is a
    sequence of FunctionDesc objects, ordered by slot number.
    """
    roots = [ etree.parse(xmlFile).getroot() for xmlFile in xmlFiles ]
    return getFunctionsFromRoots(roots)

def getFunctionsFromRoots(roots):
    functions = {}
    for root in roots:
        for func in _getFunctionList(root):
            functions[func.name] = func
    functions = functions.values()

    # Sort the function list by name.
    functions = sorted(functions, key=lambda f: f.name)

    # Assign a slot number to each function. This isn't strictly necessary,
    # since you can just look at the index in the list, but it makes it easier
    # to include the slot when formatting output.
    for i in range(len(functions)):
        functions[i] = functions[i]._replace(slot=i)

    return functions

def readSymbolsFile(symbols_file):
    """
    Returns the set of function names based on a list of symbols in a text
    file.
    """
    symbols = set()
    with open(symbols_file) as f:
        qualifier_optional = '(optional)'
        for line in f.readlines():

            # Strip comments
            line = line.split('#')[0]
            line = line.strip()
            if not line:
                continue

            # Line format:
            # [qualifier] symbol
            qualifier = None
            symbol = None

            fields = line.split()
            if len(fields) == 1:
                symbol = fields[0]
            elif len(fields) == 2:
                qualifier = fields[0]
                symbol = fields[1]
            else:
                raise ValueError(symbols_file + ': invalid format: ' + line)

            # The only supported qualifier is 'optional', which means the
            # symbol doesn't have to be exported by the library
            if qualifier and not qualifier == qualifier_optional:
                raise ValueError(symbols_file + ': invalid qualifier: ' + qualifier)

            # For our purposes here, we expect generated functions to be
            # mandatory symbols.
            if qualifier != qualifier_optional:
                symbols.add(symbol)
    return symbols

def getExportNamesFromRoots(target, roots):
    """
    Goes through the <feature> tags from gl.xml and returns a set of OpenGL
    functions that a library should export.

    target should be one of "gl", "gldispatch", "opengl", "glesv1", or
    "glesv2".
    """
    featureNames = _LIBRARY_FEATURE_NAMES[target]
    if (featureNames == None):
        return set(func.name for func in getFunctionsFromRoots(roots))

    names = set()
    for root in roots:
        features = []
        for featElem in root.findall("feature"):
            if (featElem.get("name") in featureNames):
                features.append(featElem)
        for featElem in root.findall("extensions/extension"):
            if (featElem.get("name") in featureNames):
                features.append(featElem)
        for featElem in features:
            for commandElem in featElem.findall("require/command"):
                names.add(commandElem.get("name"))
    return names

class FunctionArg(collections.namedtuple("FunctionArg", "type name")):
    @property
    def dec(self):
        """
        Returns a "TYPE NAME" string, suitable for a function prototype.
        """
        rv = str(self.type)
        if(not rv.endswith("*")):
            rv += " "
        rv += self.name
        return rv

class FunctionDesc(collections.namedtuple("FunctionDesc", "name rt args slot")):
    def hasReturn(self):
        """
        Returns true if the function returns a value.
        """
        return (self.rt != "void")

    @property
    def decArgs(self):
        """
        Returns a string with the types and names of the arguments, as you
        would use in a function declaration.
        """
        if(len(self.args) == 0):
            return "void"
        else:
            return ", ".join(arg.dec for arg in self.args)

    @property
    def callArgs(self):
        """
        Returns a string with the names of the arguments, as you would use in a
        function call.
        """
        return ", ".join(arg.name for arg in self.args)

    @property
    def basename(self):
        assert(self.name.startswith("gl"))
        return self.name[2:]

def _getFunctionList(root):
    for elem in root.findall("commands/command"):
        yield _parseCommandElem(elem)

def _parseCommandElem(elem):
    protoElem = elem.find("proto")
    (rt, name) = _parseProtoElem(protoElem)

    args = []
    for ch in elem.findall("param"):
        # <param> tags have the same format as a <proto> tag.
        args.append(FunctionArg(*_parseProtoElem(ch)))
    func = FunctionDesc(name, rt, tuple(args), slot=None)

    return func

def _parseProtoElem(elem):
    # If I just remove the tags and string the text together, I'll get valid C code.
    text = _flattenText(elem)
    text = text.strip()
    m = re.match(r"^(.+)\b(\w+)(?:\s*\[\s*(\d*)\s*\])?$", text, re.S)
    if (m):
        typename = _fixupTypeName(m.group(1))
        name = m.group(2)
        if (m.group(3)):
            # HACK: glPathGlyphIndexRangeNV defines an argument like this:
            # GLuint baseAndCount[2]
            # Convert it to a pointer and hope for the best.
            typename += "*"
        return (typename, name)
    else:
        raise ValueError("Can't parse element %r -> %r" % (elem, text))

def _flattenText(elem):
    """
    Returns the text in an element and all child elements, with the tags
    removed.
    """
    text = ""
    if(elem.text != None):
        text = elem.text
    for ch in elem:
        text += _flattenText(ch)
        if(ch.tail != None):
            text += ch.tail
    return text

def _fixupTypeName(typeName):
    """
    Converts a typename into a more consistent format.
    """

    rv = typeName.strip()

    # Replace "GLvoid" with just plain "void".
    rv = re.sub(r"\bGLvoid\b", "void", rv)

    # Remove the vendor suffixes from types that have a suffix-less version.
    rv = re.sub(r"\b(GLhalf|GLintptr|GLsizeiptr|GLint64|GLuint64)(?:ARB|EXT|NV|ATI)\b", r"\1", rv)

    rv = re.sub(r"\bGLvoid\b", "void", rv)

    # Clear out any leading and trailing whitespace.
    rv = rv.strip()

    # Remove any whitespace before a '*'
    rv = re.sub(r"\s+\*", r"*", rv)

    # Change "foo*" to "foo *"
    rv = re.sub(r"([^\*])\*", r"\1 *", rv)

    # Condense all whitespace into a single space.
    rv = re.sub(r"\s+", " ", rv)

    return rv

