#!/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_vk_internal_shaders.py:
#  Code generation for internal Vulkan shaders. Should be run when an internal
#  shader program is changed, added or removed.
#  Because this script can be slow direct invocation is supported. But before
#  code upload please run scripts/run_code_generation.py.

from datetime import date
import io
import json
import multiprocessing
import os
import platform
import re
import subprocess
import sys

out_file_cpp = 'vk_internal_shaders_autogen.cpp'
out_file_h = 'vk_internal_shaders_autogen.h'
out_file_gni = 'vk_internal_shaders_autogen.gni'

is_windows = platform.system() == 'Windows'
is_linux = platform.system() == 'Linux'

# Templates for the generated files:
template_shader_library_cpp = u"""// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {input_file_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.
//
// {out_file_name}:
//   Pre-generated shader library for the ANGLE Vulkan back-end.

#include "libANGLE/renderer/vulkan/vk_internal_shaders_autogen.h"

namespace rx
{{
namespace vk
{{
namespace
{{
{internal_shader_includes}

// This is SPIR-V binary blob and the size.
struct ShaderBlob
{{
    const uint32_t *code;
    size_t codeSize;
}};

{shader_tables_cpp}

angle::Result GetShader(Context *context,
                        RefCounted<ShaderAndSerial> *shaders,
                        const ShaderBlob *shaderBlobs,
                        size_t shadersCount,
                        uint32_t shaderFlags,
                        RefCounted<ShaderAndSerial> **shaderOut)
{{
    ASSERT(shaderFlags < shadersCount);
    RefCounted<ShaderAndSerial> &shader = shaders[shaderFlags];
    *shaderOut                          = &shader;

    if (shader.get().valid())
    {{
        return angle::Result::Continue;
    }}

    // Create shader lazily. Access will need to be locked for multi-threading.
    const ShaderBlob &shaderCode = shaderBlobs[shaderFlags];
    ASSERT(shaderCode.code != nullptr);

    return InitShaderAndSerial(context, &shader.get(), shaderCode.code, shaderCode.codeSize);
}}
}}  // anonymous namespace


ShaderLibrary::ShaderLibrary()
{{
}}

ShaderLibrary::~ShaderLibrary()
{{
}}

void ShaderLibrary::destroy(VkDevice device)
{{
    {shader_destroy_calls}
}}

{shader_get_functions_cpp}
}}  // namespace vk
}}  // namespace rx
"""

template_shader_library_h = u"""// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {input_file_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.
//
// {out_file_name}:
//   Pre-generated shader library for the ANGLE Vulkan back-end.

#ifndef LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_
#define LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_

#include "libANGLE/renderer/vulkan/vk_utils.h"

namespace rx
{{
namespace vk
{{
namespace InternalShader
{{
{shader_variation_definitions}
}}  // namespace InternalShader

class ShaderLibrary final : angle::NonCopyable
{{
  public:
    ShaderLibrary();
    ~ShaderLibrary();

    void destroy(VkDevice device);

    {shader_get_functions_h}

  private:
    {shader_tables_h}
}};
}}  // namespace vk
}}  // namespace rx

#endif  // LIBANGLE_RENDERER_VULKAN_VK_INTERNAL_SHADERS_AUTOGEN_H_
"""

template_shader_includes_gni = u"""# GENERATED FILE - DO NOT EDIT.
# Generated by {script_name} using data from {input_file_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.
#
# {out_file_name}:
#   List of generated shaders for inclusion in ANGLE's build process.

angle_vulkan_internal_shaders = [
{shaders_list}
]
"""


# Gets the constant variable name for a generated shader.
def get_var_name(output, prefix='k'):
    return prefix + output.replace(".", "_")


# Gets the namespace name given to constants generated from shader_file
def get_namespace_name(shader_file):
    return get_var_name(os.path.basename(shader_file), '')


# Gets the namespace name given to constants generated from shader_file
def get_variation_table_name(shader_file, prefix='k'):
    return get_var_name(os.path.basename(shader_file), prefix) + '_shaders'


# Gets the internal ID string for a particular shader.
def get_shader_id(shader):
    file = os.path.splitext(os.path.basename(shader))[0]
    return file.replace(".", "_")


# Returns the name of the generated SPIR-V file for a shader.
def get_output_path(name):
    return os.path.join('shaders', 'gen', name + ".inc")


# Finds a path to GN's out directory
def get_linux_glslang_exe_path():
    return '../../../../tools/glslang/glslang_validator'


def get_win_glslang_exe_path():
    return get_linux_glslang_exe_path() + '.exe'


def get_glslang_exe_path():
    glslang_exe = get_win_glslang_exe_path() if is_windows else get_linux_glslang_exe_path()
    if not os.path.isfile(glslang_exe):
        raise Exception('Could not find %s' % glslang_exe)
    return glslang_exe


# Generates the code for a shader blob array entry.
def gen_shader_blob_entry(shader):
    var_name = get_var_name(os.path.basename(shader))[0:-4]
    return "{%s, %s}" % (var_name, "sizeof(%s)" % var_name)


def slash(s):
    return s.replace('\\', '/')


def gen_shader_include(shader):
    return '#include "libANGLE/renderer/vulkan/%s"' % slash(shader)


def get_variations_path(shader):
    variation_file = shader + '.json'
    return variation_file if os.path.exists(variation_file) else None


def get_shader_variations(shader):
    variation_file = get_variations_path(shader)
    if variation_file is None:
        # If there is no variation file, assume none.
        return ({}, [])

    with open(variation_file) as fin:
        variations = json.loads(fin.read())
        flags = {}
        enums = []

        for key, value in variations.iteritems():
            if key == "Description":
                continue
            elif key == "Flags":
                flags = value
            elif len(value) > 0:
                enums.append((key, value))

        # sort enums so the ones with the most waste ends up last, reducing the table size
        enums.sort(key=lambda enum: (1 << (len(enum[1]) - 1).bit_length()) / float(len(enum[1])))

        return (flags, enums)


def get_variation_bits(flags, enums):
    flags_bits = len(flags)
    enum_bits = [(len(enum[1]) - 1).bit_length() for enum in enums]
    return (flags_bits, enum_bits)


def next_enum_variation(enums, enum_indices):
    """Loop through indices from [0, 0, ...] to [L0-1, L1-1, ...]
    where Li is len(enums[i]).  The list can be thought of as a number with many
    digits, where each digit is in [0, Li), and this function effectively implements
    the increment operation, with the least-significant digit being the first item."""
    for i in range(len(enums)):
        current = enum_indices[i]
        # if current digit has room, increment it.
        if current + 1 < len(enums[i][1]):
            enum_indices[i] = current + 1
            return True
        # otherwise reset it to 0 and carry to the next digit.
        enum_indices[i] = 0

    # if this is reached, the number has overflowed and the loop is finished.
    return False


compact_newlines_regex = re.compile(r"\n\s*\n", re.MULTILINE)


def cleanup_preprocessed_shader(shader_text):
    return compact_newlines_regex.sub('\n\n', shader_text.strip())


class CompileQueue:

    class AppendPreprocessorOutput:

        def __init__(self, shader_file, preprocessor_args, output_path):
            # Asynchronously launch the preprocessor job.
            self.process = subprocess.Popen(
                preprocessor_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            # Store the file name for output to be appended to.
            self.output_path = output_path
            # Store info for error description.
            self.shader_file = shader_file

        def wait(self, queue):
            (out, err) = self.process.communicate()
            if self.process.returncode == 0:
                # Use unix line endings.
                out = out.replace('\r\n', '\n')
                # Clean up excessive empty lines.
                out = cleanup_preprocessed_shader(out)
                # Comment it out!
                out = '\n'.join([('// ' + line).strip() for line in out.splitlines()])
                # Append preprocessor output to the output file.
                with open(self.output_path, 'ab') as incfile:
                    incfile.write('\n\n// Generated from:\n//\n')
                    incfile.write(out + '\n')
                out = None
            return (out, err, self.process.returncode, None,
                    "Error running preprocessor on " + self.shader_file)

    class CompileToSPIRV:

        def __init__(self, shader_file, shader_basename, variation_string, output_path,
                     compile_args, preprocessor_args):
            # Asynchronously launch the compile job.
            self.process = subprocess.Popen(
                compile_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            # Store info for launching the preprocessor.
            self.preprocessor_args = preprocessor_args
            self.output_path = output_path
            # Store info for job and error description.
            self.shader_file = shader_file
            self.shader_basename = shader_basename
            self.variation_string = variation_string

        def wait(self, queue):
            (out, err) = self.process.communicate()
            if self.process.returncode == 0:
                # Insert the preprocessor job in the queue.
                queue.append(
                    CompileQueue.AppendPreprocessorOutput(self.shader_file, self.preprocessor_args,
                                                          self.output_path))
            # If all the output says is the source file name, don't bother printing it.
            if out.strip() == self.shader_file:
                out = None
            description = self.output_path + ': ' + self.shader_basename + self.variation_string
            return (out, err, self.process.returncode, description,
                    "Error compiling " + self.shader_file)

    def __init__(self):
        # Compile with as many CPU threads are detected.  Once a shader is compiled, another job is
        # automatically added to the queue to append the preprocessor output to the generated file.
        self.queue = []
        self.thread_count = multiprocessing.cpu_count()

    def _wait_first(self, ignore_output=False):
        (out, err, returncode, description, exception_description) = self.queue[0].wait(self.queue)
        self.queue.pop(0)
        if not ignore_output:
            if description:
                print description
            if out and out.strip():
                print out.strip()
            if err and err.strip():
                print err
            if returncode != 0:
                return exception_description
        return None

    # Wait for all pending tasks.  If called after error is detected, ignore_output can be used to
    # make sure errors in later jobs are suppressed to avoid cluttering the output.  This is
    # because the same compile error is likely present in other variations of the same shader and
    # outputting the same error multiple times is not useful.
    def _wait_all(self, ignore_output=False):
        exception_description = None
        while len(self.queue) > 0:
            this_job_exception = self._wait_first(ignore_output)
            # If encountered an error, keep it to be raised, ignoring errors from following jobs.
            if this_job_exception and not ignore_output:
                exception_description = this_job_exception
                ignore_output = True

        return exception_description

    def add_job(self, shader_file, shader_basename, variation_string, output_path, compile_args,
                preprocessor_args):
        # If the queue is full, wait until there is at least one slot available.
        while len(self.queue) >= self.thread_count:
            exception = self._wait_first(False)
            # If encountered an exception, cleanup following jobs and raise it.
            if exception:
                self._wait_all(True)
                raise Exception(exception)

        # Add a compile job
        self.queue.append(
            CompileQueue.CompileToSPIRV(shader_file, shader_basename, variation_string,
                                        output_path, compile_args, preprocessor_args))

    def finish(self):
        exception = self._wait_all(False)
        # If encountered an exception, cleanup following jobs and raise it.
        if exception is not None:
            raise Exception(exception)


# If the option is just a string, that's the name.  Otherwise, it could be
# [ name, arg1, ..., argN ].  In that case, name is option[0] and option[1:] are extra arguments
# that need to be passed to glslang_validator for this variation.
def get_variation_name(option):
    return option if isinstance(option, unicode) else option[0]


def get_variation_args(option):
    return [] if isinstance(option, unicode) else option[1:]


def compile_variation(glslang_path, compile_queue, shader_file, shader_basename, flags, enums,
                      flags_active, enum_indices, flags_bits, enum_bits, output_shaders):

    glslang_args = [glslang_path]

    # generate -D defines and the output file name
    #
    # The variations are given a bit pattern to be able to OR different flags into a variation. The
    # least significant bits are the flags, where there is one bit per flag.  After that, each enum
    # takes up as few bits as needed to count that many enum values.
    variation_bits = 0
    variation_string = ''
    variation_extra_args = []
    for f in range(len(flags)):
        if flags_active & (1 << f):
            flag = flags[f]
            flag_name = get_variation_name(flag)
            variation_extra_args += get_variation_args(flag)
            glslang_args.append('-D' + flag_name + '=1')

            variation_bits |= 1 << f
            variation_string += '|' + flag_name

    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum = enums[e][1][enum_indices[e]]
        enum_name = get_variation_name(enum)
        variation_extra_args += get_variation_args(enum)
        glslang_args.append('-D' + enum_name + '=1')

        variation_bits |= enum_indices[e] << current_bit_start
        current_bit_start += enum_bits[e]
        variation_string += '|' + enum_name

    output_name = '%s.%08X' % (shader_basename, variation_bits)
    output_path = get_output_path(output_name)
    output_shaders.append(output_path)

    if glslang_path is not None:
        glslang_preprocessor_output_args = glslang_args + ['-E']
        glslang_preprocessor_output_args.append(shader_file)  # Input GLSL shader

        glslang_args += ['-V']  # Output mode is Vulkan
        glslang_args += ['-Os']  # Optimize by default.
        glslang_args += ['-g0']  # Strip debug info to save on binary size.
        glslang_args += ['--variable-name', get_var_name(output_name)]  # C-style variable name
        glslang_args += variation_extra_args  # Add other flags, or override -Os or -g0
        glslang_args += ['-o', output_path]  # Output file
        glslang_args.append(shader_file)  # Input GLSL shader

        compile_queue.add_job(shader_file, shader_basename, variation_string, output_path,
                              glslang_args, glslang_preprocessor_output_args)


class ShaderAndVariations:

    def __init__(self, shader_file):
        self.shader_file = shader_file
        (self.flags, self.enums) = get_shader_variations(shader_file)
        get_variation_bits(self.flags, self.enums)
        (self.flags_bits, self.enum_bits) = get_variation_bits(self.flags, self.enums)
        # Maximum index value has all flags set and all enums at max value.
        max_index = (1 << self.flags_bits) - 1
        current_bit_start = self.flags_bits
        for (name, values), bits in zip(self.enums, self.enum_bits):
            max_index |= (len(values) - 1) << current_bit_start
            current_bit_start += bits
        # Minimum array size is one more than the maximum value.
        self.array_len = max_index + 1


def get_variation_definition(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    flags = shader_and_variation.flags
    enums = shader_and_variation.enums
    flags_bits = shader_and_variation.flags_bits
    enum_bits = shader_and_variation.enum_bits
    array_len = shader_and_variation.array_len

    namespace_name = get_namespace_name(shader_file)

    definition = 'namespace %s\n{\n' % namespace_name
    if len(flags) > 0:
        definition += 'enum flags\n{\n'
        definition += ''.join([
            'k%s = 0x%08X,\n' % (get_variation_name(flags[f]), 1 << f) for f in range(len(flags))
        ])
        definition += '};\n'

    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum = enums[e]
        enum_name = enum[0]
        definition += 'enum %s\n{\n' % enum_name
        definition += ''.join([
            'k%s = 0x%08X,\n' % (get_variation_name(enum[1][v]), v << current_bit_start)
            for v in range(len(enum[1]))
        ])
        definition += '};\n'
        current_bit_start += enum_bits[e]

    definition += 'constexpr size_t kArrayLen = 0x%08X;\n' % array_len

    definition += '}  // namespace %s\n' % namespace_name
    return definition


def get_shader_table_h(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    flags = shader_and_variation.flags
    enums = shader_and_variation.enums

    table_name = get_variation_table_name(shader_file, 'm')

    table = 'RefCounted<ShaderAndSerial> %s[' % table_name

    namespace_name = "InternalShader::" + get_namespace_name(shader_file)

    table += '%s::kArrayLen' % namespace_name

    table += '];'
    return table


def get_shader_table_cpp(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    enums = shader_and_variation.enums
    flags_bits = shader_and_variation.flags_bits
    enum_bits = shader_and_variation.enum_bits
    array_len = shader_and_variation.array_len

    # Cache max and mask value of each enum to quickly know when a possible variation is invalid
    enum_maxes = []
    enum_masks = []
    current_bit_start = flags_bits

    for e in range(len(enums)):
        enum_values = enums[e][1]
        enum_maxes.append((len(enum_values) - 1) << current_bit_start)
        enum_masks.append(((1 << enum_bits[e]) - 1) << current_bit_start)
        current_bit_start += enum_bits[e]

    table_name = get_variation_table_name(shader_file)
    var_name = get_var_name(os.path.basename(shader_file))

    table = 'constexpr ShaderBlob %s[] = {\n' % table_name

    for variation in range(array_len):
        # if any variation is invalid, output an empty entry
        if any([(variation & enum_masks[e]) > enum_maxes[e] for e in range(len(enums))]):
            table += '{nullptr, 0}, // 0x%08X\n' % variation
        else:
            entry = '%s_%08X' % (var_name, variation)
            table += '{%s, sizeof(%s)},\n' % (entry, entry)

    table += '};'
    return table


def get_get_function_h(shader_and_variation):
    shader_file = shader_and_variation.shader_file

    function_name = get_var_name(os.path.basename(shader_file), 'get')

    definition = 'angle::Result %s' % function_name
    definition += '(Context *context, uint32_t shaderFlags, RefCounted<ShaderAndSerial> **shaderOut);'

    return definition


def get_get_function_cpp(shader_and_variation):
    shader_file = shader_and_variation.shader_file
    enums = shader_and_variation.enums

    function_name = get_var_name(os.path.basename(shader_file), 'get')
    namespace_name = "InternalShader::" + get_namespace_name(shader_file)
    member_table_name = get_variation_table_name(shader_file, 'm')
    constant_table_name = get_variation_table_name(shader_file)

    definition = 'angle::Result ShaderLibrary::%s' % function_name
    definition += '(Context *context, uint32_t shaderFlags, RefCounted<ShaderAndSerial> **shaderOut)\n{\n'
    definition += 'return GetShader(context, %s, %s, ArraySize(%s), shaderFlags, shaderOut);\n}\n' % (
        member_table_name, constant_table_name, constant_table_name)

    return definition


def get_destroy_call(shader_and_variation):
    shader_file = shader_and_variation.shader_file

    table_name = get_variation_table_name(shader_file, 'm')

    destroy = 'for (RefCounted<ShaderAndSerial> &shader : %s)\n' % table_name
    destroy += '{\nshader.get().destroy(device);\n}'
    return destroy


def shader_path(shader):
    return '"%s"' % slash(shader)


def main():
    # STEP 0: Handle inputs/outputs for run_code_generation.py's auto_script
    shaders_dir = os.path.join('shaders', 'src')
    if not os.path.isdir(shaders_dir):
        raise Exception("Could not find shaders directory")

    print_inputs = len(sys.argv) == 2 and sys.argv[1] == 'inputs'
    print_outputs = len(sys.argv) == 2 and sys.argv[1] == 'outputs'
    # If an argument X is given that's not inputs or outputs, compile shaders that match *X*.
    # This is useful in development to build only the shader of interest.
    shader_files_to_compile = os.listdir(shaders_dir)
    if not (print_inputs or print_outputs or len(sys.argv) < 2):
        shader_files_to_compile = [f for f in shader_files_to_compile if f.find(sys.argv[1]) != -1]

    valid_extensions = ['.vert', '.frag', '.comp']
    input_shaders = sorted([
        os.path.join(shaders_dir, shader)
        for shader in os.listdir(shaders_dir)
        if any([os.path.splitext(shader)[1] == ext for ext in valid_extensions])
    ])
    if print_inputs:
        glslang_binaries = [get_linux_glslang_exe_path(), get_win_glslang_exe_path()]
        glslang_binary_hashes = [path + '.sha1' for path in glslang_binaries]
        input_shaders_variations = [get_variations_path(shader) for shader in input_shaders]
        input_shaders_variations = [
            variations for variations in input_shaders_variations if variations is not None
        ]
        print(",".join(input_shaders + input_shaders_variations + glslang_binary_hashes))
        return 0

    # STEP 1: Call glslang to generate the internal shaders into small .inc files.
    # Iterates over the shaders and call glslang with the right arguments.

    glslang_path = None
    if not print_outputs:
        glslang_path = get_glslang_exe_path()

    output_shaders = []

    input_shaders_and_variations = [
        ShaderAndVariations(shader_file) for shader_file in input_shaders
    ]

    compile_queue = CompileQueue()

    for shader_and_variation in input_shaders_and_variations:
        shader_file = shader_and_variation.shader_file
        flags = shader_and_variation.flags
        enums = shader_and_variation.enums
        flags_bits = shader_and_variation.flags_bits
        enum_bits = shader_and_variation.enum_bits

        # an array where each element i is in [0, len(enums[i])),
        # telling which enum is currently selected
        enum_indices = [0] * len(enums)

        output_name = os.path.basename(shader_file)

        while True:
            do_compile = not print_outputs and output_name in shader_files_to_compile
            # a number where each bit says whether a flag is active or not,
            # with values in [0, 2^len(flags))
            for flags_active in range(1 << len(flags)):
                compile_variation(glslang_path if do_compile else None, compile_queue, shader_file,
                                  output_name, flags, enums, flags_active, enum_indices,
                                  flags_bits, enum_bits, output_shaders)

            if not next_enum_variation(enums, enum_indices):
                break

    output_shaders = sorted(output_shaders)
    outputs = output_shaders + [out_file_cpp, out_file_h]

    if print_outputs:
        print(','.join(outputs))
        return 0

    compile_queue.finish()

    # STEP 2: Consolidate the .inc files into an auto-generated cpp/h library.
    with open(out_file_cpp, 'w') as outfile:
        includes = "\n".join([gen_shader_include(shader) for shader in output_shaders])
        shader_tables_cpp = '\n'.join(
            [get_shader_table_cpp(s) for s in input_shaders_and_variations])
        shader_destroy_calls = '\n'.join(
            [get_destroy_call(s) for s in input_shaders_and_variations])
        shader_get_functions_cpp = '\n'.join(
            [get_get_function_cpp(s) for s in input_shaders_and_variations])

        outcode = template_shader_library_cpp.format(
            script_name=__file__,
            copyright_year=date.today().year,
            out_file_name=out_file_cpp,
            input_file_name='shaders/src/*',
            internal_shader_includes=includes,
            shader_tables_cpp=shader_tables_cpp,
            shader_destroy_calls=shader_destroy_calls,
            shader_get_functions_cpp=shader_get_functions_cpp)
        outfile.write(outcode)
        outfile.close()

    with open(out_file_h, 'w') as outfile:
        shader_variation_definitions = '\n'.join(
            [get_variation_definition(s) for s in input_shaders_and_variations])
        shader_get_functions_h = '\n'.join(
            [get_get_function_h(s) for s in input_shaders_and_variations])
        shader_tables_h = '\n'.join([get_shader_table_h(s) for s in input_shaders_and_variations])
        outcode = template_shader_library_h.format(
            script_name=__file__,
            copyright_year=date.today().year,
            out_file_name=out_file_h,
            input_file_name='shaders/src/*',
            shader_variation_definitions=shader_variation_definitions,
            shader_get_functions_h=shader_get_functions_h,
            shader_tables_h=shader_tables_h)
        outfile.write(outcode)
        outfile.close()

    # STEP 3: Create a gni file with the generated files.
    with io.open(out_file_gni, 'w', newline='\n') as outfile:
        outcode = template_shader_includes_gni.format(
            script_name=__file__,
            copyright_year=date.today().year,
            out_file_name=out_file_gni,
            input_file_name='shaders/src/*',
            shaders_list=',\n'.join([shader_path(shader) for shader in output_shaders]))
        outfile.write(outcode)
        outfile.close()

    return 0


if __name__ == '__main__':
    sys.exit(main())
