mod editor;
mod mixer;
mod ruler;
mod tempo;
mod tracks;

use crate::{
    consts::{
        state_ids::METRONOME_TRACK_ID,
        widget_piano::{
            KEYBOARD_WIDTH, MAIN_SPLIT_SPACING, RIGHT_SCROLL_GUTTER_WIDTH, TOOLS_STRIP_WIDTH,
        },
        workspace::{MIN_TIMELINE_BARS, PLAYHEAD_WIDTH_PX, TIMELINE_LEFT_INSET_PX},
    },
    gui::visible_bars_to_zoom_slider,
    message::{DraggedClip, Message, SnapMode},
    state::{ClipPeaks, MidiClipPreviewMap, State},
    widget::{midi_edit, pitch_correction},
};
use editor::{EditorViewArgs, OwnedEditorViewArgs};
use iced::{
    Background, Color, Element, Length, Point,
    widget::{Id, Space, Stack, column, container, lazy, mouse_area, pin, row, scrollable, slider},
};
use maolan_widgets::{
    horizontal_scrollbar::HorizontalScrollbar, vertical_scrollbar::VerticalScrollbar,
};
use ruler::RulerViewArgs;
use std::{collections::HashMap, path::PathBuf};
use tempo::TempoViewArgs;

pub use crate::consts::workspace_ids::{
    EDITOR_SCROLL_ID, EDITOR_TIMELINE_SCROLL_ID, PIANO_RULER_SCROLL_ID, PIANO_TEMPO_SCROLL_ID,
    TRACKS_SCROLL_ID, WORKSPACE_RULER_SCROLL_ID, WORKSPACE_TEMPO_SCROLL_ID,
};

pub(crate) fn timeline_sample_to_x_f64(sample: f64, pixels_per_sample: f32, inset_px: f32) -> f32 {
    inset_px + (sample as f32 * pixels_per_sample).max(0.0)
}

pub(crate) fn timeline_sample_to_x(sample: usize, pixels_per_sample: f32, inset_px: f32) -> f32 {
    timeline_sample_to_x_f64(sample as f64, pixels_per_sample, inset_px)
}

pub(crate) fn timeline_x_to_sample_f32(x: f32, pixels_per_sample: f32, inset_px: f32) -> f32 {
    if pixels_per_sample <= 1.0e-9 {
        0.0
    } else {
        ((x - inset_px).max(0.0) / pixels_per_sample).max(0.0)
    }
}

fn clip_kind_key(kind: maolan_engine::kind::Kind) -> u8 {
    match kind {
        maolan_engine::kind::Kind::Audio => 0,
        maolan_engine::kind::Kind::MIDI => 1,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ClipSnapEdge {
    pub clip_id: crate::state::ClipId,
    pub sample: usize,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct VisibleTrackWindow {
    pub start_index: usize,
    pub end_index: usize,
    pub top_padding: f32,
    pub bottom_padding: f32,
}

fn compute_visible_track_window(
    track_heights: &[f32],
    scroll_y: f32,
    viewport_height: f32,
) -> VisibleTrackWindow {
    if track_heights.is_empty() {
        return VisibleTrackWindow {
            start_index: 0,
            end_index: 0,
            top_padding: 0.0,
            bottom_padding: 0.0,
        };
    }

    const OVERSCAN_PX: f32 = 240.0;

    let total_height = track_heights.iter().sum::<f32>().max(1.0);
    let viewport_height = viewport_height.max(track_heights[0]).min(total_height);
    let max_scroll = (total_height - viewport_height).max(0.0);
    let scroll_top = scroll_y.clamp(0.0, 1.0) * max_scroll;
    let visible_top = (scroll_top - OVERSCAN_PX).max(0.0);
    let visible_bottom = (scroll_top + viewport_height + OVERSCAN_PX).min(total_height);

    let mut top_padding = 0.0;
    let mut start_index = 0;
    while start_index < track_heights.len()
        && top_padding + track_heights[start_index] <= visible_top
    {
        top_padding += track_heights[start_index];
        start_index += 1;
    }

    let mut bottom_edge = top_padding;
    let mut end_index = start_index;
    while end_index < track_heights.len() && bottom_edge < visible_bottom {
        bottom_edge += track_heights[end_index];
        end_index += 1;
    }

    if start_index == end_index {
        end_index = (start_index + 1).min(track_heights.len());
        bottom_edge = top_padding + track_heights[start_index.min(track_heights.len() - 1)];
    }

    VisibleTrackWindow {
        start_index,
        end_index,
        top_padding,
        bottom_padding: (total_height - bottom_edge).max(0.0),
    }
}

pub struct Workspace {
    state: State,
    editor: editor::Editor,
    mixer: mixer::Mixer,
    midi_edit: midi_edit::MIDIEdit,
    pitch_correction: pitch_correction::PitchCorrection,
    ruler: ruler::Ruler,
    tempo: tempo::Tempo,
    tracks: tracks::Tracks,
}

pub struct WorkspaceViewArgs<'a> {
    pub session_root: Option<&'a PathBuf>,
    pub playhead_samples: Option<f64>,
    pub transport_active: bool,
    pub pixels_per_sample: f32,
    pub beat_pixels: f32,
    pub samples_per_bar: f32,
    pub loop_range_samples: Option<(usize, usize)>,
    pub punch_range_samples: Option<(usize, usize)>,
    pub snap_mode: SnapMode,
    pub samples_per_beat: f64,
    pub zoom_visible_bars: f32,
    pub editor_scroll_x: f32,
    pub mixer_scroll_x: f32,
    pub window_width: f32,
    pub window_height: f32,
    pub editor_scroll_y: f32,
    pub track_drag_active: bool,
    pub tracks_resize_hovered: bool,
    pub mixer_resize_hovered: bool,
    pub tracks_visible: bool,
    pub editor_visible: bool,
    pub mixer_visible: bool,
    pub active_clip_drag: Option<&'a DraggedClip>,
    pub active_clip_target_track: Option<&'a str>,
    pub active_clip_target_valid: bool,
    pub active_clip_snap_adjust_samples: f32,
    pub active_clip_snap_targets: &'a [crate::state::ClipId],
    pub recording_preview_bounds: Option<(usize, usize)>,
    pub recording_preview_peaks: Option<&'a HashMap<String, ClipPeaks>>,
    pub midi_clip_previews: Option<&'a MidiClipPreviewMap>,
    pub shift_pressed: bool,
    pub selected_tempo_points: Vec<usize>,
    pub selected_time_signature_points: Vec<usize>,
    pub mixer_level_edit_track: Option<&'a str>,
    pub mixer_level_edit_input: &'a str,
    pub sample_rate: f64,
}

impl Workspace {
    pub fn new(state: State) -> Self {
        Self {
            state: state.clone(),
            editor: editor::Editor::new(state.clone()),
            mixer: mixer::Mixer::new(state.clone()),
            midi_edit: midi_edit::MIDIEdit::new(state.clone()),
            pitch_correction: pitch_correction::PitchCorrection::new(state.clone()),
            ruler: ruler::Ruler::new(),
            tempo: tempo::Tempo::new(),
            tracks: tracks::Tracks::new(state.clone()),
        }
    }

    pub fn update(&mut self, _message: &Message) {}

    pub fn set_midi_edit_midnam_note_names(
        &mut self,
        names: &std::collections::HashMap<u8, String>,
    ) {
        self.midi_edit.set_midnam_note_names(names);
    }

    fn collect_clip_snap_edges(&self) -> Vec<ClipSnapEdge> {
        let state = self.state.blocking_read();
        let mut edges = Vec::new();
        for track in &state.tracks {
            for (clip_idx, clip) in track.audio.clips.iter().enumerate() {
                let clip_id = crate::state::ClipId {
                    track_idx: track.name.clone(),
                    clip_idx,
                    kind: maolan_engine::kind::Kind::Audio,
                };
                edges.push(ClipSnapEdge {
                    clip_id: clip_id.clone(),
                    sample: clip.start,
                });
                edges.push(ClipSnapEdge {
                    clip_id,
                    sample: clip.start.saturating_add(clip.length),
                });
            }
            for (clip_idx, clip) in track.midi.clips.iter().enumerate() {
                let clip_id = crate::state::ClipId {
                    track_idx: track.name.clone(),
                    clip_idx,
                    kind: maolan_engine::kind::Kind::MIDI,
                };
                edges.push(ClipSnapEdge {
                    clip_id: clip_id.clone(),
                    sample: clip.start,
                });
                edges.push(ClipSnapEdge {
                    clip_id,
                    sample: clip.start.saturating_add(clip.length),
                });
            }
        }
        edges.sort_unstable_by(|a, b| {
            a.sample
                .cmp(&b.sample)
                .then_with(|| a.clip_id.track_idx.cmp(&b.clip_id.track_idx))
                .then_with(|| a.clip_id.clip_idx.cmp(&b.clip_id.clip_idx))
                .then_with(|| clip_kind_key(a.clip_id.kind).cmp(&clip_kind_key(b.clip_id.kind)))
        });
        edges.dedup();
        edges
    }

    fn playhead_line() -> Element<'static, Message> {
        container("")
            .width(Length::Fixed(PLAYHEAD_WIDTH_PX))
            .height(Length::Fill)
            .style(|_theme| container::Style {
                background: Some(Background::Color(Color {
                    r: 0.95,
                    g: 0.18,
                    b: 0.14,
                    a: 0.95,
                })),
                ..container::Style::default()
            })
            .into()
    }

    pub fn view<'a>(&'a self, args: WorkspaceViewArgs<'a>) -> Element<'a, Message> {
        let WorkspaceViewArgs {
            session_root,
            playhead_samples,
            transport_active,
            pixels_per_sample,
            beat_pixels,
            samples_per_bar,
            loop_range_samples,
            punch_range_samples,
            snap_mode,
            samples_per_beat,
            zoom_visible_bars,
            editor_scroll_x,
            mixer_scroll_x,
            window_width,
            window_height,
            editor_scroll_y,
            track_drag_active,
            tracks_resize_hovered,
            mixer_resize_hovered,
            tracks_visible,
            editor_visible,
            mixer_visible,
            active_clip_drag,
            active_clip_target_track,
            active_clip_target_valid,
            active_clip_snap_adjust_samples,
            active_clip_snap_targets,
            recording_preview_bounds,
            recording_preview_peaks,
            midi_clip_previews,
            shift_pressed,
            selected_tempo_points,
            selected_time_signature_points,
            mixer_level_edit_track,
            mixer_level_edit_input,
            sample_rate,
        } = args;
        let (
            tracks_width,
            tracks_width_px,
            max_end_samples,
            tracks_total_height,
            track_heights,
            tempo,
            time_signature,
            tempo_points,
            time_signature_points,
            mixer_height_px,
            markers,
        ) = {
            let state = self.state.blocking_read();
            let max_end_samples = state
                .tracks
                .iter()
                .map(|track| {
                    let audio_max = track
                        .audio
                        .clips
                        .iter()
                        .map(|clip| clip.start.saturating_add(clip.length))
                        .max()
                        .unwrap_or(0);
                    let midi_max = track
                        .midi
                        .clips
                        .iter()
                        .map(|clip| clip.start.saturating_add(clip.length))
                        .max()
                        .unwrap_or(0);
                    audio_max.max(midi_max)
                })
                .max()
                .unwrap_or(0);
            let track_heights = state
                .tracks
                .iter()
                .filter(|track| track.name != METRONOME_TRACK_ID)
                .map(|track| track.height)
                .collect::<Vec<_>>();
            let markers = state
                .session_markers
                .iter()
                .map(|m| (m.sample, m.name.clone()))
                .collect::<Vec<_>>();
            (
                state.tracks_width,
                match state.tracks_width {
                    Length::Fixed(width) => width,
                    _ => 200.0,
                },
                max_end_samples,
                track_heights.iter().sum::<f32>().max(1.0),
                track_heights,
                state.tempo,
                (state.time_signature_num, state.time_signature_denom),
                state
                    .tempo_points
                    .iter()
                    .map(|p| (p.sample, p.bpm))
                    .collect::<Vec<_>>(),
                state
                    .time_signature_points
                    .iter()
                    .map(|p| (p.sample, p.numerator, p.denominator))
                    .collect::<Vec<_>>(),
                match state.mixer_height {
                    Length::Fixed(height) => height,
                    _ => 300.0,
                },
                markers,
            )
        };
        const TOP_CHROME_ESTIMATE_PX: f32 = 72.0;
        const MIXER_SPLITTER_HEIGHT_PX: f32 = 3.0;
        let track_viewport_height = (window_height
            - TOP_CHROME_ESTIMATE_PX
            - if mixer_visible {
                mixer_height_px + MIXER_SPLITTER_HEIGHT_PX
            } else {
                0.0
            }
            - self.tempo.height()
            - self.ruler.height())
        .max(160.0);
        let visible_track_window =
            compute_visible_track_window(&track_heights, editor_scroll_y, track_viewport_height);
        let tracks_visible_window = if track_drag_active {
            VisibleTrackWindow {
                start_index: 0,
                end_index: track_heights.len(),
                top_padding: 0.0,
                bottom_padding: 0.0,
            }
        } else {
            visible_track_window
        };
        let min_visible_samples = (samples_per_bar * zoom_visible_bars).max(1.0) as usize;
        let min_timeline_samples = (samples_per_bar * MIN_TIMELINE_BARS).max(1.0) as usize;
        let right_padding_samples = ((samples_per_bar * zoom_visible_bars) * 0.5).max(1.0) as usize;
        let playhead_extent_samples = playhead_samples
            .map(|sample| sample.max(0.0) as usize)
            .unwrap_or(0)
            .saturating_add(right_padding_samples);
        let content_extent_samples = max_end_samples.saturating_add(right_padding_samples);
        let timeline_samples = max_end_samples
            .max(playhead_extent_samples)
            .max(content_extent_samples)
            .max(min_visible_samples)
            .max(min_timeline_samples);
        let editor_content_width = (timeline_samples as f32 * pixels_per_sample).max(1.0);
        let workspace_content_height =
            self.tempo.height() + self.ruler.height() + tracks_total_height;
        let track_context_menu_overlay = {
            let state = self.state.blocking_read();
            tracks::track_context_menu_overlay(&state, track_viewport_height - 36.0)
        };
        let clip_context_menu_overlay = {
            let state = self.state.blocking_read();
            editor::clip_context_menu_overlay(&state, transport_active)
        };
        let playhead_x_timeline = playhead_samples.map(|sample| {
            timeline_sample_to_x_f64(sample, pixels_per_sample, TIMELINE_LEFT_INSET_PX)
        });
        let clip_snap_edges = self.collect_clip_snap_edges();

        let editor_render_hash = self.editor.render_hash(&EditorViewArgs {
            session_root,
            pixels_per_sample,
            samples_per_bar,
            snap_mode,
            samples_per_beat,
            active_clip_drag,
            active_target_track: active_clip_target_track,
            active_target_valid: active_clip_target_valid,
            active_clip_snap_adjust_samples,
            active_clip_snap_targets,
            recording_preview_bounds,
            recording_preview_peaks,
            midi_clip_previews,
            visible_track_window,
        });
        let editor = self.editor.clone();
        let editor_args_owned = OwnedEditorViewArgs {
            session_root: session_root.cloned(),
            pixels_per_sample,
            samples_per_bar,
            snap_mode,
            samples_per_beat,
            active_clip_drag: active_clip_drag.cloned(),
            active_target_track: active_clip_target_track.map(str::to_string),
            active_target_valid: active_clip_target_valid,
            active_clip_snap_adjust_samples,
            active_clip_snap_targets: active_clip_snap_targets.to_vec(),
            recording_preview_bounds,
            recording_preview_peaks: recording_preview_peaks.cloned(),
            midi_clip_previews: midi_clip_previews.cloned(),
            visible_track_window,
        };
        let editor_body: Element<'_, Message> = lazy(editor_render_hash, move |_| {
            editor.clone().into_view_owned(editor_args_owned.clone())
        })
        .into();
        let editor_with_playhead = if let Some(x) = playhead_x_timeline {
            Stack::from_vec(vec![
                editor_body,
                pin(Self::playhead_line())
                    .position(Point::new(x.max(0.0), 0.0))
                    .into(),
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
        } else {
            editor_body
        };

        let editor_timeline_scrolled = scrollable(
            container(editor_with_playhead)
                .width(Length::Fixed(editor_content_width))
                .height(Length::Fixed(tracks_total_height)),
        )
        .id(Id::new(EDITOR_TIMELINE_SCROLL_ID))
        .direction(scrollable::Direction::Horizontal(
            scrollable::Scrollbar::hidden(),
        ))
        .on_scroll(|viewport| Message::EditorScrollXChanged(viewport.relative_offset().x))
        .width(Length::Fill)
        .height(Length::Fixed(tracks_total_height));

        let right_lanes_scrolled = scrollable(editor_timeline_scrolled)
            .id(Id::new(EDITOR_SCROLL_ID))
            .direction(scrollable::Direction::Vertical(
                scrollable::Scrollbar::hidden(),
            ))
            .on_scroll(|viewport| Message::EditorScrollYChanged(viewport.relative_offset().y))
            .width(Length::Fill)
            .height(Length::Fill);
        let right_lanes_with_scrollbar: Element<'_, Message> = row![
            right_lanes_scrolled,
            VerticalScrollbar::new(
                tracks_total_height,
                editor_scroll_y,
                Message::EditorScrollYChanged,
            )
            .width(Length::Fixed(16.0))
            .height(Length::Fill),
        ]
        .spacing(0)
        .width(Length::Fill)
        .height(Length::Fill)
        .into();

        let h_scroll = HorizontalScrollbar::new(
            editor_content_width,
            editor_scroll_x,
            Message::EditorScrollXChanged,
        )
        .width(Length::Fill)
        .height(Length::Fixed(16.0));

        let editor_with_zoom = right_lanes_with_scrollbar;
        let tracks_scrolled = scrollable(self.tracks.view(tracks_visible_window))
            .id(Id::new(TRACKS_SCROLL_ID))
            .direction(scrollable::Direction::Vertical(
                scrollable::Scrollbar::hidden(),
            ))
            .on_scroll(|viewport| Message::EditorScrollYChanged(viewport.relative_offset().y))
            .width(tracks_width)
            .height(Length::Fill);

        let tempo_scrolled: Element<'_, Message> = scrollable(self.tempo.view(TempoViewArgs {
            bpm: tempo,
            time_signature,
            pixels_per_sample,
            playhead_x: playhead_x_timeline.map(|x| x.max(0.0)),
            punch_range_samples,
            clip_snap_edges: clip_snap_edges.clone(),
            snap_mode,
            samples_per_beat,
            samples_per_bar: samples_per_bar as f64,
            content_width: editor_content_width,
            tempo_points,
            time_signature_points,
            shift_pressed,
            selected_tempo_points,
            selected_time_signature_points,
            timeline_left_inset_px: TIMELINE_LEFT_INSET_PX,
            clip_start_samples: 0,
            sample_rate,
            markers: markers.clone(),
        }))
        .id(Id::new(WORKSPACE_TEMPO_SCROLL_ID))
        .direction(scrollable::Direction::Horizontal(
            scrollable::Scrollbar::hidden(),
        ))
        .on_scroll(|viewport| Message::EditorScrollXChanged(viewport.relative_offset().x))
        .height(Length::Fixed(self.tempo.height()))
        .into();
        let ruler_scrolled: Element<'_, Message> = scrollable(self.ruler.view(RulerViewArgs {
            playhead_x: playhead_x_timeline.map(|x| x.max(0.0)),
            beat_pixels,
            pixels_per_sample,
            loop_range_samples,
            clip_snap_edges: clip_snap_edges.clone(),
            snap_mode,
            samples_per_beat,
            content_width: editor_content_width,
            timeline_left_inset_px: TIMELINE_LEFT_INSET_PX,
            clip_start_samples: 0,
        }))
        .id(Id::new(WORKSPACE_RULER_SCROLL_ID))
        .direction(scrollable::Direction::Horizontal(
            scrollable::Scrollbar::hidden(),
        ))
        .on_scroll(|viewport| Message::EditorScrollXChanged(viewport.relative_offset().x))
        .height(Length::Fixed(self.ruler.height()))
        .into();

        let right_panel = column![
            tempo_scrolled,
            ruler_scrolled,
            container(editor_with_zoom).height(Length::Fixed(tracks_total_height)),
        ]
        .width(Length::Fill);

        let left_panel = column![
            container("")
                .width(tracks_width)
                .height(Length::Fixed(self.tempo.height()))
                .style(|_theme| container::Style {
                    background: Some(Background::Color(Color {
                        r: 0.1,
                        g: 0.1,
                        b: 0.1,
                        a: 1.0,
                    })),
                    ..container::Style::default()
                }),
            container("")
                .width(tracks_width)
                .height(Length::Fixed(self.ruler.height()))
                .style(|_theme| container::Style {
                    background: Some(Background::Color(Color {
                        r: 0.1,
                        g: 0.1,
                        b: 0.1,
                        a: 1.0,
                    })),
                    ..container::Style::default()
                }),
            tracks_scrolled,
        ]
        .width(tracks_width);

        let tracks_splitter = mouse_area(column![
            container("")
                .width(Length::Fixed(3.0))
                .height(Length::Fixed(self.tempo.height()))
                .style(|_theme| container::Style {
                    background: Some(Background::Color(Color {
                        r: 0.5,
                        g: 0.5,
                        b: 0.5,
                        a: 0.5,
                    })),
                    ..container::Style::default()
                }),
            container("")
                .width(Length::Fixed(3.0))
                .height(Length::Fixed(self.ruler.height()))
                .style(|_theme| container::Style {
                    background: Some(Background::Color(Color {
                        r: 0.5,
                        g: 0.5,
                        b: 0.5,
                        a: 0.5,
                    })),
                    ..container::Style::default()
                }),
            container("")
                .width(Length::Fixed(3.0))
                .height(Length::Fill)
                .style(move |_theme| container::Style {
                    background: Some(Background::Color(Color {
                        r: 0.7,
                        g: 0.7,
                        b: 0.7,
                        a: if tracks_resize_hovered { 0.95 } else { 0.6 },
                    })),
                    ..container::Style::default()
                }),
        ])
        .on_enter(Message::TracksResizeHover(true))
        .on_exit(Message::TracksResizeHover(false))
        .on_press(Message::TracksResizeStart);

        let shared_workspace: Element<'_, Message> = match (tracks_visible, editor_visible) {
            (true, true) => row![left_panel, tracks_splitter, right_panel]
                .height(Length::Fixed(workspace_content_height))
                .into(),
            (true, false) => row![left_panel]
                .height(Length::Fixed(workspace_content_height))
                .into(),
            (false, true) => row![right_panel]
                .height(Length::Fixed(workspace_content_height))
                .into(),
            (false, false) => container("")
                .width(Length::Fill)
                .height(Length::Fixed(workspace_content_height))
                .into(),
        };

        let shared_workspace: Element<'_, Message> = {
            let mut stack = Stack::new().push(shared_workspace);
            if let Some((anchor, menu)) = track_context_menu_overlay {
                stack = stack.push(pin(menu).position(Point::new(
                    anchor.x.max(0.0),
                    self.tempo.height() + self.ruler.height() + anchor.y.max(0.0),
                )));
            }
            if let Some((anchor, menu)) = clip_context_menu_overlay {
                stack = stack.push(pin(menu).position(Point::new(
                    tracks_width_px + 3.0 + anchor.x.max(0.0),
                    self.tempo.height() + self.ruler.height() + anchor.y.max(0.0),
                )));
            }
            stack.into()
        };

        let editor_footer: Element<'_, Message> = if editor_visible {
            container(
                row![
                    Space::new().width(Length::Fixed(if tracks_visible {
                        tracks_width_px + 3.0
                    } else {
                        0.0
                    })),
                    container(
                        row![
                            h_scroll,
                            slider(
                                0.0..=1.0,
                                visible_bars_to_zoom_slider(zoom_visible_bars),
                                Message::ZoomSliderChanged,
                            )
                            .step(0.001)
                            .width(Length::Fixed(105.0)),
                        ]
                        .spacing(8),
                    )
                    .width(Length::Fill)
                    .height(Length::Fixed(16.0))
                    .padding([0, 8]),
                ]
                .height(Length::Fill)
                .align_y(iced::alignment::Vertical::Bottom),
            )
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
        } else {
            Space::new().into()
        };

        let workspace_with_footer = Stack::from_vec(vec![
            shared_workspace,
            container(editor_footer)
                .width(Length::Fill)
                .height(Length::Fill)
                .into(),
        ])
        .width(Length::Fill)
        .height(Length::Fill);
        let workspace_body: Element<'_, Message> = if mixer_visible {
            column![
                workspace_with_footer,
                mouse_area(
                    container("")
                        .width(Length::Fill)
                        .height(Length::Fixed(3.0))
                        .style(move |_theme| container::Style {
                            background: Some(Background::Color(Color {
                                r: 0.7,
                                g: 0.7,
                                b: 0.7,
                                a: if mixer_resize_hovered { 0.95 } else { 0.6 },
                            })),
                            ..container::Style::default()
                        }),
                )
                .on_enter(Message::MixerResizeHover(true))
                .on_exit(Message::MixerResizeHover(false))
                .on_press(Message::MixerResizeStart),
                self.mixer.view(
                    mixer_level_edit_track,
                    mixer_level_edit_input,
                    window_width,
                    mixer_scroll_x,
                ),
            ]
            .width(Length::Fill)
            .into()
        } else {
            column![workspace_with_footer].width(Length::Fill).into()
        };
        container(workspace_body)
            .style(|_theme| crate::style::app_background())
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    }

    pub fn piano_view<'a>(&'a self, args: WorkspaceViewArgs<'a>) -> Element<'a, Message> {
        let WorkspaceViewArgs {
            playhead_samples,
            pixels_per_sample,
            beat_pixels,
            samples_per_bar,
            snap_mode,
            samples_per_beat,
            shift_pressed,
            mixer_visible: _,
            selected_tempo_points,
            selected_time_signature_points,
            sample_rate,
            ..
        } = args;

        let (
            tempo,
            time_signature,
            tempo_points,
            time_signature_points,
            clip_start_samples,
            clip_length_samples,
            zoom_x,
            markers,
        ) = {
            let state = self.state.blocking_read();
            let markers = state
                .session_markers
                .iter()
                .map(|m| (m.sample, m.name.clone()))
                .collect::<Vec<_>>();
            (
                state.tempo,
                (state.time_signature_num, state.time_signature_denom),
                state
                    .tempo_points
                    .iter()
                    .map(|p| (p.sample, p.bpm))
                    .collect::<Vec<_>>(),
                state
                    .time_signature_points
                    .iter()
                    .map(|p| (p.sample, p.numerator, p.denominator))
                    .collect::<Vec<_>>(),
                state
                    .piano
                    .as_ref()
                    .map(|roll| roll.clip_start_samples)
                    .unwrap_or(0),
                state
                    .piano
                    .as_ref()
                    .map(|roll| roll.clip_length_samples)
                    .unwrap_or(samples_per_bar.max(1.0) as usize),
                state.piano_zoom_x,
                markers,
            )
        };
        let horizontal_zoom = zoom_x.max(1.0);
        let horizontal_pixels_per_sample = (pixels_per_sample * horizontal_zoom).max(0.0001);
        let horizontal_beat_pixels = (beat_pixels * horizontal_zoom).max(0.0001);
        let timeline_content_width =
            (clip_length_samples.max(1) as f32 * horizontal_pixels_per_sample).max(1.0);
        let playhead_x = playhead_samples.map(|sample| {
            ((sample as f32 - clip_start_samples as f32) * horizontal_pixels_per_sample).max(0.0)
        });

        let piano_content = self
            .midi_edit
            .view(pixels_per_sample, samples_per_bar, playhead_x);

        container(
            column![
                row![
                    container("")
                        .width(Length::Fixed(TOOLS_STRIP_WIDTH + MAIN_SPLIT_SPACING,))
                        .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(KEYBOARD_WIDTH))
                        .height(Length::Fill),
                    scrollable(container(self.tempo.view(TempoViewArgs {
                        bpm: tempo,
                        time_signature,
                        pixels_per_sample: horizontal_pixels_per_sample,
                        playhead_x,
                        punch_range_samples: None,
                        clip_snap_edges: self.collect_clip_snap_edges(),
                        snap_mode,
                        samples_per_beat,
                        samples_per_bar: samples_per_bar as f64,
                        content_width: timeline_content_width,
                        tempo_points,
                        time_signature_points,
                        shift_pressed,
                        selected_tempo_points,
                        selected_time_signature_points,
                        timeline_left_inset_px: 0.0,
                        clip_start_samples: 0,
                        sample_rate,
                        markers,
                    })))
                    .id(Id::new(PIANO_TEMPO_SCROLL_ID))
                    .direction(scrollable::Direction::Horizontal(
                        scrollable::Scrollbar::hidden(),
                    ))
                    .on_scroll(|viewport| Message::PianoScrollXChanged(
                        viewport.relative_offset().x
                    ))
                    .width(Length::Fill)
                    .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
                        .height(Length::Fill),
                ]
                .width(Length::Fill)
                .height(Length::Fixed(self.tempo.height())),
                row![
                    container("")
                        .width(Length::Fixed(TOOLS_STRIP_WIDTH + MAIN_SPLIT_SPACING,))
                        .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(KEYBOARD_WIDTH))
                        .height(Length::Fill),
                    scrollable(container(self.ruler.view(RulerViewArgs {
                        playhead_x,
                        beat_pixels: horizontal_beat_pixels,
                        pixels_per_sample: horizontal_pixels_per_sample,
                        loop_range_samples: None,
                        clip_snap_edges: self.collect_clip_snap_edges(),
                        snap_mode,
                        samples_per_beat,
                        content_width: timeline_content_width,
                        timeline_left_inset_px: 0.0,
                        clip_start_samples,
                    })))
                    .id(Id::new(PIANO_RULER_SCROLL_ID))
                    .direction(scrollable::Direction::Horizontal(
                        scrollable::Scrollbar::hidden(),
                    ))
                    .on_scroll(|viewport| Message::PianoScrollXChanged(
                        viewport.relative_offset().x
                    ))
                    .width(Length::Fill)
                    .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
                        .height(Length::Fill),
                ]
                .width(Length::Fill)
                .height(Length::Fixed(self.ruler.height())),
                piano_content,
            ]
            .width(Length::Fill)
            .height(Length::Fill),
        )
        .style(|_theme| crate::style::app_background())
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    pub fn pitch_correction_view<'a>(
        &'a self,
        args: WorkspaceViewArgs<'a>,
    ) -> Element<'a, Message> {
        let WorkspaceViewArgs {
            playhead_samples,
            pixels_per_sample,
            beat_pixels,
            samples_per_bar,
            snap_mode,
            samples_per_beat,
            shift_pressed,
            selected_tempo_points,
            selected_time_signature_points,
            sample_rate,
            ..
        } = args;
        let (
            clip_length_samples,
            zoom_x,
            tempo,
            time_signature,
            tempo_points,
            time_signature_points,
            markers,
        ) = {
            let state = self.state.blocking_read();
            let markers = state
                .session_markers
                .iter()
                .map(|m| (m.sample, m.name.clone()))
                .collect::<Vec<_>>();
            (
                state
                    .pitch_correction
                    .as_ref()
                    .map(|roll| roll.clip_length_samples)
                    .unwrap_or(samples_per_bar.max(1.0) as usize),
                state.piano_zoom_x,
                state.tempo,
                (state.time_signature_num, state.time_signature_denom),
                state
                    .tempo_points
                    .iter()
                    .map(|p| (p.sample, p.bpm))
                    .collect::<Vec<_>>(),
                state
                    .time_signature_points
                    .iter()
                    .map(|p| (p.sample, p.numerator, p.denominator))
                    .collect::<Vec<_>>(),
                markers,
            )
        };
        let horizontal_zoom = zoom_x.max(1.0);
        let horizontal_pixels_per_sample = (pixels_per_sample * horizontal_zoom).max(0.0001);
        let horizontal_beat_pixels = (beat_pixels * horizontal_zoom).max(0.0001);
        let timeline_content_width =
            (clip_length_samples.max(1) as f32 * horizontal_pixels_per_sample).max(1.0);
        let playhead_x =
            playhead_samples.map(|sample| (sample as f32 * horizontal_pixels_per_sample).max(0.0));
        let pitch_correction_content =
            self.pitch_correction
                .view(pixels_per_sample, samples_per_bar, playhead_x);

        container(
            column![
                row![
                    container("")
                        .width(Length::Fixed(TOOLS_STRIP_WIDTH + MAIN_SPLIT_SPACING,))
                        .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(KEYBOARD_WIDTH))
                        .height(Length::Fill),
                    scrollable(container(self.tempo.view(TempoViewArgs {
                        bpm: tempo,
                        time_signature,
                        pixels_per_sample: horizontal_pixels_per_sample,
                        playhead_x,
                        punch_range_samples: None,
                        clip_snap_edges: self.collect_clip_snap_edges(),
                        snap_mode,
                        samples_per_beat,
                        samples_per_bar: samples_per_bar as f64,
                        content_width: timeline_content_width,
                        tempo_points,
                        time_signature_points,
                        shift_pressed,
                        selected_tempo_points,
                        selected_time_signature_points,
                        timeline_left_inset_px: 0.0,
                        clip_start_samples: 0,
                        sample_rate,
                        markers,
                    })))
                    .id(Id::new(PIANO_TEMPO_SCROLL_ID))
                    .direction(scrollable::Direction::Horizontal(
                        scrollable::Scrollbar::hidden(),
                    ))
                    .on_scroll(|viewport| Message::PianoScrollXChanged(
                        viewport.relative_offset().x
                    ))
                    .width(Length::Fill)
                    .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
                        .height(Length::Fill),
                ]
                .width(Length::Fill)
                .height(Length::Fixed(self.tempo.height())),
                row![
                    container("")
                        .width(Length::Fixed(TOOLS_STRIP_WIDTH + MAIN_SPLIT_SPACING,))
                        .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(KEYBOARD_WIDTH))
                        .height(Length::Fill),
                    scrollable(container(self.ruler.view(RulerViewArgs {
                        playhead_x,
                        beat_pixels: horizontal_beat_pixels,
                        pixels_per_sample: horizontal_pixels_per_sample,
                        loop_range_samples: None,
                        clip_snap_edges: self.collect_clip_snap_edges(),
                        snap_mode,
                        samples_per_beat,
                        content_width: timeline_content_width,
                        timeline_left_inset_px: 0.0,
                        clip_start_samples: 0,
                    })))
                    .id(Id::new(PIANO_RULER_SCROLL_ID))
                    .direction(scrollable::Direction::Horizontal(
                        scrollable::Scrollbar::hidden(),
                    ))
                    .on_scroll(|viewport| Message::PianoScrollXChanged(
                        viewport.relative_offset().x
                    ))
                    .width(Length::Fill)
                    .height(Length::Fill),
                    container("")
                        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
                        .height(Length::Fill),
                ]
                .width(Length::Fill)
                .height(Length::Fixed(self.ruler.height())),
                pitch_correction_content,
            ]
            .width(Length::Fill)
            .height(Length::Fill),
        )
        .style(|_theme| crate::style::app_background())
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use tokio::sync::RwLock;

    #[test]
    fn update_is_a_no_op() {
        let state = Arc::new(RwLock::new(crate::state::StateData::default()));
        let mut workspace = Workspace::new(state);

        workspace.update(&Message::Cancel);
    }

    #[test]
    fn timeline_sample_to_x_f64_calculates_correctly() {
        let x = timeline_sample_to_x_f64(100.0, 0.5, 10.0);
        assert!((x - 60.0).abs() < 0.01);
    }

    #[test]
    fn timeline_sample_to_x_calculates_correctly() {
        let x = timeline_sample_to_x(100, 0.5, 10.0);
        assert!((x - 60.0).abs() < 0.01);
    }

    #[test]
    fn timeline_x_to_sample_f32_calculates_correctly() {
        let sample = timeline_x_to_sample_f32(60.0, 0.5, 10.0);
        assert!((sample - 100.0).abs() < 0.01);
    }

    #[test]
    fn timeline_x_to_sample_f32_handles_zero_pixels_per_sample() {
        let sample = timeline_x_to_sample_f32(100.0, 0.0, 10.0);
        assert_eq!(sample, 0.0);
    }

    #[test]
    fn clip_kind_key_returns_expected_values() {
        assert_eq!(clip_kind_key(maolan_engine::kind::Kind::Audio), 0);
        assert_eq!(clip_kind_key(maolan_engine::kind::Kind::MIDI), 1);
    }

    #[test]
    fn clip_snap_edge_creation() {
        let edge = ClipSnapEdge {
            clip_id: crate::state::ClipId {
                track_idx: "track1".to_string(),
                clip_idx: 0,
                kind: maolan_engine::kind::Kind::Audio,
            },
            sample: 100,
        };
        assert_eq!(edge.sample, 100);
    }

    #[test]
    fn visible_track_window_creation() {
        let window = VisibleTrackWindow {
            start_index: 0,
            end_index: 5,
            top_padding: 10.0,
            bottom_padding: 20.0,
        };
        assert_eq!(window.start_index, 0);
        assert_eq!(window.end_index, 5);
        assert!((window.top_padding - 10.0).abs() < f32::EPSILON);
    }

    #[test]
    fn compute_visible_track_window_empty() {
        let window = compute_visible_track_window(&[], 0.0, 100.0);
        assert_eq!(window.start_index, 0);
        assert_eq!(window.end_index, 0);
    }

    #[test]
    fn workspace_new_creates_instance() {
        let state = Arc::new(RwLock::new(crate::state::StateData::default()));
        let workspace = Workspace::new(state);
        let _ = &workspace;
    }
}
