const PI: f32 = 3.141592653589793;
const NUM_STEPS: i32 = 32;
const ITER_GEOMETRY: i32 = 3;
const ITER_FRAGMENT: i32 = 5;
const MAX_POLYGON_VERTICES: u32 = 16u;

struct Uniforms {
    view: mat4x4<f32>,
    projection: mat4x4<f32>,
    inv_view_projection: mat4x4<f32>,
    camera_position: vec4<f32>,
    resolution: vec2<f32>,
    time: f32,
    z_near: f32,
    z_far: f32,
    _padding0: f32,
    _padding1: f32,
    _padding2: f32,
    _padding3: f32,
    _padding4: f32,
    _padding5: f32,
    _padding6: f32,
}

struct WaterParams {
    base_color: vec4<f32>,
    water_color: vec4<f32>,
    base_height: f32,
    wave_height: f32,
    choppy: f32,
    speed: f32,
    frequency: f32,
    specular_strength: f32,
    fresnel_power: f32,
    edge_feather_distance: f32,
}

struct WaterPolygon {
    vertices: array<vec4<f32>, 16>,
    vertex_count: u32,
    use_bounds: u32,
    _padding0: f32,
    _padding1: f32,
}

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

struct FragmentOutput {
    @location(0) color: vec4<f32>,
    @builtin(frag_depth) depth: f32,
}

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<uniform> water_params: WaterParams;
@group(0) @binding(2) var<uniform> water_polygon: WaterPolygon;

fn point_in_polygon(point: vec2<f32>) -> bool {
    let vertex_count = water_polygon.vertex_count;
    if (vertex_count < 3u) {
        return false;
    }

    var inside = false;
    var j = vertex_count - 1u;

    for (var i = 0u; i < vertex_count; i = i + 1u) {
        let vi = water_polygon.vertices[i].xy;
        let vj = water_polygon.vertices[j].xy;

        if ((vi.y > point.y) != (vj.y > point.y)) {
            let x_intersect = (vj.x - vi.x) * (point.y - vi.y) / (vj.y - vi.y) + vi.x;
            if (point.x < x_intersect) {
                inside = !inside;
            }
        }

        j = i;
    }

    return inside;
}

fn distance_to_segment(point: vec2<f32>, segment_a: vec2<f32>, segment_b: vec2<f32>) -> f32 {
    let ab = segment_b - segment_a;
    let ap = point - segment_a;
    let t = clamp(dot(ap, ab) / max(dot(ab, ab), 0.0001), 0.0, 1.0);
    let closest = segment_a + ab * t;
    return length(point - closest);
}

fn distance_to_polygon_edge(point: vec2<f32>) -> f32 {
    let vertex_count = water_polygon.vertex_count;
    if (vertex_count < 3u) {
        return 0.0;
    }

    var min_dist = 1000000.0;
    var j = vertex_count - 1u;

    for (var i = 0u; i < vertex_count; i = i + 1u) {
        let vi = water_polygon.vertices[i].xy;
        let vj = water_polygon.vertices[j].xy;
        let d = distance_to_segment(point, vi, vj);
        min_dist = min(min_dist, d);
        j = i;
    }

    return min_dist;
}

fn hash(p: vec2<f32>) -> f32 {
    var p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
    p3 = p3 + dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

fn noise(p: vec2<f32>) -> f32 {
    let i = floor(p);
    let f = fract(p);
    let u = f * f * (3.0 - 2.0 * f);
    return -1.0 + 2.0 * mix(
        mix(hash(i + vec2<f32>(0.0, 0.0)), hash(i + vec2<f32>(1.0, 0.0)), u.x),
        mix(hash(i + vec2<f32>(0.0, 1.0)), hash(i + vec2<f32>(1.0, 1.0)), u.x),
        u.y
    );
}

fn sea_octave(uv_in: vec2<f32>, choppy: f32) -> f32 {
    var uv = uv_in + noise(uv_in);
    var wv = 1.0 - abs(sin(uv));
    let swv = abs(cos(uv));
    wv = mix(wv, swv, wv);
    return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}

fn get_sea_time() -> f32 {
    return 1.0 + uniforms.time * water_params.speed;
}

fn map(p: vec3<f32>, iter: i32) -> f32 {
    var freq = water_params.frequency;
    var amp = water_params.wave_height;
    var choppy = water_params.choppy;
    var uv = p.xz;
    uv.x = uv.x * 0.75;
    let sea_time = get_sea_time();
    let octave_m = mat2x2<f32>(1.6, 1.2, -1.2, 1.6);

    var d: f32;
    var h: f32 = 0.0;
    for (var i: i32 = 0; i < iter; i = i + 1) {
        d = sea_octave((uv + sea_time) * freq, choppy);
        d = d + sea_octave((uv - sea_time) * freq, choppy);
        h = h + d * amp;
        uv = octave_m * uv;
        freq = freq * 1.9;
        amp = amp * 0.22;
        choppy = mix(choppy, 1.0, 0.2);
    }
    return p.y - (water_params.base_height + h);
}

fn map_detailed(p: vec3<f32>) -> f32 {
    return map(p, ITER_FRAGMENT);
}

fn get_sky_color(e: vec3<f32>) -> vec3<f32> {
    let y_val = (max(e.y, 0.0) * 0.8 + 0.2) * 0.8;
    return vec3<f32>(pow(1.0 - y_val, 2.0), 1.0 - y_val, 0.6 + (1.0 - y_val) * 0.4) * 1.1;
}

fn get_normal(p: vec3<f32>, eps: f32) -> vec3<f32> {
    var n: vec3<f32>;
    n.y = map_detailed(p);
    n.x = map_detailed(vec3<f32>(p.x + eps, p.y, p.z)) - n.y;
    n.z = map_detailed(vec3<f32>(p.x, p.y, p.z + eps)) - n.y;
    n.y = eps;
    return normalize(n);
}

fn heightmap_tracing(ori: vec3<f32>, dir: vec3<f32>) -> vec3<f32> {
    var tm: f32 = 0.0;
    var tx: f32 = 2000.0;
    var hx = map(ori + dir * tx, ITER_GEOMETRY);
    if (hx > 0.0) {
        return ori + dir * tx;
    }
    var hm = map(ori, ITER_GEOMETRY);
    var tmid: f32 = 0.0;
    for (var i: i32 = 0; i < NUM_STEPS; i = i + 1) {
        let denom = hm - hx;
        let frac = hm / max(abs(denom), 0.0001) * sign(denom);
        tmid = mix(tm, tx, clamp(frac, 0.0, 1.0));
        let p = ori + dir * tmid;
        let hmid = map(p, ITER_GEOMETRY);
        if (hmid < 0.0) {
            tx = tmid;
            hx = hmid;
        } else {
            tm = tmid;
            hm = hmid;
        }
    }
    return ori + dir * tmid;
}

fn diffuse_factor(n: vec3<f32>, l: vec3<f32>, p: f32) -> f32 {
    return pow(dot(n, l) * 0.4 + 0.6, p);
}

fn specular_factor(n: vec3<f32>, l: vec3<f32>, e: vec3<f32>, s: f32) -> f32 {
    let nrm = (s + 8.0) / (PI * 8.0);
    return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
}

fn get_sea_color(p: vec3<f32>, n: vec3<f32>, l: vec3<f32>, eye: vec3<f32>, dist: vec3<f32>) -> vec3<f32> {
    let sea_base = water_params.base_color.xyz;
    let sea_water_color = water_params.water_color.xyz;

    var fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
    fresnel = pow(fresnel, water_params.fresnel_power) * 0.65;

    let reflected = get_sky_color(reflect(eye, n));
    let refracted = sea_base + diffuse_factor(n, l, 80.0) * sea_water_color * 0.12;
    var color = mix(refracted, reflected, fresnel);

    let atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
    color = color + sea_water_color * (p.y - water_params.base_height - water_params.wave_height) * 0.18 * atten;
    color = color + vec3<f32>(specular_factor(n, l, eye, 60.0)) * water_params.specular_strength;

    return color;
}

@vertex
fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
    var out: VertexOutput;
    var positions = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>(3.0, -1.0),
        vec2<f32>(-1.0, 3.0)
    );
    let pos = positions[vertex_index];
    out.position = vec4<f32>(pos, 0.0, 1.0);
    out.uv = pos * 0.5 + 0.5;
    return out;
}

@fragment
fn fragment_main(in: VertexOutput) -> FragmentOutput {
    var out: FragmentOutput;

    let screen_uv = in.uv;
    var ndc = screen_uv * 2.0 - 1.0;

    let clip_far = vec4<f32>(ndc.x, ndc.y, 0.0, 1.0);
    let world_pos_h = uniforms.inv_view_projection * clip_far;
    let world_pos = world_pos_h.xyz / world_pos_h.w;

    let ori = uniforms.camera_position.xyz;
    let dir = normalize(world_pos - ori);

    if (dir.y >= 0.0) {
        discard;
    }

    let p = heightmap_tracing(ori, dir);
    let point_xz = vec2<f32>(p.x, p.z);

    var edge_alpha = 1.0;

    if (water_polygon.use_bounds == 1u) {
        if (!point_in_polygon(point_xz)) {
            discard;
        }

        let edge_dist = distance_to_polygon_edge(point_xz);
        let feather_dist = water_params.edge_feather_distance;
        if (feather_dist > 0.0) {
            edge_alpha = smoothstep(0.0, feather_dist, edge_dist);
        }
    }

    let dist = p - ori;
    let distance_to_water = length(dist);
    let n = get_normal(p, dot(dist, dist) * (0.1 / uniforms.resolution.x));
    let light = normalize(vec3<f32>(0.0, 1.0, 0.8));

    let sea_color = get_sea_color(p, n, light, dir, dist);
    let sky_color = get_sky_color(dir);

    let horizon_blend = smoothstep(-0.02, 0.0, dir.y);
    let distance_fog = smoothstep(800.0, 1800.0, distance_to_water);
    let fog_blend = max(horizon_blend, distance_fog);

    if (fog_blend > 0.995) {
        discard;
    }

    let color = mix(sea_color, sky_color, fog_blend);

    let water_clip = uniforms.projection * uniforms.view * vec4<f32>(p, 1.0);
    let water_ndc_depth = water_clip.z / water_clip.w;

    let final_alpha = edge_alpha * (1.0 - fog_blend);
    if (final_alpha < 0.01) {
        discard;
    }

    out.color = vec4<f32>(pow(color, vec3<f32>(0.65)), final_alpha);
    out.depth = water_ndc_depth;
    return out;
}
