#!/usr/bin/python3
# Copyright 2024 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_wgpu_format_table.py:
#  Code generation for wgpu format map. See wgpu_format_map.json for data source.
#  NOTE: don't run this script directly. Run scripts/run_code_generation.py.

import json
import math
import pprint
import os
import re
import sys

sys.path.append('..')
import angle_format

template_table_autogen_cpp = """// GENERATED FILE - DO NOT EDIT.
// Generated by {script_name} using data from {input_file_name}
//
// Copyright 2024 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}:
//   Queries for full WebGPU format information based on GL format.

#include "libANGLE/renderer/wgpu/wgpu_format_utils.h"

#include "image_util/loadimage.h"

using namespace angle;

namespace rx
{{
namespace webgpu
{{

void Format::initialize(const angle::Format &angleFormat)
{{
    switch (angleFormat.id)
    {{
{format_case_data}
        default:
            UNREACHABLE();
            break;
    }}
}}

WGPUTextureFormat GetWgpuTextureFormatFromFormatID(angle::FormatID formatID)
{{
    static constexpr angle::FormatMap<WGPUTextureFormat> kMap = {{
{image_format_id_cases}
    }};

    return kMap[formatID];
}}

angle::FormatID GetFormatIDFromWgpuTextureFormat(WGPUTextureFormat wgpuFormat)
{{
    switch (wgpuFormat)
    {{
{wgpu_image_format_cases}
        default:
            UNREACHABLE();
            return angle::FormatID::NONE;
    }}
}}

WGPUVertexFormat GetWgpuVertexFormatFromFormatID(angle::FormatID formatID)
{{
    static constexpr angle::FormatMap<WGPUVertexFormat> kMap = {{
{buffer_format_id_cases}
    }};

    return kMap[formatID];
}}

angle::FormatID GetFormatIDFromWgpuBufferFormat(WGPUVertexFormat wgpuFormat)
{{
    switch (wgpuFormat)
    {{
{wgpu_buffer_format_cases}
        default:
            UNREACHABLE();
            return angle::FormatID::NONE;
    }}
}}
}}  // namespace webgpu
}}  // namespace rx
"""

empty_format_entry_template = """case angle::FormatID::{format_id}:
// This format is not implemented in WebGPU.
break;
"""

format_entry_template = """case angle::FormatID::{format_id}:
mIntendedGLFormat = {internal_format};
{image_template}
{buffer_template}
break;
"""

image_snorm_template = """mActualImageFormatID = {image};
mImageInitializerFunction = {image_initializer};
mIsRenderable = false;"""

image_basic_template = """mActualImageFormatID = {image};
mImageInitializerFunction = {image_initializer};
mIsRenderable = true;"""

image_struct_template = "{{{image}, {image_initializer}}}"

image_fallback_template = """{{
static constexpr ImageFormatInitInfo kInfo[] = {{{image_list}}};
initImageFallback(kInfo, ArraySize(kInfo));
}}"""

buffer_basic_template = """mActualBufferFormatID = {buffer};
mVertexLoadFunction = {vertex_load_function};
mVertexLoadRequiresConversion = {vertex_load_converts};"""

buffer_struct_template = """{{{buffer}, {vertex_load_function}, {vertex_load_converts}}}"""

buffer_fallback_template = """{{
static constexpr BufferFormatInitInfo kInfo[] = {{{buffer_list}}};
initBufferFallback(kInfo, ArraySize(kInfo));
}}"""


def verify_wgpu_image_map_keys(angle_to_gl, wgpu_json_data):
    """Verify that the keys in WebGPU format tables exist in the ANGLE table.  If they don't, the
    entry in the WebGPU file is incorrect and needs to be fixed."""

    no_error = True
    for table in ["image_map", "buffer_map", "fallbacks"]:
        for angle_format in wgpu_json_data[table].keys():
            if not angle_format in angle_to_gl.keys():
                print("Invalid format " + angle_format + " in wgpu_format_map.json in " + table)
                no_error = False

    return no_error


def get_vertex_copy_function(src_format, dst_format, wgpu_format):
    if 'R10G10B10A2' in src_format:
        # When the R10G10B10A2 type can't be used by the vertex buffer,
        # it needs to be converted to the type which can be used by it.
        is_signed = 'false' if 'UINT' in src_format or 'UNORM' in src_format or 'USCALED' in src_format else 'true'
        normalized = 'true' if 'NORM' in src_format else 'false'
        to_float = 'false' if 'INT' in src_format else 'true'
        to_half = to_float
        return 'CopyXYZ10W2ToXYZWFloatVertexData<%s, %s, %s, %s>' % (is_signed, normalized,
                                                                     to_float, to_half)
    return angle_format.get_vertex_copy_function(src_format, dst_format)


def gen_format_case(angle, internal_format, wgpu_json_data):
    wgpu_image_map = wgpu_json_data["image_map"]
    wgpu_buffer_map = wgpu_json_data["buffer_map"]
    wgpu_fallbacks = wgpu_json_data["fallbacks"]
    args = dict(
        format_id=angle, internal_format=internal_format, image_template="", buffer_template="")

    if ((angle not in wgpu_image_map) and (angle not in wgpu_buffer_map) and
        (angle not in wgpu_fallbacks)):
        return empty_format_entry_template.format(**args)

    # get_formats returns override format (if any) + fallbacks
    # this was necessary to support D32_UNORM. There is no appropriate override that allows
    # us to fallback to D32_FLOAT, so now we leave the image override empty and function will
    # give us the fallbacks.
    def get_formats(format, type):
        fallbacks = wgpu_fallbacks.get(format, {}).get(type, [])
        if not isinstance(fallbacks, list):
            fallbacks = [fallbacks]

        if (format in wgpu_image_map and type == "image") or (format in wgpu_buffer_map and
                                                              type == "buffer"):
            assert format not in fallbacks
            fallbacks = [format] + fallbacks

        return fallbacks

    def image_args(format):
        return dict(
            image="angle::FormatID::" + format,
            image_initializer=angle_format.get_internal_format_initializer(
                internal_format, format))

    def buffer_args(format):
        wgpu_buffer_format = wgpu_buffer_map[format]
        return dict(
            buffer="angle::FormatID::" + format,
            vertex_load_function=get_vertex_copy_function(angle, format, wgpu_buffer_format),
            vertex_load_converts='false' if angle == format else 'true',
        )

    images = get_formats(angle, "image")
    if len(images) == 1:
        if 'SNORM' in angle:
            args.update(image_template=image_snorm_template)
        else:
            args.update(image_template=image_basic_template)
        args.update(image_args(images[0]))
    elif len(images) > 1:
        args.update(
            image_template=image_fallback_template,
            image_list=", ".join(image_struct_template.format(**image_args(i)) for i in images))

    buffers = get_formats(angle, "buffer")
    if len(buffers) == 1 and buffers[0] in wgpu_buffer_map:
        args.update(buffer_template=buffer_basic_template)
        args.update(buffer_args(buffers[0]))
    elif len(buffers) > 1:
        args.update(
            buffer_template=buffer_fallback_template,
            buffer_list=", ".join(
                buffer_struct_template.format(**buffer_args(i)) for i in buffers))

    return format_entry_template.format(**args).format(**args)


def get_format_id_case(format_id, format_type, wgpu_format):
    # WGPUVertexFormat_Undefined was replaced with WGPUVertexFormat(0u)
    # in https://dawn-review.googlesource.com/c/dawn/+/193360
    if 'Undefined' in wgpu_format and 'VertexFormat' in format_type:
        return "{angle::FormatID::%s, WGPU%s(0u)}" % (format_id, format_type)
    return "{angle::FormatID::%s, WGPU%s_%s}" % (format_id, format_type, wgpu_format)


def get_wgpu_format_case(format_type, format_id, wgpu_format):
    # WGPUVertexFormat_Undefined was replaced with WGPUVertexFormat(0u)
    # in https://dawn-review.googlesource.com/c/dawn/+/193360
    # so there is no 'case' needed for it.
    if 'Undefined' in wgpu_format and 'VertexFormat' in format_type:
        return ''
    # don't generate the reverse mapping for the external format slots because they _all_ map
    # to WGPU_FORMAT_UNDEFINED and so clash with NONE
    if 'EXTERNAL' in format_id:
        return ''
    return """\
        case WGPU%s_%s:
            return angle::FormatID::%s;
""" % (format_type, wgpu_format, format_id)


def main():

    input_file_name = 'wgpu_format_map.json'
    out_file_name = 'wgpu_format_table_autogen.cpp'

    # auto_script parameters.
    if len(sys.argv) > 1:
        inputs = ['../angle_format.py', '../angle_format_map.json', input_file_name]
        outputs = [out_file_name]

        if sys.argv[1] == 'inputs':
            print(','.join(inputs))
        elif sys.argv[1] == 'outputs':
            print(','.join(outputs))
        else:
            print('Invalid script parameters')
            return 1
        return 0

    angle_to_gl = angle_format.load_inverse_table(os.path.join('..', 'angle_format_map.json'))
    wgpu_json_data = angle_format.load_json(input_file_name)

    if not verify_wgpu_image_map_keys(angle_to_gl, wgpu_json_data):
        return 1

    image_format_id_cases = [
        get_format_id_case(format_id, "TextureFormat", wgpu_format)
        for format_id, wgpu_format in sorted(wgpu_json_data["image_map"].items())
    ]

    wgpu_image_format_cases = [
        get_wgpu_format_case("TextureFormat", format_id, wgpu_format)
        for format_id, wgpu_format in sorted(wgpu_json_data["image_map"].items())
    ]

    buffer_format_id_cases = [
        get_format_id_case(format_id, "VertexFormat", wgpu_format)
        for format_id, wgpu_format in sorted(wgpu_json_data["buffer_map"].items())
    ]

    wgpu_buffer_format_cases = [
        get_wgpu_format_case("VertexFormat", format_id, wgpu_format)
        for format_id, wgpu_format in sorted(wgpu_json_data["buffer_map"].items())
    ]

    wgpu_cases = [
        gen_format_case(angle, gl, wgpu_json_data) for angle, gl in sorted(angle_to_gl.items())
    ]

    output_cpp = template_table_autogen_cpp.format(
        format_case_data="\n".join(wgpu_cases),
        image_format_id_cases=",\n".join(image_format_id_cases),
        wgpu_image_format_cases="".join(wgpu_image_format_cases),
        buffer_format_id_cases=",\n".join(buffer_format_id_cases),
        wgpu_buffer_format_cases="".join(wgpu_buffer_format_cases),
        script_name=os.path.basename(__file__),
        out_file_name=out_file_name,
        input_file_name=input_file_name)

    with open(out_file_name, 'wt') as out_file:
        out_file.write(output_cpp)
        out_file.close()
    return 0


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