const TERRAIN_CACHE_SIZE: i32 = 512;

fn terrain_scramble(value: u32) -> u32 {
    var x = value ^ (value >> 17u);
    x *= 0xED5AD4BBu;
    x ^= x >> 11u;
    x *= 0xAC4C1B51u;
    x ^= x >> 15u;
    x *= 0x31848BABu;
    x ^= x >> 14u;
    return x;
}

fn terrain_hash_cell(cell: vec2<i32>, salt: u32) -> u32 {
    var p = vec2<u32>(bitcast<u32>(cell.x), bitcast<u32>(cell.y))
        ^ vec2<u32>(salt, salt * 0x9E3779B9u);
    p = p * vec2<u32>(1664525u, 1664525u) + vec2<u32>(1013904223u, 1013904223u);
    p.x += p.y * 1664525u;
    p.y += p.x * 1664525u;
    p ^= p >> vec2<u32>(16u, 16u);
    p.x += p.y * 1664525u;
    p.y += p.x * 1664525u;
    return p.x ^ terrain_scramble(p.y);
}

fn terrain_gradient(cell: vec2<i32>, salt: u32) -> vec2<f32> {
    let angle = f32(terrain_hash_cell(cell, salt)) * 2.3283064e-10 * 6.2831853;
    return vec2<f32>(cos(angle), sin(angle));
}

fn terrain_perlin(p: vec2<f32>, salt: u32) -> f32 {
    let base = floor(p);
    let fraction = p - base;
    let cell = vec2<i32>(base);
    let fade = fraction * fraction * fraction * (fraction * (fraction * 6.0 - 15.0) + 10.0);
    let d00 = dot(terrain_gradient(cell, salt), fraction);
    let d10 = dot(
        terrain_gradient(cell + vec2<i32>(1, 0), salt),
        fraction - vec2<f32>(1.0, 0.0),
    );
    let d01 = dot(
        terrain_gradient(cell + vec2<i32>(0, 1), salt),
        fraction - vec2<f32>(0.0, 1.0),
    );
    let d11 = dot(
        terrain_gradient(cell + vec2<i32>(1, 1), salt),
        fraction - vec2<f32>(1.0, 1.0),
    );
    let x0 = mix(d00, d10, fade.x);
    let x1 = mix(d01, d11, fade.x);
    return mix(x0, x1, fade.y) * 1.4142;
}

fn terrain_fbm(p: vec2<f32>, octaves: u32, salt: u32) -> f32 {
    var amplitude = 0.5;
    var frequency = 1.0;
    var total = 0.0;
    var normalization = 0.0;
    for (var octave = 0u; octave < octaves; octave++) {
        total += terrain_perlin(p * frequency, salt + octave * 101u) * amplitude;
        normalization += amplitude;
        amplitude *= 0.5;
        frequency *= 2.0;
    }
    return total / normalization;
}

fn terrain_perlin_d(p: vec2<f32>, salt: u32) -> vec3<f32> {
    let base = floor(p);
    let fraction = p - base;
    let cell = vec2<i32>(base);
    let fade = fraction * fraction * fraction * (fraction * (fraction * 6.0 - 15.0) + 10.0);
    let fade_derivative =
        30.0 * fraction * fraction * (fraction * (fraction - 2.0) + vec2<f32>(1.0, 1.0));
    let g00 = terrain_gradient(cell, salt);
    let g10 = terrain_gradient(cell + vec2<i32>(1, 0), salt);
    let g01 = terrain_gradient(cell + vec2<i32>(0, 1), salt);
    let g11 = terrain_gradient(cell + vec2<i32>(1, 1), salt);
    let d00 = dot(g00, fraction);
    let d10 = dot(g10, fraction - vec2<f32>(1.0, 0.0));
    let d01 = dot(g01, fraction - vec2<f32>(0.0, 1.0));
    let d11 = dot(g11, fraction - vec2<f32>(1.0, 1.0));
    let k1 = d10 - d00;
    let k2 = d01 - d00;
    let k3 = d00 - d10 - d01 + d11;
    let value = d00 + k1 * fade.x + k2 * fade.y + k3 * fade.x * fade.y;
    let gradient_mix = mix(mix(g00, g10, fade.x), mix(g01, g11, fade.x), fade.y);
    let derivative = gradient_mix
        + vec2<f32>(
            fade_derivative.x * (k1 + k3 * fade.y),
            fade_derivative.y * (k2 + k3 * fade.x),
        );
    return vec3<f32>(value, derivative.x, derivative.y) * 1.4142;
}

fn terrain_fbm_eroded(p: vec2<f32>, octaves: u32, salt: u32, sharpness: f32) -> f32 {
    var amplitude = 0.5;
    var frequency = 1.0;
    var total = 0.0;
    var gradient_sum = vec2<f32>(0.0, 0.0);
    for (var octave = 0u; octave < octaves; octave++) {
        let noise = terrain_perlin_d(p * frequency, salt + octave * 101u);
        gradient_sum += noise.yz;
        total += amplitude * noise.x
            / (1.0 + sharpness * dot(gradient_sum, gradient_sum));
        amplitude *= 0.5;
        frequency *= 2.0;
    }
    return total * (1.0 + sharpness);
}

fn terrain_spline(points: array<vec4<f32>, 4>, value: f32) -> f32 {
    var table = points;
    var previous = table[0].xy;
    var output = previous.y;
    for (var index = 0u; index < 8u; index++) {
        let point = table[index / 2u];
        var current = point.xy;
        if (index & 1u) == 1u {
            current = point.zw;
        }
        if value <= current.x {
            let span = max(current.x - previous.x, 1e-5);
            let t = clamp((value - previous.x) / span, 0.0, 1.0);
            return mix(previous.y, current.y, t);
        }
        previous = current;
        output = current.y;
    }
    return output;
}

fn terrain_cache_load(
    cache: texture_2d_array<f32>,
    layer: u32,
    texel: vec2<i32>,
) -> f32 {
    let wrapped = vec2<i32>(
        ((texel.x % TERRAIN_CACHE_SIZE) + TERRAIN_CACHE_SIZE) % TERRAIN_CACHE_SIZE,
        ((texel.y % TERRAIN_CACHE_SIZE) + TERRAIN_CACHE_SIZE) % TERRAIN_CACHE_SIZE,
    );
    return textureLoad(cache, wrapped, i32(layer), 0).r;
}

fn terrain_cache_sample(
    cache: texture_2d_array<f32>,
    layer: u32,
    texel_size: f32,
    world_xz: vec2<f32>,
) -> f32 {
    let texel_coords = world_xz / texel_size - vec2<f32>(0.5, 0.5);
    let base = floor(texel_coords);
    let fraction = texel_coords - base;
    let cell = vec2<i32>(base);
    let h00 = terrain_cache_load(cache, layer, cell);
    let h10 = terrain_cache_load(cache, layer, cell + vec2<i32>(1, 0));
    let h01 = terrain_cache_load(cache, layer, cell + vec2<i32>(0, 1));
    let h11 = terrain_cache_load(cache, layer, cell + vec2<i32>(1, 1));
    let x0 = mix(h00, h10, fraction.x);
    let x1 = mix(h01, h11, fraction.x);
    return mix(x0, x1, fraction.y);
}
