use std::{fs, io};

use bone_kernel::SolidMesh;
use bone_types::{AngleTolerance, ChordHeightTolerance};

use super::blob::BlobHash;
use super::folder::{
    DocumentFolder, FolderError, FolderErrorKind, atomic_write_cache, ensure_scaffold,
    read_to_string,
};
use super::ron_io;

pub fn write_tessellation(
    folder: &DocumentFolder,
    hash: BlobHash,
    chord: ChordHeightTolerance,
    angle: AngleTolerance,
    mesh: &SolidMesh,
) -> Result<(), FolderError> {
    ensure_scaffold(folder)?;
    let path = folder.tessellation_path(hash, chord, angle);
    let ron = ron_io::to_string(mesh).map_err(|source| {
        FolderError::from(FolderErrorKind::Ron {
            path: path.clone(),
            source,
        })
    })?;
    atomic_write_cache(&path, ron.as_bytes())
}

pub fn read_tessellation(
    folder: &DocumentFolder,
    hash: BlobHash,
    chord: ChordHeightTolerance,
    angle: AngleTolerance,
) -> Result<Option<SolidMesh>, FolderError> {
    let path = folder.tessellation_path(hash, chord, angle);
    let text = match read_to_string(&path) {
        Ok(text) => text,
        Err(error) => {
            return match error.into_kind() {
                FolderErrorKind::Io { source, .. } if source.kind() == io::ErrorKind::NotFound => {
                    Ok(None)
                }
                other => Err(FolderError::from(other)),
            };
        }
    };
    match ron_io::from_str(&text) {
        Ok(mesh) => Ok(Some(mesh)),
        Err(source) => {
            tracing::warn!(
                path = %path.display(),
                error = %source,
                "evicting unreadable tessellation cache entry"
            );
            let _ = fs::remove_file(&path);
            Ok(None)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{read_tessellation, write_tessellation};
    use crate::io::{DocumentFolder, write_solid};
    use bone_kernel::{
        BrepSolid, Circle2, Curve2Kind, ExtrudeFeature, ExtrudeProfile, Line2, ProfileEdge,
        ProfileLoop, SolidMesh, evaluate_extrude,
    };
    use bone_types::{
        AngleTolerance, ChordHeightTolerance, FeatureId, Length, Plane3, Point2, PositiveLength,
        SketchEntityId, SketchId, Tolerance, UnitVec3, millimeter,
    };
    use slotmap::{Key, SlotMap};

    const TOL: Tolerance = Tolerance::new(1.0e-9);

    fn xy_plane() -> Plane3 {
        let Ok(plane) = Plane3::new(
            bone_types::Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            TOL,
        ) else {
            panic!("orthonormal axes");
        };
        plane
    }

    fn blind(depth_mm: f64) -> ExtrudeFeature {
        let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
            panic!("positive depth");
        };
        ExtrudeFeature::blind(SketchId::null(), depth)
    }

    fn solid(loop_: ProfileLoop, depth_mm: f64) -> BrepSolid {
        let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
        let profile = ExtrudeProfile::new(xy_plane(), vec![loop_]);
        let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(depth_mm)) else {
            panic!("profile extrudes");
        };
        solid
    }

    fn cube() -> BrepSolid {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let line = |a: Point2, b: Point2| {
            let Ok(segment) = Line2::new(a, b, TOL) else {
                panic!("distinct endpoints");
            };
            Curve2Kind::Line(segment)
        };
        let corners = [
            Point2::from_mm(0.0, 0.0),
            Point2::from_mm(4.0, 0.0),
            Point2::from_mm(4.0, 2.0),
            Point2::from_mm(0.0, 2.0),
        ];
        let edges = (0..4)
            .map(|index| {
                ProfileEdge::new(
                    line(corners[index], corners[(index + 1) % 4]),
                    entities.insert(()),
                    entities.insert(()),
                )
            })
            .collect();
        solid(ProfileLoop::Open(edges), 5.0)
    }

    fn cylinder() -> BrepSolid {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let Ok(disk) = Circle2::new(
            Point2::from_mm(0.0, 0.0),
            Length::new::<millimeter>(5.0),
            TOL,
        ) else {
            panic!("positive radius");
        };
        solid(
            ProfileLoop::Closed {
                curve: Curve2Kind::Circle(disk),
                curve_entity: entities.insert(()),
            },
            8.0,
        )
    }

    fn mesh(solid: &BrepSolid, chord: ChordHeightTolerance, angle: AngleTolerance) -> SolidMesh {
        let Ok(mesh) = solid.tessellate(chord, angle) else {
            panic!("tessellates");
        };
        mesh
    }

    fn temp_folder() -> (tempfile::TempDir, DocumentFolder) {
        let Ok(dir) = tempfile::tempdir() else {
            panic!("temp dir");
        };
        let folder = DocumentFolder::new(dir.path());
        (dir, folder)
    }

    fn round_trips(solid: &BrepSolid) {
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let original = mesh(solid, chord, angle);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, solid) else {
            panic!("write solid");
        };
        let Ok(()) = write_tessellation(&folder, hash, chord, angle, &original) else {
            panic!("write tessellation");
        };
        let Ok(Some(restored)) = read_tessellation(&folder, hash, chord, angle) else {
            panic!("read tessellation");
        };
        assert_eq!(original, restored);
        assert_eq!(original.generation(), restored.generation());
    }

    #[test]
    fn cube_round_trips_through_cache() {
        round_trips(&cube());
    }

    #[test]
    fn curved_solid_round_trips_exactly() {
        round_trips(&cylinder());
    }

    #[test]
    fn tessellation_lands_under_caches() {
        let solid = cube();
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let Ok(()) = write_tessellation(&folder, hash, chord, angle, &mesh(&solid, chord, angle))
        else {
            panic!("write tessellation");
        };
        let path = folder.tessellation_path(hash, chord, angle);
        assert!(path.exists());
        assert!(path.starts_with(folder.caches_dir()));
        assert!(
            path.extension()
                .is_some_and(|ext| ext.eq_ignore_ascii_case("tess"))
        );
    }

    #[test]
    fn missing_cache_entry_is_a_miss() {
        let solid = cube();
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let Ok(None) = read_tessellation(&folder, hash, chord, angle) else {
            panic!("a cold cache entry must read back as a miss");
        };
    }

    #[test]
    fn damaged_cache_entry_is_a_miss_and_is_evicted() {
        let solid = cube();
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let Ok(()) = write_tessellation(&folder, hash, chord, angle, &mesh(&solid, chord, angle))
        else {
            panic!("write tessellation");
        };
        let path = folder.tessellation_path(hash, chord, angle);
        let Ok(()) = std::fs::write(&path, b"SolidMesh(faces:[FaceMesh(") else {
            panic!("truncate the cache entry");
        };

        let Ok(None) = read_tessellation(&folder, hash, chord, angle) else {
            panic!(
                "a reproducible-from-source cache entry that no longer parses must read as a miss"
            );
        };
        assert!(
            !path.exists(),
            "a damaged cache entry is evicted so the next write replaces it"
        );
    }

    #[test]
    fn distinct_tiers_coexist() {
        let solid = cylinder();
        let coarse_chord = ChordHeightTolerance::from_mm(0.5);
        let coarse_angle = AngleTolerance::from_radians(1.0);
        let fine_chord = ChordHeightTolerance::from_mm(0.001);
        let fine_angle = AngleTolerance::from_radians(0.02);
        let coarse = mesh(&solid, coarse_chord, coarse_angle);
        let fine = mesh(&solid, fine_chord, fine_angle);
        assert_ne!(coarse, fine, "two tiers are genuinely different meshes");

        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        assert_ne!(
            folder.tessellation_path(hash, coarse_chord, coarse_angle),
            folder.tessellation_path(hash, fine_chord, fine_angle),
            "distinct tiers address distinct files"
        );

        let Ok(()) = write_tessellation(&folder, hash, coarse_chord, coarse_angle, &coarse) else {
            panic!("write coarse");
        };
        let Ok(()) = write_tessellation(&folder, hash, fine_chord, fine_angle, &fine) else {
            panic!("write fine");
        };

        let Ok(Some(restored_coarse)) =
            read_tessellation(&folder, hash, coarse_chord, coarse_angle)
        else {
            panic!("read coarse");
        };
        let Ok(Some(restored_fine)) = read_tessellation(&folder, hash, fine_chord, fine_angle)
        else {
            panic!("read fine");
        };
        assert_eq!(
            coarse, restored_coarse,
            "coarse tier survives the fine write"
        );
        assert_eq!(fine, restored_fine);
    }

    #[test]
    fn write_tessellation_lays_down_cache_scaffold_before_first_save() {
        let solid = cube();
        let chord = ChordHeightTolerance::from_mm(0.05);
        let angle = AngleTolerance::from_radians(0.2);
        let (_dir, folder) = temp_folder();
        let hash = crate::io::BlobHash::of(b"scaffold-probe");
        let Ok(()) = write_tessellation(&folder, hash, chord, angle, &mesh(&solid, chord, angle))
        else {
            panic!("write tessellation");
        };
        assert!(
            folder.caches_dir().join("CACHEDIR.TAG").exists(),
            "CACHEDIR.TAG present so backup tools skip the cache"
        );
        assert!(
            folder.caches_dir().join(".gitignore").exists(),
            "caches/.gitignore present"
        );
        assert!(
            folder.path().join(".gitignore").exists(),
            "root .gitignore present so cache blobs are not committed"
        );
    }
}
