@group(0) @binding(0) var<uniform> uniforms: GrassUniforms;
@group(0) @binding(1) var<storage, read> types: array<GrassType>;
@group(0) @binding(2) var<storage, read> blades: array<GrassBlade>;
@group(0) @binding(3) var<storage, read> far_tiles: array<GrassTileEntry>;
@group(0) @binding(4) var height_texture: texture_2d_array<f32>;

struct FarVarying {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) world_xz: vec2<f32>,
};

@vertex
fn vs_far(
    @builtin(vertex_index) vertex_index: u32,
    @builtin(instance_index) instance_index: u32,
) -> FarVarying {
    var corners = array<vec2<f32>, 6>(
        vec2<f32>(0.0, 0.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(1.0, 1.0),
    );
    let entry = far_tiles[min(instance_index, GRASS_MAX_FAR_TILES - 1u)].data;
    let origin = vec2<f32>(entry.xy) * GRASS_TILE_SIZE;
    let cell_index = vertex_index / 6u;
    let cell = vec2<f32>(
        f32(cell_index % GRASS_FAR_TILE_DIVISIONS),
        f32(cell_index / GRASS_FAR_TILE_DIVISIONS),
    );
    let corner = corners[vertex_index % 6u];
    let step = GRASS_FAR_TILE_SPAN / f32(GRASS_FAR_TILE_DIVISIONS);
    let world_xz = origin + (cell + corner) * step;
    let ground = uniforms.radii.w
        + grass_sample_height(
            height_texture,
            world_xz,
            uniforms.height_map,
            uniforms.height_bounds,
            distance(world_xz, uniforms.camera_position.xz),
        );
    let world = vec3<f32>(world_xz.x, ground + 0.02, world_xz.y);

    var output: FarVarying;
    output.clip_position = uniforms.view_projection * vec4<f32>(world, 1.0);
    output.world_xz = world_xz;
    return output;
}

@fragment
fn fs_far(input: FarVarying) -> @location(0) vec4<f32> {
    let camera_xz = uniforms.camera_position.xz;
    let view_distance = distance(input.world_xz, camera_xz);
    if view_distance > uniforms.radii.z {
        discard;
    }

    let type_count = uniforms.counts.x;
    let dither_cell = vec2<i32>(floor(input.world_xz * 6.0));
    let dither = grass_hash01(grass_hash_tile(dither_cell, 53u));
    let type_index = grass_select_type(input.world_xz, uniforms.params.x, type_count, dither);
    let blade_type = types[type_index];
    let clump = grass_voronoi(input.world_xz, uniforms.params.y * 2.0, 23u);

    let understory = blade_type.color_base.rgb * 0.55;
    let canopy = mix(blade_type.color_base.rgb, blade_type.color_tip.rgb, 0.55);
    let canopy_blend = smoothstep(uniforms.radii.y * 0.4, uniforms.radii.y, view_distance);
    var albedo = mix(understory, canopy, canopy_blend);
    let clump_strength = 1.0 - smoothstep(150.0, 400.0, view_distance);
    albedo *= 1.0 + (0.2 * clump.cell_value - 0.1) * clump_strength;

    let shimmer = grass_wind_sample(input.world_xz, uniforms.params.w, uniforms.wind);
    albedo *= 1.0 + (0.16 * shimmer - 0.08) * canopy_blend;

    let diffuse_term = max(uniforms.sun_direction.y, 0.0);
    let color = albedo * (uniforms.ambient.rgb + uniforms.sun_color.rgb * diffuse_term / GRASS_PI) * 0.8;
    return vec4<f32>(color, 1.0);
}
