import package::util::{Spotlight, MAX_SPOTLIGHTS, calculate_spotlight_data};

struct AccumulationUniforms {
    terrain_min_xz: vec2<f32>,
    terrain_max_xz: vec2<f32>,
    decay_rate: f32,
    delta_time: f32,
    spotlight_count: u32,
    _padding: u32,
    light_view_projections: array<mat4x4<f32>, MAX_SPOTLIGHTS>,
    shadow_bias: f32,
    terrain_height_scale: f32,
    _padding3: f32,
    _padding4: f32,
    spotlights: array<Spotlight, MAX_SPOTLIGHTS>,
}

@group(0) @binding(0)
var previous_light: texture_2d<f32>;

@group(0) @binding(1)
var light_sampler: sampler;

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

@group(0) @binding(3)
var heightmap: texture_2d<f32>;

@group(0) @binding(4)
var heightmap_sampler: sampler;

@group(0) @binding(5)
var shadow_map: texture_depth_2d_array;

@group(0) @binding(6)
var shadow_sampler: sampler_comparison;

@group(0) @binding(7)
var snow_depth: texture_2d<f32>;

@group(0) @binding(8)
var snow_depth_sampler: sampler;

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

@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
    var out: VertexOutput;

    let x = f32((vertex_index << 1u) & 2u);
    let y = f32(vertex_index & 2u);
    out.position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
    out.uv = vec2<f32>(x, 1.0 - y);

    return out;
}

fn sample_shadow_map_local(world_pos: vec3<f32>, spotlight_index: u32) -> f32 {
    let light_space_pos = uniforms.light_view_projections[spotlight_index] * vec4<f32>(world_pos, 1.0);
    let proj_coords = light_space_pos.xyz / light_space_pos.w;
    let ndc_coords = proj_coords * vec3<f32>(0.5, -0.5, 1.0) + vec3<f32>(0.5, 0.5, 0.0);

    if ndc_coords.x < 0.0 || ndc_coords.x > 1.0 ||
       ndc_coords.y < 0.0 || ndc_coords.y > 1.0 ||
       ndc_coords.z < 0.0 || ndc_coords.z > 1.0 {
        return 1.0;
    }

    let depth = ndc_coords.z - uniforms.shadow_bias;
    let shadow = textureSampleCompare(shadow_map, shadow_sampler, ndc_coords.xy, spotlight_index, depth);

    return shadow;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let prev_light = textureSample(previous_light, light_sampler, in.uv).r;

    let world_xz = mix(uniforms.terrain_min_xz, uniforms.terrain_max_xz, in.uv);
    let terrain_height = textureSampleLevel(heightmap, heightmap_sampler, in.uv, 0.0).r * uniforms.terrain_height_scale;
    let depth = textureSampleLevel(snow_depth, snow_depth_sampler, in.uv, 0.0).r;
    let snow_surface_height = terrain_height + depth;
    let snow_surface_pos = vec3<f32>(world_xz.x, snow_surface_height, world_xz.y);

    var current_light = 0.0;
    let tile_scale = 2.0;
    let surface_normal = vec3<f32>(0.0, 1.0, 0.0);

    for (var i = 0u; i < uniforms.spotlight_count; i++) {
        let shadow = sample_shadow_map_local(snow_surface_pos, i);
        if shadow > 0.0 {
            let spotlight = uniforms.spotlights[i];
            let data = calculate_spotlight_data(snow_surface_pos, surface_normal, spotlight, tile_scale, shadow);
            let light = f32(data.is_lit);
            current_light = max(current_light, light);
        }
    }

    var accumulated: f32;
    if current_light > 0.01 {
        accumulated = current_light;
    } else {
        let decay_factor = exp(-uniforms.decay_rate * uniforms.delta_time * 60.0);
        accumulated = prev_light * decay_factor;

        if accumulated < 0.01 {
            accumulated = 0.0;
        }
    }

    return vec4<f32>(accumulated, 0.0, 0.0, 1.0);
}
