use std::collections::HashMap;

use nalgebra_glm::{Vec2, Vec3, Vec4};

#[cfg(feature = "audio")]
use crate::ecs::audio::components::{AudioBus, AudioSource};
#[cfg(feature = "audio")]
use crate::ecs::audio::resources::{audio_engine_get_sound, audio_engine_is_initialized};
use crate::ecs::camera::components::Projection;
use crate::ecs::cutscene::components::{
    Cutscene, CutsceneAction, CutsceneShot, camera_look_rotation, sample_camera_path,
};
use crate::ecs::cutscene::resources::CutsceneOverlay;
use crate::ecs::primitives::Visibility;
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::{TextOverflow, UiCanvasData};
use crate::ecs::ui::state::{UiBase, UiStateTrait};
use crate::ecs::ui::units::{Ab, Rl};
use crate::ecs::ui::widgets::world_canvas::{ui_canvas_clear, ui_canvas_rect};
use crate::ecs::ui::widgets::world_layout::{ui_set_text, ui_set_visible};
use crate::ecs::window::resources::{window_scale_factor, window_viewport_size};
use crate::ecs::world::commands::despawn_recursive_immediate;
#[cfg(feature = "audio")]
use crate::ecs::world::commands::spawn_entities;
#[cfg(feature = "audio")]
use crate::ecs::world::{AUDIO_SOURCE, GLOBAL_TRANSFORM, LOCAL_TRANSFORM, NAME};
use crate::ecs::world::{Entity, World};
use crate::render::wgpu::passes::geometry::UiLayer;

#[derive(Default)]
struct FrameOverlay {
    fade_color: Vec3,
    fade_alpha: f32,
    fade_start: f32,
    letterbox: f32,
    letterbox_start: f32,
    title: Option<String>,
    title_alpha: f32,
    speaker: Option<String>,
    dialogue: Option<String>,
    focus: Option<(f32, f32)>,
    focus_start: f32,
    grade: Option<(f32, f32, f32)>,
    grade_start: f32,
}

enum CameraSource {
    Shot(CutsceneShot),
    Follow {
        actor: String,
        eye_offset: Vec3,
        look_height: f32,
        field_of_view_degrees: f32,
    },
    LookAt {
        actor: String,
        look_height: f32,
        damping: f32,
    },
}

pub fn advance_cutscene_system(world: &mut World) {
    let playing = world.resources.cutscene.playing;
    let holding = world.resources.cutscene.holding;

    if !playing && !holding {
        if let Some(overlay) = world.resources.cutscene.overlay {
            reset_overlay(world, overlay);
        }
        clear_post_effects(world);
        stop_cutscene_audio(world);
        world.resources.cutscene.camera_smoothed = None;
        return;
    }

    if world.resources.cutscene.active.is_none() {
        world.resources.cutscene.playing = false;
        world.resources.cutscene.holding = false;
        return;
    }

    let overlay = match world.resources.cutscene.overlay {
        Some(overlay) => overlay,
        None => {
            let overlay = build_overlay(world);
            world.resources.cutscene.overlay = Some(overlay);
            overlay
        }
    };

    let delta = if playing {
        world.resources.window.timing.delta_time
    } else {
        0.0
    };
    let cutscene = world.resources.cutscene.active.take().unwrap();
    let bindings = world.resources.cutscene.bindings.clone();
    let camera = world.resources.cutscene.camera;
    let speed = world.resources.cutscene.speed;
    let looping = world.resources.cutscene.looping;
    let characters_per_second = world.resources.cutscene.dialogue_characters_per_second;

    let duration = cutscene.duration();
    let cursor = world.resources.cutscene.marker_cursor;
    let mut time = world.resources.cutscene.time + delta * speed;
    let mut wrapped = false;
    let mut just_finished = false;
    if playing && time >= duration {
        if looping && duration > 0.0 {
            time %= duration;
            wrapped = true;
        } else {
            time = duration;
            just_finished = true;
        }
    }

    if playing {
        if wrapped {
            fire_discrete_events(world, &cutscene, cursor, duration);
            fire_discrete_events(world, &cutscene, -1.0, time);
        } else {
            fire_discrete_events(world, &cutscene, cursor, time);
        }
        world.resources.cutscene.marker_cursor = time;
    }

    let frame = evaluate_timeline(
        world,
        &cutscene,
        &bindings,
        camera,
        time,
        delta,
        characters_per_second,
    );
    update_overlay(world, overlay, &frame);
    reap_cutscene_audio(world, delta);

    world.resources.cutscene.active = Some(cutscene);
    world.resources.cutscene.time = time;

    if just_finished {
        if let Some(next) = world.resources.cutscene.queue.pop_front() {
            world.resources.cutscene.active = Some(next);
            world.resources.cutscene.time = 0.0;
            world.resources.cutscene.marker_cursor = -1.0;
            world.resources.cutscene.camera_smoothed = None;
            world.resources.cutscene.playing = true;
            world.resources.cutscene.finished = false;
            world.resources.cutscene.holding = false;
        } else {
            world.resources.cutscene.playing = false;
            world.resources.cutscene.finished = true;
            world.resources.cutscene.holding = world.resources.cutscene.hold_on_finish;
        }
    }
}

fn fire_discrete_events(world: &mut World, cutscene: &Cutscene, low: f32, high: f32) {
    let mut triggers: Vec<String> = Vec::new();
    let mut sounds: Vec<(String, f32)> = Vec::new();
    let mut musics: Vec<(String, f32)> = Vec::new();
    for event in &cutscene.events {
        if event.start > low && event.start <= high {
            match &event.action {
                CutsceneAction::Trigger { id } => triggers.push(id.clone()),
                CutsceneAction::Sound { clip, volume } => sounds.push((clip.clone(), *volume)),
                CutsceneAction::Music { track, volume } => musics.push((track.clone(), *volume)),
                _ => {}
            }
        }
    }
    world.resources.cutscene.fired_markers.extend(triggers);
    for (clip, volume) in sounds {
        fire_sound(world, &clip, volume);
    }
    for (track, volume) in musics {
        fire_music(world, &track, volume);
    }
}

#[cfg(feature = "audio")]
const SOUND_ONESHOT_LIFETIME_SECONDS: f32 = 8.0;

#[cfg(feature = "audio")]
fn fire_sound(world: &mut World, clip: &str, volume: f32) {
    if !audio_engine_is_initialized(&world.resources.audio)
        || audio_engine_get_sound(&world.resources.audio, clip).is_none()
    {
        return;
    }
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | AUDIO_SOURCE,
        1,
    )[0];
    world.core.set_audio_source(
        entity,
        AudioSource::new(clip.to_string())
            .with_volume(volume)
            .with_bus(AudioBus::Sfx)
            .playing(),
    );
    world
        .resources
        .cutscene
        .sound_oneshots
        .push((entity, SOUND_ONESHOT_LIFETIME_SECONDS));
}

#[cfg(not(feature = "audio"))]
fn fire_sound(_world: &mut World, _clip: &str, _volume: f32) {}

#[cfg(feature = "audio")]
fn fire_music(world: &mut World, track: &str, volume: f32) {
    if !audio_engine_is_initialized(&world.resources.audio)
        || audio_engine_get_sound(&world.resources.audio, track).is_none()
    {
        return;
    }
    if world.resources.cutscene.music_track.as_deref() == Some(track) {
        return;
    }
    let entity = match world.resources.cutscene.music_entity {
        Some(entity) => entity,
        None => {
            let entity = spawn_entities(
                world,
                NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | AUDIO_SOURCE,
                1,
            )[0];
            world.resources.cutscene.music_entity = Some(entity);
            entity
        }
    };
    world.core.set_audio_source(
        entity,
        AudioSource::new(track.to_string())
            .with_volume(volume)
            .with_looping(true)
            .with_bus(AudioBus::Music)
            .playing(),
    );
    world.resources.cutscene.music_track = Some(track.to_string());
}

#[cfg(not(feature = "audio"))]
fn fire_music(_world: &mut World, _track: &str, _volume: f32) {}

fn reap_cutscene_audio(world: &mut World, delta: f32) {
    let entries = std::mem::take(&mut world.resources.cutscene.sound_oneshots);
    let mut survivors = Vec::with_capacity(entries.len());
    for (entity, remaining) in entries {
        let next = remaining - delta;
        if next <= 0.0 {
            despawn_recursive_immediate(world, entity);
        } else {
            survivors.push((entity, next));
        }
    }
    world.resources.cutscene.sound_oneshots = survivors;
}

fn stop_cutscene_audio(world: &mut World) {
    if let Some(entity) = world.resources.cutscene.music_entity.take() {
        despawn_recursive_immediate(world, entity);
    }
    world.resources.cutscene.music_track = None;
    let entries = std::mem::take(&mut world.resources.cutscene.sound_oneshots);
    for (entity, _) in entries {
        despawn_recursive_immediate(world, entity);
    }
}

fn evaluate_timeline(
    world: &mut World,
    cutscene: &Cutscene,
    bindings: &HashMap<String, Entity>,
    camera: Option<Entity>,
    time: f32,
    delta: f32,
    characters_per_second: f32,
) -> FrameOverlay {
    let mut frame = FrameOverlay::default();
    let mut camera_source: Option<CameraSource> = None;
    let mut camera_start = f32::NEG_INFINITY;
    let mut shake_offset = Vec3::zeros();
    let mut handheld_eye = Vec3::zeros();
    let mut handheld_target = Vec3::zeros();
    let mut animations: HashMap<String, (String, bool, f32, f32, f32)> = HashMap::new();

    for event in &cutscene.events {
        if time < event.start {
            continue;
        }
        let progress = event.eased_progress(time);
        match &event.action {
            CutsceneAction::CameraMove { from, to } => {
                if event.start >= camera_start {
                    camera_start = event.start;
                    camera_source = Some(CameraSource::Shot(from.interpolate(*to, progress)));
                }
            }
            CutsceneAction::CameraFollow {
                actor,
                eye_offset,
                look_height,
                field_of_view_degrees,
            } => {
                if event.start >= camera_start {
                    camera_start = event.start;
                    camera_source = Some(CameraSource::Follow {
                        actor: actor.clone(),
                        eye_offset: *eye_offset,
                        look_height: *look_height,
                        field_of_view_degrees: *field_of_view_degrees,
                    });
                }
            }
            CutsceneAction::CameraLookAt {
                actor,
                look_height,
                damping,
            } => {
                if event.start >= camera_start {
                    camera_start = event.start;
                    camera_source = Some(CameraSource::LookAt {
                        actor: actor.clone(),
                        look_height: *look_height,
                        damping: *damping,
                    });
                }
            }
            CutsceneAction::CameraPath { waypoints } => {
                if event.start >= camera_start {
                    camera_start = event.start;
                    camera_source =
                        Some(CameraSource::Shot(sample_camera_path(waypoints, progress)));
                }
            }
            CutsceneAction::CameraShake {
                amplitude,
                frequency,
            } => {
                if time < event.end() {
                    let falloff = 1.0 - progress;
                    let magnitude = amplitude * falloff;
                    let phase = time * frequency;
                    shake_offset += Vec3::new(
                        (phase * 1.0).sin() * magnitude,
                        (phase * 1.7 + 1.3).sin() * magnitude,
                        (phase * 0.7 + 3.1).sin() * magnitude * 0.5,
                    );
                }
            }
            CutsceneAction::Handheld {
                position_amplitude,
                target_amplitude,
                frequency,
            } => {
                if time < event.end() {
                    let phase = time * frequency;
                    handheld_eye += Vec3::new(
                        phase.sin() * position_amplitude,
                        (phase * 0.8 + 1.7).sin() * position_amplitude * 0.7,
                        (phase * 1.3 + 4.2).sin() * position_amplitude,
                    );
                    handheld_target += Vec3::new(
                        (phase * 1.1 + 2.0).sin() * target_amplitude,
                        (phase * 0.9 + 0.5).sin() * target_amplitude,
                        (phase * 1.4 + 5.0).sin() * target_amplitude,
                    );
                }
            }
            CutsceneAction::ActorMove { actor, from, to } => {
                if let Some(&entity) = bindings.get(actor) {
                    let position = from + (to - from) * progress;
                    apply_actor_translation(world, entity, position);
                }
            }
            CutsceneAction::ActorTurn {
                actor,
                from_yaw_radians,
                to_yaw_radians,
            } => {
                if let Some(&entity) = bindings.get(actor) {
                    let yaw = from_yaw_radians + (to_yaw_radians - from_yaw_radians) * progress;
                    apply_actor_yaw(world, entity, yaw);
                }
            }
            CutsceneAction::ActorVisible { actor, visible } => {
                if let Some(&entity) = bindings.get(actor) {
                    apply_actor_visibility(world, entity, *visible);
                }
            }
            CutsceneAction::ActorAnimation {
                actor,
                clip,
                looping,
                speed,
                blend,
            } => {
                let replace = animations
                    .get(actor)
                    .is_none_or(|(_, _, _, _, start)| event.start >= *start);
                if replace {
                    animations.insert(
                        actor.clone(),
                        (clip.clone(), *looping, *speed, *blend, event.start),
                    );
                }
            }
            CutsceneAction::Dialogue { speaker, line } => {
                if time < event.end() {
                    let line = localize(world, line);
                    frame.speaker = speaker.clone();
                    frame.dialogue =
                        Some(typewriter(&line, time - event.start, characters_per_second));
                }
            }
            CutsceneAction::Title { line } => {
                if time < event.end() {
                    frame.title = Some(localize(world, line));
                    frame.title_alpha = title_alpha(time - event.start, event.duration);
                }
            }
            CutsceneAction::Grade {
                exposure_ev,
                saturation,
                contrast,
            } => {
                if event.start >= frame.grade_start {
                    frame.grade_start = event.start;
                    let exposure = exposure_ev.0 + (exposure_ev.1 - exposure_ev.0) * progress;
                    let saturation = saturation.0 + (saturation.1 - saturation.0) * progress;
                    let contrast = contrast.0 + (contrast.1 - contrast.0) * progress;
                    frame.grade = Some((exposure, saturation, contrast));
                }
            }
            CutsceneAction::Trigger { .. }
            | CutsceneAction::Sound { .. }
            | CutsceneAction::Music { .. } => {}
            CutsceneAction::Fade { from, to, color } => {
                if event.start >= frame.fade_start {
                    frame.fade_start = event.start;
                    frame.fade_alpha = from + (to - from) * progress;
                    frame.fade_color = *color;
                }
            }
            CutsceneAction::Letterbox { from, to } => {
                if event.start >= frame.letterbox_start {
                    frame.letterbox_start = event.start;
                    frame.letterbox = from + (to - from) * progress;
                }
            }
            CutsceneAction::FocusPull {
                from_distance,
                to_distance,
                range,
            } => {
                if event.start >= frame.focus_start {
                    frame.focus_start = event.start;
                    let distance = from_distance + (to_distance - from_distance) * progress;
                    frame.focus = Some((distance, *range));
                }
            }
        }
    }

    let mut camera_damping = 0.0;
    let camera_shot = camera_source.map(|source| match source {
        CameraSource::Shot(shot) => shot,
        CameraSource::Follow {
            actor,
            eye_offset,
            look_height,
            field_of_view_degrees,
        } => {
            let focus = actor_position(world, bindings, &actor);
            CutsceneShot {
                eye: focus + eye_offset,
                target: focus + Vec3::new(0.0, look_height, 0.0),
                field_of_view_degrees,
                roll_degrees: 0.0,
            }
        }
        CameraSource::LookAt {
            actor,
            look_height,
            damping,
        } => {
            camera_damping = damping;
            let focus = actor_position(world, bindings, &actor);
            let eye = camera
                .and_then(|entity| world.core.get_local_transform(entity))
                .map(|transform| transform.translation)
                .unwrap_or_default();
            let field_of_view_degrees = camera
                .and_then(|entity| world.core.get_camera(entity))
                .and_then(|component| match &component.projection {
                    Projection::Perspective(perspective) => {
                        Some(perspective.y_fov_rad.to_degrees())
                    }
                    _ => None,
                })
                .unwrap_or(45.0);
            CutsceneShot {
                eye,
                target: focus + Vec3::new(0.0, look_height, 0.0),
                field_of_view_degrees,
                roll_degrees: 0.0,
            }
        }
    });

    let camera_shot = camera_shot.map(|shot| {
        if camera_damping > 0.0 {
            let smoothed = match world.resources.cutscene.camera_smoothed {
                Some(previous) => previous.interpolate(shot, (camera_damping * delta).min(1.0)),
                None => shot,
            };
            world.resources.cutscene.camera_smoothed = Some(smoothed);
            smoothed
        } else {
            world.resources.cutscene.camera_smoothed = None;
            shot
        }
    });

    if let (Some(camera), Some(mut shot)) = (camera, camera_shot) {
        shot.eye += shake_offset + handheld_eye;
        shot.target += shake_offset + handheld_target;
        apply_camera_shot(world, camera, shot);
    }

    for (actor, (clip, looping, speed, blend, _)) in &animations {
        if let Some(&entity) = bindings.get(actor) {
            apply_actor_animation(world, entity, clip, *looping, *speed, *blend);
        }
    }

    frame
}

fn actor_position(world: &World, bindings: &HashMap<String, Entity>, actor: &str) -> Vec3 {
    bindings
        .get(actor)
        .and_then(|entity| world.core.get_local_transform(*entity))
        .map(|transform| transform.translation)
        .unwrap_or_default()
}

fn localize(world: &World, line: &str) -> String {
    world
        .resources
        .cutscene
        .localization
        .get(line)
        .cloned()
        .unwrap_or_else(|| line.to_string())
}

fn apply_actor_animation(
    world: &mut World,
    entity: Entity,
    clip: &str,
    looping: bool,
    speed: f32,
    blend: f32,
) {
    let clip_index = world.core.get_animation_player(entity).and_then(|player| {
        player
            .clips
            .iter()
            .position(|candidate| candidate.name == clip)
    });
    if let Some(index) = clip_index
        && let Some(player) = world.core.get_animation_player_mut(entity)
    {
        player.blend_to(index, blend);
        player.looping = looping;
        player.speed = speed;
        player.playing = true;
    }
}

fn typewriter(line: &str, elapsed: f32, characters_per_second: f32) -> String {
    if characters_per_second <= 0.0 {
        return line.to_string();
    }
    let total = line.chars().count();
    let revealed = ((elapsed.max(0.0) * characters_per_second).floor() as usize).min(total);
    line.chars().take(revealed).collect()
}

fn apply_camera_shot(world: &mut World, camera: Entity, shot: CutsceneShot) {
    if let Some(transform) = world.core.get_local_transform_mut(camera) {
        transform.translation = shot.eye;
        transform.rotation = camera_look_rotation(shot.eye, shot.target, shot.roll_degrees);
    }
    if let Some(camera_component) = world.core.get_camera_mut(camera)
        && let Projection::Perspective(perspective) = &mut camera_component.projection
    {
        perspective.y_fov_rad = shot.field_of_view_degrees.to_radians();
    }
    mark_local_transform_dirty(world, camera);
}

fn apply_actor_translation(world: &mut World, entity: Entity, position: Vec3) {
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.translation = position;
        mark_local_transform_dirty(world, entity);
    }
}

fn apply_actor_yaw(world: &mut World, entity: Entity, yaw_radians: f32) {
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.rotation = nalgebra_glm::quat_angle_axis(yaw_radians, &Vec3::y());
        mark_local_transform_dirty(world, entity);
    }
}

fn apply_actor_visibility(world: &mut World, entity: Entity, visible: bool) {
    if let Some(visibility) = world.core.get_visibility_mut(entity) {
        visibility.visible = visible;
    } else {
        world
            .core
            .add_components(entity, crate::ecs::world::VISIBILITY);
        world.core.set_visibility(entity, Visibility { visible });
    }
}

fn title_alpha(local_time: f32, duration: f32) -> f32 {
    if duration <= 0.0 {
        return 1.0;
    }
    let fade = (duration * 0.35).min(0.7);
    if fade <= 0.0 {
        return 1.0;
    }
    if local_time < fade {
        (local_time / fade).clamp(0.0, 1.0)
    } else if local_time > duration - fade {
        ((duration - local_time) / fade).clamp(0.0, 1.0)
    } else {
        1.0
    }
}

fn update_overlay(world: &mut World, overlay: CutsceneOverlay, frame: &FrameOverlay) {
    apply_post_effects(world, frame);

    let scale = window_scale_factor(world).max(0.01);
    let (physical_width, physical_height) = window_viewport_size(world).unwrap_or((1280, 720));
    let size = Vec2::new(
        physical_width as f32 / scale,
        physical_height as f32 / scale,
    );

    ui_canvas_clear(world, overlay.canvas);

    let bar_height = size.y * 0.12 * frame.letterbox.clamp(0.0, 1.0);
    if bar_height > 0.5 {
        let black = Vec4::new(0.0, 0.0, 0.0, 1.0);
        ui_canvas_rect(
            world,
            overlay.canvas,
            Vec2::zeros(),
            Vec2::new(size.x, bar_height),
            black,
            0.0,
        );
        ui_canvas_rect(
            world,
            overlay.canvas,
            Vec2::new(0.0, size.y - bar_height),
            Vec2::new(size.x, bar_height),
            black,
            0.0,
        );
    }

    if frame.fade_alpha > 0.001 {
        let color = Vec4::new(
            frame.fade_color.x,
            frame.fade_color.y,
            frame.fade_color.z,
            frame.fade_alpha.clamp(0.0, 1.0),
        );
        ui_canvas_rect(world, overlay.canvas, Vec2::zeros(), size, color, 0.0);
    }

    match &frame.title {
        Some(line) => {
            ui_set_text(world, overlay.title, line);
            set_node_alpha(
                world,
                overlay.title,
                Vec4::new(1.0, 0.96, 0.88, frame.title_alpha),
            );
            ui_set_visible(world, overlay.title, true);
        }
        None => ui_set_visible(world, overlay.title, false),
    }

    match &frame.dialogue {
        Some(line) => {
            ui_set_visible(world, overlay.dialogue_panel, true);
            ui_set_text(world, overlay.dialogue, line);
            set_node_alpha(world, overlay.dialogue, Vec4::new(0.96, 0.96, 1.0, 1.0));
            ui_set_visible(world, overlay.dialogue, true);
            match &frame.speaker {
                Some(speaker) => {
                    ui_set_text(world, overlay.speaker, speaker);
                    set_node_alpha(world, overlay.speaker, Vec4::new(1.0, 0.82, 0.4, 1.0));
                    ui_set_visible(world, overlay.speaker, true);
                }
                None => ui_set_visible(world, overlay.speaker, false),
            }
        }
        None => {
            ui_set_visible(world, overlay.dialogue_panel, false);
            ui_set_visible(world, overlay.dialogue, false);
            ui_set_visible(world, overlay.speaker, false);
        }
    }
}

fn reset_overlay(world: &mut World, overlay: CutsceneOverlay) {
    ui_canvas_clear(world, overlay.canvas);
    ui_set_visible(world, overlay.title, false);
    ui_set_visible(world, overlay.speaker, false);
    ui_set_visible(world, overlay.dialogue, false);
    ui_set_visible(world, overlay.dialogue_panel, false);
}

fn apply_post_effects(world: &mut World, frame: &FrameOverlay) {
    let settings = &mut world.resources.render_settings;
    match frame.focus {
        Some((distance, range)) => {
            settings.depth_of_field.enabled = true;
            settings.depth_of_field.focus_distance = distance;
            settings.depth_of_field.focus_range = range;
        }
        None => settings.depth_of_field.enabled = false,
    }
    if let Some((exposure_ev, saturation, contrast)) = frame.grade {
        settings.color_grading.exposure_compensation_ev = exposure_ev;
        settings.color_grading.saturation = saturation;
        settings.color_grading.contrast = contrast;
    }
}

fn clear_post_effects(world: &mut World) {
    let settings = &mut world.resources.render_settings;
    settings.depth_of_field.enabled = false;
    settings.color_grading.exposure_compensation_ev = 0.0;
    settings.color_grading.saturation = 1.0;
    settings.color_grading.contrast = 1.0;
}

fn set_node_alpha(world: &mut World, entity: Entity, color: Vec4) {
    if let Some(node_color) = world.ui.get_ui_node_color_mut(entity) {
        node_color.colors[UiBase::INDEX] = Some(color);
        node_color.computed_color = color;
    }
}

fn build_overlay(world: &mut World) -> CutsceneOverlay {
    world.resources.retained_ui.enabled = true;

    let outline = Vec4::new(0.0, 0.0, 0.0, 0.85);
    let mut tree = UiTreeBuilder::new(world);

    let canvas = tree
        .add_node()
        .boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
        .with_layer(UiLayer::Tooltips)
        .with_clip()
        .entity();

    let title = tree
        .add_node()
        .boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
        .with_text("", 58.0)
        .with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
        .with_text_outline(outline, 0.4)
        .color_raw::<UiBase>(Vec4::new(1.0, 0.96, 0.88, 1.0))
        .with_layer(UiLayer::Popups)
        .with_z_index(10)
        .entity();

    let dialogue_panel = tree
        .add_node()
        .boundary(Rl(Vec2::new(8.0, 72.0)), Rl(Vec2::new(92.0, 88.0)))
        .rect(12.0)
        .color_raw::<UiBase>(Vec4::new(0.03, 0.04, 0.06, 0.62))
        .with_layer(UiLayer::Popups)
        .with_z_index(5)
        .entity();

    let speaker = tree
        .add_node()
        .boundary(Rl(Vec2::new(0.0, 68.0)), Rl(Vec2::new(100.0, 74.0)))
        .with_text("", 22.0)
        .with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
        .with_text_outline(outline, 0.4)
        .color_raw::<UiBase>(Vec4::new(1.0, 0.82, 0.4, 1.0))
        .with_layer(UiLayer::Popups)
        .with_z_index(10)
        .entity();

    let dialogue = tree
        .add_node()
        .boundary(Rl(Vec2::new(12.0, 74.0)), Rl(Vec2::new(88.0, 86.0)))
        .with_text("", 28.0)
        .with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
        .with_text_overflow(TextOverflow::Wrap)
        .with_text_outline(outline, 0.4)
        .color_raw::<UiBase>(Vec4::new(0.96, 0.96, 1.0, 1.0))
        .with_layer(UiLayer::Popups)
        .with_z_index(10)
        .entity();

    tree.finish();

    world.ui.set_ui_canvas(
        canvas,
        UiCanvasData {
            commands: Vec::new(),
            hit_test_enabled: false,
            command_bounds: Vec::new(),
        },
    );

    let overlay = CutsceneOverlay {
        canvas,
        title,
        speaker,
        dialogue,
        dialogue_panel,
    };
    reset_overlay(world, overlay);
    overlay
}
