# Copyright © 2020 Hoe Hao Cheng
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# 

import re
from xml.etree import ElementTree
from typing import List,Tuple

class Version:
    device_version = (1,0,0)
    struct_version = (1,0)

    def __init__(self, version, struct=()):
        self.device_version = version

        if not struct:
            self.struct_version = (version[0], version[1])
        else:
            self.struct_version = struct

    # e.g. "VK_MAKE_VERSION(1,2,0)"
    def version(self):
        return ("VK_MAKE_VERSION("
               + str(self.device_version[0])
               + ","
               + str(self.device_version[1])
               + ","
               + str(self.device_version[2])
               + ")")

    # e.g. "10"
    def struct(self):
        return (str(self.struct_version[0])+str(self.struct_version[1]))

    # the sType of the extension's struct
    # e.g. VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT
    # for VK_EXT_transform_feedback and struct="FEATURES"
    def stype(self, struct: str):
        return ("VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_"
                + str(self.struct_version[0]) + "_" + str(self.struct_version[1])
                + '_' + struct)

class Extension:
    name           = None
    alias          = None
    is_required    = False
    is_nonstandard = False
    enable_conds   = None
    core_since     = None

    # these are specific to zink_device_info.py:
    has_properties      = False
    has_features        = False
    guard               = False
    features_promoted   = False
    properties_promoted = False
    

    # these are specific to zink_instance.py:
    platform_guard = None

    def __init__(self, name, alias="", required=False, nonstandard=False,
                 properties=False, features=False, conditions=None, guard=False):
        self.name = name
        self.alias = alias
        self.is_required = required
        self.is_nonstandard = nonstandard
        self.has_properties = properties
        self.has_features = features
        self.enable_conds = conditions
        self.guard = guard

        if alias == "" and (properties == True or features == True):
            raise RuntimeError("alias must be available when properties and/or features are used")

    # e.g.: "VK_EXT_robustness2" -> "robustness2"
    def pure_name(self):
        return '_'.join(self.name.split('_')[2:])
    
    # e.g.: "VK_EXT_robustness2" -> "EXT_robustness2"
    def name_with_vendor(self):
        return self.name[3:]
    
    # e.g.: "VK_EXT_robustness2" -> "Robustness2"
    def name_in_camel_case(self):
        return "".join([x.title() for x in self.name.split('_')[2:]])

    # e.g.: "VK_EXT_robustness2" -> "VK_EXT_ROBUSTNESS_2"
    def name_in_snake_uppercase(self):
        def replace(original):
            # we do not split the types into two, e.g. INT_32
            match_types = re.match(".*(int|float)(8|16|32|64)$", original)

            # do not match win32
            match_os = re.match(".*win32$", original)

            # try to match extensions with alphanumeric names, like robustness2
            match_alphanumeric = re.match("([a-z]+)(\d+)", original)

            if match_types is not None or match_os is not None:
                return original.upper()

            if match_alphanumeric is not None:
                return (match_alphanumeric[1].upper()
                        + '_' 
                        + match_alphanumeric[2])

            return original.upper()

        replaced = list(map(replace, self.name.split('_')))
        return '_'.join(replaced)

    # e.g.: "VK_EXT_robustness2" -> "ROBUSTNESS_2"
    def pure_name_in_snake_uppercase(self):
        return '_'.join(self.name_in_snake_uppercase().split('_')[2:])

    # e.g.: "VK_EXT_robustness2" -> "VK_EXT_ROBUSTNESS_2_EXTENSION_NAME"
    def extension_name(self):
        return self.name_in_snake_uppercase() + "_EXTENSION_NAME"

    # generate a C string literal for the extension
    def extension_name_literal(self):
        return '"' + self.name + '"'

    # get the field in zink_device_info that refers to the extension's
    # feature/properties struct
    # e.g. rb2_<suffix> for VK_EXT_robustness2
    def field(self, suffix: str):
        return self.alias + '_' + suffix

    def physical_device_struct(self, struct: str):
        if self.name_in_camel_case().endswith(struct):
            struct = ""

        return ("VkPhysicalDevice"
                + self.name_in_camel_case()
                + struct
                + self.vendor())

    # the sType of the extension's struct
    # e.g. VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT
    # for VK_EXT_transform_feedback and struct="FEATURES"
    def stype(self, struct: str):
        return ("VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_" 
                + self.pure_name_in_snake_uppercase()
                + '_' + struct + '_' 
                + self.vendor())

    # e.g. EXT in VK_EXT_robustness2
    def vendor(self):
        return self.name.split('_')[1]

# Type aliases
Layer = Extension

class ExtensionRegistryEntry:
    # type of extension - right now it's either "instance" or "device"
    ext_type          = ""
    # the version in which the extension is promoted to core VK
    promoted_in       = None
    # functions added by the extension are referred to as "commands" in the registry
    device_commands   = None
    pdevice_commands  = None
    instance_commands = None
    constants         = None
    features_struct   = None
    features_fields   = None
    features_promoted = False
    properties_struct = None
    properties_fields = None
    properties_promoted = False
    # some instance extensions are locked behind certain platforms
    platform_guard    = ""

class ExtensionRegistry:
    # key = extension name, value = registry entry
    registry = dict()

    def __init__(self, vkxml_path: str):
        vkxml = ElementTree.parse(vkxml_path)

        commands_type = dict()
        command_aliases = dict()
        platform_guards = dict()
        struct_aliases = dict()

        for cmd in vkxml.findall("commands/command"):
            name = cmd.find("./proto/name")

            if name is not None and name.text:
                commands_type[name.text] = cmd.find("./param/type").text
            elif cmd.get("name") is not None:
                command_aliases[cmd.get("name")] = cmd.get("alias")

        for typ in vkxml.findall("types/type"):
            if typ.get("category") != "struct":
                continue

            name = typ.get("name")
            alias = typ.get("alias")

            if name and alias:
                struct_aliases[name] = alias

        for (cmd, alias) in command_aliases.items():
            commands_type[cmd] = commands_type[alias]

        for platform in vkxml.findall("platforms/platform"):
            name = platform.get("name")
            guard = platform.get("protect")
            platform_guards[name] = guard

        for ext in vkxml.findall("extensions/extension"):
            # Reserved extensions are marked with `supported="disabled"`
            if ext.get("supported") == "disabled":
                continue

            name = ext.attrib["name"]

            entry = ExtensionRegistryEntry()
            entry.ext_type = ext.attrib["type"]
            entry.promoted_in = self.parse_promotedto(ext.get("promotedto"))

            entry.device_commands = []
            entry.pdevice_commands = []
            entry.instance_commands = []
            entry.features_fields = []
            entry.properties_fields = []

            for cmd in ext.findall("require/command"):
                cmd_name = cmd.get("name")
                if cmd_name:
                    if commands_type[cmd_name] in ("VkDevice", "VkCommandBuffer", "VkQueue"):
                        entry.device_commands.append(cmd_name)
                    elif commands_type[cmd_name] in ("VkPhysicalDevice"):
                        entry.pdevice_commands.append(cmd_name)
                    else:
                        entry.instance_commands.append(cmd_name)

            entry.constants = []
            for enum in ext.findall("require/enum"):
                enum_name = enum.get("name")
                enum_extends = enum.get("extends")
                # we are only interested in VK_*_EXTENSION_NAME, which does not
                # have an "extends" attribute
                if not enum_extends:
                    entry.constants.append(enum_name)

            for ty in ext.findall("require/type"):
                ty_name = ty.get("name")
                if (self.is_features_struct(ty_name) and
                    entry.features_struct is None):
                    entry.features_struct = ty_name
                    
                elif (self.is_properties_struct(ty_name) and
                      entry.properties_struct is None):
                    entry.properties_struct = ty_name

            if entry.features_struct:
                struct_name = entry.features_struct
                if entry.features_struct in struct_aliases:
                    struct_name = struct_aliases[entry.features_struct]
                    entry.features_promoted = True

                elif entry.promoted_in is not None:
                    # if the extension is promoted but a core-Vulkan alias is not
                    # available for the features, then consider the features struct
                    # non-core-promoted
                    entry.features_promoted = False

                for field in vkxml.findall("./types/type[@name='{}']/member".format(struct_name)):
                    field_name = field.find("name").text
                    
                    # we ignore sType and pNext since they are irrelevant
                    if field_name not in ["sType", "pNext"]:
                        entry.features_fields.append(field_name)

            if entry.properties_struct:
                struct_name = entry.properties_struct
                if entry.properties_struct in struct_aliases:
                    struct_name = struct_aliases[entry.properties_struct]
                    entry.properties_promoted = True

                elif entry.promoted_in is not None:
                    # if the extension is promoted but a core-Vulkan alias is not
                    # available for the properties, then it is not promoted to core
                    entry.properties_promoted = False
                
                for field in vkxml.findall("./types/type[@name='{}']/member".format(struct_name)):
                    field_name = field.find("name").text

                    # we ignore sType and pNext since they are irrelevant
                    if field_name not in ["sType", "pNext"]:
                        entry.properties_fields.append(field_name)

            if ext.get("platform") is not None:
                entry.platform_guard = platform_guards[ext.get("platform")]

            self.registry[name] = entry

    def in_registry(self, ext_name: str):
        return ext_name in self.registry

    def get_registry_entry(self, ext_name: str):
        if self.in_registry(ext_name):
            return self.registry[ext_name]

    # Parses e.g. "VK_VERSION_x_y" to integer tuple (x, y)
    # For any erroneous inputs, None is returned
    def parse_promotedto(self, promotedto: str):
        result = None

        if promotedto and promotedto.startswith("VK_VERSION_"):
            (major, minor) = promotedto.split('_')[-2:]
            result = (int(major), int(minor))

        return result

    def is_features_struct(self, struct: str):
        return re.match(r"VkPhysicalDevice.*Features.*", struct) is not None

    def is_properties_struct(self, struct: str):
        return re.match(r"VkPhysicalDevice.*Properties.*", struct) is not None
