mmd-anim-cli 0.1.9

CLI diagnostics and roundtrip tools for mmd-anim
use std::{path::Path, process::ExitCode};

use crate::{diagnostics_suffix, read_file, unsupported_format_error, write_file};

pub(crate) fn parse_pmx_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let parsed = mmd_anim_format::parse_pmx_model(&data)?;
    println!(
        "PMX parser: vertices={} faces={} materials={} bones={} morphs={} displayFrames={} rigidBodies={} joints={} softBodies={}{}",
        parsed.metadata.counts.vertices,
        parsed.metadata.counts.faces,
        parsed.metadata.counts.materials,
        parsed.metadata.counts.bones,
        parsed.metadata.counts.morphs,
        parsed.metadata.counts.display_frames,
        parsed.metadata.counts.rigid_bodies,
        parsed.metadata.counts.joints,
        parsed.metadata.counts.soft_bodies,
        diagnostics_suffix(parsed.diagnostics.len())
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn parse_format_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    match mmd_anim_format::detect_mmd_format(&data, path.file_name().and_then(|v| v.to_str())) {
        mmd_anim_format::MmdFormatKind::Pmx => parse_pmx_summary(path),
        mmd_anim_format::MmdFormatKind::Pmd => {
            let parsed = mmd_anim_format::parse_pmd_model(&data)?;
            println!(
                "PMD parser: vertices={} faces={} materials={} bones={} ik={} morphs={} displayFrames={} rigidBodies={} joints={}",
                parsed.metadata.counts.vertices,
                parsed.metadata.counts.faces,
                parsed.metadata.counts.materials,
                parsed.metadata.counts.bones,
                parsed.metadata.counts.ik,
                parsed.metadata.counts.morphs,
                parsed.metadata.counts.display_frames,
                parsed.metadata.counts.rigid_bodies,
                parsed.metadata.counts.joints
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::Vmd => {
            let parsed = mmd_anim_format::parse_vmd_animation(&data)?;
            println!(
                "VMD parser: boneFrames={} morphFrames={} cameraFrames={} lightFrames={} selfShadowFrames={} propertyFrames={} maxFrame={}",
                parsed.metadata.counts.bones,
                parsed.metadata.counts.morphs,
                parsed.metadata.counts.cameras,
                parsed.metadata.counts.lights,
                parsed.metadata.counts.self_shadows,
                parsed.metadata.counts.properties,
                parsed.metadata.max_frame
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::Vpd => {
            let parsed = mmd_anim_format::parse_vpd_pose(&data)?;
            println!(
                "VPD parser: bones={}{}",
                parsed.bone_count,
                diagnostics_suffix(parsed.diagnostics.len())
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::Pmm => {
            let parsed = mmd_anim_format::parse_pmm_manifest(&data)?;
            let model_slot_flag_counts = parsed
                .display_state
                .model_slot_flag_counts
                .iter()
                .map(|(flag, count)| format!("{flag}:{count}"))
                .collect::<Vec<_>>()
                .join(",");
            let asset_kind_counts = parsed
                .asset_summary
                .kind_counts
                .iter()
                .map(|(kind, count)| format!("{kind}:{count}"))
                .collect::<Vec<_>>()
                .join(",");
            let asset_extension_counts = parsed
                .asset_summary
                .extension_counts
                .iter()
                .map(|(extension, count)| format!("{extension}:{count}"))
                .collect::<Vec<_>>()
                .join(",");
            let first_model_slot_padding = parsed
                .model_slots
                .first()
                .map(|slot| slot.trailing_zero_padding_bytes.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let first_model_slot_next_non_zero = parsed
                .model_slots
                .first()
                .and_then(|slot| slot.next_non_zero_offset)
                .map(|offset| offset.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let document_models = parsed
                .document_summary
                .as_ref()
                .map(|document| document.counts.models.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let document_bone_keyframes = parsed
                .document_summary
                .as_ref()
                .map(|document| document.counts.bone_keyframes.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let document_morph_keyframes = parsed
                .document_summary
                .as_ref()
                .map(|document| document.counts.morph_keyframes.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_camera_keyframes = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (g.camera.initial_keyframes + g.camera.keyframes).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_light_keyframes = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (g.light.initial_keyframes + g.light.keyframes).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_accessory_count = parsed
                .document_global_summary
                .as_ref()
                .map(|g| g.accessories.accessory_count.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_accessory_keyframes = parsed
                .document_global_summary
                .as_ref()
                .map(|g| g.accessories.keyframes.to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_gravity_keyframes = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (g.gravity.initial_keyframes + g.gravity.keyframes).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_self_shadow_keyframes = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (g.self_shadow.initial_keyframes + g.self_shadow.keyframes).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_audio_path = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (!g.settings.audio_path.is_empty()).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_bg_video_path = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (!g.settings.background_video_path.is_empty()).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            let global_bg_image_path = parsed
                .document_global_summary
                .as_ref()
                .map(|g| (!g.settings.background_image_path.is_empty()).to_string())
                .unwrap_or_else(|| "unknown".to_owned());
            println!(
                "PMM parser: references={} version={} parsedVersion={} models={} accessories={} motions={} audio={} images={} videos={} modelAssets={} modelSlots={} firstModelSlotPadding={} firstModelSlotNextNonZeroOffset={} documentModels={} documentBoneKeyframes={} documentMorphKeyframes={} headerTextEntries={} audioAssets={} assetConfidence=high:{} medium:{} low:{} assetKindCounts={} assetExtensionCounts={} screen={}x{} timelineFrames={} timelineRange={}..{} frameRate={} durationSeconds={} displayLayout={} selectedModelIndex={} documentModelCount={} declaredModelSlotCount={} modelSlotCount={} nonZeroModelSlotCount={} modelSlotFlags={} activeModelSlotIndices={} emptyModelSlotIndices={} modelSlotFlagCounts={} accessorySlotCount={} globalCameraKeyframes={} globalLightKeyframes={} globalAccessoryCount={} globalAccessoryKeyframes={} globalGravityKeyframes={} globalSelfShadowKeyframes={} globalAudioPath={} globalBgVideoPath={} globalBgImagePath={}{}",
                parsed.asset_summary.reference_count,
                parsed.version,
                parsed
                    .parsed_version
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed.model_paths.len(),
                parsed.accessory_paths.len(),
                parsed.motion_paths.len(),
                parsed.audio_paths.len(),
                parsed.image_paths.len(),
                parsed.video_paths.len(),
                parsed.model_assets.len(),
                parsed.model_slots.len(),
                first_model_slot_padding,
                first_model_slot_next_non_zero,
                document_models,
                document_bone_keyframes,
                document_morph_keyframes,
                parsed.header_text_entries.len(),
                parsed.audio_assets.len(),
                parsed.asset_summary.high_confidence_count,
                parsed.asset_summary.medium_confidence_count,
                parsed.asset_summary.low_confidence_count,
                asset_kind_counts,
                asset_extension_counts,
                parsed
                    .project_settings
                    .screen_width
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .project_settings
                    .screen_height
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .project_settings
                    .timeline_frame_count
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .timeline
                    .start_frame
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .timeline
                    .end_frame_exclusive
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .project_settings
                    .frame_rate
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .timeline
                    .duration_seconds
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed.display_state.layout,
                parsed
                    .display_state
                    .selected_model_index
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .display_state
                    .document_model_count
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed
                    .display_state
                    .declared_model_slot_count
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                parsed.display_state.model_slot_count,
                parsed.display_state.non_zero_model_slot_count,
                parsed
                    .display_state
                    .model_slot_flags
                    .iter()
                    .map(|flag| flag.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
                parsed
                    .display_state
                    .active_model_slot_indices
                    .iter()
                    .map(|index| index.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
                parsed
                    .display_state
                    .empty_model_slot_indices
                    .iter()
                    .map(|index| index.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
                model_slot_flag_counts,
                parsed
                    .display_state
                    .accessory_slot_count
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "unknown".to_owned()),
                global_camera_keyframes,
                global_light_keyframes,
                global_accessory_count,
                global_accessory_keyframes,
                global_gravity_keyframes,
                global_self_shadow_keyframes,
                global_audio_path,
                global_bg_video_path,
                global_bg_image_path,
                diagnostics_suffix(parsed.diagnostics.len())
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::X | mmd_anim_format::MmdFormatKind::Vac => {
            let parsed = mmd_anim_format::parse_accessory_manifest(
                &data,
                path.file_name().and_then(|v| v.to_str()),
            )?;
            println!(
                "{} parser: byteLength={} meshes={} materials={} textures={}{}",
                parsed.format.to_uppercase(),
                parsed.byte_length,
                parsed.mesh_count,
                parsed.material_count,
                parsed.texture_references.len(),
                diagnostics_suffix(parsed.diagnostics.len())
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::Nmd => {
            let parsed = mmd_anim_format::parse_nmd_manifest(&data)?;
            println!(
                "NMD parser: byteLength={} annotations={} globalTracks={} bundles={{accessory:{}, bone:{}, camera:{}, light:{}, model:{}, morph:{}, selfShadow:{}, unknown:{}}}{}",
                parsed.byte_length,
                parsed.metadata.annotation_count,
                parsed.global_track_count,
                parsed.keyframe_bundles.accessory,
                parsed.keyframe_bundles.bone,
                parsed.keyframe_bundles.camera,
                parsed.keyframe_bundles.light,
                parsed.keyframe_bundles.model,
                parsed.keyframe_bundles.morph,
                parsed.keyframe_bundles.self_shadow,
                parsed.keyframe_bundles.unknown,
                diagnostics_suffix(parsed.diagnostics.len())
            );
            Ok(ExitCode::SUCCESS)
        }
        mmd_anim_format::MmdFormatKind::Unknown => Err(unsupported_format_error(path)),
    }
}

pub(crate) fn parse_format_json(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let text = parse_format_json_text(path)?;
    println!("{text}");
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn parse_format_json_to_file(
    path: &Path,
    output: &Path,
) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let text = parse_format_json_text(path)?;
    write_file(output, text.as_bytes())?;
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn parse_format_json_text(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
    let value = parse_format_json_value(path)?;
    Ok(serde_json::to_string_pretty(&value)?)
}

fn parse_format_json_value(path: &Path) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let value = match mmd_anim_format::detect_mmd_format(
        &data,
        path.file_name().and_then(|v| v.to_str()),
    ) {
        mmd_anim_format::MmdFormatKind::Pmx => {
            serde_json::to_value(mmd_anim_format::parse_pmx_model(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::Pmd => {
            serde_json::to_value(mmd_anim_format::parse_pmd_model(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::Vmd => {
            serde_json::to_value(mmd_anim_format::parse_vmd_animation(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::Vpd => {
            serde_json::to_value(mmd_anim_format::parse_vpd_pose(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::Pmm => {
            serde_json::to_value(mmd_anim_format::parse_pmm_manifest(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::X | mmd_anim_format::MmdFormatKind::Vac => {
            serde_json::to_value(mmd_anim_format::parse_accessory_manifest(
                &data,
                path.file_name().and_then(|v| v.to_str()),
            )?)?
        }
        mmd_anim_format::MmdFormatKind::Nmd => {
            serde_json::to_value(mmd_anim_format::parse_nmd_manifest(&data)?)?
        }
        mmd_anim_format::MmdFormatKind::Unknown => return Err(unsupported_format_error(path)),
    };
    Ok(value)
}