#define MAX_BONES 100
#define FIXED_PIPELINE_TECHNIQUE 0xffffffffu

cbuffer LegacySkinnedFrame : register(b0)
{
    row_major float4x4 WorldViewProjection;
    row_major float4x4 ViewProjection;
    row_major float4x4 World;
    row_major float4x4 Bones[MAX_BONES];
    float4 LightDirection;
    float4 LightColor;
    float4 AmbientColor;
    float4 EdgeColor;
    float OutlineWidth;
    uint ShaderTechnique;
    uint AlphaTestEnabled;
    uint AlphaFunction;
    float AlphaReference;
    float3 FramePadding;
    uint ColorOperation;
    uint AlphaOperation;
    uint2 TextureStagePadding;
};

Texture2D DiffuseTexture : register(t0);
Texture2D ToonTexture : register(t1);
SamplerState DiffuseSampler : register(s0);

#define D3DTOP_DISABLE 1u
#define D3DTOP_SELECTARG1 2u
#define D3DTOP_SELECTARG2 3u
#define D3DTOP_MODULATE 4u
#define D3DTOP_MODULATE2X 5u
#define D3DTOP_MODULATE4X 6u
#define D3DTOP_ADD 7u
#define D3DTOP_ADDSIGNED 8u
#define D3DTOP_ADDSIGNED2X 9u
#define D3DTOP_SUBTRACT 10u
#define D3DTOP_ADDSMOOTH 11u
#define D3DTOP_BLENDDIFFUSEALPHA 12u
#define D3DTOP_BLENDTEXTUREALPHA 13u
#define D3DTOP_BLENDFACTORALPHA 14u
#define D3DTOP_BLENDTEXTUREALPHAPM 15u
#define D3DTOP_BLENDCURRENTALPHA 16u
#define D3DTOP_PREMODULATE 17u
#define D3DTOP_MODULATEALPHA_ADDCOLOR 18u
#define D3DTOP_MODULATECOLOR_ADDALPHA 19u
#define D3DTOP_MODULATEINVALPHA_ADDCOLOR 20u
#define D3DTOP_MODULATEINVCOLOR_ADDALPHA 21u
#define D3DTOP_DOTPRODUCT3 24u
#define D3DTOP_MULTIPLYADD 25u
#define D3DTOP_LERP 26u

#define D3DTA_SELECTMASK 0x0fu
#define D3DTA_DIFFUSE 0u
#define D3DTA_CURRENT 1u
#define D3DTA_TEXTURE 2u
#define D3DTA_TFACTOR 3u
#define D3DTA_SPECULAR 4u
#define D3DTA_TEMP 5u
#define D3DTA_CONSTANT 6u
#define D3DTA_COMPLEMENT 0x10u
#define D3DTA_ALPHAREPLICATE 0x20u

// Stable stage-0 arguments used by the legacy character path.  Only the
// small operation set explicitly requested by the game is emulated below.
static const uint ColorArgument0 = D3DTA_CURRENT;
static const uint ColorArgument1 = D3DTA_TEXTURE;
static const uint ColorArgument2 = D3DTA_DIFFUSE;
static const uint AlphaArgument0 = D3DTA_CURRENT;
static const uint AlphaArgument1 = D3DTA_TEXTURE;
static const uint AlphaArgument2 = D3DTA_DIFFUSE;
static const uint TextureFactor = 0xffffffffu;
static const uint TextureStageConstant = 0xffffffffu;

float4 DecodeD3DColor(uint value)
{
    return float4(
        (value >> 16) & 255u,
        (value >> 8) & 255u,
        value & 255u,
        (value >> 24) & 255u) / 255.0f;
}

float4 ResolveTextureArgument(
    uint argument,
    float4 textureColor,
    float4 diffuseColor,
    float4 currentColor,
    float4 factorColor,
    float4 constantColor)
{
    const uint selector = argument & D3DTA_SELECTMASK;
    float4 value = currentColor;
    if (selector == D3DTA_DIFFUSE) value = diffuseColor;
    else if (selector == D3DTA_TEXTURE) value = textureColor;
    else if (selector == D3DTA_TFACTOR) value = factorColor;
    else if (selector == D3DTA_SPECULAR) value = float4(0.0f, 0.0f, 0.0f, 1.0f);
    else if (selector == D3DTA_CONSTANT) value = constantColor;

    if ((argument & D3DTA_ALPHAREPLICATE) != 0u)
        value = value.aaaa;
    if ((argument & D3DTA_COMPLEMENT) != 0u)
        value = 1.0f - value;
    return value;
}

float4 ApplyTextureOperation(
    uint operation,
    float4 argument0,
    float4 argument1,
    float4 argument2,
    float4 textureColor,
    float4 diffuseColor,
    float4 currentColor,
    float4 factorColor)
{
    float4 result = argument1 * argument2;
    if (operation == D3DTOP_SELECTARG1) result = argument1;
    else if (operation == D3DTOP_SELECTARG2) result = argument2;
    else if (operation == D3DTOP_MODULATE2X) result = 2.0f * argument1 * argument2;
    else if (operation == D3DTOP_MODULATE4X) result = 4.0f * argument1 * argument2;
    return saturate(result);
}

float4 ApplyLegacyTextureStage(float4 textureColor, float4 diffuseColor)
{
    uint safeColorOperation = ColorOperation;
    if (safeColorOperation != D3DTOP_MODULATE &&
        safeColorOperation != D3DTOP_MODULATE2X &&
        safeColorOperation != D3DTOP_MODULATE4X &&
        safeColorOperation != D3DTOP_SELECTARG1)
        safeColorOperation = D3DTOP_MODULATE;

    uint safeAlphaOperation = AlphaOperation;
    if (safeAlphaOperation != D3DTOP_MODULATE &&
        safeAlphaOperation != D3DTOP_SELECTARG1 &&
        safeAlphaOperation != D3DTOP_SELECTARG2)
        safeAlphaOperation = D3DTOP_MODULATE;

    const float4 currentColor = diffuseColor;
    const float4 factorColor = DecodeD3DColor(TextureFactor);
    const float4 constantColor = DecodeD3DColor(TextureStageConstant);

    const float4 colorArgument0 = ResolveTextureArgument(
        ColorArgument0, textureColor, diffuseColor, currentColor, factorColor, constantColor);
    const float4 colorArgument1 = ResolveTextureArgument(
        ColorArgument1, textureColor, diffuseColor, currentColor, factorColor, constantColor);
    const float4 colorArgument2 = ResolveTextureArgument(
        ColorArgument2, textureColor, diffuseColor, currentColor, factorColor, constantColor);
    const float4 alphaArgument0 = ResolveTextureArgument(
        AlphaArgument0, textureColor, diffuseColor, currentColor, factorColor, constantColor);
    const float4 alphaArgument1 = ResolveTextureArgument(
        AlphaArgument1, textureColor, diffuseColor, currentColor, factorColor, constantColor);
    const float4 alphaArgument2 = ResolveTextureArgument(
        AlphaArgument2, textureColor, diffuseColor, currentColor, factorColor, constantColor);

    const float4 colorResult = ApplyTextureOperation(
        safeColorOperation, colorArgument0, colorArgument1, colorArgument2,
        textureColor, diffuseColor, currentColor, factorColor);
    const float4 alphaResult = ApplyTextureOperation(
        safeAlphaOperation, alphaArgument0, alphaArgument1, alphaArgument2,
        textureColor, diffuseColor, currentColor, factorColor);
    return float4(colorResult.rgb, alphaResult.a);
}

struct VSInput
{
    float3 Position : POSITION0;
    float BlendWeight : BLENDWEIGHT0;
    uint4 BlendIndices : BLENDINDICES0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
    float ToonCoord : TEXCOORD1;
};

struct VSOutlineOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR0;
    float2 TexCoord : TEXCOORD0;
};

void SkinVertex(VSInput input, out float4 position, out float3 normal)
{
    const uint firstBone = min(input.BlendIndices.x, (uint)(MAX_BONES - 1));
    const uint secondBone = min(input.BlendIndices.y, (uint)(MAX_BONES - 1));
    const float firstWeight = saturate(input.BlendWeight);
    const float secondWeight = 1.0f - firstWeight;
    const float4 localPosition = float4(input.Position, 1.0f);
    position =
        mul(localPosition, Bones[firstBone]) * firstWeight +
        mul(localPosition, Bones[secondBone]) * secondWeight;
    normal =
        mul(input.Normal, (float3x3)Bones[firstBone]) * firstWeight +
        mul(input.Normal, (float3x3)Bones[secondBone]) * secondWeight;
}

VSOutput VSMain(VSInput input)
{
    VSOutput output;
    float4 skinnedPosition;
    float3 skinnedNormal;
    SkinVertex(input, skinnedPosition, skinnedNormal);
    output.Position = mul(skinnedPosition, WorldViewProjection);
    output.Normal = normalize(skinnedNormal);
    output.TexCoord = input.TexCoord;
    const float3 worldNormal = normalize(mul(skinnedNormal, (float3x3)World));
    output.ToonCoord = saturate(dot(worldNormal, normalize(LightDirection.xyz)));
    return output;
}

VSOutlineOutput VSOutline(VSInput input)
{
    VSOutlineOutput output;
    float4 skinnedPosition;
    float3 skinnedNormal;
    SkinVertex(input, skinnedPosition, skinnedNormal);
    // Match the D3D9 shader: expand the back faces in 3D world space and
    // project afterwards.  Adding OutlineWidth directly in clip space made
    // the border several times thicker than the original renderer.
    float4 worldPosition = mul(skinnedPosition, World);
    const float3 worldNormal = normalize(mul(float4(skinnedNormal, 0.0f), World).xyz);
    worldPosition.xyz += worldNormal * OutlineWidth;
    output.Position = mul(worldPosition, ViewProjection);
    output.Color = EdgeColor;
    output.TexCoord = input.TexCoord;
    return output;
}

bool PassesAlphaTest(float alpha)
{
    if (AlphaFunction == 1) return false;
    if (AlphaFunction == 2) return alpha < AlphaReference;
    if (AlphaFunction == 3) return alpha == AlphaReference;
    if (AlphaFunction == 4) return alpha <= AlphaReference;
    if (AlphaFunction == 5) return alpha > AlphaReference;
    if (AlphaFunction == 6) return alpha != AlphaReference;
    if (AlphaFunction == 7) return alpha >= AlphaReference;
    return true;
}

float4 PSMain(VSOutput input) : SV_TARGET
{
    const float4 diffuse = DiffuseTexture.Sample(DiffuseSampler, input.TexCoord);
    float4 color = diffuse;
    if (ShaderTechnique == FIXED_PIPELINE_TECHNIQUE)
    {
        const float lighting = saturate(dot(normalize(input.Normal), -normalize(LightDirection.xyz)));
        float4 diffuseColor = AmbientColor + LightColor * lighting;
        diffuseColor.a = 1.0f;
        color = ApplyLegacyTextureStage(diffuse, diffuseColor);
    }
    else
    {
        const bool cartoon = ShaderTechnique == 0 || ShaderTechnique == 1 || ShaderTechnique == 4 ||
                             ShaderTechnique == 5 || ShaderTechnique == 6 || ShaderTechnique == 7;
        if (cartoon)
        {
            const float3 toon = ToonTexture.Sample(DiffuseSampler, float2(input.ToonCoord, 0.0f)).rgb;
            const bool bright = ShaderTechnique == 6 || ShaderTechnique == 7;
            color.rgb = saturate(diffuse.rgb + toon - (bright ? 0.0f : 0.5f));
        }
    }
    if (AlphaTestEnabled != 0)
        clip(PassesAlphaTest(color.a) ? 1.0f : -1.0f);
    return color;
}

float4 PSOutline(VSOutlineOutput input) : SV_TARGET
{
    // Alpha character/equipment textures are often cards.  Discard their
    // transparent texels in the outline pass as D3D9 did, otherwise the
    // expanded back faces produce large black shapes around wings and hair.
    if (ShaderTechnique == 5 || ShaderTechnique == 7)
    {
        const float alpha = DiffuseTexture.Sample(DiffuseSampler, input.TexCoord).a;
        clip(PassesAlphaTest(alpha) ? 1.0f : -1.0f);
    }
    return input.Color;
}
