import os
import re
import sys
from subprocess import Popen, PIPE
import json

VERTEX_SHADER_EXT = '.vsh.glsl'
FRAG_SHADER_EXT = '.fsh.glsl'
VERTEX_SHADER_EXT_OUT = '.vert'
FRAG_SHADER_EXT_OUT = '.frag'

SHADERS_LIB_COMMON_PATTERN = '// Common'
SHADERS_LIB_VS_PATTERN = '// VS'
SHADERS_LIB_FS_PATTERN = '// FS'
SHADERS_LIB_COMMON_INDEX = 0
SHADERS_LIB_VS_INDEX = 1
SHADERS_LIB_FS_INDEX = 2

UNIFORMS = 'uniforms'
SAMPLERS = 'samplers'
IS_STORAGE = 'is_storage'

debug_output = False


# Read index file which contains program to shaders bindings.
def read_index_file(file_path):
    gpu_programs = dict()
    with open(file_path, 'r') as f:
        index = 0
        for line in f:
            line_parts = line.strip().split()
            if len(line_parts) != 3:
                print('Incorrect GPU program definition : ' + line)
                exit(1)

            vertex_shader = next(f for f in line_parts if f.endswith(VERTEX_SHADER_EXT))
            fragment_shader = next(f for f in line_parts if f.endswith(FRAG_SHADER_EXT))

            if not vertex_shader:
                print('Vertex shader not found in GPU program definition : ' + line)
                exit(1)

            if not fragment_shader:
                print('Fragment shader not found in GPU program definition : ' + line)
                exit(1)

            if line_parts[0] in gpu_programs.keys():
                print('More than one definition of %s gpu program' % line_parts[0])
                exit(1)

            gpu_programs[index] = (vertex_shader, fragment_shader, line_parts[0])
            index += 1

    gpu_programs_cache = dict()
    for (k, v) in gpu_programs.items():
        gpu_programs_cache[v[2]] = (v[0], v[1], k)

    return gpu_programs_cache


# Read GLSL-file with common shader functions.
def read_shaders_lib_file(file_path):
    shaders_library = ['', '', '']
    with open(file_path, 'r') as f:
        shaders_lib_content = f.read()
    if len(shaders_lib_content) == 0:
        return shaders_library

    common_index = shaders_lib_content.find(SHADERS_LIB_COMMON_PATTERN)
    if common_index < 0:
        print('Common functions block is not found in ' + file_path)
        exit(1)
    vs_index = shaders_lib_content.find(SHADERS_LIB_VS_PATTERN)
    if vs_index < 0:
        print('Vertex shaders functions block is not found in ' + file_path)
        exit(1)
    fs_index = shaders_lib_content.find(SHADERS_LIB_FS_PATTERN)
    if fs_index < 0:
        print('Vertex shaders functions block is not found in ' + file_path)
        exit(1)
    if not (common_index < vs_index < fs_index):
        print('Order of functions block is incorrect in ' + file_path)
        exit(1)

    shaders_library[SHADERS_LIB_COMMON_INDEX] = shaders_lib_content[common_index:vs_index - 1]
    shaders_library[SHADERS_LIB_VS_INDEX] = shaders_lib_content[vs_index:fs_index - 1]
    shaders_library[SHADERS_LIB_FS_INDEX] = shaders_lib_content[fs_index:]

    return shaders_library


def get_shaders_lib_content(shader_file, shaders_library):
    lib_content = shaders_library[SHADERS_LIB_COMMON_INDEX]
    if shader_file.find(VERTEX_SHADER_EXT) >= 0:
        lib_content += shaders_library[SHADERS_LIB_VS_INDEX]
    elif shader_file.find(FRAG_SHADER_EXT) >= 0:
        lib_content += shaders_library[SHADERS_LIB_FS_INDEX]
    return lib_content


def get_shader_line(line, layout_counters, reflection_dict):
    if line.lstrip().startswith('//') or line == '\n' or len(line) == 0:
        return None

    output_line = line.rstrip()

    if output_line.find('layout (binding') >= 0:
        if output_line.find('sampler') >= 0:
            match = re.search(r"binding\s*=\s*(\d+)", output_line)
            sampler_match = re.search(r"sampler2D(?:Array)?\s+(\w+)", output_line)
            if match and sampler_match:
                binding_index = int(match.group(1))
                sampler_name = sampler_match.group(1)
                if binding_index == 0:
                    print('Binding index must not be 0 for sampler in the line: ' + line)
                    exit(1)
                layout_counters[SAMPLERS][sampler_name] = binding_index
            else:
                print('Sampler name or binding index is not found in the line: ' + line)
                exit(1)
        else:
            match = re.search(r"binding\s*=\s*(\d+)", output_line)
            if match:
                if output_line.find("buffer UBO") >= 0:
                    if IS_STORAGE in reflection_dict and reflection_dict[IS_STORAGE] != True:
                        print('You cannot mix uniform and storage buffer in one program')
                        exit(1)
                    reflection_dict[IS_STORAGE] = True
                else:
                    if IS_STORAGE in reflection_dict and reflection_dict[IS_STORAGE] != False:
                        print('You cannot mix uniform and storage buffer in one program')
                        exit(1)
                    reflection_dict[IS_STORAGE] = False
                binding_index = int(match.group(1))
                if binding_index != 0:
                    print('Binding index must be 0 in the line: ' + line)
                    exit(1)
            else:
                print('Binding index is not found in the line: ' + line)
                exit(1)
            layout_counters[UNIFORMS] += 1

    return output_line


def generate_spirv_compatible_glsl_shader(output_file, shader_file, shader_dir, shaders_library,
                                          layout_counters, reflection_dict):
    output_file.write('#version 310 es\n')
    output_file.write('precision highp float;\n')
    output_file.write('#define LOW_P lowp\n')
    output_file.write('#define MEDIUM_P mediump\n')
    output_file.write('#define HIGH_P highp\n')
    output_file.write('#define VULKAN_MODE\n')
    is_fragment_shader = shader_file.find(FRAG_SHADER_EXT) >= 0
    lib_content = get_shaders_lib_content(shader_file, shaders_library)
    conditional_started = False
    conditional_skip = False
    for line in open(os.path.join(shader_dir, shader_file)):
        # Remove some useless conditional compilation.
        if conditional_started and line.lstrip().startswith('#else'):
            conditional_skip = True
            continue
        if conditional_started and line.lstrip().startswith('#endif'):
            conditional_skip = False
            conditional_started = False
            continue
        if conditional_skip:
            continue
        if line.lstrip().startswith('#ifdef ENABLE_VTF'):
            conditional_started = True
            continue

        if line.lstrip().startswith('void main'):
            # Write reflection for uniforms block.
            uniforms_index = 'vs_uni';
            if is_fragment_shader:
                uniforms_index = 'fs_uni'
            if layout_counters[UNIFORMS] > 0:
                reflection_dict[uniforms_index] = 0
            else:
                reflection_dict[uniforms_index] = -1

            # Write reflection for samplers.
            sample_index = 'tex'
            if not sample_index in reflection_dict:
                reflection_dict[sample_index] = []
            for (s, idx) in layout_counters[SAMPLERS].items():
                sampler = {'name': s, 'idx': idx, 'frag':int(is_fragment_shader)}
                reflection_dict[sample_index].append(sampler)

            # Write shaders library.
            for lib_line in lib_content.splitlines():
                shader_line = get_shader_line(lib_line, layout_counters, reflection_dict)
                if shader_line:
                    output_file.write('%s\n' % shader_line)

        shader_line = get_shader_line(line, layout_counters, reflection_dict)
        if shader_line:
            output_file.write('%s\n' % shader_line)

    layout_counters[UNIFORMS] = 0
    layout_counters[SAMPLERS] = dict()


# Execute external program.
def execute_external(options):
    p = Popen(options, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    output, err = p.communicate()
    rc = p.returncode
    if rc != 0:
        for line in err.split(b'\n'):
            print(line.decode('utf-8'))


# Generate SPIR-V shader from GLSL source.
def generate_shader(shader, shader_dir, generation_dir, shaders_library, program_name,
                    layout_counters, output_name, reflection_dict, glslc_path):
    output_path = os.path.join(generation_dir, output_name)
    with open(output_path, 'w') as file:
        generate_spirv_compatible_glsl_shader(file, shader, shader_dir, shaders_library,
                                              layout_counters, reflection_dict)
    spv_path = output_path + '.spv'
    try:
        execute_external([glslc_path, '-c', output_path, '-o', spv_path, '-std=310es', '--target-env=vulkan'])
        if debug_output:
            debug_dir = os.path.join(generation_dir, 'debug', program_name)
            debug_path = os.path.join(debug_dir, output_name)
            if not os.path.exists(debug_dir):
                os.makedirs(debug_dir)
            execute_external([glslc_path, '-S', output_path, '-o', debug_path + '.spv.txt', '-std=310es','--target-env=vulkan'])
            os.rename(output_path, debug_path)
        else:
            os.remove(output_path)
    except:
        print('Could not generate SPIR-V for the shader {0}. Most likely glslc from Android NDK is not found.'.format(shader))
        os.remove(output_path)
        exit(1)
    return spv_path


def write_shader_to_pack(pack_file, shader_file_name):
    offset = pack_file.tell()
    with open(shader_file_name, 'rb') as shader_file:
        pack_file.write(shader_file.read())
    os.remove(shader_file_name)
    return offset, pack_file.tell() - offset


if __name__ == '__main__':
    if len(sys.argv) < 7:
        print('Usage : ' + sys.argv[0] + ' <shader_dir> <index_file> <shaders_lib> <generation_dir> <glslc_path> [--debug]')
        exit(1)

    shader_dir = sys.argv[1]
    index_file_name = sys.argv[2]
    shaders_lib_file = sys.argv[3]
    generation_dir = sys.argv[4]
    glslc_path = sys.argv[5]

    if len(sys.argv) >= 7:
        debug_output = (sys.argv[6] == '--debug')

    shaders = [file for file in os.listdir(shader_dir) if
               os.path.isfile(os.path.join(shader_dir, file)) and (
                   file.endswith(VERTEX_SHADER_EXT) or file.endswith(FRAG_SHADER_EXT))]

    gpu_programs_cache = read_index_file(os.path.join(shader_dir, index_file_name))
    shaders_library = read_shaders_lib_file(os.path.join(shader_dir, shaders_lib_file))
    reflection = []
    current_offset = 0
    with open(os.path.join(generation_dir, 'shaders_pack.spv'), 'wb') as pack_file:
        for (k, v) in gpu_programs_cache.items():
            layout_counters = {UNIFORMS: 0, SAMPLERS: dict()}
            reflection_dict = {'prg': v[2], 'info': dict()}
            vs_offset = write_shader_to_pack(pack_file, generate_shader(v[0], shader_dir, generation_dir,
                                                                        shaders_library, k,
                                                                        layout_counters, k + VERTEX_SHADER_EXT_OUT,
                                                                        reflection_dict['info'], glslc_path))
            reflection_dict['vs_off'] = vs_offset[0]
            reflection_dict['vs_size'] = vs_offset[1]
            fs_offset = write_shader_to_pack(pack_file, generate_shader(v[1], shader_dir, generation_dir,
                                                                        shaders_library, k,
                                                                        layout_counters, k + FRAG_SHADER_EXT_OUT,
                                                                        reflection_dict['info'], glslc_path))
            reflection_dict['fs_off'] = fs_offset[0]
            reflection_dict['fs_size'] = fs_offset[1]
            reflection.append(reflection_dict)
    with open(os.path.join(generation_dir, 'reflection.json'), 'w') as reflection_file:
        reflection_file.write(json.dumps(reflection, separators=(',',':')))
