use core::array;
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
use std::rc::Rc;
use sycamore::{prelude::*, motion::create_raf};
use web_sys::{
    console,
    window,
    KeyboardEvent,
    MouseEvent,
    WebGl2RenderingContext,
    WebGlBuffer,
    WebGlProgram,
    WebGlShader,
    WebGlUniformLocation,
    wasm_bindgen::{JsCast, JsValue}
};

use crate::{
    AppState,
    assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
};

// --- color ---

const COLOR_SIZE: usize = 3;
type ColorWithOpacity = [f32; COLOR_SIZE + 1];

fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
    let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
    color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
    color_with_opacity[COLOR_SIZE] = opacity;
    color_with_opacity
}

// --- scene data ---

struct SceneSpheres {
    representations: Vec<DVector<f64>>,
    colors_with_opacity: Vec<ColorWithOpacity>,
    highlights: Vec<f32>
}

impl SceneSpheres {
    fn new() -> SceneSpheres{
        SceneSpheres {
            representations: Vec::new(),
            colors_with_opacity: Vec::new(),
            highlights: Vec::new()
        }
    }
    
    fn len_i32(&self) -> i32 {
        self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
    }
    
    fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
        self.representations.push(representation);
        self.colors_with_opacity.push(combine_channels(color, opacity));
        self.highlights.push(highlight);
    }
}

struct ScenePoints {
    representations: Vec<DVector<f64>>,
    colors_with_opacity: Vec<ColorWithOpacity>,
    highlights: Vec<f32>,
    selections: Vec<f32>
}

impl ScenePoints {
    fn new() -> ScenePoints {
        ScenePoints {
            representations: Vec::new(),
            colors_with_opacity: Vec::new(),
            highlights: Vec::new(),
            selections: Vec::new()
        }
    }
    
    fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
        self.representations.push(representation);
        self.colors_with_opacity.push(combine_channels(color, opacity));
        self.highlights.push(highlight);
        self.selections.push(if selected { 1.0 } else { 0.0 });
    }
}

pub struct Scene {
    spheres: SceneSpheres,
    points: ScenePoints
}

impl Scene {
    fn new() -> Scene {
        Scene {
            spheres: SceneSpheres::new(),
            points: ScenePoints::new()
        }
    }
}

pub trait DisplayItem {
    fn show(&self, scene: &mut Scene, selected: bool);
    
    // the smallest positive depth, represented as a multiple of `dir`, where
    // the line generated by `dir` hits the element. returns `None` if the line
    // misses the element
    fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>;
}

impl DisplayItem for Sphere {
    fn show(&self, scene: &mut Scene, selected: bool) {
        /* SCAFFOLDING */
        const DEFAULT_OPACITY: f32 = 0.5;
        const GHOST_OPACITY: f32 = 0.2;
        const HIGHLIGHT: f32 = 0.2;
        
        let representation = self.representation.get_clone_untracked();
        let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
        let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
        let highlight = if selected { 1.0 } else { HIGHLIGHT };
        scene.spheres.push(representation, color, opacity, highlight);
    }
    
    // this method should be kept synchronized with `sphere_cast` in
    // `spheres.frag`, which does essentially the same thing on the GPU side
    fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, _pixel_size: f64) -> Option<f64> {
        // if `a/b` is less than this threshold, we approximate
        // `a*u^2 + b*u + c` by the linear function `b*u + c`
        const DEG_THRESHOLD: f64 = 1e-9;
        
        let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
        let a = -rep[3] * dir.norm_squared();
        let b = rep.rows_range(..3).dot(&dir);
        let c = -rep[4];
        
        let adjust = 4.0*a*c/(b*b);
        if adjust < 1.0 {
            // as long as `b` is non-zero, the linear approximation of
            //
            //   a*u^2 + b*u + c
            //
            // at `u = 0` will reach zero at a finite depth `u_lin`. the root of
            // the quadratic adjacent to `u_lin` is stored in `lin_root`. if
            // both roots have the same sign, `lin_root` will be the one closer
            // to `u = 0`
            let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
            let lin_root = -(2.0*c)/b / square_rect_ratio;
            if a.abs() > DEG_THRESHOLD * b.abs() {
                if lin_root > 0.0 {
                    Some(lin_root)
                } else {
                    let other_root = -b/(2.*a) * square_rect_ratio;
                    (other_root > 0.0).then_some(other_root)
                }
            } else {
                (lin_root > 0.0).then_some(lin_root)
            }
        } else {
            // the line through `dir` misses the sphere completely
            None
        }
    }
}

impl DisplayItem for Point {
    fn show(&self, scene: &mut Scene, selected: bool) {
        /* SCAFFOLDING */
        const GHOST_OPACITY: f32 = 0.4;
        const HIGHLIGHT: f32 = 0.5;
        
        let representation = self.representation.get_clone_untracked();
        let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
        let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
        let highlight = if selected { 1.0 } else { HIGHLIGHT };
        scene.points.push(representation, color, opacity, highlight, selected);
    }
    
    /* SCAFFOLDING */
    fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> {
        let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
        if rep[2] < 0.0 {
            // this constant should be kept synchronized with `point.frag`
            const POINT_RADIUS_PX: f64 = 4.0;
            
            // find the radius of the point in screen projection units
            let point_radius_proj = POINT_RADIUS_PX * pixel_size;
            
            // find the squared distance between the screen projections of the
            // ray and the point
            let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
            let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
            let dist_sq = (dir_proj - rep_proj).norm_squared();
            
            // if the ray hits the point, return its depth
            if dist_sq < point_radius_proj * point_radius_proj {
                Some(rep[2] / dir[2])
            } else {
                None
            }
        } else {
            None
        }
    }
}

// --- WebGL utilities ---

fn compile_shader(
    context: &WebGl2RenderingContext,
    shader_type: u32,
    source: &str,
) -> WebGlShader {
    let shader = context.create_shader(shader_type).unwrap();
    context.shader_source(&shader, source);
    context.compile_shader(&shader);
    shader
}

fn set_up_program(
    context: &WebGl2RenderingContext,
    vertex_shader_source: &str,
    fragment_shader_source: &str
) -> WebGlProgram {
    // compile the shaders
    let vertex_shader = compile_shader(
        &context,
        WebGl2RenderingContext::VERTEX_SHADER,
        vertex_shader_source,
    );
    let fragment_shader = compile_shader(
        &context,
        WebGl2RenderingContext::FRAGMENT_SHADER,
        fragment_shader_source,
    );
    
    // create the program and attach the shaders
    let program = context.create_program().unwrap();
    context.attach_shader(&program, &vertex_shader);
    context.attach_shader(&program, &fragment_shader);
    context.link_program(&program);
    
    /* DEBUG */
    // report whether linking succeeded
    let link_status = context
        .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
        .as_bool()
        .unwrap();
    let link_msg = if link_status {
        "Linked successfully"
    } else {
        "Linking failed"
    };
    console::log_1(&JsValue::from(link_msg));
    
    program
}

fn get_uniform_array_locations<const N: usize>(
    context: &WebGl2RenderingContext,
    program: &WebGlProgram,
    var_name: &str,
    member_name_opt: Option<&str>
) -> [Option<WebGlUniformLocation>; N] {
    array::from_fn(|n| {
        let name = match member_name_opt {
            Some(member_name) => format!("{var_name}[{n}].{member_name}"),
            None => format!("{var_name}[{n}]")
        };
        context.get_uniform_location(&program, name.as_str())
    })
}

// bind the given vertex buffer object to the given vertex attribute
fn bind_to_attribute(
    context: &WebGl2RenderingContext,
    attr_index: u32,
    attr_size: i32,
    buffer: &Option<WebGlBuffer>
) {
    context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
    context.vertex_attrib_pointer_with_i32(
        attr_index,
        attr_size,
        WebGl2RenderingContext::FLOAT,
        false, // don't normalize
        0, // zero stride
        0, // zero offset
    );
}

// load the given data into a new vertex buffer object
fn load_new_buffer(
    context: &WebGl2RenderingContext,
    data: &[f32]
) -> Option<WebGlBuffer> {
    // create a buffer and bind it to ARRAY_BUFFER
    let buffer = context.create_buffer();
    context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
    
    // load the given data into the buffer. this block is unsafe because
    // `Float32Array::view` creates a raw view into our module's
    // `WebAssembly.Memory` buffer. allocating more memory will change the
    // buffer, invalidating the view, so we have to make sure we don't allocate
    // any memory until the view is dropped. we're okay here because the view is
    // used as soon as it's created
    unsafe {
        context.buffer_data_with_array_buffer_view(
            WebGl2RenderingContext::ARRAY_BUFFER,
            &js_sys::Float32Array::view(&data),
            WebGl2RenderingContext::STATIC_DRAW,
        );
    }
    
    buffer
}

fn bind_new_buffer_to_attribute(
    context: &WebGl2RenderingContext,
    attr_index: u32,
    attr_size: i32,
    data: &[f32]
) {
    let buffer = load_new_buffer(context, data);
    bind_to_attribute(context, attr_index, attr_size, &buffer);
}

// the direction in camera space that a mouse event is pointing along
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
    let target: web_sys::Element = event.target().unwrap().unchecked_into();
    let rect = target.get_bounding_client_rect();
    let width = rect.width();
    let height = rect.height();
    let shortdim = width.min(height);
    
    // this constant should be kept synchronized with `spheres.frag` and
    // `point.vert`
    const FOCAL_SLOPE: f64 = 0.3;
    
    (
        Vector3::new(
            FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
            FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
            -1.0
        ),
        FOCAL_SLOPE * 2.0 / shortdim
    )
}

// --- display component ---

#[component]
pub fn Display() -> View {
    let state = use_context::<AppState>();
    
    // canvas
    let display = create_node_ref();
    
    // viewpoint
    let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
    
    // navigation
    let pitch_up = create_signal(0.0);
    let pitch_down = create_signal(0.0);
    let yaw_right = create_signal(0.0);
    let yaw_left = create_signal(0.0);
    let roll_ccw = create_signal(0.0);
    let roll_cw = create_signal(0.0);
    let zoom_in = create_signal(0.0);
    let zoom_out = create_signal(0.0);
    let turntable = create_signal(false); /* BENCHMARKING */
    
    // manipulation
    let translate_neg_x = create_signal(0.0);
    let translate_pos_x = create_signal(0.0);
    let translate_neg_y = create_signal(0.0);
    let translate_pos_y = create_signal(0.0);
    let translate_neg_z = create_signal(0.0);
    let translate_pos_z = create_signal(0.0);
    let shrink_neg = create_signal(0.0);
    let shrink_pos = create_signal(0.0);
    
    // change listener
    let scene_changed = create_signal(true);
    create_effect(move || {
        state.assembly.elements.with(|elts| {
            for elt in elts {
                elt.representation().track();
                elt.ghost().track();
            }
        });
        state.selection.track();
        scene_changed.set(true);
    });
    
    /* INSTRUMENTS */
    const SAMPLE_PERIOD: i32 = 60;
    let mut last_sample_time = 0.0;
    let mut frames_since_last_sample = 0;
    let mean_frame_interval = create_signal(0.0);
    
    let assembly_for_raf = state.assembly.clone();
    on_mount(move || {
        // timing
        let mut last_time = 0.0;
        
        // viewpoint
        const ROT_SPEED: f64 = 0.4; // in radians per second
        const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
        const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
        let mut orientation = DMatrix::<f64>::identity(5, 5);
        let mut rotation = DMatrix::<f64>::identity(5, 5);
        let mut location_z: f64 = 5.0;
        
        // manipulation
        const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
        const SHRINKING_SPEED: f64 = 0.15; // in length units per second
        
        // display parameters
        const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
        const DEBUG_MODE: i32 = 0; /* DEBUG */
        
        /* INSTRUMENTS */
        let performance = window().unwrap().performance().unwrap();
        
        // get the display canvas
        let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
        let ctx = canvas
            .get_context("webgl2")
            .unwrap()
            .unwrap()
            .dyn_into::<WebGl2RenderingContext>()
            .unwrap();
        
        // disable depth testing
        ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
        
        // set blend mode
        ctx.enable(WebGl2RenderingContext::BLEND);
        ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
        
        // set up the sphere rendering program
        let sphere_program = set_up_program(
            &ctx,
            include_str!("identity.vert"),
            include_str!("spheres.frag")
        );
        
        // set up the point rendering program
        let point_program = set_up_program(
            &ctx,
            include_str!("point.vert"),
            include_str!("point.frag")
        );
        
        /* DEBUG */
        // print the maximum number of vectors that can be passed as
        // uniforms to a fragment shader. the OpenGL ES 3.0 standard
        // requires this maximum to be at least 224, as discussed in the
        // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
        // here:
        //
        //   https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
        //
        // there are also other size limits. for example, on Aaron's
        // machine, the the length of a float or genType array seems to be
        // capped at 1024 elements
        console::log_2(
            &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
            &JsValue::from("uniform vectors available")
        );
        
        // find the sphere program's vertex attribute
        let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
        
        // find the sphere program's uniforms
        const SPHERE_MAX: usize = 200;
        let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
        let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
            &ctx, &sphere_program, "sphere_list", Some("sp")
        );
        let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
            &ctx, &sphere_program, "sphere_list", Some("lt")
        );
        let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
            &ctx, &sphere_program, "color_list", None
        );
        let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
            &ctx, &sphere_program, "highlight_list", None
        );
        let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
        let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
        let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
        let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
        
        // load the viewport vertex positions into a new vertex buffer object
        const VERTEX_CNT: usize = 6;
        let viewport_positions: [f32; 3*VERTEX_CNT] = [
            // northwest triangle
            -1.0, -1.0, 0.0,
            -1.0,  1.0, 0.0,
             1.0,  1.0, 0.0,
            // southeast triangle
            -1.0, -1.0, 0.0,
             1.0,  1.0, 0.0,
             1.0, -1.0, 0.0
        ];
        let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
        
        // find the point program's vertex attributes
        let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
        let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
        let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
        let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
        
        // set up a repainting routine
        let (_, start_animation_loop, _) = create_raf(move || {
            // get the time step
            let time = performance.now();
            let time_step = 0.001*(time - last_time);
            last_time = time;
            
            // get the navigation state
            let pitch_up_val = pitch_up.get();
            let pitch_down_val = pitch_down.get();
            let yaw_right_val = yaw_right.get();
            let yaw_left_val = yaw_left.get();
            let roll_ccw_val = roll_ccw.get();
            let roll_cw_val = roll_cw.get();
            let zoom_in_val = zoom_in.get();
            let zoom_out_val = zoom_out.get();
            let turntable_val = turntable.get(); /* BENCHMARKING */
            
            // get the manipulation state
            let translate_neg_x_val = translate_neg_x.get();
            let translate_pos_x_val = translate_pos_x.get();
            let translate_neg_y_val = translate_neg_y.get();
            let translate_pos_y_val = translate_pos_y.get();
            let translate_neg_z_val = translate_neg_z.get();
            let translate_pos_z_val = translate_pos_z.get();
            let shrink_neg_val = shrink_neg.get();
            let shrink_pos_val = shrink_pos.get();
            
            // update the assembly's orientation
            let ang_vel = {
                let pitch = pitch_up_val - pitch_down_val;
                let yaw = yaw_right_val - yaw_left_val;
                let roll = roll_ccw_val - roll_cw_val;
                if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
                    ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
                } else {
                    Vector3::zeros()
                }
            } /* BENCHMARKING */ + if turntable_val {
                Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
            } else {
                Vector3::zeros()
            };
            let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
            rotation_sp.copy_from(
                Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
            );
            orientation = &rotation * &orientation;
            
            // update the assembly's location
            let zoom = zoom_out_val - zoom_in_val;
            location_z *= (time_step * ZOOM_SPEED * zoom).exp();
            
            // manipulate the assembly
            if state.selection.with(|sel| sel.len() == 1) {
                let sel = state.selection.with(
                    |sel| sel.into_iter().next().unwrap().clone()
                );
                let translate_x = translate_pos_x_val - translate_neg_x_val;
                let translate_y = translate_pos_y_val - translate_neg_y_val;
                let translate_z = translate_pos_z_val - translate_neg_z_val;
                let shrink = shrink_pos_val - shrink_neg_val;
                let translating =
                    translate_x != 0.0
                    || translate_y != 0.0
                    || translate_z != 0.0;
                if translating || shrink != 0.0 {
                    let elt_motion = {
                        let u = if translating {
                            TRANSLATION_SPEED * Vector3::new(
                                translate_x, translate_y, translate_z
                            ).normalize()
                        } else {
                            Vector3::zeros()
                        };
                        time_step * DVector::from_column_slice(
                            &[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
                        )
                    };
                    assembly_for_raf.deform(
                        vec![
                            ElementMotion {
                                element: sel,
                                velocity: elt_motion.as_view()
                            }
                        ]
                    );
                    scene_changed.set(true);
                }
            }
            
            if scene_changed.get() {
                const SPACE_DIM: usize = 3;
                const COLOR_SIZE: usize = 3;
                
                /* INSTRUMENTS */
                // measure mean frame interval
                frames_since_last_sample += 1;
                if frames_since_last_sample >= SAMPLE_PERIOD {
                    mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
                    last_sample_time = time;
                    frames_since_last_sample = 0;
                }
                
                // --- get the assembly ---
                
                let mut scene = Scene::new();
                
                // find the map from assembly space to world space
                let location = {
                    let u = -location_z;
                    DMatrix::from_column_slice(5, 5, &[
                        1.0, 0.0,   0.0, 0.0, 0.0,
                        0.0, 1.0,   0.0, 0.0, 0.0,
                        0.0, 0.0,   1.0, 0.0,   u,
                        0.0, 0.0, 2.0*u, 1.0, u*u,
                        0.0, 0.0,   0.0, 0.0, 1.0
                    ])
                };
                let asm_to_world = &location * &orientation;
                
                // set up the scene
                state.assembly.elements.with_untracked(
                    |elts| for elt in elts {
                        let selected = state.selection.with(|sel| sel.contains(elt));
                        elt.show(&mut scene, selected);
                    }
                );
                let sphere_cnt = scene.spheres.len_i32();
                
                // --- draw the spheres ---
                
                // use the sphere rendering program
                ctx.use_program(Some(&sphere_program));
                
                // enable the sphere program's vertex attribute
                ctx.enable_vertex_attrib_array(viewport_position_attr);
                
                // write the spheres in world coordinates
                let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
                    |rep| (&asm_to_world * rep).cast::<f32>()
                ).collect();
                
                // set the resolution
                let width = canvas.width() as f32;
                let height = canvas.height() as f32;
                ctx.uniform2f(resolution_loc.as_ref(), width, height);
                ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
                
                // pass the scene data
                ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
                for n in 0..sphere_reps_world.len() {
                    let v = &sphere_reps_world[n];
                    ctx.uniform3fv_with_f32_array(
                        sphere_sp_locs[n].as_ref(),
                        v.rows(0, 3).as_slice()
                    );
                    ctx.uniform2fv_with_f32_array(
                        sphere_lt_locs[n].as_ref(),
                        v.rows(3, 2).as_slice()
                    );
                    ctx.uniform4fv_with_f32_array(
                        sphere_color_locs[n].as_ref(),
                        &scene.spheres.colors_with_opacity[n]
                    );
                    ctx.uniform1f(
                        sphere_highlight_locs[n].as_ref(),
                        scene.spheres.highlights[n]
                    );
                }
                
                // pass the display parameters
                ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
                ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
                
                // bind the viewport vertex position buffer to the position
                // attribute in the vertex shader
                bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
                
                // draw the scene
                ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
                
                // disable the sphere program's vertex attribute
                ctx.disable_vertex_attrib_array(viewport_position_attr);
                
                // --- draw the points ---
                
                if !scene.points.representations.is_empty() {
                    // use the point rendering program
                    ctx.use_program(Some(&point_program));
                    
                    // enable the point program's vertex attributes
                    ctx.enable_vertex_attrib_array(point_position_attr);
                    ctx.enable_vertex_attrib_array(point_color_attr);
                    ctx.enable_vertex_attrib_array(point_highlight_attr);
                    ctx.enable_vertex_attrib_array(point_selection_attr);
                    
                    // write the points in world coordinates
                    let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
                    let point_positions = DMatrix::from_columns(
                        &scene.points.representations.into_iter().map(
                            |rep| &asm_to_world_sp * rep
                        ).collect::<Vec<_>>().as_slice()
                    ).cast::<f32>();
                    
                    // load the point positions and colors into new buffers and
                    // bind them to the corresponding attributes in the vertex
                    // shader
                    bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
                    bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
                    bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
                    bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
                    
                    // draw the scene
                    ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
                    
                    // disable the point program's vertex attributes
                    ctx.disable_vertex_attrib_array(point_position_attr);
                    ctx.disable_vertex_attrib_array(point_color_attr);
                    ctx.disable_vertex_attrib_array(point_highlight_attr);
                    ctx.disable_vertex_attrib_array(point_selection_attr);
                }
                
                // --- update the display state ---
                
                // update the viewpoint
                assembly_to_world.set(asm_to_world);
                
                // clear the scene change flag
                scene_changed.set(
                    pitch_up_val != 0.0
                    || pitch_down_val != 0.0
                    || yaw_left_val != 0.0
                    || yaw_right_val != 0.0
                    || roll_cw_val != 0.0
                    || roll_ccw_val != 0.0
                    || zoom_in_val != 0.0
                    || zoom_out_val != 0.0
                    || turntable_val /* BENCHMARKING */
                );
            } else {
                frames_since_last_sample = 0;
                mean_frame_interval.set(-1.0);
            }
        });
        start_animation_loop();
    });
    
    let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
        let mut navigating = true;
        let shift = event.shift_key();
        match event.key().as_str() {
            "ArrowUp" if shift => zoom_in.set(value),
            "ArrowDown" if shift => zoom_out.set(value),
            "ArrowUp" => pitch_up.set(value),
            "ArrowDown" => pitch_down.set(value),
            "ArrowRight" if shift => roll_cw.set(value),
            "ArrowLeft" if shift => roll_ccw.set(value),
            "ArrowRight" => yaw_right.set(value),
            "ArrowLeft" => yaw_left.set(value),
            _ => navigating = false
        };
        if navigating {
            scene_changed.set(true);
            event.prevent_default();
        }
    };
    
    let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
        let mut manipulating = true;
        let shift = event.shift_key();
        match event.key().as_str() {
            "d" | "D" => translate_pos_x.set(value),
            "a" | "A" => translate_neg_x.set(value),
            "w" | "W" if shift => translate_neg_z.set(value),
            "s" | "S" if shift => translate_pos_z.set(value),
            "w" | "W" => translate_pos_y.set(value),
            "s" | "S" => translate_neg_y.set(value),
            "]" | "}" => shrink_neg.set(value),
            "[" | "{" => shrink_pos.set(value),
            _ => manipulating = false
        };
        if manipulating {
            event.prevent_default();
        }
    };
    
    view! {
        /* TO DO */
        // switch back to integer-valued parameters when that becomes possible
        // again
        canvas(
            ref=display,
            width="600",
            height="600",
            tabindex="0",
            on:keydown=move |event: KeyboardEvent| {
                if event.key() == "Shift" {
                    // swap navigation inputs
                    roll_cw.set(yaw_right.get());
                    roll_ccw.set(yaw_left.get());
                    zoom_in.set(pitch_up.get());
                    zoom_out.set(pitch_down.get());
                    yaw_right.set(0.0);
                    yaw_left.set(0.0);
                    pitch_up.set(0.0);
                    pitch_down.set(0.0);
                    
                    // swap manipulation inputs
                    translate_pos_z.set(translate_neg_y.get());
                    translate_neg_z.set(translate_pos_y.get());
                    translate_pos_y.set(0.0);
                    translate_neg_y.set(0.0);
                } else {
                    if event.key() == "Enter" { /* BENCHMARKING */
                        turntable.set_fn(|turn| !turn);
                        scene_changed.set(true);
                    }
                    set_nav_signal(&event, 1.0);
                    set_manip_signal(&event, 1.0);
                }
            },
            on:keyup=move |event: KeyboardEvent| {
                if event.key() == "Shift" {
                    // swap navigation inputs
                    yaw_right.set(roll_cw.get());
                    yaw_left.set(roll_ccw.get());
                    pitch_up.set(zoom_in.get());
                    pitch_down.set(zoom_out.get());
                    roll_cw.set(0.0);
                    roll_ccw.set(0.0);
                    zoom_in.set(0.0);
                    zoom_out.set(0.0);
                    
                    // swap manipulation inputs
                    translate_pos_y.set(translate_neg_z.get());
                    translate_neg_y.set(translate_pos_z.get());
                    translate_pos_z.set(0.0);
                    translate_neg_z.set(0.0);
                } else {
                    set_nav_signal(&event, 0.0);
                    set_manip_signal(&event, 0.0);
                }
            },
            on:blur=move |_| {
                pitch_up.set(0.0);
                pitch_down.set(0.0);
                yaw_right.set(0.0);
                yaw_left.set(0.0);
                roll_ccw.set(0.0);
                roll_cw.set(0.0);
            },
            on:click=move |event: MouseEvent| {
                // find the nearest element along the pointer direction
                let (dir, pixel_size) = event_dir(&event);
                console::log_1(&JsValue::from(dir.to_string()));
                let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
                let tangible_elts = state.assembly.elements
                    .get_clone_untracked()
                    .into_iter()
                    .filter(|elt| !elt.ghost().get());
                for elt in tangible_elts {
                    match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
                        Some(depth) => match clicked {
                            Some((_, best_depth)) => {
                                if depth < best_depth {
                                    clicked = Some((elt, depth))
                                }
                            },
                            None => clicked = Some((elt, depth))
                        }
                        None => ()
                    };
                }
                
                // if we clicked something, select it
                match clicked {
                    Some((elt, _)) => state.select(&elt, event.shift_key()),
                    None => state.selection.update(|sel| sel.clear())
                };
            }
        )
    }
}