//! Versioned scene text migrations.

use shared::ActorKind;

pub const DEFAULT_SUN_ILLUMINANCE_LUX: f32 = 100_000.0;

/// Migrates v1 scene text to schema v2 (ActorKind backfill + directional lux fix).
pub fn migrate_v1_to_v2(body: &str) -> String {
    let mut out = body.to_string();
    out = migrate_directional_light_intensity(&out);
    out = backfill_actor_kinds(&out);
    out
}

/// Migrates v2 animated imported-model actors to the dedicated skinned renderer contract.
///
/// `ModelRef` and `SkinnedMeshRenderer` intentionally share the same serialized source fields, so
/// the migration can preserve stable asset IDs and cached paths without lossy reconstruction.
pub fn migrate_v2_to_v3(body: &str) -> String {
    let mut result = String::with_capacity(body.len() + 256);
    let mut rest = body;
    while let Some(index) = rest.find("components: {") {
        result.push_str(&rest[..index]);
        let after = &rest[index..];
        let Some(close) = find_components_close(after) else {
            result.push_str(after);
            return result;
        };
        let block = &after[..close];
        if block.contains("\"shared::animation::AnimationControllerDesc\"")
            && block.contains("\"shared::components::ModelRef\"")
        {
            result.push_str(
                &block
                    .replace(
                        "\"shared::components::ModelRef\"",
                        "\"shared::animation::SkinnedMeshRenderer\"",
                    )
                    .replace(
                        "\"shared::components::ActorKind\": ImportedModel",
                        "\"shared::components::ActorKind\": SkinnedMesh",
                    )
                    .replace(
                        "\"shared::components::ActorKind\":ImportedModel",
                        "\"shared::components::ActorKind\":SkinnedMesh",
                    ),
            );
        } else {
            result.push_str(block);
        }
        rest = &after[close..];
    }
    result.push_str(rest);
    result
}

/// Migrates schema-v3 documents into the registry-driven renderer/component foundation.
///
/// The v4 authoring types deliberately use serde defaults for material-slot sets and component
/// enable state, so normal loading can remain read-only. The explicit project upgrader later
/// materializes shared Material assets and canonical slot IDs on disk.
pub fn migrate_v3_to_v4(body: &str) -> String {
    body.to_string()
}

fn backfill_actor_kinds(text: &str) -> String {
    let mut result = String::with_capacity(text.len() + 1024);
    let mut rest = text;
    while let Some(idx) = rest.find("components: {") {
        result.push_str(&rest[..idx]);
        let after = &rest[idx..];
        let Some(close) = find_components_close(after) else {
            result.push_str(after);
            break;
        };
        let block = &after[..close];
        let tail = &after[close..];
        if block.contains("\"shared::components::ActorKind\"") {
            result.push_str(block);
        } else if block.contains("\"shared::components::LevelObject\"") {
            let kind = infer_kind_from_block(block);
            result.push_str(&inject_actor_kind(block, kind));
        } else {
            result.push_str(block);
        }
        rest = tail;
    }
    result.push_str(rest);
    result
}

fn find_components_close(after: &str) -> Option<usize> {
    let mut depth = 0;
    let bytes = after.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        match b {
            b'{' => depth += 1,
            b'}' => {
                depth -= 1;
                if depth == 0 {
                    return Some(i + 1);
                }
            }
            _ => {}
        }
    }
    None
}

fn inject_actor_kind(block: &str, kind: &str) -> String {
    let Some(close) = block.rfind('}') else {
        return block.to_string();
    };
    format!(
        "{block_prefix}\"shared::components::ActorKind\": {kind},\n      {close_brace}",
        block_prefix = &block[..close],
        close_brace = &block[close..]
    )
}

fn infer_kind_from_block(block: &str) -> &'static str {
    if block.contains("\"shared::components::PlayerSpawn\"") {
        return actor_kind_ron(ActorKind::PlayerSpawn);
    }
    if block.contains("\"shared::components::WeaponSpawn\"") {
        return actor_kind_ron(ActorKind::WeaponSpawn);
    }
    if block.contains("\"shared::components::TriggerVolume\"") {
        return actor_kind_ron(ActorKind::TriggerVolume);
    }
    if block.contains("\"shared::components::PostProcessVolumeDesc\"") {
        return actor_kind_ron(ActorKind::PostProcessVolume);
    }
    if block.contains("\"shared::components::TeamSpawn\"") {
        return actor_kind_ron(ActorKind::TeamSpawn);
    }
    if block.contains("\"shared::components::ObjectiveMarker\"") {
        return actor_kind_ron(ActorKind::Objective);
    }
    if block.contains("\"shared::components::LightDesc\"") {
        return actor_kind_ron(ActorKind::Light);
    }
    if block.contains("\"shared::animation::SkinnedMeshRenderer\"") {
        return actor_kind_ron(ActorKind::SkinnedMesh);
    }
    if block.contains("\"shared::components::ModelRef\"") {
        return actor_kind_ron(ActorKind::ImportedModel);
    }
    if block.contains("\"shared::components::BrushDesc\"") {
        return actor_kind_ron(ActorKind::Brush);
    }
    if block.contains("\"shared::components::Primitive\"")
        || block.contains("\"shared::components::StaticMeshRenderer\"")
    {
        return actor_kind_ron(ActorKind::StaticMesh);
    }
    if block.contains("\"shared::components::PrefabRef\"")
        || block.contains("\"shared::components::PrefabInstance\"")
    {
        return actor_kind_ron(ActorKind::PrefabAnchor);
    }
    if block.contains("\"shared::components::AudioSourceDesc\"") {
        return actor_kind_ron(ActorKind::AudioSource);
    }
    if block.contains("\"shared::components::AudioListenerDesc\"") {
        return actor_kind_ron(ActorKind::AudioListener);
    }
    if block.contains("\"shared::navigation::Navigation") {
        return actor_kind_ron(ActorKind::Navigation);
    }
    actor_kind_ron(ActorKind::Empty)
}

fn actor_kind_ron(kind: ActorKind) -> &'static str {
    match kind {
        ActorKind::Empty => "Empty",
        ActorKind::Brush => "Brush",
        ActorKind::StaticMesh => "StaticMesh",
        ActorKind::SkinnedMesh => "SkinnedMesh",
        ActorKind::ImportedModel => "ImportedModel",
        ActorKind::Light => "Light",
        ActorKind::PrefabAnchor => "PrefabAnchor",
        ActorKind::PlayerSpawn => "PlayerSpawn",
        ActorKind::WeaponSpawn => "WeaponSpawn",
        ActorKind::TriggerVolume => "TriggerVolume",
        ActorKind::PostProcessVolume => "PostProcessVolume",
        ActorKind::TeamSpawn => "TeamSpawn",
        ActorKind::Objective => "Objective",
        ActorKind::AudioSource => "AudioSource",
        ActorKind::AudioListener => "AudioListener",
        ActorKind::Navigation => "Navigation",
    }
}

fn migrate_directional_light_intensity(text: &str) -> String {
    let mut out = String::with_capacity(text.len());
    let mut rest = text;
    while let Some(start) = rest.find("\"shared::components::LightDesc\"") {
        out.push_str(&rest[..start]);
        let block_start = start;
        let Some(rel_end) = find_light_desc_block_end(&rest[block_start..]) else {
            out.push_str(&rest[block_start..]);
            return out;
        };
        let end = block_start + rel_end;
        let block = &rest[block_start..end];
        let migrated = if block.contains("kind: Directional") {
            fix_directional_intensity(block)
        } else {
            block.to_string()
        };
        out.push_str(&migrated);
        rest = &rest[end..];
    }
    out.push_str(rest);
    out
}

fn find_light_desc_block_end(text: &str) -> Option<usize> {
    let open = text.find('(')?;
    let mut depth = 0;
    for (i, ch) in text[open..].char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    return Some(open + i + 1);
                }
            }
            _ => {}
        }
    }
    None
}

fn fix_directional_intensity(block: &str) -> String {
    let Some(intensity_idx) = block.find("intensity:") else {
        return block.to_string();
    };
    let after = &block[intensity_idx + "intensity:".len()..];
    let trimmed = after.trim_start();
    let end = trimmed.find([',', ')']).unwrap_or(trimmed.len());
    let value_str = trimmed[..end].trim();
    let Ok(value) = value_str.parse::<f32>() else {
        return block.to_string();
    };
    if value >= 1000.0 {
        return block.to_string();
    }
    let replacement = format!("intensity: {}", DEFAULT_SUN_ILLUMINANCE_LUX);
    block.replace(&format!("intensity: {value_str}"), &replacement)
}

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

    #[test]
    fn backfills_actor_kind_for_primitive_entity() {
        let body = r#"(
  resources: {},
  entities: {
    1: (
      components: {
        "shared::components::LevelObject": (),
        "shared::components::Primitive": (
          shape: Box,
          size: (1.0, 1.0, 1.0),
        ),
      },
    ),
  },
)"#;
        let migrated = migrate_v1_to_v2(body);
        assert!(migrated.contains("ActorKind"));
        assert!(migrated.contains("StaticMesh"));
    }

    #[test]
    fn backfills_actor_kind_for_brush_entity() {
        let body = r#"(
  resources: {},
  entities: {
    1: (
      components: {
        "shared::components::LevelObject": (),
        "shared::components::BrushDesc": (
          kind: Additive,
          faces: [],
          cast_shadows: true,
          receive_shadows: true,
        ),
      },
    ),
  },
)"#;
        let migrated = migrate_v1_to_v2(body);
        assert!(migrated.contains("ActorKind"));
        assert!(migrated.contains("Brush"));
    }

    #[test]
    fn v2_animation_actor_moves_to_dedicated_skinned_renderer() {
        let body = r#"(
  resources: {},
  entities: {
    1: (components: {
      "shared::components::LevelObject": (),
      "shared::components::ActorKind": ImportedModel,
      "shared::components::ModelRef": (asset_id: "hero", path: "models/hero.glb", scene_index: 0),
      "shared::animation::AnimationControllerDesc": (skeleton: None, states: [], default_state: "", default_crossfade_seconds: 0.2),
    }),
    2: (components: {
      "shared::components::ActorKind": ImportedModel,
      "shared::components::ModelRef": (asset_id: "prop", path: "models/prop.glb", scene_index: 0),
    }),
  },
)"#;

        let migrated = migrate_v2_to_v3(body);

        assert!(migrated.contains("\"shared::animation::SkinnedMeshRenderer\""));
        assert!(migrated.contains("\"shared::components::ActorKind\": SkinnedMesh"));
        assert_eq!(
            migrated.matches("\"shared::components::ModelRef\"").count(),
            1,
            "generic scene instance must remain a ModelRef"
        );
    }

    #[test]
    fn backfills_actor_kind_for_audio_source_entity() {
        let body = r#"(
  resources: {},
  entities: {
    1: (
      components: {
        "shared::components::LevelObject": (),
        "shared::components::AudioSourceDesc": (
          clip: None,
          gain_db: 0.0,
          pitch: 1.0,
          looping: false,
          autoplay: true,
          spatial_blend: 1.0,
          attenuation: (min_distance: 1.0, max_distance: 25.0, rolloff: Inverse, rolloff_factor: 1.0),
          bus: "sfx",
        ),
      },
    ),
  },
)"#;
        let migrated = migrate_v1_to_v2(body);
        assert!(migrated.contains("ActorKind"));
        assert!(migrated.contains("AudioSource"));
    }

    #[test]
    fn fixes_low_directional_lux() {
        let body = r#"(
  resources: {},
  entities: {
    1: (
      components: {
        "shared::components::LightDesc": (
          kind: Directional,
          color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
          intensity: 10.0,
          range: 0.0,
          shadows: true,
        ),
      },
    ),
  },
)"#;
        let migrated = migrate_v1_to_v2(body);
        assert!(migrated.contains("intensity: 100000"));
    }
}
