#include "pipeline.hpp"

#include "../internal.hpp"
#include "../webgpu/gpu.hpp"

#include <algorithm>
#include <array>
#include <string_view>

#include <RmlUi/Core/Vertex.h>
#include <tracy/Tracy.hpp>

namespace aurora::rmlui {
namespace {
using namespace std::string_view_literals;

wgpu::BindGroupLayout g_commonBindGroupLayout;
wgpu::BindGroupLayout g_imageBindGroupLayout;
wgpu::BindGroupLayout g_uniformBindGroupLayout;
wgpu::Sampler g_sampler;

constexpr uint32_t DynamicGroup1 = 1u << 1u;
constexpr uint32_t DynamicGroup2 = 1u << 2u;

constexpr uint64_t CommonUniformBindingSize = AURORA_ALIGN(sizeof(UniformBlock), 16);
constexpr uint64_t ExtraUniformBindingSize =
    AURORA_ALIGN(std::max({sizeof(BlurUniformBlock), sizeof(DropShadowUniformBlock), sizeof(ColorMatrixUniformBlock),
                           sizeof(GradientUniformBlock), sizeof(SeedResampleUniformBlock)}),
                 16);

constexpr std::string_view vertexSource = R"(
struct VertexInput {
    @location(0) position: vec2<f32>,
    @location(1) uv: vec2<f32>,
    @location(2) color: vec4<f32>,
};

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) color: vec4<f32>,
    @location(1) uv: vec2<f32>,
};

struct Uniforms {
    mvp: mat4x4<f32>,
    translation: vec4<f32>,
    gamma: f32,
};

@group(0) @binding(0) var<uniform> uniforms: Uniforms;

@vertex
fn main(in: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    var translatedPos = uniforms.translation.xy + in.position;

    out.position = uniforms.mvp * vec4<f32>(translatedPos, 0.0, 1.0);
    out.color = in.color;
    out.uv = in.uv;
    return out;
}
)"sv;

constexpr std::string_view fragmentSource = R"(
struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) color: vec4<f32>,
    @location(1) uv: vec2<f32>,
};

struct Uniforms {
    mvp: mat4x4<f32>,
    translation: vec4<f32>,
    gamma: f32,
};

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;

@fragment
fn main(in: VertexOutput) -> @location(0) vec4<f32> {
    let color = in.color * textureSample(t, s, in.uv);
    if (uniforms.gamma == 1.0) {
        return color;
    }
    let corrected_color = pow(color.rgb, vec3<f32>(uniforms.gamma));
    return vec4<f32>(corrected_color, color.a);
}
)"sv;

constexpr std::string_view gradientFragmentSource = R"(
struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) color: vec4<f32>,
    @location(1) uv: vec2<f32>,
};

struct Uniforms {
    mvp: mat4x4<f32>,
    translation: vec4<f32>,
    gamma: f32,
};

struct GradientUniforms {
    function: i32,
    num_stops: i32,
    p: vec2<f32>,
    v: vec2<f32>,
    padding: vec2<f32>,
    stop_colors: array<vec4<f32>, 16>,
    stop_positions: array<vec4<f32>, 4>,
};

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var<uniform> gradient: GradientUniforms;

const LINEAR: i32 = 0;
const RADIAL: i32 = 1;
const CONIC: i32 = 2;
const REPEATING_LINEAR: i32 = 3;
const REPEATING_RADIAL: i32 = 4;
const REPEATING_CONIC: i32 = 5;
const PI: f32 = 3.14159265;

fn bayer_dither(position: vec4<f32>) -> f32 {
    let bayer = array<u32, 64>(
        0u, 32u, 8u, 40u, 2u, 34u, 10u, 42u,
        48u, 16u, 56u, 24u, 50u, 18u, 58u, 26u,
        12u, 44u, 4u, 36u, 14u, 46u, 6u, 38u,
        60u, 28u, 52u, 20u, 62u, 30u, 54u, 22u,
        3u, 35u, 11u, 43u, 1u, 33u, 9u, 41u,
        51u, 19u, 59u, 27u, 49u, 17u, 57u, 25u,
        15u, 47u, 7u, 39u, 13u, 45u, 5u, 37u,
        63u, 31u, 55u, 23u, 61u, 29u, 53u, 21u
    );
    let x = u32(position.x) % 8u;
    let y = u32(position.y) % 8u;
    return (f32(bayer[x + y * 8u]) / 64.0 - 0.5) / 255.0;
}

fn stop_position(index: i32) -> f32 {
    let stop_index = u32(index);
    let group_index = stop_index / 4u;
    let component_index = stop_index % 4u;
    return gradient.stop_positions[group_index][component_index];
}

fn stop_color_mix(t: f32) -> vec4<f32> {
    var color = gradient.stop_colors[0];

    for (var i = 1; i < 16; i = i + 1) {
        if (i < gradient.num_stops) {
            color = mix(color, gradient.stop_colors[u32(i)], smoothstep(stop_position(i - 1), stop_position(i), t));
        }
    }

    return color;
}

@fragment
fn main(in: VertexOutput) -> @location(0) vec4<f32> {
    var t = 0.0;

    if (gradient.function == LINEAR || gradient.function == REPEATING_LINEAR) {
        let dist_square = dot(gradient.v, gradient.v);
        let v = in.uv - gradient.p;
        t = dot(gradient.v, v) / dist_square;
    } else if (gradient.function == RADIAL || gradient.function == REPEATING_RADIAL) {
        let v = in.uv - gradient.p;
        t = length(gradient.v * v);
    } else if (gradient.function == CONIC || gradient.function == REPEATING_CONIC) {
        let v = in.uv - gradient.p;
        let rotated = vec2<f32>(
            gradient.v.x * v.x + gradient.v.y * v.y,
            -gradient.v.y * v.x + gradient.v.x * v.y
        );
        t = 0.5 + atan2(-rotated.x, rotated.y) / (2.0 * PI);
    }

    if (gradient.function == REPEATING_LINEAR ||
        gradient.function == REPEATING_RADIAL ||
        gradient.function == REPEATING_CONIC) {
        let t0 = stop_position(0);
        let t1 = stop_position(gradient.num_stops - 1);
        let span = t1 - t0;
        t = t0 + (t - t0) - span * floor((t - t0) / span);
    }

    let color = in.color * stop_color_mix(t);
    if (uniforms.gamma == 1.0) {
        return color;
    }
    let corrected_color = pow(color.rgb, vec3<f32>(uniforms.gamma));
    let dithered_color = clamp(corrected_color + vec3<f32>(bayer_dither(in.position)), vec3<f32>(0.0), vec3<f32>(1.0));
    return vec4<f32>(dithered_color, color.a);
}
)"sv;

constexpr std::string_view fullscreenVertexSource = R"(
struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
};

var<private> pos: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
    vec2(-1.0, 1.0),
    vec2(-1.0, -3.0),
    vec2(3.0, 1.0),
);
var<private> uvs: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
    vec2(0.0, 0.0),
    vec2(0.0, 2.0),
    vec2(2.0, 0.0),
);

@vertex
fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput {
    var out: VertexOutput;
    out.position = vec4<f32>(pos[vtxIdx], 0.0, 1.0);
    out.uv = uvs[vtxIdx];
    return out;
}
)"sv;

constexpr std::string_view blurVertexSource = R"(
struct BlurUniforms {
    texel_offset: vec2<f32>,
    radius: f32,
    padding: f32,
    tex_coord_min: vec2<f32>,
    tex_coord_max: vec2<f32>,
    weights: vec4<f32>,
};

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv0: vec2<f32>,
    @location(1) uv1: vec2<f32>,
    @location(2) uv2: vec2<f32>,
    @location(3) uv3: vec2<f32>,
    @location(4) uv4: vec2<f32>,
    @location(5) uv5: vec2<f32>,
    @location(6) uv6: vec2<f32>,
};

@group(2) @binding(0) var<uniform> blur: BlurUniforms;

const BLUR_NUM_WEIGHTS: i32 = 4;

var<private> pos: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
    vec2(-1.0, 1.0),
    vec2(-1.0, -3.0),
    vec2(3.0, 1.0),
);
var<private> uvs: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
    vec2(0.0, 0.0),
    vec2(0.0, 2.0),
    vec2(2.0, 0.0),
);

fn blur_uv(uv: vec2<f32>, index: i32) -> vec2<f32> {
    return uv - f32(index - BLUR_NUM_WEIGHTS + 1) * blur.texel_offset;
}

@vertex
fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput {
    let uv = uvs[vtxIdx];
    var out: VertexOutput;
    out.position = vec4<f32>(pos[vtxIdx], 0.0, 1.0);
    out.uv0 = blur_uv(uv, 0);
    out.uv1 = blur_uv(uv, 1);
    out.uv2 = blur_uv(uv, 2);
    out.uv3 = blur_uv(uv, 3);
    out.uv4 = blur_uv(uv, 4);
    out.uv5 = blur_uv(uv, 5);
    out.uv6 = blur_uv(uv, 6);
    return out;
}
)"sv;

constexpr std::string_view blitFragmentSource = R"(
@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    return textureSample(t, s, uv);
}
)"sv;

constexpr std::string_view opaqueBlitFragmentSource = R"(
@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let color = textureSample(t, s, uv);
    return vec4<f32>(color.rgb, 1.0);
}
)"sv;

constexpr std::string_view seedResampleFragmentSource = R"(
struct SeedUniforms {
    sampler_mode: u32,
    frame_width: f32,
    frame_height: f32,
    pad: u32,
};

@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var<uniform> uniforms: SeedUniforms;

fn sample_by_pixel(pixel: vec2<i32>) -> vec4<f32> {
    let source_dims = textureDimensions(t);
    let max_coord = vec2<i32>(source_dims) - vec2<i32>(1, 1);
    let coord = clamp(pixel, vec2<i32>(0, 0), max_coord);
    return textureLoad(t, coord, 0);
}

fn sample_area(frag_position: vec4<f32>) -> vec4<f32> {
    let source_size = vec2<f32>(textureDimensions(t));
    let target_size = max(vec2<f32>(uniforms.frame_width, uniforms.frame_height), vec2<f32>(1.0, 1.0));

    let source_min = clamp((frag_position.xy - vec2<f32>(0.5, 0.5)) / target_size,
                           vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0)) * source_size;
    let source_max = clamp((frag_position.xy + vec2<f32>(0.5, 0.5)) / target_size,
                           vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0)) * source_size;

    let first_pixel = vec2<i32>(floor(source_min));
    let last_pixel = vec2<i32>(ceil(source_max));
    let max_iterations: i32 = 16;

    var avg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
    var total_weight = 0.0;

    for (var iy: i32 = 0; iy < max_iterations; iy = iy + 1) {
        let source_y = first_pixel.y + iy;
        if (source_y < last_pixel.y) {
            let y0 = f32(source_y);
            let weight_y = max(min(source_max.y, y0 + 1.0) - max(source_min.y, y0), 0.0);

            for (var ix: i32 = 0; ix < max_iterations; ix = ix + 1) {
                let source_x = first_pixel.x + ix;
                if (source_x < last_pixel.x) {
                    let x0 = f32(source_x);
                    let weight_x = max(min(source_max.x, x0 + 1.0) - max(source_min.x, x0), 0.0);
                    let weight = weight_x * weight_y;
                    avg_color += weight * sample_by_pixel(vec2<i32>(source_x, source_y));
                    total_weight += weight;
                }
            }
        }
    }

    return avg_color / max(total_weight, 0.000001);
}

@fragment
fn main(@builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    var color = textureSample(t, s, uv);
    if (uniforms.sampler_mode == 1u) {
        color = sample_area(position);
    }
    return vec4(color.rgb, 1.0);
}
)"sv;

constexpr std::string_view colorMatrixFragmentSource = R"(
struct ColorMatrixUniforms {
    matrix: mat4x4<f32>,
};

@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var<uniform> color_matrix: ColorMatrixUniforms;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let tex_color = textureSample(t, s, uv);
    let transformed_color = (color_matrix.matrix * tex_color).rgb;
    return vec4<f32>(transformed_color, tex_color.a);
}
)"sv;

constexpr std::string_view maskImageFragmentSource = R"(
@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var mask_t: texture_2d<f32>;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let tex_color = textureSample(t, s, uv);
    let mask_alpha = textureSample(mask_t, s, uv).a;
    return tex_color * mask_alpha;
}
)"sv;

constexpr std::string_view blurFragmentSource = R"(
struct BlurUniforms {
    texel_offset: vec2<f32>,
    radius: f32,
    padding: f32,
    tex_coord_min: vec2<f32>,
    tex_coord_max: vec2<f32>,
    weights: vec4<f32>,
};

@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var<uniform> blur: BlurUniforms;

fn get_weight(index: i32) -> f32 {
    return blur.weights[u32(abs(index))];
}

fn sample_blur(sample_uv: vec2<f32>, offset_index: i32) -> vec4<f32> {
    let in_region = step(blur.tex_coord_min, sample_uv) * step(sample_uv, blur.tex_coord_max);
    return textureSample(t, s, sample_uv) * get_weight(offset_index) * in_region.x * in_region.y;
}

@fragment
fn main(@location(0) uv0: vec2<f32>, @location(1) uv1: vec2<f32>, @location(2) uv2: vec2<f32>,
        @location(3) uv3: vec2<f32>, @location(4) uv4: vec2<f32>, @location(5) uv5: vec2<f32>,
        @location(6) uv6: vec2<f32>) -> @location(0) vec4<f32> {
    var color = sample_blur(uv0, -3);
    color += sample_blur(uv1, -2);
    color += sample_blur(uv2, -1);
    color += sample_blur(uv3, 0);
    color += sample_blur(uv4, 1);
    color += sample_blur(uv5, 2);
    color += sample_blur(uv6, 3);
    return color;
}
)"sv;

constexpr std::string_view regionBlitFragmentSource = R"(
struct BlurUniforms {
    texel_offset: vec2<f32>,
    radius: f32,
    padding: f32,
    tex_coord_min: vec2<f32>,
    tex_coord_max: vec2<f32>,
    weights: vec4<f32>,
};

@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var<uniform> blur: BlurUniforms;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let sample_uv = mix(blur.tex_coord_min, blur.tex_coord_max, uv);
    return textureSample(t, s, sample_uv);
}
)"sv;

constexpr std::string_view dropShadowFragmentSource = R"(
struct DropShadowUniforms {
    color: vec4<f32>,
    uv_offset: vec2<f32>,
    tex_coord_min: vec2<f32>,
    tex_coord_max: vec2<f32>,
};

@group(0) @binding(1) var s: sampler;
@group(1) @binding(0) var t: texture_2d<f32>;
@group(2) @binding(0) var<uniform> shadow: DropShadowUniforms;

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let sample_uv = uv - shadow.uv_offset;
    let in_region = step(shadow.tex_coord_min, sample_uv) * step(sample_uv, shadow.tex_coord_max);
    let alpha = textureSample(t, s, sample_uv).a * in_region.x * in_region.y;
    return shadow.color * alpha;
}
)"sv;

wgpu::ComputeState compile_shader(std::string_view wgslSource, std::string_view label) {
  const wgpu::ShaderSourceWGSL source{
      wgpu::ShaderSourceWGSL::Init{
          .nextInChain = nullptr,
          .code = wgslSource,
      },
  };
  const wgpu::ShaderModuleDescriptor desc{
      .nextInChain = &source,
      .label = label,
  };
  return {
      .module = webgpu::g_device.CreateShaderModule(&desc),
      .entryPoint = "main",
  };
}

wgpu::BlendState blend_state(BlendMode mode) {
  switch (mode) {
  case BlendMode::Premultiplied:
    return {
        .color =
            {
                .operation = wgpu::BlendOperation::Add,
                .srcFactor = wgpu::BlendFactor::One,
                .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha,
            },
        .alpha =
            {
                .operation = wgpu::BlendOperation::Add,
                .srcFactor = wgpu::BlendFactor::One,
                .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha,
            },
    };
  case BlendMode::Opacity:
    return {
        .color =
            {
                .operation = wgpu::BlendOperation::Add,
                .srcFactor = wgpu::BlendFactor::Constant,
                .dstFactor = wgpu::BlendFactor::Zero,
            },
        .alpha =
            {
                .operation = wgpu::BlendOperation::Add,
                .srcFactor = wgpu::BlendFactor::Constant,
                .dstFactor = wgpu::BlendFactor::Zero,
            },
    };
  case BlendMode::None:
  default:
    return {};
  }
}

const wgpu::PipelineLayout create_pipeline_layout(PipelineKind kind) {
  std::array<wgpu::BindGroupLayout, 3> layouts{};
  uint32_t layoutCount = 0;
  layouts[layoutCount++] = g_commonBindGroupLayout;

  switch (kind) {
  case PipelineKind::Gradient:
    layouts[layoutCount++] = g_uniformBindGroupLayout;
    break;
  case PipelineKind::MaskImage:
    layouts[layoutCount++] = g_imageBindGroupLayout;
    layouts[layoutCount++] = g_imageBindGroupLayout;
    break;
  case PipelineKind::Blur:
  case PipelineKind::RegionBlit:
  case PipelineKind::DropShadow:
  case PipelineKind::ColorMatrix:
  case PipelineKind::SeedResample:
    layouts[layoutCount++] = g_imageBindGroupLayout;
    layouts[layoutCount++] = g_uniformBindGroupLayout;
    break;
  case PipelineKind::Geometry:
  case PipelineKind::Blit:
  case PipelineKind::OpaqueBlit:
  case PipelineKind::Opacity:
  default:
    layouts[layoutCount++] = g_imageBindGroupLayout;
    break;
  }

  const wgpu::PipelineLayoutDescriptor layoutDesc{
      .bindGroupLayoutCount = layoutCount,
      .bindGroupLayouts = layouts.data(),
  };
  return webgpu::g_device.CreatePipelineLayout(&layoutDesc);
}

const std::string_view fragment_source(PipelineKind kind) {
  switch (kind) {
  case PipelineKind::Geometry:
    return fragmentSource;
  case PipelineKind::Gradient:
    return gradientFragmentSource;
  case PipelineKind::OpaqueBlit:
    return opaqueBlitFragmentSource;
  case PipelineKind::SeedResample:
    return seedResampleFragmentSource;
  case PipelineKind::Blur:
    return blurFragmentSource;
  case PipelineKind::RegionBlit:
    return regionBlitFragmentSource;
  case PipelineKind::DropShadow:
    return dropShadowFragmentSource;
  case PipelineKind::ColorMatrix:
    return colorMatrixFragmentSource;
  case PipelineKind::MaskImage:
    return maskImageFragmentSource;
  case PipelineKind::Blit:
  case PipelineKind::Opacity:
  default:
    return blitFragmentSource;
  }
}

const std::string_view vertex_source(VertexLayoutKind kind) {
  switch (kind) {
  case VertexLayoutKind::Geometry:
    return vertexSource;
  case VertexLayoutKind::BlurFullscreen:
    return blurVertexSource;
  case VertexLayoutKind::Fullscreen:
  default:
    return fullscreenVertexSource;
  }
}

} // namespace

void initialize_pipeline() {
  constexpr std::array commonEntries{
      wgpu::BindGroupLayoutEntry{
          .binding = 0,
          .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment,
          .buffer =
              {
                  .type = wgpu::BufferBindingType::Uniform,
                  .hasDynamicOffset = true,
              },
      },
      wgpu::BindGroupLayoutEntry{
          .binding = 1,
          .visibility = wgpu::ShaderStage::Fragment,
          .sampler =
              {
                  .type = wgpu::SamplerBindingType::Filtering,
              },
      },
  };
  const wgpu::BindGroupLayoutDescriptor commonDesc{
      .entryCount = commonEntries.size(),
      .entries = commonEntries.data(),
  };
  g_commonBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&commonDesc);

  constexpr std::array imageEntries{
      wgpu::BindGroupLayoutEntry{
          .binding = 0,
          .visibility = wgpu::ShaderStage::Fragment,
          .texture =
              {
                  .sampleType = wgpu::TextureSampleType::Float,
                  .viewDimension = wgpu::TextureViewDimension::e2D,
              },
      },
  };
  const wgpu::BindGroupLayoutDescriptor imageDesc{
      .entryCount = imageEntries.size(),
      .entries = imageEntries.data(),
  };
  g_imageBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&imageDesc);

  constexpr std::array uniformEntries{
      wgpu::BindGroupLayoutEntry{
          .binding = 0,
          .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment,
          .buffer =
              {
                  .type = wgpu::BufferBindingType::Uniform,
                  .hasDynamicOffset = true,
              },
      },
  };
  const wgpu::BindGroupLayoutDescriptor uniformDesc{
      .entryCount = uniformEntries.size(),
      .entries = uniformEntries.data(),
  };
  g_uniformBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&uniformDesc);

  constexpr wgpu::SamplerDescriptor samplerDesc{
      .addressModeU = wgpu::AddressMode::Repeat,
      .addressModeV = wgpu::AddressMode::Repeat,
      .addressModeW = wgpu::AddressMode::Repeat,
      .magFilter = wgpu::FilterMode::Linear,
      .minFilter = wgpu::FilterMode::Linear,
      .mipmapFilter = wgpu::MipmapFilterMode::Linear,
      .maxAnisotropy = 1,
  };
  g_sampler = webgpu::g_device.CreateSampler(&samplerDesc);
}

void shutdown_pipeline() {
  g_commonBindGroupLayout = {};
  g_imageBindGroupLayout = {};
  g_uniformBindGroupLayout = {};
  g_sampler = {};
}

gfx::BindGroupRef texture_bind_group_ref(const wgpu::TextureView& view) {
  const std::array entries{
      wgpu::BindGroupEntry{
          .binding = 0,
          .textureView = view,
      },
  };
  const wgpu::BindGroupDescriptor desc{
      .layout = g_imageBindGroupLayout,
      .entryCount = entries.size(),
      .entries = entries.data(),
  };
  return gfx::bind_group_ref(desc);
}

gfx::BindGroupRef common_bind_group_ref() {
  const std::array entries{
      wgpu::BindGroupEntry{
          .binding = 0,
          .buffer = gfx::g_uniformBuffer,
          .offset = 0,
          .size = CommonUniformBindingSize,
      },
      wgpu::BindGroupEntry{
          .binding = 1,
          .sampler = g_sampler,
      },
  };
  const wgpu::BindGroupDescriptor desc{
      .layout = g_commonBindGroupLayout,
      .entryCount = entries.size(),
      .entries = entries.data(),
  };
  return gfx::bind_group_ref(desc);
}

gfx::BindGroupRef uniform_bind_group_ref() {
  const std::array entries{
      wgpu::BindGroupEntry{
          .binding = 0,
          .buffer = gfx::g_uniformBuffer,
          .offset = 0,
          .size = ExtraUniformBindingSize,
      },
  };
  const wgpu::BindGroupDescriptor desc{
      .layout = g_uniformBindGroupLayout,
      .entryCount = entries.size(),
      .entries = entries.data(),
  };
  return gfx::bind_group_ref(desc);
}

wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) {
  ZoneScoped;
  const auto kind = static_cast<PipelineKind>(config.kind);
  const auto vertexLayoutKind = static_cast<VertexLayoutKind>(config.vertexLayout);
  const auto colorFormat = static_cast<wgpu::TextureFormat>(config.colorFormat);
  const auto stencilFormat = static_cast<wgpu::TextureFormat>(config.stencilFormat);
  const auto stencilMode = static_cast<StencilMode>(config.stencilMode);
  const auto blendMode = static_cast<BlendMode>(config.blendMode);

  const auto vertexShader = compile_shader(vertex_source(vertexLayoutKind), "RmlUi Vertex Shader");
  const auto fragmentShader = compile_shader(fragment_source(kind), "RmlUi Fragment Shader");

  constexpr std::array vertexAttributes{
      wgpu::VertexAttribute{
          .format = wgpu::VertexFormat::Float32x2,
          .offset = offsetof(Rml::Vertex, position),
          .shaderLocation = 0,
      },
      wgpu::VertexAttribute{
          .format = wgpu::VertexFormat::Float32x2,
          .offset = offsetof(Rml::Vertex, tex_coord),
          .shaderLocation = 1,
      },
      wgpu::VertexAttribute{
          .format = wgpu::VertexFormat::Unorm8x4,
          .offset = offsetof(Rml::Vertex, colour),
          .shaderLocation = 2,
      },
  };
  const std::array vertexBufferLayouts{
      wgpu::VertexBufferLayout{
          .stepMode = wgpu::VertexStepMode::Vertex,
          .arrayStride = sizeof(Rml::Vertex),
          .attributeCount = vertexAttributes.size(),
          .attributes = vertexAttributes.data(),
      },
  };

  const auto blend = blend_state(blendMode);
  const wgpu::ColorTargetState colorState{
      .format = colorFormat,
      .blend = blendMode == BlendMode::None ? nullptr : &blend,
      .writeMask = static_cast<wgpu::ColorWriteMask>(config.colorWriteMask),
  };
  const wgpu::FragmentState fragmentState{
      .module = fragmentShader.module,
      .entryPoint = fragmentShader.entryPoint,
      .targetCount = 1,
      .targets = &colorState,
  };

  wgpu::DepthStencilState depthStencilState{};
  const wgpu::DepthStencilState* depthStencil = nullptr;
  if (stencilMode != StencilMode::None) {
    wgpu::CompareFunction compare = wgpu::CompareFunction::Always;
    wgpu::StencilOperation passOp = wgpu::StencilOperation::Keep;
    switch (stencilMode) {
    case StencilMode::EqualKeep:
      compare = wgpu::CompareFunction::Equal;
      break;
    case StencilMode::ClipReplace:
      passOp = wgpu::StencilOperation::Replace;
      break;
    case StencilMode::ClipIntersect:
      compare = wgpu::CompareFunction::Equal;
      passOp = wgpu::StencilOperation::IncrementClamp;
      break;
    case StencilMode::AlwaysKeep:
    case StencilMode::None:
    default:
      break;
    }
    const wgpu::StencilFaceState face{
        .compare = compare,
        .failOp = wgpu::StencilOperation::Keep,
        .depthFailOp = wgpu::StencilOperation::Keep,
        .passOp = passOp,
    };
    depthStencilState = {
        .format = stencilFormat,
        .stencilFront = face,
        .stencilBack = face,
        .stencilReadMask = 0xFF,
        .stencilWriteMask = 0xFF,
    };
    depthStencil = &depthStencilState;
  }

  const bool hasVertexBuffer = vertexLayoutKind == VertexLayoutKind::Geometry;
  const auto pipelineLayout = create_pipeline_layout(kind);
  const auto label = fmt::format("RmlUi Pipeline {}", config.kind);
  const wgpu::RenderPipelineDescriptor pipelineDesc{
      .label = label.c_str(),
      .layout = pipelineLayout,
      .vertex =
          {
              .module = vertexShader.module,
              .entryPoint = vertexShader.entryPoint,
              .bufferCount = hasVertexBuffer ? vertexBufferLayouts.size() : 0,
              .buffers = hasVertexBuffer ? vertexBufferLayouts.data() : nullptr,
          },
      .primitive =
          {
              .topology = wgpu::PrimitiveTopology::TriangleList,
              .stripIndexFormat = wgpu::IndexFormat::Undefined,
              .frontFace = wgpu::FrontFace::CW,
              .cullMode = wgpu::CullMode::None,
          },
      .depthStencil = depthStencil,
      .multisample =
          {
              .count = config.sampleCount,
          },
      .fragment = &fragmentState,
  };
  return webgpu::g_device.CreateRenderPipeline(&pipelineDesc);
}

void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) {
  if (!gfx::bind_pipeline(data.pipeline, pass)) {
    return;
  }

  const auto commonBindGroup = gfx::find_bind_group(common_bind_group_ref());
  const std::array commonOffsets{data.uniformRange.offset};
  pass.SetBindGroup(0, commonBindGroup, commonOffsets.size(), commonOffsets.data());

  if (data.bindGroup1 != 0) {
    const auto bindGroup = gfx::find_bind_group(data.bindGroup1);
    if ((data.dynamicBindGroupMask & DynamicGroup1) != 0) {
      const std::array offsets{data.bindGroup1DynamicOffset};
      pass.SetBindGroup(1, bindGroup, offsets.size(), offsets.data());
    } else {
      pass.SetBindGroup(1, bindGroup);
    }
  }

  if (data.bindGroup2 != 0) {
    const auto bindGroup = gfx::find_bind_group(data.bindGroup2);
    if ((data.dynamicBindGroupMask & DynamicGroup2) != 0) {
      const std::array offsets{data.bindGroup2DynamicOffset};
      pass.SetBindGroup(2, bindGroup, offsets.size(), offsets.data());
    } else {
      pass.SetBindGroup(2, bindGroup);
    }
  }

  if (data.hasBlendConstant != 0) {
    const wgpu::Color color{data.blendConstant[0], data.blendConstant[1], data.blendConstant[2], data.blendConstant[3]};
    pass.SetBlendConstant(&color);
  }
  pass.SetStencilReference(data.stencilRef);

  if (static_cast<DrawKind>(data.drawKind) == DrawKind::Geometry) {
    pass.SetVertexBuffer(0, gfx::g_vertexBuffer, data.vertexRange.offset, data.vertexRange.size);
    pass.SetIndexBuffer(gfx::g_indexBuffer, wgpu::IndexFormat::Uint32, data.indexRange.offset, data.indexRange.size);
    pass.DrawIndexed(data.indexCount);
  } else {
    pass.Draw(data.vertexCount);
  }
}

uint32_t sampler_mode() noexcept {
  switch (webgpu::get_resampler()) {
  case SAMPLER_AREA:
    return 1;
  case SAMPLER_BILINEAR:
  default:
    return 0;
  }
}

} // namespace aurora::rmlui
