use std::collections::HashSet;

use bone_types::{EdgeLabel, FaceLabel, SketchEntityId, SolidKey, VertexLabel};
use ron::extensions::Extensions;
use serde::{Deserialize, Serialize};
use truck_modeling::{EdgeID, Face, FaceID, Shell, Solid, VertexID};

use super::build::{SolidLabeling, assemble};
use super::edges::EdgeCurve3;
use super::{BrepError, BrepSolid, LabelKind};

const GRID_DECIMALS: usize = 6;

fn blob_options() -> ron::Options {
    ron::Options::default().with_default_extension(Extensions::UNWRAP_NEWTYPES)
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EdgeReattach {
    pub label: EdgeLabel,
    pub curve: Option<EdgeCurve3>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BrepReattach {
    faces: Vec<FaceLabel>,
    edges: Vec<EdgeReattach>,
    vertices: Vec<VertexLabel>,
    closed_curves: Vec<SketchEntityId>,
}

impl BrepReattach {
    #[must_use]
    pub fn faces(&self) -> &[FaceLabel] {
        &self.faces
    }

    #[must_use]
    pub fn edges(&self) -> &[EdgeReattach] {
        &self.edges
    }

    #[must_use]
    pub fn vertices(&self) -> &[VertexLabel] {
        &self.vertices
    }
}

pub(super) fn ordered_faces(solid: &Solid) -> Vec<FaceID> {
    solid
        .boundaries()
        .iter()
        .flat_map(Shell::face_iter)
        .map(Face::id)
        .collect()
}

pub(super) fn ordered_edges(solid: &Solid) -> Vec<EdgeID> {
    let mut seen = HashSet::new();
    solid
        .edge_iter()
        .map(|edge| edge.id())
        .filter(|id| seen.insert(*id))
        .collect()
}

pub(super) fn ordered_vertices(solid: &Solid) -> Vec<VertexID> {
    let mut seen = HashSet::new();
    solid
        .vertex_iter()
        .map(|vertex| vertex.id())
        .filter(|id| seen.insert(*id))
        .collect()
}

pub(crate) fn capture(solid: &Solid, labeling: &SolidLabeling) -> Result<BrepReattach, BrepError> {
    let faces = ordered_faces(solid)
        .into_iter()
        .map(|id| {
            labeling
                .faces
                .get(&id)
                .copied()
                .ok_or(BrepError::MissingLabel {
                    kind: LabelKind::Face,
                })
        })
        .collect::<Result<Vec<_>, _>>()?;
    let edges = ordered_edges(solid)
        .into_iter()
        .map(|id| {
            let label = labeling
                .edges
                .get(&id)
                .copied()
                .ok_or(BrepError::MissingLabel {
                    kind: LabelKind::Edge,
                })?;
            Ok(EdgeReattach {
                label,
                curve: labeling.edge_curves.get(&id).cloned(),
            })
        })
        .collect::<Result<Vec<_>, BrepError>>()?;
    let vertices = ordered_vertices(solid)
        .into_iter()
        .map(|id| {
            labeling
                .vertices
                .get(&id)
                .copied()
                .ok_or(BrepError::MissingLabel {
                    kind: LabelKind::Vertex,
                })
        })
        .collect::<Result<Vec<_>, _>>()?;
    let mut closed_curves: Vec<SketchEntityId> = labeling.closed_curves.iter().copied().collect();
    closed_curves.sort_unstable();
    Ok(BrepReattach {
        faces,
        edges,
        vertices,
        closed_curves,
    })
}

pub(super) fn to_labeling(
    solid: &Solid,
    reattach: &BrepReattach,
) -> Result<SolidLabeling, BrepError> {
    let face_ids = ordered_faces(solid);
    let edge_ids = ordered_edges(solid);
    let vertex_ids = ordered_vertices(solid);
    expect_count(LabelKind::Face, face_ids.len(), reattach.faces.len())?;
    expect_count(LabelKind::Edge, edge_ids.len(), reattach.edges.len())?;
    expect_count(LabelKind::Vertex, vertex_ids.len(), reattach.vertices.len())?;

    let faces = face_ids
        .iter()
        .zip(&reattach.faces)
        .map(|(id, label)| (*id, *label))
        .collect();
    let edges = edge_ids
        .iter()
        .zip(&reattach.edges)
        .map(|(id, edge)| (*id, edge.label))
        .collect();
    let edge_curves = edge_ids
        .iter()
        .zip(&reattach.edges)
        .filter_map(|(id, edge)| edge.curve.clone().map(|curve| (*id, curve)))
        .collect();
    let vertices = vertex_ids
        .iter()
        .zip(&reattach.vertices)
        .map(|(id, label)| (*id, *label))
        .collect();
    let closed_curves = reattach.closed_curves.iter().copied().collect();
    Ok(SolidLabeling {
        faces,
        edges,
        vertices,
        closed_curves,
        edge_curves,
    })
}

fn expect_count(kind: LabelKind, found: usize, expected: usize) -> Result<(), BrepError> {
    if found == expected {
        Ok(())
    } else {
        Err(BrepError::ReattachMismatch {
            kind,
            found,
            expected,
        })
    }
}

fn quantize(value: f64) -> String {
    let prec = GRID_DECIMALS;
    let text = format!("{value:.prec$}");
    if text.bytes().all(|byte| matches!(byte, b'-' | b'0' | b'.')) {
        format!("{:.prec$}", 0.0)
    } else {
        text
    }
}

pub(super) fn content_key(solid: &Solid) -> SolidKey {
    let mut vertices: Vec<String> = solid
        .vertex_iter()
        .map(|vertex| {
            let point = vertex.point();
            format!(
                "{},{},{}",
                quantize(point.x),
                quantize(point.y),
                quantize(point.z)
            )
        })
        .collect();
    vertices.sort_unstable();
    vertices.dedup();

    let mut edges: Vec<String> = solid
        .edge_iter()
        .map(|edge| {
            let front = edge.front().point();
            let back = edge.back().point();
            let mut ends = [
                format!(
                    "{},{},{}",
                    quantize(front.x),
                    quantize(front.y),
                    quantize(front.z)
                ),
                format!(
                    "{},{},{}",
                    quantize(back.x),
                    quantize(back.y),
                    quantize(back.z)
                ),
            ];
            ends.sort_unstable();
            ends.join("|")
        })
        .collect();
    edges.sort_unstable();
    edges.dedup();

    let canonical = format!(
        "t:{}/{}/{}\nv:{}\n{}\ne:{}\n{}\n",
        ordered_faces(solid).len(),
        ordered_edges(solid).len(),
        ordered_vertices(solid).len(),
        vertices.len(),
        vertices.join("\n"),
        edges.len(),
        edges.join("\n")
    );
    let digest = blake3::hash(canonical.as_bytes());
    let mut key = [0u8; 16];
    key.copy_from_slice(&digest.as_bytes()[..16]);
    SolidKey::from_bytes(key)
}

impl BrepSolid {
    pub fn to_blob(&self) -> Result<Vec<u8>, BrepError> {
        blob_options()
            .to_string(self.arena.solid())
            .map(String::into_bytes)
            .map_err(|_| BrepError::BlobSerialize)
    }

    #[must_use]
    pub fn reattach_data(&self) -> &BrepReattach {
        &self.reattach
    }

    #[must_use]
    pub fn content_key(&self) -> SolidKey {
        content_key(self.arena.solid())
    }

    pub fn from_blob(bytes: &[u8], reattach: &BrepReattach) -> Result<BrepSolid, BrepError> {
        let text = core::str::from_utf8(bytes).map_err(|_| BrepError::BlobParse)?;
        let solid: Solid = blob_options()
            .from_str(text)
            .map_err(|_| BrepError::BlobParse)?;
        let labeling = to_labeling(&solid, reattach)?;
        assemble(solid, &labeling)
    }
}

#[cfg(test)]
mod tests {
    use super::quantize;

    #[test]
    fn quantize_collapses_sub_grid_magnitudes_to_unsigned_zero() {
        assert_eq!(quantize(0.0), "0.000000");
        assert_eq!(quantize(-0.0), "0.000000");
        assert_eq!(quantize(4.0e-7), "0.000000");
        assert_eq!(quantize(-4.0e-7), "0.000000");
        assert_eq!(quantize(-1.0e-12), "0.000000");
        assert_eq!(quantize(4.0e-7), quantize(-4.0e-7));
    }

    #[test]
    fn quantize_keeps_signed_values_above_the_grid() {
        assert_eq!(quantize(-1.0e-6), "-0.000001");
        assert_eq!(quantize(1.0e-6), "0.000001");
        assert_eq!(quantize(-2.5), "-2.500000");
    }
}
