use bone_kernel::BrepSolid;

use super::blob::{BlobHash, BlobKind};
use super::folder::{
    DocumentFolder, FolderError, FolderErrorKind, ensure_scaffold, read_bytes, read_to_string,
    write_if_absent,
};
use super::labels::LabelSidecar;

pub fn write_solid(folder: &DocumentFolder, solid: &BrepSolid) -> Result<BlobHash, FolderError> {
    ensure_scaffold(folder)?;
    let blob = solid.to_blob().map_err(|source| {
        FolderError::from(FolderErrorKind::Blob {
            path: folder.blobs_dir(),
            source,
        })
    })?;
    let sidecar_ron = LabelSidecar::capture(solid).to_ron().map_err(|source| {
        FolderError::from(FolderErrorKind::Ron {
            path: folder.blobs_dir(),
            source,
        })
    })?;
    let hash = BlobHash::of_pair(&blob, sidecar_ron.as_bytes());
    write_if_absent(
        &folder.blob_path(hash, BlobKind::LABELS),
        sidecar_ron.as_bytes(),
    )?;
    write_if_absent(&folder.blob_path(hash, BlobKind::BREP), &blob)?;
    Ok(hash)
}

pub fn read_solid(folder: &DocumentFolder, hash: BlobHash) -> Result<BrepSolid, FolderError> {
    let brep_path = folder.blob_path(hash, BlobKind::BREP);
    let labels_path = folder.blob_path(hash, BlobKind::LABELS);
    let blob = read_bytes(&brep_path)?;
    let sidecar_text = read_to_string(&labels_path)?;
    let found = BlobHash::of_pair(&blob, sidecar_text.as_bytes());
    if found != hash {
        return Err(FolderError::from(FolderErrorKind::BlobHashMismatch {
            path: brep_path,
            found: found.truncated_128_hex(),
            expected: hash.truncated_128_hex(),
        }));
    }
    let sidecar = LabelSidecar::from_ron(&sidecar_text).map_err(|source| {
        FolderError::from(FolderErrorKind::Ron {
            path: labels_path.clone(),
            source,
        })
    })?;
    let solid = BrepSolid::from_blob(&blob, sidecar.reattach()).map_err(|source| {
        FolderError::from(FolderErrorKind::Blob {
            path: brep_path,
            source,
        })
    })?;
    if sidecar.matches(solid.content_key()) {
        Ok(solid)
    } else {
        Err(FolderError::from(FolderErrorKind::SidecarMismatch {
            path: labels_path,
        }))
    }
}

#[cfg(test)]
mod tests {
    use super::{read_solid, write_solid};
    use crate::io::DocumentFolder;
    use bone_kernel::{
        BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature,
        ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop,
        evaluate_extrude,
    };
    use bone_types::{
        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 {
            sketch: SketchId::null(),
            direction: ExtrudeDirection::Normal {
                sense: ExtrudeSense::Forward,
            },
            end_condition: ExtrudeEndCondition::Blind { depth },
            draft: None,
            thin_wall: None,
            merge_result: MergeResult::Merge,
        }
    }

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

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

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

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

    fn face_labels(solid: &BrepSolid) -> Vec<bone_types::FaceLabel> {
        solid
            .iter_faces()
            .map(bone_kernel::BrepFace::label)
            .collect()
    }

    fn round_trip(loops: Vec<ProfileLoop>, depth_mm: f64) {
        let solid = build(loops, depth_mm);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let Ok(restored) = read_solid(&folder, hash) else {
            panic!("read solid");
        };
        assert_eq!(face_labels(&solid), face_labels(&restored));
        assert_eq!(solid.content_key(), restored.content_key());
        assert!(restored.validate(TOL).is_ok());
    }

    #[test]
    fn cube_round_trips_through_folder() {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        round_trip(vec![rectangle(&mut entities)], 5.0);
    }

    #[test]
    fn cylinder_round_trips_through_folder() {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        round_trip(vec![cylinder(&mut entities)], 8.0);
    }

    #[test]
    fn blob_and_sidecar_files_are_written() {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let solid = build(vec![rectangle(&mut entities)], 3.0);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        assert!(folder.blob_path(hash, crate::io::BlobKind::BREP).exists());
        assert!(folder.blob_path(hash, crate::io::BlobKind::LABELS).exists());
    }

    #[test]
    fn corrupt_sidecar_is_rejected() {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let solid = build(vec![rectangle(&mut entities)], 3.0);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let other = build(vec![cylinder(&mut entities)], 3.0);
        let Ok(foreign) = crate::io::labels::LabelSidecar::capture(&other).to_ron() else {
            panic!("serialize foreign sidecar");
        };
        let labels_path = folder.blob_path(hash, crate::io::BlobKind::LABELS);
        let Ok(()) = std::fs::write(&labels_path, foreign) else {
            panic!("overwrite sidecar");
        };
        assert!(read_solid(&folder, hash).is_err());
    }

    #[test]
    fn identical_geometry_distinct_labels_do_not_collide() {
        let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
        let first = features.insert(());
        let second = features.insert(());
        assert_ne!(first, second);

        let mut entities_a: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let mut entities_b: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let profile_a = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut entities_a)]);
        let profile_b = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut entities_b)]);
        let Ok(solid_a) = evaluate_extrude(first, &profile_a, &blind(5.0)) else {
            panic!("first extrudes");
        };
        let Ok(solid_b) = evaluate_extrude(second, &profile_b, &blind(5.0)) else {
            panic!("second extrudes");
        };

        assert_eq!(solid_a.content_key(), solid_b.content_key());
        assert_ne!(face_labels(&solid_a), face_labels(&solid_b));

        let (_dir, folder) = temp_folder();
        let Ok(hash_a) = write_solid(&folder, &solid_a) else {
            panic!("write first");
        };
        let Ok(hash_b) = write_solid(&folder, &solid_b) else {
            panic!("write second");
        };
        assert_ne!(hash_a, hash_b, "distinct labelings get distinct blob paths");

        let Ok(restored_a) = read_solid(&folder, hash_a) else {
            panic!("read first");
        };
        let Ok(restored_b) = read_solid(&folder, hash_b) else {
            panic!("read second");
        };
        assert_eq!(face_labels(&solid_a), face_labels(&restored_a));
        assert_eq!(face_labels(&solid_b), face_labels(&restored_b));
    }

    #[cfg(unix)]
    #[test]
    fn write_solid_skips_rewrite_when_blob_already_present() {
        use std::os::unix::fs::MetadataExt;
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let solid = build(vec![rectangle(&mut entities)], 4.0);
        let (_dir, folder) = temp_folder();
        let Ok(hash) = write_solid(&folder, &solid) else {
            panic!("first write");
        };
        let brep = folder.blob_path(hash, crate::io::BlobKind::BREP);
        let labels = folder.blob_path(hash, crate::io::BlobKind::LABELS);
        let Ok(brep_meta) = std::fs::metadata(&brep) else {
            panic!("brep metadata");
        };
        let Ok(labels_meta) = std::fs::metadata(&labels) else {
            panic!("labels metadata");
        };

        let Ok(again) = write_solid(&folder, &solid) else {
            panic!("second write");
        };
        assert_eq!(hash, again);
        let Ok(brep_after) = std::fs::metadata(&brep) else {
            panic!("brep metadata after");
        };
        let Ok(labels_after) = std::fs::metadata(&labels) else {
            panic!("labels metadata after");
        };
        assert_eq!(
            brep_meta.ino(),
            brep_after.ino(),
            "a present brep blob keeps its inode because the rewrite is skipped"
        );
        assert_eq!(
            labels_meta.ino(),
            labels_after.ino(),
            "a present sidecar is not rewritten"
        );
    }

    #[test]
    fn write_solid_lays_down_scaffold_before_first_save() {
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let solid = build(vec![rectangle(&mut entities)], 3.0);
        let (_dir, folder) = temp_folder();
        let Ok(_hash) = write_solid(&folder, &solid) else {
            panic!("write solid");
        };
        let Ok(gitattributes) = std::fs::read_to_string(folder.path().join(".gitattributes"))
        else {
            panic!(".gitattributes must exist so .brep/.labels are declared text eol=lf");
        };
        assert!(gitattributes.contains("*.brep text eol=lf"));
        assert!(gitattributes.contains("*.labels text eol=lf"));
        assert!(folder.caches_dir().join("CACHEDIR.TAG").exists());
    }
}
