use nalgebra::{DMatrix, DVector};
use rustc_hash::FxHashMap;
use slab::Slab;
use std::collections::BTreeSet;
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */

use crate::engine::{realize_gram, PartialMatrix};

#[derive(Clone, PartialEq)]
pub struct Element {
    pub id: String,
    pub label: String,
    pub color: [f32; 3],
    pub rep: DVector<f64>,
    pub constraints: Signal<BTreeSet<usize>>,
    
    // internal properties, not reflected in any view
    pub index: usize
}

impl Element {
    pub fn new(
        id: String,
        label: String,
        color: [f32; 3],
        rep: DVector<f64>
    ) -> Element {
        Element {
            id: id,
            label: label,
            color: color,
            rep: rep,
            constraints: create_signal(BTreeSet::default()),
            index: 0
        }
    }
}
            

#[derive(Clone)]
pub struct Constraint {
    pub args: (usize, usize),
    pub rep: Signal<f64>,
    pub rep_text: Signal<String>,
    pub rep_valid: Signal<bool>,
    pub active: Signal<bool>
}

// a complete, view-independent description of an assembly
#[derive(Clone)]
pub struct Assembly {
    // elements and constraints
    pub elements: Signal<Slab<Element>>,
    pub constraints: Signal<Slab<Constraint>>,
    
    // indexing
    pub elements_by_id: Signal<FxHashMap<String, usize>>
}

impl Assembly {
    pub fn new() -> Assembly {
        Assembly {
            elements: create_signal(Slab::new()),
            constraints: create_signal(Slab::new()),
            elements_by_id: create_signal(FxHashMap::default())
        }
    }
    
    // --- inserting elements and constraints ---
    
    // insert an element into the assembly without checking whether we already
    // have an element with the same identifier. any element that does have the
    // same identifier will get kicked out of the `elements_by_id` index
    fn insert_element_unchecked(&self, elt: Element) {
        let id = elt.id.clone();
        let key = self.elements.update(|elts| elts.insert(elt));
        self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
    }
    
    pub fn try_insert_element(&self, elt: Element) -> bool {
        let can_insert = self.elements_by_id.with_untracked(
            |elts_by_id| !elts_by_id.contains_key(&elt.id)
        );
        if can_insert {
            self.insert_element_unchecked(elt);
        }
        can_insert
    }
    
    pub fn insert_new_element(&self) {
        // find the next unused identifier in the default sequence
        let mut id_num = 1;
        let mut id = format!("sphere{}", id_num);
        while self.elements_by_id.with_untracked(
            |elts_by_id| elts_by_id.contains_key(&id)
        ) {
            id_num += 1;
            id = format!("sphere{}", id_num);
        }
        
        // create and insert a new element
        self.insert_element_unchecked(
            Element::new(
                id,
                format!("Sphere {}", id_num),
                [0.75_f32, 0.75_f32, 0.75_f32],
                DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5])
            )
        );
    }
    
    pub fn insert_constraint(&self, constraint: Constraint) {
        let args = constraint.args;
        let key = self.constraints.update(|csts| csts.insert(constraint));
        let arg_constraints = self.elements.with(
            |elts| (elts[args.0].constraints, elts[args.1].constraints)
        );
        arg_constraints.0.update(|csts| csts.insert(key));
        arg_constraints.1.update(|csts| csts.insert(key));
    }
    
    // --- realization ---
    
    pub fn realize(&self) {
        // index the elements
        self.elements.update_silent(|elts| {
            for (index, (_, elt)) in elts.into_iter().enumerate() {
                elt.index = index;
            }
        });
        
        // set up the Gram matrix and the initial configuration matrix
        let (gram, guess) = self.elements.with_untracked(|elts| {
            // set up the off-diagonal part of the Gram matrix
            let mut gram_to_be = PartialMatrix::new();
            self.constraints.with_untracked(|csts| {
                for (_, cst) in csts {
                    if cst.active.get_untracked() && cst.rep_valid.get_untracked() {
                        let args = cst.args;
                        let row = elts[args.0].index;
                        let col = elts[args.1].index;
                        gram_to_be.push_sym(row, col, cst.rep.get_untracked());
                    }
                }
            });
            
            // set up the initial configuration matrix and the diagonal of the
            // Gram matrix
            let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
            for (_, elt) in elts {
                let index = elt.index;
                gram_to_be.push_sym(index, index, 1.0);
                guess_to_be.set_column(index, &elt.rep);
            }
            
            (gram_to_be, guess_to_be)
        });
        
        /* DEBUG */
        // log the Gram matrix
        console::log_1(&JsValue::from("Gram matrix:"));
        gram.log_to_console();
        
        /* DEBUG */
        // log the initial configuration matrix
        console::log_1(&JsValue::from("Old configuration:"));
        for j in 0..guess.nrows() {
            let mut row_str = String::new();
            for k in 0..guess.ncols() {
                row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str());
            }
            console::log_1(&JsValue::from(row_str));
        }
        
        // look for a configuration with the given Gram matrix
        let (config, success, history) = realize_gram(
            &gram, guess, &[],
            1.0e-12, 0.5, 0.9, 1.1, 200, 110
        );
        
        /* DEBUG */
        // report the outcome of the search
        console::log_1(&JsValue::from(
            if success {
                "Target accuracy achieved!"
            } else {
                "Failed to reach target accuracy"
            }
        ));
        console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
        console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
        
        if success {
            // read out the solution
            self.elements.update(|elts| {
                for (_, elt) in elts.iter_mut() {
                    elt.rep.set_column(0, &config.column(elt.index));
                }
            });
        }
    }
}