from __future__ import annotations

import re
from itertools import groupby
from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union

from ..enums import (
    PassType,
    SerializedPropertyType,
    ShaderCompilerPlatform,
    ShaderGpuProgramType,
    TextureDimension,
)
from ..helpers import CompressionHelper
from ..streams import EndianBinaryReader

if TYPE_CHECKING:
    from ..classes import (
        SerializedPass,
        SerializedProperties,
        SerializedProperty,
        SerializedShader,
        SerializedShaderState,
        SerializedSubProgram,
        SerializedSubShader,
        SerializedTagMap,
        Shader,
    )

HEADER = """
//////////////////////////////////////////
//
// NOTE: This is *not* a valid shader file
//
///////////////////////////////////////////
"""[1:]

T = TypeVar("T")


def export_shader(m_Shader: Shader) -> str:
    if m_Shader.m_SubProgramBlob:  # 5.3 - 5.4
        decompressedBytes = CompressionHelper.decompress_lz4(
            bytes(m_Shader.m_SubProgramBlob), m_Shader.decompressedSize
        )

        blobReader = EndianBinaryReader(decompressedBytes)
        program = ShaderProgram(blobReader, m_Shader.object_reader.version)

        return HEADER + program.Export(bytes(m_Shader.m_Script).decode("utf8"))

    if m_Shader.compressedBlob:  # 5.5 and up
        return HEADER + ConvertSerializedShader(m_Shader)

    return HEADER + bytes(m_Shader.m_Script).decode("utf8")


def ConvertSerializedShader(m_Shader: Shader) -> str:
    shaderPrograms = []

    platformNumber = len(m_Shader.platforms)
    compressed_blob = bytes(m_Shader.compressedBlob)

    def get_entry(array: Union[List[T], List[List[T]]], index: int) -> T:
        item = array[index]
        if isinstance(item, List):
            return item[0]
        return item

    for i in range(platformNumber):
        if i >= len(m_Shader.compressedLengths) or i >= len(m_Shader.decompressedLengths):
            # m_Shader.platforms shouldn't be longer than m_shader.[de]compressedLengths, but it is
            break

        compressedSize = get_entry(m_Shader.compressedLengths, i)
        decompressedSize = get_entry(m_Shader.decompressedLengths, i)
        offset = get_entry(m_Shader.offsets, i)

        compressedBytes = compressed_blob[offset : offset + compressedSize]
        decompressedBytes = CompressionHelper.decompress_lz4(compressedBytes, decompressedSize)

        shaderPrograms.append(
            ShaderProgram(
                EndianBinaryReader(decompressedBytes, endian="<"),
                m_Shader.object_reader.version,
            )
        )

    return ConvertSerializedShaderParsedForm(m_Shader.m_ParsedForm, m_Shader.platforms, shaderPrograms)


def ConvertSerializedShaderParsedForm(
    m_ParsedForm: SerializedShader,
    platforms: List[int],
    shaderPrograms: List[ShaderProgram],
) -> str:
    sb: List[str] = [
        'Shader "{0}" {{\n'.format(m_ParsedForm.m_Name),
        ConvertSerializedProperties(m_ParsedForm.m_PropInfo),
        *[
            ConvertSerializedSubShader(m_SubShader, platforms, shaderPrograms)
            for m_SubShader in m_ParsedForm.m_SubShaders
        ],
    ]

    if m_ParsedForm.m_FallbackName:
        sb.append('Fall back "{0}"\n'.format(m_ParsedForm.m_FallbackName))

    if m_ParsedForm.m_CustomEditorName:
        sb.append('CustomEditor "{0}"\n'.format(m_ParsedForm.m_CustomEditorName))

    sb.append("}")
    return "".join(sb)


def ConvertSerializedSubShader(
    m_SubShader: SerializedSubShader,
    platforms: List[int],
    shaderPrograms: List[ShaderProgram],
) -> str:
    sb = ["SubShader {\n"]

    if m_SubShader.m_LOD != 0:
        sb.append(" LOD {0}\n".format(m_SubShader.m_LOD))

    sb.append(ConvertSerializedTagMap(m_SubShader.m_Tags, 1))

    sb.extend(ConvertSerializedPass(m_Passe, platforms, shaderPrograms) for m_Passe in m_SubShader.m_Passes)
    sb.append("}\n")
    return "".join(sb)


def ConvertSerializedPass(m_Passe: SerializedPass, platforms: List[int], shaderPrograms: List[ShaderProgram]) -> str:
    sb = []
    if m_Passe.m_Type == PassType.kPassTypeNormal:
        sb.append(" Pass ")
    elif m_Passe.m_Type == PassType.kPassTypeUse:
        sb.append(" UsePass ")
    elif m_Passe.m_Type == PassType.kPassTypeGrab:
        sb.append(" GrabPass ")

    if m_Passe.m_Type == PassType.kPassTypeUse:
        sb.append('"{0}"\n'.format(m_Passe.m_UseName))
    else:
        sb.append("{\n")

        if m_Passe.m_Type == PassType.kPassTypeGrab:
            if m_Passe.m_TextureName:
                sb.append(' "{0}"\n'.format(m_Passe.m_TextureName))

        else:
            sb.append(ConvertSerializedShaderState(m_Passe.m_State))

            if len(m_Passe.progVertex.m_SubPrograms) > 0:
                sb.append('Program "vp" {\n')
                sb.append(ConvertSerializedSubPrograms(m_Passe.progVertex.m_SubPrograms, platforms, shaderPrograms))
                sb.append("}\n")

            if len(m_Passe.progFragment.m_SubPrograms) > 0:
                sb.append('Program "fp" {\n')
                sb.append(ConvertSerializedSubPrograms(m_Passe.progFragment.m_SubPrograms, platforms, shaderPrograms))
                sb.append("}\n")

            if len(m_Passe.progGeometry.m_SubPrograms) > 0:
                sb.append('Program "gp" {\n')
                sb.append(ConvertSerializedSubPrograms(m_Passe.progGeometry.m_SubPrograms, platforms, shaderPrograms))
                sb.append("}\n")

            if len(m_Passe.progHull.m_SubPrograms) > 0:
                sb.append('Program "hp" {\n')
                sb.append(ConvertSerializedSubPrograms(m_Passe.progHull.m_SubPrograms, platforms, shaderPrograms))
                sb.append("}\n")

            if len(m_Passe.progDomain.m_SubPrograms) > 0:
                sb.append('Program "dp" {\n')
                sb.append(ConvertSerializedSubPrograms(m_Passe.progDomain.m_SubPrograms, platforms, shaderPrograms))
                sb.append("}\n")

        sb.append("}\n")

    return "".join(sb)


def ConvertSerializedSubPrograms(
    m_SubPrograms: List[SerializedSubProgram],
    platforms: List[int],
    shaderPrograms: List[ShaderProgram],
) -> str:
    sb = []

    def indexFunc(x: SerializedSubProgram):
        return x.m_BlobIndex

    def typeFunc(x: SerializedSubProgram):
        return x.m_GpuProgramType

    groups = groupby(sorted(m_SubPrograms, key=indexFunc), indexFunc)

    for _, _group in groups:
        group = list(_group)
        programs = groupby(sorted(group, key=typeFunc), typeFunc)

        for programKey, _programList in programs:
            subPrograms = list(_programList)
            isTier = len(subPrograms) > 1
            for i in range(len(platforms)):
                if i >= len(shaderPrograms):
                    # platforms shouldn't be longer than shaderPrograms, but it is
                    break

                platform = platforms[i]

                if CheckGpuProgramUsable(platform, programKey):
                    for subProgram in subPrograms:
                        sb.append('SubProgram "{0} '.format(GetPlatformString(platform)))

                        if isTier:
                            sb.append("hw_tier{0:02} ".format(subProgram.m_ShaderHardwareTier))

                        sb.append('" {\n')
                        sb.append(shaderPrograms[i].m_SubPrograms[subProgram.m_BlobIndex].Export())

                        sb.append("\n}\n")

                    break

    return "".join(sb)


def ConvertSerializedShaderState(m_State: SerializedShaderState) -> str:
    sb = []
    if m_State.m_Name:
        sb.append(' Name "{0}"\n'.format(m_State.m_Name))

    if m_State.m_LOD != 0:
        sb.append("  LOD {0}\n".format(m_State.m_LOD))

    sb.append(ConvertSerializedTagMap(m_State.m_Tags, 2))

    # sb.append(ConvertSerializedShaderRTBlendState(m_State.rtBlend))

    if m_State.alphaToMask.val > 0:
        sb.append(" AlphaToMask On\n")

    if m_State.zClip and m_State.zClip.val != 1:
        sb.append(" ZClip Off\n")

    if m_State.zTest.val != 4:
        sb.append(" ZTest ")
        if m_State.zTest.val == 0:  # kFuncDisabled
            sb.append("Off")
        elif m_State.zTest.val == 1:  # kFuncNever
            sb.append("Never")
        elif m_State.zTest.val == 2:  # kFuncLess
            sb.append("Less")
        elif m_State.zTest.val == 3:  # kFuncEqual
            sb.append("Equal")
        elif m_State.zTest.val == 5:  # kFuncGreater
            sb.append("Greater")
        elif m_State.zTest.val == 6:  # kFuncNotEqual
            sb.append("NotEqual")
        elif m_State.zTest.val == 7:  # kFuncGEqual
            sb.append("GEqual")
        elif m_State.zTest.val == 8:  # kFuncAlways
            sb.append("Always")

        sb.append("\n")

    if m_State.zWrite.val != 1:  # ZWrite On
        sb.append(" ZWrite Off\n")

    if m_State.culling.val != 2:  # Cull Back
        sb.append(" Cull ")
        if m_State.culling.val == 0:  # kCullOff
            sb.append("Off")
        elif m_State.culling.val == 1:  # kCullFront
            sb.append("Front")

        sb.append("\n")

    if m_State.offsetFactor.val != 0 or m_State.offsetUnits.val != 0:
        sb.append(" Offset {0}, {1}\n".format(m_State.offsetFactor.val, m_State.offsetUnits.val))

    # TODO Stencil

    # TODO Fog

    if m_State.lighting:
        sb.append("  Lighting {0}\n".format(m_State.lighting and "On" or "Off"))

    sb.append("  GpuProgramID {0}\n".format(m_State.gpuProgramID))
    return "".join(sb)


def ConvertSerializedShaderRTBlendState(rbBlend):
    # TODO Blend
    sb = []
    return "".join(sb)


def ConvertSerializedTagMap(m_Tags: SerializedTagMap, intent: int) -> str:
    if m_Tags.tags:
        return "".join(
            [
                " " * intent,
                "Tags { ",
                *[f'"{key}" = "{value}" ' for key, value in m_Tags.tags],
                "}\n",
            ]
        )
    return ""


def ConvertSerializedProperties(m_PropInfo: SerializedProperties) -> str:
    return "\n".join(
        [
            "Properties {\n",
            *[ConvertSerializedProperty(m_Prop) for m_Prop in m_PropInfo.m_Props],
            "}\n",
        ]
    )


def ConvertSerializedProperty(m_Prop: SerializedProperty) -> str:
    sb = ["[{0}] ".format(m_Attribute) for m_Attribute in m_Prop.m_Attributes]
    sb.append('{0} ("{1}", '.format(m_Prop.m_Name, m_Prop.m_Description))

    if m_Prop.m_Type == SerializedPropertyType.kColor:
        sb.append("Color")
    elif m_Prop.m_Type == SerializedPropertyType.kVector:
        sb.append("Vector")
    elif m_Prop.m_Type == SerializedPropertyType.kFloat:
        sb.append("Float")
    elif m_Prop.m_Type == SerializedPropertyType.kRange:
        sb.append("Range({0:g}, {1:g})".format(m_Prop.m_DefValue_1_, m_Prop.m_DefValue_2_))
    elif m_Prop.m_Type == SerializedPropertyType.kTexture:
        if m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimAny:
            sb.append("any")
        elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim2D:
            sb.append("2D")
        elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim3D:
            sb.append("3D")
        elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimCUBE:
            sb.append("Cube")
        elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim2DArray:
            sb.append("2DArray")
        elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimCubeArray:
            sb.append("CubeArray")

    sb.append(") = ")

    if m_Prop.m_Type in [SerializedPropertyType.kColor, SerializedPropertyType.kVector]:
        sb.append(
            "({0:g},{1:g},{2:g},{3:g})".format(
                m_Prop.m_DefValue_0_,
                m_Prop.m_DefValue_1_,
                m_Prop.m_DefValue_2_,
                m_Prop.m_DefValue_3_,
            )
        )
    elif m_Prop.m_Type in [
        SerializedPropertyType.kFloat,
        SerializedPropertyType.kRange,
    ]:
        sb.append(m_Prop.m_DefValue_0_)
    elif m_Prop.m_Type == SerializedPropertyType.kTexture:
        sb.append('"{0}" {{ }}'.format(m_Prop.m_DefTexture.m_DefaultName))
    else:
        raise ValueError(m_Prop.m_Type)

    sb.append("\n")
    return "".join(map(str, sb))


def CheckGpuProgramUsable(platform: int, programType: int) -> bool:
    if platform == ShaderCompilerPlatform.kShaderCompPlatformGL:
        return programType == ShaderGpuProgramType.kShaderGpuProgramGLLegacy
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D9:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM20
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM30
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM20
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM30
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXbox360:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM40
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM50
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM40
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM50
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM40
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM50
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11HullSM50
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX11DomainSM50
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES20:
        return programType == ShaderGpuProgramType.kShaderGpuProgramGLES
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformNaCl:
        raise NotImplementedError
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformFlash:
        raise NotImplementedError
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11_9x:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramDX10Level9Vertex
            or programType == ShaderGpuProgramType.kShaderGpuProgramDX10Level9Pixel
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES3Plus:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramGLES31AEP
            or programType == ShaderGpuProgramType.kShaderGpuProgramGLES31
            or programType == ShaderGpuProgramType.kShaderGpuProgramGLES3
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSP2:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS4:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOne:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSM:
        raise NotImplementedError
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformMetal:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramMetalVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramMetalVS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformOpenGLCore:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramGLCore32
            or programType == ShaderGpuProgramType.kShaderGpuProgramGLCore41
            or programType == ShaderGpuProgramType.kShaderGpuProgramGLCore43
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformN3DS:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformWiiU:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformVulkan:
        return programType == ShaderGpuProgramType.kShaderGpuProgramSPIRV
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformSwitch:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOneD3D12:
        return (
            programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS
            or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS
        )
    else:
        raise NotImplementedError


def GetPlatformString(platform: int):
    if platform == ShaderCompilerPlatform.kShaderCompPlatformGL:
        return "openGL"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D9:
        return "d3d9"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXbox360:
        return "xbox360"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS3:
        return "ps3"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11:
        return "d3d11"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES20:
        return "gles"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformNaCl:
        return "glesdesktop"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformFlash:
        return "flash"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11_9x:
        return "d3d11_9x"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES3Plus:
        return "gles3"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSP2:
        return "psp2"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS4:
        return "ps4"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOne:
        return "xboxone"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSM:
        return "psm"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformMetal:
        return "metal"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformOpenGLCore:
        return "glcore"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformN3DS:
        return "n3ds"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformWiiU:
        return "wiiu"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformVulkan:
        return "vulkan"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformSwitch:
        return "switch"
    elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOneD3D12:
        return "xboxone+_d3d12"
    else:
        return "unknown"


class ShaderProgram:
    m_SubPrograms: List[ShaderSubProgram]

    def __init__(self, reader: EndianBinaryReader, version: Tuple[int, int, int, int]):
        subProgramCapacity = reader.read_int()
        self.m_SubPrograms = [None] * subProgramCapacity

        if version >= (2019, 3):  # 2019.3 and up
            entrySize = 12
        else:
            entrySize = 8

        for i in range(subProgramCapacity):
            reader.Position = 4 + i * entrySize
            offset = reader.read_int()
            reader.Position = offset
            self.m_SubPrograms[i] = ShaderSubProgram(reader)

    def Export(self, shader: str) -> str:
        shader = re.sub(
            r"GpuProgramIndex (.+)",
            lambda math: self.m_SubPrograms[int(math.group(1))].Export(),
            shader,
        )

        return shader


class ShaderSubProgram:
    m_Version: int
    m_Keywords: List[str]
    m_ProgramCode: bytes
    m_LocalKeywords: Optional[List[str]]

    def __init__(self, reader: EndianBinaryReader):
        # LoadGpuProgramFromData
        # 201509030 - Unity 5.3
        # 201510240 - Unity 5.4
        # 201608170 - Unity 5.5
        # 201609010 - Unity 5.6, 2017.1 & 2017.2
        # 201708220 - Unity 2017.3, Unity 2017.4 & Unity 2018.1
        # 201802150 - Unity 2018.2 & Unity 2018.3
        # 201806140 - Unity 2019.1~2020.1
        # 202012090 - Unity 2021.2

        self.m_Version = reader.read_int()
        self.m_ProgramType = ShaderGpuProgramType(reader.read_int())
        reader.Position += 12

        if self.m_Version >= 201608170:
            reader.Position += 4

        m_KeywordSize = reader.read_int()
        self.m_Keywords = [reader.read_aligned_string() for _ in range(m_KeywordSize)]

        if 201806140 <= self.m_Version < 202012090:
            m_LocalKeywordsSize = reader.read_int()
            self.m_LocalKeywords = [reader.read_aligned_string() for _ in range(m_LocalKeywordsSize)]
        else:
            self.m_LocalKeywords = None

        self.m_ProgramCode = reader.read_byte_array()
        reader.align_stream()

    def Export(self) -> str:
        sb = []

        if len(self.m_Keywords) > 0:
            sb.append("Keywords { ")
            for keyword in self.m_Keywords:
                sb.append('"{0}" '.format(keyword))

            sb.append("}\n")

        if (
            getattr(self, "m_LocalKeywords") is not None  # noqa: B009
            and len(self.m_LocalKeywords) > 0  # type: ignore
        ):
            sb.append("Local Keywords { ")
            for keyword in self.m_LocalKeywords:  # type: ignore
                sb.append('"{0}" '.format(keyword))

            sb.append("}\n")

        sb.append('"')

        if len(self.m_ProgramCode) > 0:
            if self.m_ProgramType in [
                ShaderGpuProgramType.kShaderGpuProgramGLLegacy,
                ShaderGpuProgramType.kShaderGpuProgramGLES31AEP,
                ShaderGpuProgramType.kShaderGpuProgramGLES31,
                ShaderGpuProgramType.kShaderGpuProgramGLES3,
                ShaderGpuProgramType.kShaderGpuProgramGLES,
                ShaderGpuProgramType.kShaderGpuProgramGLCore32,
                ShaderGpuProgramType.kShaderGpuProgramGLCore41,
                ShaderGpuProgramType.kShaderGpuProgramGLCore43,
            ]:
                sb.append(bytes(self.m_ProgramCode).decode("utf8"))
            elif self.m_ProgramType in [
                ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM20,
                ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM30,
                ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM20,
                ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM30,
            ]:
                sb.append("// shader disassembly not supported on DXBC")
            elif self.m_ProgramType in [
                ShaderGpuProgramType.kShaderGpuProgramDX10Level9Vertex,
                ShaderGpuProgramType.kShaderGpuProgramDX10Level9Pixel,
                ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM40,
                ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM50,
                ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM40,
                ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM50,
                ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM40,
                ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM50,
                ShaderGpuProgramType.kShaderGpuProgramDX11HullSM50,
                ShaderGpuProgramType.kShaderGpuProgramDX11DomainSM50,
            ]:
                sb.append("// shader disassembly not supported on DXBC")
            elif self.m_ProgramType in [
                ShaderGpuProgramType.kShaderGpuProgramMetalVS,
                ShaderGpuProgramType.kShaderGpuProgramMetalFS,
            ]:
                reader = EndianBinaryReader(self.m_ProgramCode, endian="<")
                fourCC = reader.read_u_int()
                if fourCC == 0xF00DCAFE:
                    offset = reader.read_int()
                    reader.Position = offset

                _entryName = reader.read_string_to_null()
                buff = reader.read_bytes(int(reader.Length - reader.Position))
                sb.append(bytes(buff).decode("utf8"))
            elif self.m_ProgramType == ShaderGpuProgramType.kShaderGpuProgramSPIRV:
                # TODO SpirVShaderConverter
                pass
            elif self.m_ProgramType in [
                ShaderGpuProgramType.kShaderGpuProgramConsoleVS,
                ShaderGpuProgramType.kShaderGpuProgramConsoleFS,
                ShaderGpuProgramType.kShaderGpuProgramConsoleHS,
                ShaderGpuProgramType.kShaderGpuProgramConsoleDS,
                ShaderGpuProgramType.kShaderGpuProgramConsoleGS,
            ]:
                sb.append(bytes(self.m_ProgramCode).decode("utf8"))
            else:
                sb.append("//shader disassembly not supported on {0}".format(self.m_ProgramType))

        sb.append('"')
        return "".join(sb)
