#!/usr/bin/python
# Copyright 2018 The ANGLE Project Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# gen_builtin_symbols.py:
#  Code generation for the built-in symbol tables.

from collections import OrderedDict
from datetime import date
import argparse
import json
import re
import os

def set_working_dir():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    os.chdir(script_dir)

set_working_dir()

parser = argparse.ArgumentParser()
parser.add_argument('--dump-intermediate-json', help='Dump parsed function data as a JSON file builtin_functions.json', action="store_true")
args = parser.parse_args()

template_immutablestringtest_cpp = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {function_data_source_name}.
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// ImmutableString_test_autogen.cpp:
//   Tests for matching script-generated hashes with runtime computed hashes.

#include "compiler/translator/ImmutableString.h"
#include "gtest/gtest.h"

namespace sh
{{

TEST(ImmutableStringTest, ScriptGeneratedHashesMatch)
{{
{script_generated_hash_tests}
}}

}}  // namespace sh
"""

# The header file has a "get" function for each variable. They are used in traversers.
# Note that we don't currently include get_function_declarations, as they are unused.
template_builtin_header = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {variable_data_source_name} and
// {function_data_source_name}.
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// BuiltIn_autogen.h:
//   Compile-time initialized built-ins.

#ifndef COMPILER_TRANSLATOR_BUILTIN_AUTOGEN_H_
#define COMPILER_TRANSLATOR_BUILTIN_AUTOGEN_H_

namespace sh
{{

class TVariable;

namespace BuiltInVariable
{{

{get_variable_declarations}

}}  // namespace BuiltInVariable

}}  // namespace sh

#endif  // COMPILER_TRANSLATOR_BUILTIN_AUTOGEN_H_
"""

# By having the variables defined in a cpp file we ensure that there's just one instance of each of the declared variables.
template_symboltable_cpp = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {variable_data_source_name} and
// {function_data_source_name}.
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// SymbolTable_autogen.cpp:
//   Compile-time initialized built-ins.

#include "compiler/translator/SymbolTable.h"

#include "angle_gl.h"
#include "compiler/translator/ImmutableString.h"
#include "compiler/translator/StaticType.h"
#include "compiler/translator/Symbol.h"
#include "compiler/translator/SymbolUniqueId.h"
#include "compiler/translator/SymbolTable.h"

namespace sh
{{

class BuiltInId
{{
public:

{builtin_id_declarations}

}};  // namespace BuiltInId

// Since some of the BuiltInId declarations are used outside of constexpr expressions, we need to
// have these definitions without an initializer. C++17 should eventually remove the need for this.
{builtin_id_definitions}

const int TSymbolTable::kLastBuiltInId = {last_builtin_id};

namespace BuiltInName
{{

{name_declarations}

}}  // namespace BuiltInName

namespace BuiltInParameters
{{

{parameter_declarations}

}}  // namespace BuiltInParameters

namespace UnmangledBuiltIns
{{

{unmangled_builtin_declarations}

}}  // namespace UnmangledBuiltIns

// TODO(oetuaho): Would be nice to make this a class instead of a namespace so that we could friend
// this from TVariable. Now symbol constructors taking an id have to be public even though they're
// not supposed to be accessible from outside of here. http://anglebug.com/2390
namespace BuiltInVariable
{{

{variable_declarations}

{get_variable_definitions}

}};  // namespace BuiltInVariable

// TODO(oetuaho): Would be nice to make this a class instead of a namespace so that we could friend
// this from TFunction. Now symbol constructors taking an id have to be public even though they're
// not supposed to be accessible from outside of here. http://anglebug.com/2390
namespace BuiltInFunction
{{

{function_declarations}

}}  // namespace BuiltInFunction

void TSymbolTable::insertBuiltInVariables(sh::GLenum shaderType,
                                          ShShaderSpec spec,
                                          const ShBuiltInResources &resources)
{{
    const TSourceLoc zeroSourceLoc = {{0, 0, 0, 0}};
{insert_variables}
}}

void TSymbolTable::insertBuiltInFunctions(sh::GLenum shaderType)
{{
{insert_functions}
}}

const UnmangledBuiltIn *TSymbolTable::getUnmangledBuiltInForShaderVersion(const ImmutableString &name, int shaderVersion)
{{
    uint32_t nameHash = name.hash32();
{get_unmangled_builtin}
}}

}}  // namespace sh
"""

template_parsecontext_header = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {variable_data_source_name} and
// {function_data_source_name}.
//
// Copyright {copyright_year} The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// ParseContext_autogen.h:
//   Helpers for built-in related checks.

#ifndef COMPILER_TRANSLATOR_PARSECONTEXT_AUTOGEN_H_
#define COMPILER_TRANSLATOR_PARSECONTEXT_AUTOGEN_H_

namespace sh
{{

namespace BuiltInGroup
{{

{is_in_group_definitions}

}}  // namespace BuiltInGroup

}}  // namespace sh

#endif  // COMPILER_TRANSLATOR_PARSECONTEXT_AUTOGEN_H_

"""

parsed_variables = None

variables_json_filename = 'builtin_variables.json'
functions_txt_filename = 'builtin_function_declarations.txt'

basic_types_enumeration = [
    'Void',
    'Float',
    'Int',
    'UInt',
    'Bool',
    'AtomicCounter',
    'YuvCscStandardEXT',
    'Sampler2D',
    'Sampler3D',
    'SamplerCube',
    'Sampler2DArray',
    'SamplerExternalOES',
    'SamplerExternal2DY2YEXT',
    'Sampler2DRect',
    'Sampler2DMS',
    'ISampler2D',
    'ISampler3D',
    'ISamplerCube',
    'ISampler2DArray',
    'ISampler2DMS',
    'USampler2D',
    'USampler3D',
    'USamplerCube',
    'USampler2DArray',
    'USampler2DMS',
    'Sampler2DShadow',
    'SamplerCubeShadow',
    'Sampler2DArrayShadow',
    'Image2D',
    'IImage2D',
    'UImage2D',
    'Image3D',
    'IImage3D',
    'UImage3D',
    'Image2DArray',
    'IImage2DArray',
    'UImage2DArray',
    'ImageCube',
    'IImageCube',
    'UImageCube'
]

def get_basic_mangled_name(basic):
    index = basic_types_enumeration.index(basic)
    if index < 26:
        return chr(ord('A') + index)
    return chr(ord('a') + index - 26)

levels = ['ESSL3_1_BUILTINS', 'ESSL3_BUILTINS', 'ESSL1_BUILTINS', 'COMMON_BUILTINS']

class GroupedList:
    """"Class for storing a list of objects grouped by symbol table level and condition."""
    def __init__(self):
        self.objs = OrderedDict()
        # We need to add all the levels here instead of lazily since they must be in a specific order.
        for l in levels:
            self.objs[l] = OrderedDict()

    def add_obj(self, level, condition, name, obj):
        if (level not in levels):
            raise Exception('Unexpected level: ' + str(level))
        if condition not in self.objs[level]:
            self.objs[level][condition] = OrderedDict()
        self.objs[level][condition][name] = obj

    def has_key(self, level, condition, name):
        if (level not in levels):
            raise Exception('Unexpected level: ' + str(level))
        if condition not in self.objs[level]:
            return False
        return (name in self.objs[level][condition])

    def get(self, level, condition, name):
        if self.has_key(level, condition, name):
            return self.objs[level][condition][name]
        return None

    def iter_conditions(self, level):
        return self.objs[level].iteritems()


class TType:
    def __init__(self, glsl_header_type):
        if isinstance(glsl_header_type, basestring):
            self.data = self.parse_type(glsl_header_type)
        else:
            self.data = glsl_header_type
        self.normalize()

    def normalize(self):
        # Note that this will set primarySize and secondarySize also on genTypes. In that case they
        # are overridden when the specific types are generated.
        if 'primarySize' not in self.data:
            if ('secondarySize' in self.data):
                raise Exception('Unexpected secondarySize on type that does not have primarySize set')
            self.data['primarySize'] = 1
        if 'secondarySize' not in self.data:
            self.data['secondarySize'] = 1
        if 'precision' not in self.data:
            self.data['precision'] = 'Undefined'
        if 'qualifier' not in self.data:
            self.data['qualifier'] = 'Global'

    def get_statictype_string(self):
        template_type = 'StaticType::Get<Ebt{basic}, Ebp{precision}, Evq{qualifier}, {primarySize}, {secondarySize}>()'
        return template_type.format(**self.data)

    def get_dynamic_type_string(self):
        template_type = 'new TType(Ebt{basic}, Ebp{precision}, Evq{qualifier}, {primarySize}, {secondarySize})'
        return template_type.format(**self.data)

    def get_mangled_name(self):
        mangled_name = ''

        size_key = (self.data['secondarySize'] - 1) * 4 + self.data['primarySize'] - 1
        if size_key < 10:
            mangled_name += chr(ord('0') + size_key)
        else:
            mangled_name += chr(ord('A') + size_key - 10)

        mangled_name += get_basic_mangled_name(self.data['basic'])
        return mangled_name

    def is_vector(self):
        return self.data['primarySize'] > 1 and self.data['secondarySize'] == 1

    def is_matrix(self):
        return self.data['secondarySize'] > 1

    def get_object_size(self):
        return self.data['primarySize'] * self.data['secondarySize']

    def specific_sampler_or_image_type(self, basic_type_prefix):
        if 'genType' in self.data:
            type = {}
            if 'basic' not in self.data:
                type['basic'] = {'': 'Float', 'I': 'Int', 'U': 'UInt'}[basic_type_prefix]
                type['primarySize'] = self.data['primarySize']
            else:
                type['basic'] = basic_type_prefix + self.data['basic']
                type['primarySize'] = 1
            type['precision'] = 'Undefined'
            return TType(type)
        return self

    def specific_type(self, vec_size):
        type = {}
        if 'genType' in self.data:
            type['basic'] = self.data['basic']
            type['precision'] = self.data['precision']
            type['qualifier'] = self.data['qualifier']
            type['primarySize'] = vec_size
            type['secondarySize'] = 1
            return TType(type)
        return self

    def parse_type(self, glsl_header_type):
        if glsl_header_type.startswith('out '):
            type_obj = self.parse_type(glsl_header_type[4:])
            type_obj['qualifier'] = 'Out'
            return type_obj
        if glsl_header_type.startswith('inout '):
            type_obj = self.parse_type(glsl_header_type[6:])
            type_obj['qualifier'] = 'InOut'
            return type_obj

        basic_type_map = {
            'float': 'Float',
            'int': 'Int',
            'uint': 'UInt',
            'bool': 'Bool',
            'void': 'Void',
            'atomic_uint': 'AtomicCounter',
            'yuvCscStandardEXT': 'YuvCscStandardEXT'
        }

        if glsl_header_type in basic_type_map:
            return {'basic': basic_type_map[glsl_header_type]}

        type_obj = {}

        basic_type_prefix_map = {'': 'Float', 'i': 'Int', 'u': 'UInt', 'b': 'Bool', 'v': 'Void'}

        vec_re = re.compile(r'^([iub]?)vec([234]?)$')
        vec_match = vec_re.match(glsl_header_type)
        if vec_match:
            type_obj['basic'] = basic_type_prefix_map[vec_match.group(1)]
            if vec_match.group(2) == '':
                # Type like "ivec" that represents either ivec2, ivec3 or ivec4
                type_obj['genType'] = 'vec'
            else:
                # vec with specific size
                type_obj['primarySize'] = int(vec_match.group(2))
            return type_obj

        mat_re = re.compile(r'^mat([234])(x([234]))?$')
        mat_match = mat_re.match(glsl_header_type)
        if mat_match:
            type_obj['basic'] = 'Float'
            if len(glsl_header_type) == 4:
                mat_size = int(mat_match.group(1))
                type_obj['primarySize'] = mat_size
                type_obj['secondarySize'] = mat_size
            else:
                type_obj['primarySize'] = int(mat_match.group(1))
                type_obj['secondarySize'] = int(mat_match.group(3))
            return type_obj

        gen_re = re.compile(r'^gen([IUB]?)Type$')
        gen_match = gen_re.match(glsl_header_type)
        if gen_match:
            type_obj['basic'] = basic_type_prefix_map[gen_match.group(1).lower()]
            type_obj['genType'] = 'yes'
            return type_obj

        if glsl_header_type.startswith('sampler'):
            type_obj['basic'] = glsl_header_type[0].upper() + glsl_header_type[1:]
            return type_obj

        if glsl_header_type.startswith('gsampler') or glsl_header_type.startswith('gimage'):
            type_obj['basic'] = glsl_header_type[1].upper() + glsl_header_type[2:]
            type_obj['genType'] = 'sampler_or_image'
            return type_obj

        if glsl_header_type == 'gvec4':
            return {'primarySize': 4, 'genType': 'sampler_or_image'}
        if glsl_header_type == 'gvec3':
            return {'primarySize': 3, 'genType': 'sampler_or_image'}

        raise Exception('Unrecognized type: ' + str(glsl_header_type))

def get_parsed_functions():

    def parse_function_parameters(parameters):
        if parameters == '':
            return []
        parametersOut = []
        parameters = parameters.split(', ')
        for parameter in parameters:
            parametersOut.append(TType(parameter.strip()))
        return parametersOut

    lines = []
    with open(functions_txt_filename) as f:
        lines = f.readlines()
    lines = [line.strip() for line in lines if line.strip() != '' and not line.strip().startswith('//')]

    fun_re = re.compile(r'^(\w+) (\w+)\((.*)\);$')

    parsed_functions = OrderedDict()
    group_stack = []
    default_metadata = {}

    for line in lines:
        fun_match = fun_re.match(line)
        if line.startswith('GROUP BEGIN '):
            group_rest = line[12:].strip()
            group_parts = group_rest.split(' ', 1)
            current_group = {
                'functions': [],
                'name': group_parts[0],
                'subgroups': {}
            }
            if len(group_parts) > 1:
                group_metadata = json.loads(group_parts[1])
                current_group.update(group_metadata)
            group_stack.append(current_group)
        elif line.startswith('GROUP END '):
            group_end_name = line[10:].strip()
            current_group = group_stack[-1]
            if current_group['name'] != group_end_name:
                raise Exception('GROUP END: Unexpected function group name "' + group_end_name + '" was expecting "' + current_group['name'] + '"')
            group_stack.pop()
            is_top_level_group = (len(group_stack) == 0)
            if is_top_level_group:
                parsed_functions[current_group['name']] = current_group
                default_metadata = {}
            else:
                super_group = group_stack[-1]
                super_group['subgroups'][current_group['name']] = current_group
        elif line.startswith('DEFAULT METADATA'):
            line_rest = line[16:].strip()
            default_metadata = json.loads(line_rest)
        elif fun_match:
            return_type = fun_match.group(1)
            name = fun_match.group(2)
            parameters = fun_match.group(3)
            function_props = {
                'name': name,
                'returnType': TType(return_type),
                'parameters': parse_function_parameters(parameters)
            }
            function_props.update(default_metadata)
            group_stack[-1]['functions'].append(function_props)
        else:
            raise Exception('Unexpected function input line: ' + line)

    return parsed_functions

parsed_functions = get_parsed_functions()

if args.dump_intermediate_json:
    with open('builtin_functions.json', 'w') as outfile:
        def serialize_obj(obj):
            if isinstance(obj, TType):
                return obj.data
            else:
                raise "Cannot serialize to JSON: " + str(obj)
        json.dump(parsed_functions, outfile, indent=4, separators=(',', ': '), default=serialize_obj)

with open(variables_json_filename) as f:
    parsed_variables = json.load(f, object_pairs_hook=OrderedDict)

# Declarations of symbol unique ids
builtin_id_declarations = []

# Definitions of symbol unique ids needed for those ids used outside of constexpr expressions.
builtin_id_definitions = []

# Declarations of name string variables
name_declarations = set()

# Declarations of builtin TVariables
variable_declarations = []

# Declarations of parameter arrays for builtin TFunctions
parameter_declarations = set()

# Declarations of builtin TFunctions
function_declarations = []

# Functions for querying the pointer to a specific TVariable.
get_variable_declarations = []
get_variable_definitions = []

# Code for inserting builtin TVariables to the symbol table.
insert_variables = []

# Code for inserting builtin TFunctions to the symbol table. Grouped by condition.
insert_functions_by_condition = OrderedDict()

# Declarations of UnmangledBuiltIn objects
unmangled_builtin_declarations = set()

# Code for querying builtin function unmangled names.
unmangled_function_if_statements = GroupedList()

# Code for testing that script-generated hashes match with runtime computed hashes.
script_generated_hash_tests = OrderedDict()

# Functions for testing whether a builtin belongs in group.
is_in_group_definitions = []

id_counter = 0

def hash32(str):
    fnvOffsetBasis = 0x811c9dc5
    fnvPrime = 16777619
    hash = fnvOffsetBasis
    for c in str:
        hash = hash ^ ord(c)
        hash = hash * fnvPrime & 0xffffffff
    sanity_check = '    ASSERT_EQ(0x{hash}u, ImmutableString("{str}").hash32());'.format(hash = ('%x' % hash), str = str)
    script_generated_hash_tests.update({sanity_check: None})
    return hash

def get_suffix(props):
    if 'suffix' in props:
        return props['suffix']
    return ''

def get_extension(props):
    if 'extension' in props:
        return props['extension']
    return 'UNDEFINED'

def get_op(name, function_props):
    if 'op' not in function_props:
        raise Exception('function op not defined')
    if function_props['op'] == 'auto':
        return name[0].upper() + name[1:]
    return function_props['op']

def get_known_to_not_have_side_effects(function_props):
    if 'op' in function_props and function_props['op'] != 'CallBuiltInFunction':
        if 'hasSideEffects' in function_props:
            return 'false'
        else:
            for param in get_parameters(function_props):
                if 'qualifier' in param.data and (param.data['qualifier'] == 'Out' or param.data['qualifier'] == 'InOut'):
                    return 'false'
            return 'true'
    return 'false'

def get_parameters(function_props):
    if 'parameters' in function_props:
        return function_props['parameters']
    return []

def get_function_mangled_name(function_name, parameters):
    mangled_name = function_name + '('
    for param in parameters:
        mangled_name += param.get_mangled_name()
    return mangled_name

def get_unique_identifier_name(function_name, parameters):
    unique_name = function_name + '_'
    for param in parameters:
        unique_name += param.get_mangled_name()
    return unique_name

def get_variable_name_to_store_parameters(parameters):
    if len(parameters) == 0:
        return 'empty'
    unique_name = 'p'
    for param in parameters:
        if 'qualifier' in param.data:
            if param.data['qualifier'] == 'Out':
                unique_name += '_o_'
            if param.data['qualifier'] == 'InOut':
                unique_name += '_io_'
        unique_name += param.get_mangled_name()
    return unique_name

def gen_function_variants(function_name, function_props):
    function_variants = []
    parameters = get_parameters(function_props)
    function_is_gen_type = False
    gen_type = set()
    for param in parameters:
        if 'genType' in param.data:
            if param.data['genType'] not in ['sampler_or_image', 'vec', 'yes']:
                raise Exception('Unexpected value of genType "' + str(param.data['genType']) + '" should be "sampler_or_image", "vec", or "yes"')
            gen_type.add(param.data['genType'])
            if len(gen_type) > 1:
                raise Exception('Unexpected multiple values of genType set on the same function: ' + str(list(gen_type)))
    if len(gen_type) == 0:
        function_variants.append(function_props)
        return function_variants

    # If we have a gsampler_or_image then we're generating variants for float, int and uint
    # samplers.
    if 'sampler_or_image' in gen_type:
        types = ['', 'I', 'U']
        for type in types:
            variant_props = function_props.copy()
            variant_parameters = []
            for param in parameters:
                variant_parameters.append(param.specific_sampler_or_image_type(type))
            variant_props['parameters'] = variant_parameters
            variant_props['returnType'] = function_props['returnType'].specific_sampler_or_image_type(type)
            function_variants.append(variant_props)
        return function_variants

    # If we have a normal gentype then we're generating variants for different sizes of vectors.
    sizes = range(1, 5)
    if 'vec' in gen_type:
        sizes = range(2, 5)
    for size in sizes:
        variant_props = function_props.copy()
        variant_parameters = []
        for param in parameters:
            variant_parameters.append(param.specific_type(size))
        variant_props['parameters'] = variant_parameters
        variant_props['returnType'] = function_props['returnType'].specific_type(size)
        function_variants.append(variant_props)
    return function_variants

defined_function_variants = set()

def process_single_function_group(condition, group_name, group):
    global id_counter
    if 'functions' not in group:
        return

    for function_props in group['functions']:
        function_name = function_props['name']
        level = function_props['level']
        extension = get_extension(function_props)
        template_args = {
            'name': function_name,
            'name_with_suffix': function_name + get_suffix(function_props),
            'level': level,
            'extension': extension,
            'op': get_op(function_name, function_props),
            'known_to_not_have_side_effects': get_known_to_not_have_side_effects(function_props)
        }

        function_variants = gen_function_variants(function_name, function_props)

        template_name_declaration = 'constexpr const ImmutableString {name_with_suffix}("{name}");'
        name_declaration = template_name_declaration.format(**template_args)
        if not name_declaration in name_declarations:
            name_declarations.add(name_declaration)

        template_unmangled_if = """if (name == BuiltInName::{name_with_suffix})
{{
    return &UnmangledBuiltIns::{extension};
}}"""
        unmangled_if = template_unmangled_if.format(**template_args)
        unmangled_builtin_no_condition = unmangled_function_if_statements.get(level, 'NO_CONDITION', function_name)
        if unmangled_builtin_no_condition != None and unmangled_builtin_no_condition['extension'] == 'UNDEFINED':
            # We already have this unmangled name without a condition nor extension on the same level. No need to add a duplicate with a condition.
            pass
        elif (not unmangled_function_if_statements.has_key(level, condition, function_name)) or extension == 'UNDEFINED':
            # We don't have this unmangled builtin recorded yet or we might replace an unmangled builtin from an extension with one from core.
            unmangled_function_if_statements.add_obj(level, condition, function_name, {'code': unmangled_if, 'extension': extension})
            unmangled_builtin_declarations.add('constexpr const UnmangledBuiltIn {extension}(TExtension::{extension});'.format(**template_args))

        for function_props in function_variants:
            template_args['id'] = id_counter

            parameters = get_parameters(function_props)

            template_args['unique_name'] = get_unique_identifier_name(template_args['name_with_suffix'], parameters)

            if template_args['unique_name'] in defined_function_variants:
                continue
            defined_function_variants.add(template_args['unique_name'])

            template_args['param_count'] = len(parameters)
            template_args['return_type'] = function_props['returnType'].get_statictype_string()
            template_args['mangled_name'] = get_function_mangled_name(function_name, parameters)

            template_builtin_id_declaration = '    static constexpr const TSymbolUniqueId {unique_name} = TSymbolUniqueId({id});'
            builtin_id_declarations.append(template_builtin_id_declaration.format(**template_args))

            template_mangled_name_declaration = 'constexpr const ImmutableString {unique_name}("{mangled_name}");'
            name_declarations.add(template_mangled_name_declaration.format(**template_args))

            parameters_list = []
            for param in parameters:
                template_parameter = 'TConstParameter({param_type})'
                parameters_list.append(template_parameter.format(param_type = param.get_statictype_string()))
            template_args['parameters_var_name'] = get_variable_name_to_store_parameters(parameters)
            if len(parameters) > 0:
                template_args['parameters_list'] = ', '.join(parameters_list)
                template_parameter_list_declaration = 'constexpr const TConstParameter {parameters_var_name}[{param_count}] = {{ {parameters_list} }};'
                parameter_declarations.add(template_parameter_list_declaration.format(**template_args))
            else:
                template_parameter_list_declaration = 'constexpr const TConstParameter *{parameters_var_name} = nullptr;'
                parameter_declarations.add(template_parameter_list_declaration.format(**template_args))

            template_function_declaration = 'constexpr const TFunction kFunction_{unique_name}(BuiltInId::{unique_name}, BuiltInName::{name_with_suffix}, TExtension::{extension}, BuiltInParameters::{parameters_var_name}, {param_count}, {return_type}, BuiltInName::{unique_name}, EOp{op}, {known_to_not_have_side_effects});'
            function_declarations.append(template_function_declaration.format(**template_args))

            template_insert_function = '    insertBuiltIn({level}, &BuiltInFunction::kFunction_{unique_name});'
            insert_functions_by_condition[condition].append(template_insert_function.format(**template_args))

            id_counter += 1

def process_function_group(group_name, group):
    global id_counter
    first_id = id_counter

    condition = 'NO_CONDITION'
    if 'condition' in group:
        condition = group['condition']
    if condition not in insert_functions_by_condition:
        insert_functions_by_condition[condition] = []

    process_single_function_group(condition, group_name, group)

    if 'subgroups' in group:
        for subgroup_name, subgroup in group['subgroups'].iteritems():
            process_function_group(group_name + subgroup_name, subgroup)

    if 'queryFunction' in group:
        template_args = {
            'first_id': first_id,
            'last_id': id_counter - 1,
            'group_name': group_name
        }
        template_is_in_group_definition = """bool is{group_name}(const TFunction *func)
{{
    int id = func->uniqueId().get();
    return id >= {first_id} && id <= {last_id};
}}"""
        is_in_group_definitions.append(template_is_in_group_definition.format(**template_args))

for group_name, group in parsed_functions.iteritems():
    process_function_group(group_name, group)

def process_single_variable_group(group_name, group):
    global id_counter
    if 'variables' not in group:
        return
    for variable_name, props in group['variables'].iteritems():
        template_args = {
            'id': id_counter,
            'name': variable_name,
            'name_with_suffix': variable_name + get_suffix(props),
            'level': props['level'],
            'extension': get_extension(props)
        }

        template_builtin_id_declaration = '    static constexpr const TSymbolUniqueId {name_with_suffix} = TSymbolUniqueId({id});'
        builtin_id_declarations.append(template_builtin_id_declaration.format(**template_args))
        template_builtin_id_definition = 'constexpr const TSymbolUniqueId BuiltInId::{name_with_suffix};'
        builtin_id_definitions.append(template_builtin_id_definition.format(**template_args))

        template_name_declaration = 'constexpr const ImmutableString {name}("{name}");'
        name_declarations.add(template_name_declaration.format(**template_args))

        if 'type' in props:
            if props['type']['basic'] != 'Bool' and 'precision' not in props['type']:
                raise Exception('Missing precision for variable ' + variable_name)
            template_args['type'] = TType(props['type']).get_statictype_string()

        if 'fields' in props:
            # Handle struct and interface block definitions.
            template_args['class'] = props['class']
            template_args['fields'] = 'fields_{name_with_suffix}'.format(**template_args)
            insert_variables.append('TFieldList *{fields} = new TFieldList();'.format(**template_args))
            for field_name, field_type in props['fields'].iteritems():
                template_args['field_name'] = field_name
                template_args['field_type'] = TType(field_type).get_dynamic_type_string()
                template_name_declaration = 'constexpr const ImmutableString {field_name}("{field_name}");'
                name_declarations.add(template_name_declaration.format(**template_args))
                template_add_field = '{fields}->push_back(new TField({field_type}, BuiltInName::{field_name}, zeroSourceLoc));'
                insert_variables.append(template_add_field.format(**template_args))
            template_init_variable = '    {class} *{name_with_suffix} = new {class}(BuiltInId::{name_with_suffix}, BuiltInName::{name}, TExtension::{extension}, {fields});'
            insert_variables.append(template_init_variable.format(**template_args))
            template_insert_variable = '    insertBuiltIn({level}, {name_with_suffix});'
            if 'private' not in props or not props['private']:
                insert_variables.append(template_insert_variable.format(**template_args))

        elif 'initDynamicType' in props:
            # Handle variables whose type can't be expressed as TStaticType
            # (type is a struct or has variable array size for example).
            template_args['type_name'] = 'type_{name_with_suffix}'.format(**template_args)
            template_args['type'] = template_args['type_name']
            template_args['initDynamicType'] = props['initDynamicType'].format(**template_args)
            template_insert_variable = """    {initDynamicType}
    {type_name}->realize();
    TVariable *{name_with_suffix} = new TVariable(BuiltInId::{name_with_suffix}, BuiltInName::{name}, SymbolType::BuiltIn, TExtension::{extension}, {type});
    insertBuiltIn({level}, {name_with_suffix});"""
            insert_variables.append(template_insert_variable.format(**template_args))

        elif 'value' in props:
            # Handle variables with constant value, such as gl_MaxDrawBuffers.
            if props['value'] != 'resources':
                raise Exception('Unrecognized value source in variable properties: ' + str(props['value']))
            resources_key = variable_name[3:]
            if 'valueKey' in props:
                resources_key = props['valueKey']
            template_args['value'] = 'resources.' + resources_key
            template_args['object_size'] = TType(props['type']).get_object_size()
            template_insert_variable = """    TVariable *{name_with_suffix} = new TVariable(BuiltInId::{name_with_suffix}, BuiltInName::{name}, SymbolType::BuiltIn, TExtension::{extension}, {type});
    {{
        TConstantUnion *unionArray = new TConstantUnion[{object_size}];
        unionArray[0].setIConst({value});
        {name_with_suffix}->shareConstPointer(unionArray);
    }}
    insertBuiltIn({level}, {name_with_suffix});"""
            if template_args['object_size'] > 1:
                template_insert_variable = """    TVariable *{name_with_suffix} = new TVariable(BuiltInId::{name_with_suffix}, BuiltInName::{name}, SymbolType::BuiltIn, TExtension::{extension}, {type});
    {{
        TConstantUnion *unionArray = new TConstantUnion[{object_size}];
        for (size_t index = 0u; index < {object_size}; ++index)
        {{
            unionArray[index].setIConst({value}[index]);
        }}
        {name_with_suffix}->shareConstPointer(unionArray);
    }}
    insertBuiltIn({level}, {name_with_suffix});"""

            insert_variables.append(template_insert_variable.format(**template_args))

        else:
            # Handle variables that can be stored as constexpr TVariable like
            # gl_Position, gl_FragColor etc.
            template_variable_declaration = 'constexpr const TVariable kVar_{name_with_suffix}(BuiltInId::{name_with_suffix}, BuiltInName::{name}, SymbolType::BuiltIn, TExtension::{extension}, {type});'
            variable_declarations.append(template_variable_declaration.format(**template_args))

            template_get_variable_declaration = 'const TVariable *{name_with_suffix}();'
            get_variable_declarations.append(template_get_variable_declaration.format(**template_args))

            template_get_variable_definition = """const TVariable *{name_with_suffix}()
{{
    return &kVar_{name_with_suffix};
}}
"""
            get_variable_definitions.append(template_get_variable_definition.format(**template_args))

            template_insert_variable = '    insertBuiltIn({level}, &BuiltInVariable::kVar_{name_with_suffix});'
            insert_variables.append(template_insert_variable.format(**template_args))

        id_counter += 1

def process_variable_group(group_name, group):
    if 'condition' in group:
        insert_variables.append('  if ({condition})'.format(condition = group['condition']))
        insert_variables.append('  {')

    process_single_variable_group(group_name, group)

    if 'subgroups' in group:
        for subgroup_name, subgroup in group['subgroups'].iteritems():
            process_variable_group(subgroup_name, subgroup)

    if 'condition' in group:
        insert_variables.append('  }')

for group_name, group in parsed_variables.iteritems():
    process_variable_group(group_name, group)


output_strings = {
    'script_name': os.path.basename(__file__),
    'copyright_year': date.today().year,

    'builtin_id_declarations': '\n'.join(builtin_id_declarations),
    'builtin_id_definitions': '\n'.join(builtin_id_definitions),
    'last_builtin_id': id_counter - 1,
    'name_declarations': '\n'.join(sorted(list(name_declarations))),

    'function_data_source_name': functions_txt_filename,
    'function_declarations': '\n'.join(function_declarations),
    'parameter_declarations': '\n'.join(sorted(parameter_declarations)),

    'is_in_group_definitions': '\n'.join(is_in_group_definitions),

    'variable_data_source_name': variables_json_filename,
    'variable_declarations': '\n'.join(sorted(variable_declarations)),
    'get_variable_declarations': '\n'.join(sorted(get_variable_declarations)),
    'get_variable_definitions': '\n'.join(sorted(get_variable_definitions)),
    'insert_variables': '\n'.join(insert_variables)
}

insert_functions = []
get_unmangled_builtin = []

def get_shader_version_condition_for_level(level):
    if level == 'ESSL3_1_BUILTINS':
        return 'shaderVersion >= 310'
    elif level == 'ESSL3_BUILTINS':
        return 'shaderVersion >= 300'
    elif level == 'ESSL1_BUILTINS':
        return 'shaderVersion == 100'
    elif level == 'COMMON_BUILTINS':
        return ''
    else:
        raise Exception('Unsupported symbol table level')

for condition in insert_functions_by_condition:
    if condition != 'NO_CONDITION':
        condition_header = '  if ({condition})\n {{'.format(condition = condition)
        insert_functions.append(condition_header)
    for insert_function in insert_functions_by_condition[condition]:
        insert_functions.append(insert_function)
    if condition != 'NO_CONDITION':
        insert_functions.append('}')

for level in levels:
    level_condition = get_shader_version_condition_for_level(level)
    if level_condition != '':
        get_unmangled_builtin.append('if ({condition})\n {{'.format(condition = level_condition))

    for condition, functions in unmangled_function_if_statements.iter_conditions(level):
        if len(functions) > 0:
            if condition != 'NO_CONDITION':
                condition_header = '  if ({condition})\n {{'.format(condition = condition)
                get_unmangled_builtin.append(condition_header.replace('shaderType', 'mShaderType'))

            get_unmangled_builtin_switch = {}
            for function_name, get_unmangled_case in functions.iteritems():
                name_hash = hash32(function_name)
                if name_hash not in get_unmangled_builtin_switch:
                    get_unmangled_builtin_switch[name_hash] = []
                get_unmangled_builtin_switch[name_hash].append(get_unmangled_case['code'])

            get_unmangled_builtin.append('switch(nameHash) {')
            for name_hash, get_unmangled_cases in sorted(get_unmangled_builtin_switch.iteritems()):
                get_unmangled_builtin.append('case 0x' + ('%x' % name_hash) + 'u:\n{')
                get_unmangled_builtin += get_unmangled_cases
                get_unmangled_builtin.append('break;\n}')
            get_unmangled_builtin.append('}')

            if condition != 'NO_CONDITION':
                get_unmangled_builtin.append('}')

    if level_condition != '':
        get_unmangled_builtin.append('}')

get_unmangled_builtin.append('return nullptr;')

output_strings['insert_functions'] = '\n'.join(insert_functions)
output_strings['unmangled_builtin_declarations'] = '\n'.join(sorted(unmangled_builtin_declarations))
output_strings['get_unmangled_builtin'] = '\n'.join(get_unmangled_builtin)
output_strings['script_generated_hash_tests'] = '\n'.join(script_generated_hash_tests.iterkeys())

with open('../../tests/compiler_tests/ImmutableString_test_autogen.cpp', 'wt') as outfile_cpp:
    output_cpp = template_immutablestringtest_cpp.format(**output_strings)
    outfile_cpp.write(output_cpp)

with open('BuiltIn_autogen.h', 'wt') as outfile_header:
    output_header = template_builtin_header.format(**output_strings)
    outfile_header.write(output_header)

with open('SymbolTable_autogen.cpp', 'wt') as outfile_cpp:
    output_cpp = template_symboltable_cpp.format(**output_strings)
    outfile_cpp.write(output_cpp)

with open('ParseContext_autogen.h', 'wt') as outfile_header:
    output_header = template_parsecontext_header.format(**output_strings)
    outfile_header.write(output_header)
