use itertools::izip;
use nalgebra::Vector3;
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};

use crate::{
    engine,
    AppState,
    assembly::{
        Assembly,
        Element,
        ElementColor,
        InversiveDistanceRegulator,
        Point,
        Sphere
    },
    specified::SpecifiedValue
};

/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_gen_assemb(assembly: &Assembly) {
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("gemini_a"),
            String::from("Castor"),
            [1.00_f32, 0.25_f32, 0.00_f32],
            engine::sphere(0.5, 0.5, 0.0, 1.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("gemini_b"),
            String::from("Pollux"),
            [0.00_f32, 0.25_f32, 1.00_f32],
            engine::sphere(-0.5, -0.5, 0.0, 1.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("ursa_major"),
            String::from("Ursa major"),
            [0.25_f32, 0.00_f32, 1.00_f32],
            engine::sphere(-0.5, 0.5, 0.0, 0.75)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("ursa_minor"),
            String::from("Ursa minor"),
            [0.25_f32, 1.00_f32, 0.00_f32],
            engine::sphere(0.5, -0.5, 0.0, 0.5)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("moon_deimos"),
            String::from("Deimos"),
            [0.75_f32, 0.75_f32, 0.00_f32],
            engine::sphere(0.0, 0.15, 1.0, 0.25)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("moon_phobos"),
            String::from("Phobos"),
            [0.00_f32, 0.75_f32, 0.50_f32],
            engine::sphere(0.0, -0.15, -1.0, 0.25)
        )
    );
}

/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_low_curv_assemb(assembly: &Assembly) {
    let a = 0.75_f64.sqrt();
    let _ = assembly.try_insert_element(
        Sphere::new(
            "central".to_string(),
            "Central".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere(0.0, 0.0, 0.0, 1.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "assemb_plane".to_string(),
            "Assembly plane".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "side1".to_string(),
            "Side 1".to_string(),
            [1.00_f32, 0.00_f32, 0.25_f32],
            engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "side2".to_string(),
            "Side 2".to_string(),
            [0.25_f32, 1.00_f32, 0.00_f32],
            engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "side3".to_string(),
            "Side 3".to_string(),
            [0.00_f32, 0.25_f32, 1.00_f32],
            engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "corner1".to_string(),
            "Corner 1".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            "corner2".to_string(),
            "Corner 2".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
        )
    );
    let _ = assembly.try_insert_element(
        Sphere::new(
            String::from("corner3"),
            String::from("Corner 3"),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
        )
    );
}

/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_pointed_assemb(assembly: &Assembly) {
    let _ = assembly.try_insert_element(
        Point::new(
            "point_front".to_string(),
            "Front point".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::point(0.0, 0.0, FRAC_1_SQRT_2)
        )
    );
    let _ = assembly.try_insert_element(
        Point::new(
            "point_back".to_string(),
            "Back point".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::point(0.0, 0.0, -FRAC_1_SQRT_2)
        )
    );
    for index_x in 0..=1 {
        for index_y in 0..=1 {
            let x = index_x as f64 - 0.5;
            let y = index_y as f64 - 0.5;
            
            let _ = assembly.try_insert_element(
                Sphere::new(
                    format!("sphere{index_x}{index_y}"),
                    format!("Sphere {index_x}{index_y}"),
                    [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
                    engine::sphere(x, y, 0.0, 1.0)
                )
            );
            
            let _ = assembly.try_insert_element(
                Point::new(
                    format!("point{index_x}{index_y}"),
                    format!("Point {index_x}{index_y}"),
                    [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
                    engine::point(x, y, 0.0)
                )
            );
        }
    }
}

// to finish setting up the problem, fix the following curvatures:
//   sun      1
//   moon     5/3 = 1.666666666666666...
//   chain1   2
// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization
// failures before they happen, or resolves them after they happen. the result
// depends sensitively on the translation direction, suggesting that realization
// is failing because the engine is having trouble breaking a symmetry
// /* TO DO */
// the engine's performance on this problem is scale-dependent! with the current
// initial conditions, realization fails for any order of imposing the remaining
// curvature constraints. scaling everything up by a factor of ten, as done in
// the original problem, makes realization succeed reliably. one potentially
// relevant difference is that a lot of the numbers in the current initial
// conditions are exactly representable as floats, unlike the analogous numbers
// in the scaled-up problem. the inexact representations might break the
// symmetry that's getting the engine stuck
fn load_irisawa_hexlet_assemb(assembly: &Assembly) {
    let index_range = 1..=6;
    let colors = [
        [1.00_f32, 0.00_f32, 0.25_f32],
        [1.00_f32, 0.25_f32, 0.00_f32],
        [0.75_f32, 0.75_f32, 0.00_f32],
        [0.25_f32, 1.00_f32, 0.00_f32],
        [0.00_f32, 0.25_f32, 1.00_f32],
        [0.25_f32, 0.00_f32, 1.00_f32]
    ].into_iter();
    
    // create the spheres
    let spheres = [
        Sphere::new(
            "outer".to_string(),
            "Outer".to_string(),
            [0.5_f32, 0.5_f32, 0.5_f32],
            engine::sphere(0.0, 0.0, 0.0, 1.5)
        ),
        Sphere::new(
            "sun".to_string(),
            "Sun".to_string(),
            [0.75_f32, 0.75_f32, 0.75_f32],
            engine::sphere(0.0, -0.75, 0.0, 0.75)
        ),
        Sphere::new(
            "moon".to_string(),
            "Moon".to_string(),
            [0.25_f32, 0.25_f32, 0.25_f32],
            engine::sphere(0.0, 0.75, 0.0, 0.75)
        ),
    ].into_iter().chain(
        index_range.clone().zip(colors).map(
            |(k, color)| {
                let ang = (k as f64) * PI/3.0;
                Sphere::new(
                    format!("chain{k}"),
                    format!("Chain {k}"),
                    color,
                    engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5)
                )
            }
        )
    );
    for sphere in spheres {
        let _ = assembly.try_insert_element(sphere);
    }
    
    // put the outer sphere in ghost mode and fix its curvature
    let outer = assembly.elements_by_id.with_untracked(
        |elts_by_id| elts_by_id["outer"].clone()
    );
    outer.ghost().set(true);
    let outer_curvature_regulator = outer.regulators().with_untracked(
        |regs| regs.first().unwrap().clone()
    );
    outer_curvature_regulator.set_point().set(
        SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap()
    );
    
    // impose the desired tangencies
    let [outer, sun, moon] = ["outer", "sun", "moon"].map(
        |id| assembly.elements_by_id.with_untracked(
            |elts_by_id| elts_by_id[id].clone()
        )
    );
    let chain = index_range.map(
        |k| assembly.elements_by_id.with_untracked(
            |elts_by_id| elts_by_id[&format!("chain{k}")].clone()
        )
    );
    for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) {
        for (other_sphere, inversive_distance) in [
            (outer.clone(), "1"),
            (sun.clone(), "-1"),
            (moon.clone(), "-1"),
            (chain_sphere_next.clone(), "-1")
        ] {
            let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]);
            tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap());
            assembly.insert_regulator(Rc::new(tangency));
        }
    }
    
    let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]);
    outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
    assembly.insert_regulator(Rc::new(outer_sun_tangency));
    
    let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]);
    outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
    assembly.insert_regulator(Rc::new(outer_moon_tangency));
}

// setting the inversive distances between the vertices to -2 gives a regular
// tetrahedron with side length 1, whose insphere and circumsphere have radii
// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an
// inversive distance of -1 between the insphere and each face, and then set an
// inversive distance of 0 between the circumsphere and each vertex
fn load_radius_ratio_assemb(assembly: &Assembly) {
    let index_range = 1..=4;
    
    // create the spheres
    const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
    let spheres = [
        Sphere::new(
            "sphere_faces".to_string(),
            "Insphere".to_string(),
            GRAY,
            engine::sphere(0.0, 0.0, 0.0, 0.5)
        ),
        Sphere::new(
            "sphere_vertices".to_string(),
            "Circumsphere".to_string(),
            GRAY,
            engine::sphere(0.0, 0.0, 0.0, 0.25)
        )
    ];
    for sphere in spheres {
        let _ = assembly.try_insert_element(sphere);
    }
    
    // create the vertices
    let vertices = izip!(
        index_range.clone(),
        [
            [1.00_f32, 0.50_f32, 0.75_f32],
            [1.00_f32, 0.75_f32, 0.50_f32],
            [1.00_f32, 1.00_f32, 0.50_f32],
            [0.75_f32, 0.50_f32, 1.00_f32]
        ].into_iter(),
        [
            engine::point(-0.6, -0.8, -0.6),
            engine::point(-0.6, 0.8, 0.6),
            engine::point(0.6, -0.8, 0.6),
            engine::point(0.6, 0.8, -0.6)
        ].into_iter()
    ).map(
        |(k, color, representation)| {
            Point::new(
                format!("v{k}"),
                format!("Vertex {k}"),
                color,
                representation
            )
        }
    );
    for vertex in vertices {
        let _ = assembly.try_insert_element(vertex);
    }
    
    // create the faces
    let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize();
    let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6));
    let faces = izip!(
        index_range.clone(),
        [
            [1.00_f32, 0.00_f32, 0.25_f32],
            [1.00_f32, 0.25_f32, 0.00_f32],
            [0.75_f32, 0.75_f32, 0.00_f32],
            [0.25_f32, 0.00_f32, 1.00_f32]
        ].into_iter(),
        [
            engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0),
            engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0),
            engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0),
            engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0)
        ].into_iter()
    ).map(
        |(k, color, representation)| {
            Sphere::new(
                format!("f{k}"),
                format!("Face {k}"),
                color,
                representation
            )
        }
    );
    for face in faces {
        face.ghost().set(true);
        let _ = assembly.try_insert_element(face);
    }
    
    // impose the constraints
    for j in index_range.clone() {
        let [face_j, vertex_j] = [
            format!("f{j}"),
            format!("v{j}")
        ].map(
            |id| assembly.elements_by_id.with_untracked(
                |elts_by_id| elts_by_id[&id].clone()
            )
        );
        
        // make the faces planar
        let curvature_regulator = face_j.regulators().with_untracked(
            |regs| regs.first().unwrap().clone()
        );
        curvature_regulator.set_point().set(
            SpecifiedValue::try_from("0".to_string()).unwrap()
        );
        
        for k in index_range.clone().filter(|&index| index != j) {
            let vertex_k = assembly.elements_by_id.with_untracked(
                |elts_by_id| elts_by_id[&format!("v{k}")].clone()
            );
            
            // fix the distances between the vertices
            if j < k {
                let distance_regulator = InversiveDistanceRegulator::new(
                    [vertex_j.clone(), vertex_k.clone()]
                );
                assembly.insert_regulator(Rc::new(distance_regulator));
            }
            
            // put the vertices on the faces
            let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]);
            incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
            assembly.insert_regulator(Rc::new(incidence_regulator));
        }
    }
}

#[component]
pub fn AddRemove() -> View {
    /* DEBUG */
    let assembly_name = create_signal("general".to_string());
    create_effect(move || {
        // get name of chosen assembly
        let name = assembly_name.get_clone();
        console::log_1(
            &JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
        );
        
        batch(|| {
            let state = use_context::<AppState>();
            let assembly = &state.assembly;
            
            // clear state
            assembly.regulators.update(|regs| regs.clear());
            assembly.elements.update(|elts| elts.clear());
            assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
            state.selection.update(|sel| sel.clear());
            
            // load assembly
            match name.as_str() {
                "general" => load_gen_assemb(assembly),
                "low-curv" => load_low_curv_assemb(assembly),
                "pointed" => load_pointed_assemb(assembly),
                "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly),
                "radius-ratio" => load_radius_ratio_assemb(assembly),
                _ => ()
            };
        });
    });
    
    view! {
        div(id="add-remove") {
            button(
                on:click=|_| {
                    let state = use_context::<AppState>();
                    state.assembly.insert_element_default::<Sphere>();
                }
            ) { "Add sphere" }
            button(
                on:click=|_| {
                    let state = use_context::<AppState>();
                    state.assembly.insert_element_default::<Point>();
                }
            ) { "Add point" }
            button(
                class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
                disabled={
                    let state = use_context::<AppState>();
                    state.selection.with(|sel| sel.len() != 2)
                },
                on:click=|_| {
                    let state = use_context::<AppState>();
                    let subjects: [_; 2] = state.selection.with(
                        // the button is only enabled when two elements are
                        // selected, so we know the cast to a two-element array
                        // will succeed
                        |sel| sel
                            .clone()
                            .into_iter()
                            .collect::<Vec<_>>()
                            .try_into()
                            .unwrap()
                    );
                    state.assembly.insert_regulator(
                        Rc::new(InversiveDistanceRegulator::new(subjects))
                    );
                    state.selection.update(|sel| sel.clear());
                }
            ) { "🔗" }
            select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
                option(value="general") { "General" }
                option(value="low-curv") { "Low-curvature" }
                option(value="pointed") { "Pointed" }
                option(value="irisawa-hexlet") { "Irisawa hexlet" }
                option(value="radius-ratio") { "Radius ratio" }
                option(value="empty") { "Empty" }
            }
        }
    }
}