//! Parser seam — shared parse boundary, format dispatch, and the remote drop-in hook.
//!
//! The data types here ([`MeshResources`], [`MeshSource`]) are the only thing that
//! crosses the parse boundary. The dispatcher [`load_mesh`] is synchronous; the viewer is
//! the sole async layer (it prefetches glTF companions). 3MF cannot compile to `wasm32`,
//! so it is served by a remote parser (PM-49); [`RemoteMeshParser`] + the `Threemf` arm
//! are the drop-in site, and wiring them in does not change the [`MeshParser`] trait
//! shape or the viewer pipeline.

use crate::mesh::contract::{MeshFormat, MeshLoadError, ModelMesh};

/// Borrowed views of a mesh's companion resources (external glTF buffers/textures),
/// keyed by their exact raw URI as referenced by the primary asset.
///
/// Empty for STL, OBJ, GLB, and embedded-base64 `.gltf`. The viewer keeps an owned
/// companion store alive across the async parse and borrows it here; [`crate::mesh::gltf`]
/// inserts each entry into a `three-d-asset` `RawAssets` under the exact same URI so its
/// `base_path.join(uri)` lookup is an exact match (avoiding `RawAssets`' substring
/// fallback foot-gun).
pub(crate) struct MeshResources<'a> {
    companions: &'a [(String, Vec<u8>)],
}

impl<'a> MeshResources<'a> {
    /// No companion resources.
    pub(crate) fn empty() -> MeshResources<'static> {
        MeshResources { companions: &[] }
    }

    /// Wrap an owned companion store `(raw_uri, bytes)`.
    pub(crate) fn new(companions: &'a [(String, Vec<u8>)]) -> Self {
        MeshResources { companions }
    }

    /// Iterate `(raw_uri, bytes)` pairs.
    pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
        self.companions
            .iter()
            .map(|(uri, bytes)| (uri.as_str(), bytes.as_slice()))
    }
}

/// The bytes needed to parse one mesh: its primary asset plus any companion resources.
///
/// Passed by reference through the parse boundary so the trait shape stays fixed for the
/// remote-parser drop-in (PM-49).
pub(crate) struct MeshSource<'a> {
    /// Detected source format.
    pub format: MeshFormat,
    /// Primary asset bytes (the `.stl`/`.obj`/`.gltf`/`.glb`/`.3mf` itself).
    pub primary: &'a [u8],
    /// Companion resource bytes, keyed by exact raw URI.
    pub resources: MeshResources<'a>,
}

/// Synchronously parse a [`MeshSource`] into a [`ModelMesh`], dispatching by format.
///
/// `Stl`/`Obj`/`Gltf` are parsed client-side from the in-memory bytes. `Threemf` is not
/// supported here — it is routed to a remote parser (PM-49) by [`ProductionMeshParser`].
pub(crate) fn load_mesh(source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError> {
    match source.format {
        MeshFormat::Stl => crate::mesh::stl::load_stl(source.primary),
        MeshFormat::Obj => crate::mesh::obj::load_obj(source.primary),
        MeshFormat::Gltf => crate::mesh::gltf::load_gltf(source),
        // PM-49: ThreemfRemoteParser converts to GLB, then re-enters via Gltf.
        MeshFormat::Threemf => Err(MeshLoadError::UnsupportedFormat),
    }
}

/// Parse boundary: turn a [`MeshSource`] into a [`ModelMesh`]. Generic for static
/// dispatch (no boxed `dyn`); tests inject a counter to assert one parse per load.
///
/// **Drop-in contract (PM-49):** the only changes are a `RemoteMeshParser`
/// implementation (e.g. `ThreemfRemoteParser`) and constructing
/// `ProductionMeshParser::new(remote)` at the call site. This trait shape, the viewer
/// pipeline, and `build_parsed_mesh` are unchanged.
pub(crate) trait MeshParser {
    /// Parse the source into a mesh.
    async fn parse(&self, source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError>;
}

/// A remote (server-side) parser for formats that cannot compile to `wasm32` (3MF).
///
/// **Contract:** [`RemoteMeshParser::convert`] MUST return a **self-contained GLB**
/// (embedded buffer), because the `Threemf` arm re-enters the pipeline via `Gltf` with no
/// companion resources.
pub(crate) trait RemoteMeshParser {
    /// Convert the source bytes (e.g. a `.3mf`) into self-contained GLB bytes.
    async fn convert(&self, bytes: &[u8]) -> Result<Vec<u8>, MeshLoadError>;
}

/// No remote parser wired in: `Threemf` is unsupported until PM-49 supplies one.
pub(crate) struct NoRemote;

impl RemoteMeshParser for NoRemote {
    async fn convert(&self, _bytes: &[u8]) -> Result<Vec<u8>, MeshLoadError> {
        Err(MeshLoadError::UnsupportedFormat)
    }
}

/// Production parser: routes by format. `Stl`/`Obj`/`Gltf` parse in-memory; `Threemf`
/// delegates to a [`RemoteMeshParser`] (converting to a self-contained GLB, then
/// re-entering via `Gltf` with no companions).
pub(crate) struct ProductionMeshParser<R: RemoteMeshParser = NoRemote> {
    remote: R,
}

impl Default for ProductionMeshParser<NoRemote> {
    fn default() -> Self {
        Self { remote: NoRemote }
    }
}

impl<R: RemoteMeshParser> ProductionMeshParser<R> {
    /// Construct with a remote parser (PM-49 wires `ThreemfRemoteParser` here).
    pub(crate) fn new(remote: R) -> Self {
        Self { remote }
    }
}

impl<R: RemoteMeshParser> MeshParser for ProductionMeshParser<R> {
    async fn parse(&self, source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError> {
        match source.format {
            MeshFormat::Stl | MeshFormat::Obj | MeshFormat::Gltf => load_mesh(source),
            MeshFormat::Threemf => {
                let glb = self.remote.convert(source.primary).await?;
                let reentry = MeshSource {
                    format: MeshFormat::Gltf,
                    primary: &glb,
                    resources: MeshResources::empty(),
                };
                load_mesh(&reentry)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mesh::contract::MeshFormat;

    fn fixture(name: &str) -> Vec<u8> {
        let path = format!("{}/public/models/{name}", env!("CARGO_MANIFEST_DIR"));
        std::fs::read(&path)
            .unwrap_or_else(|_| panic!("fixture {name} should be committed at {path}"))
    }

    #[test]
    fn load_mesh_dispatches_each_format_and_rejects_threemf() {
        // Stl: a minimal binary STL tetrahedron (80-byte header + count + 4 triangles).
        let mut stl = Vec::new();
        stl.extend_from_slice(&[0u8; 80]);
        stl.extend_from_slice(&4u32.to_le_bytes());
        let v = [
            [0.0f32; 3],
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [0.0, 0.0, 1.0],
        ];
        let faces = [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]];
        for f in faces {
            stl.extend_from_slice(&[0u8; 12]); // facet normal (3 zero f32)
            for i in f {
                for &c in &v[i] {
                    stl.extend_from_slice(&c.to_le_bytes());
                }
            }
            stl.extend_from_slice(&0u16.to_le_bytes()); // attribute
        }
        let stl_mesh = load_mesh(&MeshSource {
            format: MeshFormat::Stl,
            primary: &stl,
            resources: MeshResources::empty(),
        })
        .expect("STL dispatches to load_stl");
        assert_eq!(stl_mesh.format, MeshFormat::Stl);

        // Obj: a single triangle.
        let obj_mesh = load_mesh(&MeshSource {
            format: MeshFormat::Obj,
            primary: b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n",
            resources: MeshResources::empty(),
        })
        .expect("OBJ dispatches to load_obj");
        assert_eq!(obj_mesh.format, MeshFormat::Obj);

        // Gltf: a self-contained GLB.
        let glb = fixture("box.glb");
        let gltf_mesh = load_mesh(&MeshSource {
            format: MeshFormat::Gltf,
            primary: &glb,
            resources: MeshResources::empty(),
        })
        .expect("GLB dispatches to load_gltf");
        assert_eq!(gltf_mesh.format, MeshFormat::Gltf);

        // Threemf: unsupported without a remote parser.
        assert!(matches!(
            load_mesh(&MeshSource {
                format: MeshFormat::Threemf,
                primary: b"not real 3mf",
                resources: MeshResources::empty(),
            }),
            Err(MeshLoadError::UnsupportedFormat)
        ));
    }

    #[test]
    fn production_parser_with_no_remote_rejects_threemf() {
        let parser = ProductionMeshParser::default();
        let source = MeshSource {
            format: MeshFormat::Threemf,
            primary: b"not real 3mf",
            resources: MeshResources::empty(),
        };
        // Drive the async parse on a current-thread runtime (no Send bound needed).
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();
        let result = rt.block_on(parser.parse(&source));
        assert!(matches!(result, Err(MeshLoadError::UnsupportedFormat)));
    }

    // AC.6 — drop-in proof: a stub remote returning a self-contained GLB makes a
    // `Threemf` source parse via the GLB loader, with the trait shape unchanged.
    struct StubRemote;

    impl RemoteMeshParser for StubRemote {
        async fn convert(&self, _bytes: &[u8]) -> Result<Vec<u8>, MeshLoadError> {
            // Returns a real self-contained GLB regardless of input.
            Ok(fixture("box.glb"))
        }
    }

    #[test]
    fn production_parser_with_stub_remote_parses_threemf_via_glb() {
        let parser = ProductionMeshParser::new(StubRemote);
        let source = MeshSource {
            format: MeshFormat::Threemf,
            primary: b"fake 3mf bytes",
            resources: MeshResources::empty(),
        };
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();
        let mesh = rt
            .block_on(parser.parse(&source))
            .expect("stub remote -> GLB -> ModelMesh");
        assert_eq!(mesh.format, MeshFormat::Gltf);
        assert_eq!(mesh.trimesh.triangle_count(), 12);
    }
}
