#!/usr/bin/env python
#
# Copyright (C) 2011 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ABOUT
#   This script is used to generate the trace implementations of all
#   OpenGL calls. When executed, it reads the specs for the OpenGL calls
#   from the files GLES2/gl2_api.in, GLES2/gl2ext_api.in, GLES_CM/gl_api.in,
#   and GLES_CM/glext_api.in, and generates trace versions for all the 
#   defined functions.
#
# PREREQUISITES
#   To generate C++ files, this script uses the 'pyratemp' template
#   module. The only reason to use pyratemp is that it is extremly
#   simple to install:
#   $ wget http://www.simple-is-better.org/template/pyratemp-current/pyratemp.py
#   Put the file in the GLES_trace/tools folder, or update PYTHONPATH
#   to point to wherever it was downloaded.
#
# USAGE
#   $ cd GLES_trace       - run the program from GLES2_trace folder
#   $ ./tools/genapi.py   - generates a .cpp and .h file
#   $ mv *.cpp *.h src/   - move the generated files into the src folder

import sys
import re
import pyratemp

# Constants corresponding to the protobuf DataType.Type
class DataType:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        if self.name == "pointer":  # pointers map to the INT DataType
            return "INT"
        return self.name.upper()

    def getProtobufCall(self):
        if self.name == "void":
            raise ValueError("Attempt to set void value")
        elif self.name == "char" or self.name == "byte" \
                or self.name == "pointer" or self.name == "enum":
            return "add_intvalue((int)"
        elif self.name == "int":
            return "add_intvalue("
        elif self.name == "float":
            return "add_floatvalue("
        elif self.name == "bool":
            return "add_boolvalue("
        else:
            raise ValueError("Unknown value type %s" % self.name)

DataType.VOID = DataType("void")
DataType.CHAR = DataType("char")
DataType.BYTE = DataType("byte")
DataType.ENUM = DataType("enum")
DataType.BOOL = DataType("bool")
DataType.INT = DataType("int")
DataType.FLOAT = DataType("float")
DataType.POINTER = DataType("pointer")

# mapping of GL types to protobuf DataType
GL2PROTOBUF_TYPE_MAP = {
    "GLvoid":DataType.VOID,
    "void":DataType.VOID,
    "GLchar":DataType.CHAR,
    "GLenum":DataType.ENUM,
    "GLboolean":DataType.BOOL,
    "GLbitfield":DataType.INT,
    "GLbyte":DataType.BYTE,
    "GLshort":DataType.INT,
    "GLint":DataType.INT,
    "int":DataType.INT,
    "GLsizei":DataType.INT,
    "GLubyte":DataType.BYTE,
    "GLushort":DataType.INT,
    "GLuint":DataType.INT,
    "GLfloat":DataType.FLOAT,
    "GLclampf":DataType.FLOAT,
    "GLfixed":DataType.INT,
    "GLclampx":DataType.INT,
    "GLsizeiptr":DataType.INT,
    "GLintptr":DataType.INT,
    "GLeglImageOES":DataType.POINTER,
}

API_SPECS = [
    ('GL2','../GLES2/gl2_api.in'),
    ('GL2Ext','../GLES2/gl2ext_api.in'),
    ('GL1','../GLES_CM/gl_api.in'),
    ('GL1Ext','../GLES_CM/glext_api.in'),
]

HEADER_LICENSE = """/*
 * Copyright 2011, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * THIS FILE WAS GENERATED BY A SCRIPT. DO NOT EDIT.
 */
"""

HEADER_INCLUDES = """
#include <cutils/log.h>
#include <utils/Timers.h>
#include <GLES2/gl2.h>

#include "gltrace.pb.h"
#include "gltrace_context.h"
#include "gltrace_fixup.h"
#include "gltrace_transport.h"
"""

HEADER_NAMESPACE_START = """
namespace android {
namespace gltrace {
"""

FOOTER_TEXT = """
}; // namespace gltrace
}; // namespace android
"""

TRACE_CALL_TEMPLATE = pyratemp.Template(
"""$!retType!$ GLTrace_$!func!$($!inputArgList!$) {
    GLMessage glmsg;
    GLTraceContext *glContext = getGLTraceContext();

    glmsg.set_function(GLMessage::$!func!$);
<!--(if len(parsedArgs) > 0)-->
    <!--(for argname, argtype in parsedArgs)-->

    // copy argument $!argname!$
    GLMessage_DataType *arg_$!argname!$ = glmsg.add_args();
    arg_$!argname!$->set_isarray(false);
    arg_$!argname!$->set_type(GLMessage::DataType::$!argtype!$);
    arg_$!argname!$->$!argtype.getProtobufCall()!$$!argname!$);
    <!--(end)-->
<!--(end)-->

    // call function
    nsecs_t wallStartTime = systemTime(SYSTEM_TIME_MONOTONIC);
    nsecs_t threadStartTime = systemTime(SYSTEM_TIME_THREAD);
<!--(if retType != "void")-->
    $!retType!$ retValue = glContext->hooks->gl.$!callsite!$;
<!--(else)-->
    glContext->hooks->gl.$!callsite!$;
<!--(end)-->
    nsecs_t threadEndTime = systemTime(SYSTEM_TIME_THREAD);
    nsecs_t wallEndTime = systemTime(SYSTEM_TIME_MONOTONIC);
<!--(if retType != "void")-->

    // set return value
    GLMessage_DataType *rt = glmsg.mutable_returnvalue();
    rt->set_isarray(false);
    rt->set_type(GLMessage::DataType::$!retDataType!$);
    rt->$!retDataType.getProtobufCall()!$retValue);
<!--(end)-->

    void *pointerArgs[] = {
<!--(for argname, argtype in parsedArgs)-->
    <!--(if argtype == DataType.POINTER)-->
        (void *) $!argname!$,
    <!--(end)-->
<!--(end)-->
<!--(if retDataType == DataType.POINTER)-->
        (void *) retValue,
<!--(end)-->
    };

    fixupGLMessage(glContext, wallStartTime, wallEndTime,
                              threadStartTime, threadEndTime,
                              &glmsg, pointerArgs);
    glContext->traceGLMessage(&glmsg);
<!--(if retType != "void")-->

    return retValue;
<!--(end)-->
}
""")

def getDataTypeFromKw(kw):
    """ Get the data type given declaration.
    All pointer declarations are of type DataType.POINTER

    e.g.: GLvoid -> DataType.VOID"""

    if kw.count('*') > 0:
        return DataType.POINTER
    return GL2PROTOBUF_TYPE_MAP.get(kw)

def getNameTypePair(decl):
    """ Split declaration of a variable to a tuple of (variable name, DataType).
    e.g. "const GLChar* varName" -> (varName, POINTER) """
    elements = decl.strip().split(' ')
    name = None
    if len(elements) > 1:
        name = " ".join(elements[-1:]).strip()      # last element is the name
        dataType = " ".join(elements[:-1]).strip()  # everything else is the data type

        # if name is a pointer (e.g. "*ptr"), then remove the "*" from the name
        # and add it to the data type
        pointersInName = name.count("*")            
        if pointersInName > 0:
            name = name.replace("*", "")
            dataType += "*" * pointersInName

        # if name is an array (e.g. "array[10]"), then remove the "[X]" from the name
        # and make the datatype to be a pointer
        arraysInName = name.count("[")
        if arraysInName > 0:
            name = name.split('[')[0]
            dataType += "*"
    else:
        dataType = elements[0]
    return (name, getDataTypeFromKw(dataType))

def parseArgs(arglist):
    """ Parse the argument list into a list of (var name, DataType) tuples """
    args = arglist.split(',')
    args = map(lambda x: x.strip(), args)    # remove unnecessary whitespaces
    argtypelist = map(getNameTypePair, args) # split arg into arg type and arg name
    if len(argtypelist) == 1:
        (name, argtype) = argtypelist[0]
        if argtype == DataType.VOID:
            return []

    return argtypelist

class ApiCall(object):
    """An ApiCall models all information about a single OpenGL API"""

    # Regex to match API_ENTRY specification:
    #       e.g. void API_ENTRY(glActiveTexture)(GLenum texture) {
    # the regex uses a non greedy match (?) to match the first closing paren
    API_ENTRY_REGEX = "(.*)API_ENTRY\(.*?\)\((.*?)\)"

    # Regex to match CALL_GL_API specification:
    #       e.g. CALL_GL_API(glCullFace, mode); 
    #            CALL_GL_API_RETURN(glCreateProgram);
    CALL_GL_API_REGEX = "CALL_GL_API(_RETURN)?\((.*)\);"

    def __init__(self, prefix, apientry, callsite):
        """Construct an ApiCall from its specification.

        The specification is provided by the two arguments:
        prefix: prefix to use for function names
        defn: specification line containing API_ENTRY macro
              e.g: void API_ENTRY(glActiveTexture)(GLenum texture) {
        callsite: specification line containing CALL_GL_API macro
              e.g: CALL_GL_API(glActiveTexture, texture);        
        """
        self.prefix = prefix
        self.ret = self.getReturnType(apientry)
        self.arglist = self.getArgList(apientry)

        # some functions (e.g. __glEGLImageTargetRenderbufferStorageOES), define their
        # names one way in the API_ENTRY and another way in the CALL_GL_API macros.
        # so self.func is reassigned based on what is there in the call site
        self.func = self.getFunc(callsite)
        self.callsite = self.getCallSite(callsite)

    def getReturnType(self, apientry):
        '''Extract the return type from the API_ENTRY specification'''
        m = re.search(self.API_ENTRY_REGEX, apientry)
        if not m:
            raise ValueError("%s does not match API_ENTRY specification %s" 
                             % (apientry, self.API_ENTRY_REGEX))

        return m.group(1).strip()

    def getArgList(self, apientry):
        '''Extract the argument list from the API_ENTRY specification'''
        m = re.search(self.API_ENTRY_REGEX, apientry)
        if not m:
            raise ValueError("%s does not match API_ENTRY specification %s" 
                             % (apientry, self.API_ENTRY_REGEX))

        return m.group(2).strip()

    def parseCallSite(self, callsite):
        m = re.search(self.CALL_GL_API_REGEX, callsite)
        if not m:
            raise ValueError("%s does not match CALL_GL_API specification (%s)"
                             % (callsite, self.CALL_GL_API_REGEX))

        arglist = m.group(2)
        args = arglist.split(',')
        args = map(lambda x: x.strip(), args)

        return args

    def getCallSite(self, callsite):
        '''Extract the callsite from the CALL_GL_API specification'''
        args = self.parseCallSite(callsite)
        return "%s(%s)" % (args[0], ", ".join(args[1:]))

    def getFunc(self, callsite):
        '''Extract the function name from the CALL_GL_API specification'''
        args = self.parseCallSite(callsite)
        return args[0]

    def genDeclaration(self):
        return "%s GLTrace_%s(%s);" % (self.ret, self.func, self.arglist)

    def genCode(self):
        return TRACE_CALL_TEMPLATE(func = self.func, 
                                   retType = self.ret,
                                   retDataType = getDataTypeFromKw(self.ret),
                                   inputArgList = self.arglist,
                                   callsite = self.callsite,
                                   parsedArgs = parseArgs(self.arglist),
                                   DataType=DataType)

def getApis(apiEntryFile, prefix):
    '''Get a list of all ApiCalls in provided specification file'''
    lines = open(apiEntryFile).readlines()

    apis = []
    for i in range(0, len(lines)/3):
        apis.append(ApiCall(prefix, lines[i*3], lines[i*3+1]))

    return apis

def parseAllSpecs(specs):
    apis = []
    for name, specfile in specs:
        a = getApis(specfile, name)
        print 'Parsed %s APIs from %s, # of entries = %d' % (name, specfile, len(a))
        apis.extend(a)
    return apis

def removeDuplicates(apis):
    '''Remove all duplicate function entries.

    The input list contains functions declared in GL1 and GL2 APIs.
    This will return a list that contains only the first function if there are
    multiple functions with the same name.'''
    uniqs = []
    funcs = set()
    for api in apis:
        if api.func not in funcs:
            uniqs.append(api)
            funcs.add(api.func)

    return uniqs

def genHeaders(apis, fname):
    lines = []
    lines.append(HEADER_LICENSE)
    lines.append(HEADER_NAMESPACE_START)
    prefix = ""
    for api in apis:
        if prefix != api.prefix:
            lines.append("\n// Declarations for %s APIs\n\n" % api.prefix)
            prefix = api.prefix
        lines.append(api.genDeclaration())
        lines.append("\n")
    lines.append(FOOTER_TEXT)

    with open(fname, "w") as f:
        f.writelines(lines)

def genSrcs(apis, fname):
    lines = []
    lines.append(HEADER_LICENSE)
    lines.append(HEADER_INCLUDES)
    lines.append(HEADER_NAMESPACE_START)
    prefix = ""
    for api in apis:
        if prefix != api.prefix:
            lines.append("\n// Definitions for %s APIs\n\n" % api.prefix)
            prefix = api.prefix
        lines.append(api.genCode())
        lines.append("\n")
    lines.append(FOOTER_TEXT)

    with open(fname, "w") as f:
        f.writelines(lines)

if __name__ == '__main__':
    apis = parseAllSpecs(API_SPECS)     # read in all the specfiles
    apis = removeDuplicates(apis)       # remove duplication of functions common to GL1 and GL2
    genHeaders(apis, 'gltrace_api.h')  # generate header file
    genSrcs(apis, 'gltrace_api.cpp')   # generate source file
