struct MatrixUniform {
    view_proj: mat4x4f,
    viewport: vec4f,
    glyph: vec4f,
    color: vec4f,
    render_options: vec4f,
    image: vec4f,
}

@group(0) @binding(0)
var<uniform> u: MatrixUniform;

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

@group(1) @binding(1)
var base_color_sampler: sampler;

struct MaterialParams {
    base_color: vec4f,
    metallic_roughness: vec4f,
    emissive_alpha: vec4f,
    flags: vec4f,
}

@group(1) @binding(2)
var<uniform> material: MaterialParams;

@group(1) @binding(3)
var metallic_roughness_texture: texture_2d<f32>;

@group(1) @binding(4)
var metallic_roughness_sampler: sampler;

@group(1) @binding(5)
var normal_texture: texture_2d<f32>;

@group(1) @binding(6)
var normal_sampler: sampler;

@group(1) @binding(7)
var occlusion_texture: texture_2d<f32>;

@group(1) @binding(8)
var occlusion_sampler: sampler;

@group(1) @binding(9)
var emissive_texture: texture_2d<f32>;

@group(1) @binding(10)
var emissive_sampler: sampler;

fn saturate(value: f32) -> f32 {
    return clamp(value, 0.0, 1.0);
}

fn linear_to_srgb(linear: vec3f) -> vec3f {
    let x = max(linear, vec3f(0.0));
    let low = x * 12.92;
    let high = 1.055 * pow(x, vec3f(1.0 / 2.4)) - vec3f(0.055);
    return select(high, low, x <= vec3f(0.0031308));
}

fn output_color(linear_rgb: vec3f, alpha: f32) -> vec4f {
    let clamped = clamp(linear_rgb, vec3f(0.0), vec3f(1.0));
    if (u.color.w > 0.5) {
        return vec4f(clamped, alpha);
    }

    let encoded = linear_to_srgb(clamped);
    return vec4f(encoded, alpha);
}

fn range_stop_color(index: u32) -> vec3f {
    switch index {
        case 0u: {
            return vec3f(0.140, 0.690, 0.890);
        }
        case 1u: {
            return vec3f(0.250, 0.760, 0.380);
        }
        case 2u: {
            return vec3f(1.000, 0.670, 0.180);
        }
        default: {
            return vec3f(1.000, 0.255, 0.190);
        }
    }
}

fn sample_range_color(value: f32) -> vec3f {
    let t = saturate(value);

    if (t <= 0.33) {
        let local = smoothstep(0.0, 0.33, t);
        return mix(range_stop_color(0u), range_stop_color(1u), local);
    }

    if (t <= 0.66) {
        let local = smoothstep(0.33, 0.66, t);
        return mix(range_stop_color(1u), range_stop_color(2u), local);
    }

    let local = smoothstep(0.66, 1.0, t);
    return mix(range_stop_color(2u), range_stop_color(3u), local);
}


// background
struct BackgroundVertexOutput {
    @builtin(position) clip_position: vec4f,
}

@vertex
fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput {
    let positions = array<vec2f, 3>(
        vec2f(-1.0, -3.0),
        vec2f(3.0, 1.0),
        vec2f(-1.0, 1.0),
    );

    var out: BackgroundVertexOutput;
    out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0);
    return out;
}

@fragment
fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f {
    let pixel = frag_coord.xy;
    let viewport = u.viewport.xy;
    let uv = pixel / max(viewport, vec2f(1.0, 1.0));
    var color = mix(vec3f(0.018, 0.019, 0.022), vec3f(0.038, 0.040, 0.046), 1.0 - uv.y);
    let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0)));
    color *= 1.0 - vignette * 0.22;
    color += vec3f(0.010, 0.010, 0.012) * (1.0 - smoothstep(0.0, 0.85, abs(uv.y - 0.50)));

    return output_color(color, 1.0);
}


// hand image background
struct HandImageVertexOutput {
    @builtin(position) clip_position: vec4f,
    @location(0) screen_uv: vec2f,
}

@vertex
fn vs_hand_image(@builtin(vertex_index) vertex_index: u32) -> HandImageVertexOutput {
    let positions = array<vec2f, 6>(
        vec2f(-1.0, -1.0),
        vec2f(1.0, -1.0),
        vec2f(-1.0, 1.0),
        vec2f(-1.0, 1.0),
        vec2f(1.0, -1.0),
        vec2f(1.0, 1.0),
    );
    let uvs = array<vec2f, 6>(
        vec2f(0.0, 1.0),
        vec2f(1.0, 1.0),
        vec2f(0.0, 0.0),
        vec2f(0.0, 0.0),
        vec2f(1.0, 1.0),
        vec2f(1.0, 0.0),
    );

    var out: HandImageVertexOutput;
    out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0);
    out.screen_uv = uvs[vertex_index];
    return out;
}

@fragment
fn fs_hand_image(in: HandImageVertexOutput) -> @location(0) vec4f {
    let viewport_aspect = u.viewport.x / max(u.viewport.y, 1.0);
    let image_aspect = u.image.x / max(u.image.y, 1.0);

    var uv = in.screen_uv;
    if (viewport_aspect > image_aspect) {
        let image_width = image_aspect / viewport_aspect;
        uv.x = (uv.x - (1.0 - image_width) * 0.5) / image_width;
    } else {
        let image_height = viewport_aspect / image_aspect;
        uv.y = (uv.y - (1.0 - image_height) * 0.5) / image_height;
    }

    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        discard;
    }

    let color = textureSample(base_color_texture, base_color_sampler, uv);
    return output_color(color.rgb, color.a);
}


// glyph
struct GlyphVertexInput {
    @location(0) local: vec2f,
}

struct GlyphInstanceInput {
    @location(1) world_position: vec4f,
    @location(2) style: vec4f,
}

struct GlyphVertexOutput {
    @builtin(position) clip_position: vec4f,
    @location(0) local: vec2f,
    @location(1) intensity: f32,
    @location(2) display_value: f32,
}

fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 {
    let delta = abs(point - center) - half_size;
    let outside = length(max(delta, vec2f(0.0, 0.0)));
    let inside = min(max(delta.x, delta.y), 0.0);
    let dist = outside + inside;
    return 1.0 - smoothstep(0.015, 0.045, dist);
}

fn digit_segment_on(digit: u32, segment: u32) -> bool {
    switch digit {
        case 0u: { return segment != 6u; }
        case 1u: { return segment == 1u || segment == 2u; }
        case 2u: { return segment == 0u || segment == 1u || segment == 6u || segment == 4u || segment == 3u; }
        case 3u: { return segment == 0u || segment == 1u || segment == 6u || segment == 2u || segment == 3u; }
        case 4u: { return segment == 5u || segment == 6u || segment == 1u || segment == 2u; }
        case 5u: { return segment == 0u || segment == 5u || segment == 6u || segment == 2u || segment == 3u; }
        case 6u: { return segment == 0u || segment == 5u || segment == 4u || segment == 3u || segment == 2u || segment == 6u; }
        case 7u: { return segment == 0u || segment == 1u || segment == 2u; }
        case 8u: { return true; }
        default: { return segment == 0u || segment == 1u || segment == 2u || segment == 3u || segment == 5u || segment == 6u; }
    }
}

fn seven_segment_digit_alpha(local: vec2f, digit: u32) -> f32 {
    var alpha = 0.0;
    if (digit_segment_on(digit, 0u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.70), vec2f(0.38, 0.078)));
    }
    if (digit_segment_on(digit, 1u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(0.39, 0.36), vec2f(0.078, 0.335)));
    }
    if (digit_segment_on(digit, 2u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(0.39, -0.36), vec2f(0.078, 0.335)));
    }
    if (digit_segment_on(digit, 3u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(0.0, -0.70), vec2f(0.38, 0.078)));
    }
    if (digit_segment_on(digit, 4u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(-0.39, -0.36), vec2f(0.078, 0.335)));
    }
    if (digit_segment_on(digit, 5u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(-0.39, 0.36), vec2f(0.078, 0.335)));
    }
    if (digit_segment_on(digit, 6u)) {
        alpha = max(alpha, rect_alpha(local, vec2f(0.0, 0.0), vec2f(0.35, 0.075)));
    }
    return alpha;
}

fn digit_count(value: u32) -> u32 {
    if (value >= 1000u) {
        return 4u;
    }
    if (value >= 100u) {
        return 3u;
    }
    if (value >= 10u) {
        return 2u;
    }
    return 1u;
}

fn digit_at(value: u32, slot: u32, count: u32) -> u32 {
    if (count == 4u) {
        switch slot {
            case 0u: { return (value / 1000u) % 10u; }
            case 1u: { return (value / 100u) % 10u; }
            case 2u: { return (value / 10u) % 10u; }
            default: { return value % 10u; }
        }
    }
    if (count == 3u) {
        switch slot {
            case 0u: { return (value / 100u) % 10u; }
            case 1u: { return (value / 10u) % 10u; }
            default: { return value % 10u; }
        }
    }
    if (count == 2u) {
        return select(value % 10u, (value / 10u) % 10u, slot == 0u);
    }
    return value % 10u;
}

fn number_alpha(local: vec2f, display_value: f32) -> f32 {
    let value = min(u32(max(display_value + 0.5, 0.0)), 9999u);
    let count = digit_count(value);
    let count_f = f32(count);
    let slot_width = 1.74 / count_f;
    let start_x = -slot_width * (count_f - 1.0) * 0.5;
    var alpha = 0.0;

    for (var slot = 0u; slot < 4u; slot = slot + 1u) {
        if (slot < count) {
            let center_x = start_x + f32(slot) * slot_width;
            let digit_local = vec2f((local.x - center_x) / (slot_width * 0.78), local.y / 0.92);
            let digit = digit_at(value, slot, count);
            let in_slot = step(abs(local.x - center_x), slot_width * 0.48);
            alpha = max(alpha, seven_segment_digit_alpha(digit_local, digit) * in_slot);
        }
    }

    return alpha;
}

@vertex
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
    let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
    let shaped = pow(saturate(instance.style.x), 0.9);
    let pixel_size = u.glyph.x * mix(1.08, 2.20, shaped);
    let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;

    var out: GlyphVertexOutput;
    out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
    out.local = vertex.local;
    out.intensity = instance.style.x;
    out.display_value = instance.style.y;
    return out;
}

@fragment
fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
    let alpha = number_alpha(in.local, in.display_value);
    let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
    return output_color(color, alpha);
}


// dot
struct DotVertexInput {
    @location(0) local: vec2f,
}

struct DotInstanceInput {
    @location(1) world_position: vec4f,
    @location(2) style: vec4f
}

struct DotVertexOutput {
    @builtin(position) clip_position: vec4f,
    @location(0) local: vec2f,
    @location(1) intensity: f32,
}

fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32 {
    let dist = length(local);
    return 1.0 - smoothstep(radius, radius + softness, dist);
}

@vertex
fn vs_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput {
    let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
    let intensity = saturate(instance.style.x);
    let shaped = pow(intensity, 0.9);
    let pixel_size = u.glyph.x * mix(0.72, 1.85, shaped);
    let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
    var out: DotVertexOutput;
    out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
    out.local = vertex.local;
    out.intensity = intensity;
    return out;
}

@fragment
fn fs_dot(in: DotVertexOutput) -> @location(0) vec4f {
    let intensity = saturate(in.intensity);
    let base_color = sample_range_color(intensity);

    let core = circle_alpha(in.local, 0.42, 0.055);

    let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45;

    let highlight_pos = in.local - vec2f(-0.18, 0.20);

    let highlight = circle_alpha(highlight_pos, 0.16, 0.08) * 0.45;

    let  brightness = mix(0.82, 1.22, intensity);

    let color = base_color * brightness + vec3f(1.0, 1.0, 1.0) * highlight;
    
    let alpha = max(core, glow);

    return output_color(color, alpha);
}


// model
struct ModelVertexInput {
    @location(0) position: vec3f,
    @location(1) tex_coords: vec2f,
    @location(2) normal: vec3f,
    @location(3) color: vec4f,
    @location(4) tangent: vec4f,
    @location(5) model_0: vec4f,
    @location(6) model_1: vec4f,
    @location(7) model_2: vec4f,
    @location(8) model_3: vec4f,
}

struct ModelVertexOutput {
    @builtin(position) clip_position: vec4f,
    @location(0) world_normal: vec3f,
    @location(1) world_position: vec3f,
    @location(2) tex_coords: vec2f,
    @location(3) color: vec4f,
    @location(4) world_tangent: vec4f,
}

fn inverse_transpose3x3(m: mat3x3f) -> mat3x3f {
    let c0 = m[0];
    let c1 = m[1];
    let c2 = m[2];
    let r0 = cross(c1, c2);
    let r1 = cross(c2, c0);
    let r2 = cross(c0, c1);
    let det = dot(c0, r0);
    let safe_det = select(det, select(-0.000001, 0.000001, det >= 0.0), abs(det) < 0.000001);
    let inv_det = 1.0 / safe_det;
    return mat3x3f(r0 * inv_det, r1 * inv_det, r2 * inv_det);
}

@vertex
fn vs_model(vertex: ModelVertexInput) -> ModelVertexOutput {
    let model = mat4x4f(vertex.model_0, vertex.model_1, vertex.model_2, vertex.model_3);
    let world_position = model * vec4f(vertex.position, 1.0);
    let model3 = mat3x3f(model[0].xyz, model[1].xyz, model[2].xyz);
    let normal_matrix = inverse_transpose3x3(model3);

    var out: ModelVertexOutput;
    out.clip_position = u.view_proj * world_position;
    out.world_position = world_position.xyz;
    out.world_normal = normalize(normal_matrix * vertex.normal);
    out.tex_coords = vertex.tex_coords;
    out.color = vertex.color;
    out.world_tangent = vec4f(normalize(normal_matrix * vertex.tangent.xyz), vertex.tangent.w);
    return out;
}

fn distribution_ggx(normal: vec3f, half_dir: vec3f, roughness: f32) -> f32 {
    let a = roughness * roughness;
    let a2 = a * a;
    let n_dot_h = max(dot(normal, half_dir), 0.0);
    let n_dot_h2 = n_dot_h * n_dot_h;
    let denom = (n_dot_h2 * (a2 - 1.0) + 1.0);
    return a2 / max(3.14159265 * denom * denom, 0.000001);
}

fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 {
    let r = roughness + 1.0;
    let k = (r * r) / 8.0;
    return n_dot_v / max(n_dot_v * (1.0 - k) + k, 0.000001);
}

fn geometry_smith(normal: vec3f, view_dir: vec3f, light_dir: vec3f, roughness: f32) -> f32 {
    let n_dot_v = max(dot(normal, view_dir), 0.0);
    let n_dot_l = max(dot(normal, light_dir), 0.0);
    let ggx_v = geometry_schlick_ggx(n_dot_v, roughness);
    let ggx_l = geometry_schlick_ggx(n_dot_l, roughness);
    return ggx_v * ggx_l;
}

fn fresnel_schlick(cos_theta: f32, f0: vec3f) -> vec3f {
    let factor = pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
    return f0 + (vec3f(1.0) - f0) * factor;
}

fn pbr_directional_light(
    albedo: vec3f,
    normal: vec3f,
    view_dir: vec3f,
    light_dir: vec3f,
    radiance: vec3f,
    metallic: f32,
    roughness: f32,
) -> vec3f {
    let half_dir = normalize(view_dir + light_dir);
    let n_dot_l = max(dot(normal, light_dir), 0.0);
    let n_dot_v = max(dot(normal, view_dir), 0.0);
    let h_dot_v = max(dot(half_dir, view_dir), 0.0);

    let f0 = mix(vec3f(0.04), albedo, metallic);
    let f = fresnel_schlick(h_dot_v, f0);
    let d = distribution_ggx(normal, half_dir, roughness);
    let g = geometry_smith(normal, view_dir, light_dir, roughness);
    let specular = (d * g * f) / max(4.0 * n_dot_v * n_dot_l, 0.000001);

    let k_s = f;
    let k_d = (vec3f(1.0) - k_s) * (1.0 - metallic);
    let diffuse = k_d * albedo / 3.14159265;

    return (diffuse + specular) * radiance * n_dot_l;
}

fn aces_tonemap(x: vec3f) -> vec3f {
    let a = 2.51;
    let b = 0.03;
    let c = 2.43;
    let d = 0.59;
    let e = 0.14;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3f(0.0), vec3f(1.0));
}

fn material_normal(in: ModelVertexOutput, front_facing: bool) -> vec3f {
    var n = normalize(in.world_normal);
    if (!front_facing && material.flags.w > 0.5) {
        n = -n;
    }

    if (material.flags.z < 0.5) {
        return n;
    }

    let t = normalize(in.world_tangent.xyz);
    let b = normalize(cross(n, t) * in.world_tangent.w);
    let tangent_normal_sample = textureSample(normal_texture, normal_sampler, in.tex_coords).xyz;
    var tangent_normal = tangent_normal_sample * 2.0 - vec3f(1.0);
    tangent_normal.x = tangent_normal.x * material.metallic_roughness.z;
    tangent_normal.y = tangent_normal.y * material.metallic_roughness.z;

    return normalize(mat3x3f(t, b, n) * tangent_normal);
}

@fragment
fn fs_model(in: ModelVertexOutput, @builtin(front_facing) front_facing: bool) -> @location(0) vec4f {
    let base_sample = textureSample(base_color_texture, base_color_sampler, in.tex_coords);
    let base_color = base_sample * material.base_color * in.color;
    let alpha_mode = material.emissive_alpha.w;
    var alpha = base_color.a;
    if (alpha_mode < 0.5) {
        alpha = 1.0;
    } else if (alpha_mode < 1.5) {
        if (alpha < material.flags.x) {
            discard;
        }
        alpha = 1.0;
    }

    let debug_mode = u32(u.render_options.x + 0.5);
    if (debug_mode == 1u) {
        return output_color(base_color.rgb, alpha);
    }

    let mr_sample = textureSample(
        metallic_roughness_texture,
        metallic_roughness_sampler,
        in.tex_coords,
    );
    let roughness = clamp(mr_sample.g * material.metallic_roughness.y, 0.04, 1.0);
    let metallic = clamp(mr_sample.b * material.metallic_roughness.x, 0.0, 1.0);
    let ao_sample = textureSample(occlusion_texture, occlusion_sampler, in.tex_coords).r;
    let ao = mix(1.0, ao_sample, clamp(material.metallic_roughness.w, 0.0, 1.0));
    let emissive = textureSample(emissive_texture, emissive_sampler, in.tex_coords).rgb
        * material.emissive_alpha.rgb;
    let albedo = base_color.rgb;
    let normal = material_normal(in, front_facing);

    switch debug_mode {
        case 2u: {
            return output_color(normal * 0.5 + vec3f(0.5), 1.0);
        }
        case 3u: {
            return output_color(vec3f(roughness), 1.0);
        }
        case 4u: {
            return output_color(vec3f(metallic), 1.0);
        }
        case 5u: {
            return output_color(vec3f(ao), 1.0);
        }
        case 6u: {
            return output_color(emissive, 1.0);
        }
        case 7u: {
            return output_color(vec3f(fract(in.tex_coords), 0.0), 1.0);
        }
        default: {}
    }

    let view_dir = normalize(vec3f(0.0, 0.72, 0.70));
    let key = pbr_directional_light(
        albedo,
        normal,
        view_dir,
        normalize(vec3f(-0.35, 0.82, 0.44)),
        vec3f(2.15, 2.08, 1.92),
        metallic,
        roughness,
    );
    let fill = pbr_directional_light(
        albedo,
        normal,
        view_dir,
        normalize(vec3f(0.62, 0.44, -0.36)),
        vec3f(0.50, 0.50, 0.50),
        metallic,
        roughness,
    );

    let f0 = mix(vec3f(0.04), albedo, metallic);
    let f_ambient = fresnel_schlick(max(dot(normal, view_dir), 0.0), f0);
    let ambient_color = vec3f(1.0);
    let ambient_strength = 0.08;
    // Temporary neutral linear ambient term until a real IBL/HDR environment is added.
    var ambient = vec3f(0.0);
    if (u.render_options.y > 0.5) {
        let ambient_diffuse = albedo * (1.0 - metallic) * ambient_color * ambient_strength;
        let ambient_specular = f_ambient * ambient_color * ambient_strength * (1.0 - roughness * 0.55);
        ambient = (ambient_diffuse + ambient_specular) * ao;
    }

    let exposed_color = (ambient + key + fill + emissive) * max(u.render_options.w, 0.0);
    var color = exposed_color;
    if (u.render_options.z > 0.5) {
        color = aces_tonemap(exposed_color);
    }

    return output_color(color, alpha);
}
