use super::VisibleTrackWindow;
use crate::{
    consts::{
        state_ids::METRONOME_TRACK_ID,
        state_track::{TRACK_SUBTRACK_GAP, TRACK_SUBTRACK_MIN_HEIGHT},
        workspace::{AUDIO_CLIP_SELECTED_BASE, MIDI_CLIP_SELECTED_BASE, MIN_TICK_SPACING_PX},
        workspace_editor::CHECKPOINTS,
    },
    message::{DraggedClip, Message, SnapMode},
    state::{ClipPeaks, MidiClipPreviewMap, State, StateData, Track},
};
use iced::{
    Background, Border, Color, Element, Length, Point, Rectangle, Renderer, Theme, mouse,
    widget::{
        Space, Stack, canvas,
        canvas::{Frame, Geometry, Path},
        column, container, mouse_area, pin, text,
    },
};
use maolan_engine::kind::Kind;
use maolan_widgets::clip::{
    AudioClip as AudioClipWidget, AudioClipData as WidgetAudioClipData,
    AudioClipInteraction as WidgetAudioClipInteraction, ClipEdgeMessages as WidgetClipEdgeMessages,
    MIDIClip as MIDIClipWidget, MIDIClipData as WidgetMIDIClipData,
    MIDIClipInteraction as WidgetMIDIClipInteraction,
};
use std::{
    cell::Cell,
    collections::{HashMap, HashSet},
    hash::{Hash, Hasher},
    path::PathBuf,
    sync::Arc,
};

fn widget_audio_clip_data(
    clip: &crate::state::AudioClip,
    stretch_ratio: f32,
) -> WidgetAudioClipData {
    WidgetAudioClipData {
        name: clip.name.clone(),
        start: clip.start,
        length: clip.length,
        offset: clip.offset,
        muted: clip.muted,
        max_length_samples: clip.max_length_samples,
        source_length_samples: clip.source_length_samples,
        peaks: clip.peaks.clone(),
        fade_enabled: clip.fade_enabled,
        fade_in_samples: clip.fade_in_samples,
        fade_out_samples: clip.fade_out_samples,
        grouped_clips: clip
            .grouped_clips
            .iter()
            .map(|c| widget_audio_clip_data(c, stretch_ratio))
            .collect(),
        stretch_ratio,
    }
}

fn widget_midi_clip_data(clip: &crate::state::MIDIClip) -> WidgetMIDIClipData {
    WidgetMIDIClipData {
        name: clip.name.clone(),
        start: clip.start,
        length: clip.length,
        offset: clip.offset,
        input_channel: clip.input_channel,
        muted: clip.muted,
        max_length_samples: clip.max_length_samples,
        grouped_clips: clip
            .grouped_clips
            .iter()
            .map(widget_midi_clip_data)
            .collect(),
    }
}

struct TrackElementViewArgs<'a> {
    state: &'a StateData,
    track: &'a Track,
    session_root: Option<&'a PathBuf>,
    pixels_per_sample: f32,
    samples_per_bar: f32,
    snap_mode: SnapMode,
    samples_per_beat: f64,
    active_clip_drag: Option<&'a DraggedClip>,
    active_target_track: Option<&'a str>,
    active_target_valid: bool,
    active_clip_snap_adjust_samples: f32,
    active_clip_snap_targets: &'a [crate::state::ClipId],
    recording_preview_bounds: Option<(usize, usize)>,
    recording_preview_peaks: Option<&'a HashMap<String, ClipPeaks>>,
    midi_clip_previews: Option<&'a MidiClipPreviewMap>,
}

fn automation_point_color(target: crate::message::TrackAutomationTarget) -> Color {
    match target {
        crate::message::TrackAutomationTarget::Volume => Color::from_rgba(0.98, 0.78, 0.22, 0.95),
        crate::message::TrackAutomationTarget::Balance => Color::from_rgba(0.88, 0.62, 0.24, 0.95),
        crate::message::TrackAutomationTarget::Mute => Color::from_rgba(0.95, 0.45, 0.22, 0.95),
        crate::message::TrackAutomationTarget::Lv2Parameter { .. } => {
            Color::from_rgba(0.6, 0.5, 0.95, 0.95)
        }
        crate::message::TrackAutomationTarget::Vst3Parameter { .. } => {
            Color::from_rgba(0.28, 0.82, 0.78, 0.95)
        }
        crate::message::TrackAutomationTarget::ClapParameter { .. } => {
            Color::from_rgba(0.4, 0.72, 0.98, 0.95)
        }
    }
}

#[derive(Clone)]
struct TrackBarGridCanvas {
    bar_pixels: f32,
}

#[derive(Default)]
struct TrackBarGridCanvasState {
    cache: canvas::Cache,
    last_hash: Cell<u64>,
}

impl TrackBarGridCanvas {
    fn step_for_spacing(base_px: f32, min_spacing_px: f32) -> usize {
        if base_px <= 0.0 {
            return 1;
        }
        let mut step = 1usize;
        while base_px * (step as f32) < min_spacing_px {
            step *= 2;
        }
        step
    }

    fn shape_hash(&self, bounds: Rectangle) -> u64 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        bounds.width.to_bits().hash(&mut hasher);
        bounds.height.to_bits().hash(&mut hasher);
        self.bar_pixels.to_bits().hash(&mut hasher);
        hasher.finish()
    }
}

impl canvas::Program<Message> for TrackBarGridCanvas {
    type State = TrackBarGridCanvasState;

    fn draw(
        &self,
        state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<Geometry> {
        if bounds.width <= 0.0 || bounds.height <= 0.0 {
            return vec![];
        }

        let hash = self.shape_hash(bounds);
        if state.last_hash.get() != hash {
            state.cache.clear();
            state.last_hash.set(hash);
        }

        let geom = state
            .cache
            .draw(renderer, bounds.size(), |frame: &mut Frame| {
                let step = self.bar_pixels.max(1.0)
                    * Self::step_for_spacing(self.bar_pixels, MIN_TICK_SPACING_PX) as f32;
                let color = Color::from_rgba(0.86, 0.96, 0.74, 0.14);
                let mut x = 0.0_f32;
                while x <= bounds.width + 1.0 {
                    frame.stroke(
                        &Path::line(Point::new(x, 0.0), Point::new(x, bounds.height)),
                        canvas::Stroke::default().with_color(color).with_width(1.0),
                    );
                    x += step;
                }
            });
        vec![geom]
    }
}

fn track_bar_grid_overlay(
    height: f32,
    samples_per_bar: f32,
    pixels_per_sample: f32,
) -> Element<'static, Message> {
    let bar_pixels = (samples_per_bar.max(1.0) * pixels_per_sample).max(1.0);
    canvas(TrackBarGridCanvas { bar_pixels })
        .width(Length::Fill)
        .height(Length::Fixed(height))
        .into()
}

#[derive(Clone)]
struct TrackLaneBackgroundCanvas {
    header_height: f32,
    lane_height: f32,
    audio_lanes: usize,
    midi_lanes: usize,
}

#[derive(Default)]
struct TrackLaneBackgroundCanvasState {
    cache: canvas::Cache,
    last_hash: Cell<u64>,
}

impl TrackLaneBackgroundCanvas {
    fn shape_hash(&self, bounds: Rectangle) -> u64 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        bounds.width.to_bits().hash(&mut hasher);
        bounds.height.to_bits().hash(&mut hasher);
        self.header_height.to_bits().hash(&mut hasher);
        self.lane_height.to_bits().hash(&mut hasher);
        self.audio_lanes.hash(&mut hasher);
        self.midi_lanes.hash(&mut hasher);
        hasher.finish()
    }
}

impl canvas::Program<Message> for TrackLaneBackgroundCanvas {
    type State = TrackLaneBackgroundCanvasState;

    fn draw(
        &self,
        state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<Geometry> {
        if bounds.width <= 0.0 || bounds.height <= 0.0 {
            return vec![];
        }

        let hash = self.shape_hash(bounds);
        if state.last_hash.get() != hash {
            state.cache.clear();
            state.last_hash.set(hash);
        }

        let geom = state
            .cache
            .draw(renderer, bounds.size(), |frame: &mut Frame| {
                let mut y = 0.0;
                for i in 0..self.audio_lanes {
                    let is_last = i + 1 == self.audio_lanes && self.midi_lanes == 0;
                    let h = if is_last {
                        (bounds.height - y - 1.0).max(0.0)
                    } else {
                        self.lane_height - 2.0
                    };
                    frame.fill(
                        &Path::rectangle(
                            Point::new(0.0, y + 1.0),
                            iced::Size::new(bounds.width, h),
                        ),
                        Color::from_rgba(0.15, 0.20, 0.28, 0.12),
                    );
                    y += self.lane_height + TRACK_SUBTRACK_GAP;
                }
                for i in 0..self.midi_lanes {
                    let is_last = i + 1 == self.midi_lanes;
                    let h = if is_last {
                        (bounds.height - y - 1.0).max(0.0)
                    } else {
                        self.lane_height - 2.0
                    };
                    frame.fill(
                        &Path::rectangle(
                            Point::new(0.0, y + 1.0),
                            iced::Size::new(bounds.width, h),
                        ),
                        Color::from_rgba(0.12, 0.26, 0.14, 0.12),
                    );
                    y += self.lane_height + TRACK_SUBTRACK_GAP;
                }
            });

        vec![geom]
    }
}

fn track_lane_background_overlay(
    height: f32,
    header_height: f32,
    lane_height: f32,
    audio_lanes: usize,
    midi_lanes: usize,
) -> Element<'static, Message> {
    canvas(TrackLaneBackgroundCanvas {
        header_height,
        lane_height,
        audio_lanes,
        midi_lanes,
    })
    .width(Length::Fill)
    .height(Length::Fixed(height))
    .into()
}

fn view_track_elements(args: TrackElementViewArgs<'_>) -> Element<'static, Message> {
    let TrackElementViewArgs {
        state,
        track,
        session_root,
        pixels_per_sample,
        samples_per_bar,
        snap_mode,
        samples_per_beat,
        active_clip_drag,
        active_target_track,
        active_target_valid,
        active_clip_snap_adjust_samples,
        active_clip_snap_targets,
        recording_preview_bounds,
        recording_preview_peaks,
        midi_clip_previews,
    } = args;
    let snap_sample = |sample: f32, delta_samples: f32| -> f32 {
        snap_mode.snap_sample_drag(
            sample as f64,
            delta_samples as f64,
            samples_per_beat,
            samples_per_bar as f64,
        ) as f32
    };
    let clip_snap_outline = |clip_width: f32, clip_height: f32, radius: f32| {
        container("")
            .width(Length::Fixed(clip_width))
            .height(Length::Fixed(clip_height))
            .style(move |_theme| container::Style {
                background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.0))),
                border: Border {
                    color: Color::from_rgba(1.0, 0.92, 0.18, 0.98),
                    width: 4.0,
                    radius: radius.into(),
                },
                ..container::Style::default()
            })
    };
    let mut clips: Vec<Element<'static, Message>> = vec![
        mouse_area(container("").width(Length::Fill).height(Length::Fill))
            .on_press(Message::DeselectClips)
            .into(),
    ];
    let height = track.height;
    let layout = track.lane_layout();
    let lane_height = layout.lane_height.max(12.0);
    let lane_clip_height = (lane_height - 6.0).max(12.0);
    let total_lanes = (layout.audio_lanes + layout.midi_lanes).max(1);
    let gaps = (total_lanes.saturating_sub(1)) as f32 * TRACK_SUBTRACK_GAP;
    let editor_lane_height =
        ((track.height - gaps) / total_lanes as f32).max(TRACK_SUBTRACK_MIN_HEIGHT);
    let editor_lane_top =
        |lane_index: usize| lane_index as f32 * (editor_lane_height + TRACK_SUBTRACK_GAP);
    let track_name_cloned = track.name.clone();
    let mut selected_audio_indices = HashSet::new();
    let mut selected_midi_indices = HashSet::new();
    for selected in &state.selected_clips {
        if selected.track_idx != track_name_cloned {
            continue;
        }
        match selected.kind {
            Kind::Audio => {
                selected_audio_indices.insert(selected.clip_idx);
            }
            Kind::MIDI => {
                selected_midi_indices.insert(selected.clip_idx);
            }
        }
    }
    let selected_audio_count = selected_audio_indices.len();
    let selected_midi_count = selected_midi_indices.len();
    clips.push(
        pin(track_lane_background_overlay(
            height,
            layout.header_height,
            editor_lane_height,
            if track.is_master { 0 } else { track.audio.ins },
            if track.is_master { 0 } else { track.midi.ins },
        ))
        .position(Point::new(0.0, 0.0))
        .into(),
    );
    clips.push(
        pin(track_bar_grid_overlay(
            height,
            samples_per_bar,
            pixels_per_sample,
        ))
        .position(Point::new(0.0, 0.0))
        .into(),
    );
    let visible_automation_lanes: Vec<_> = track
        .automation_lanes
        .iter()
        .filter(|lane| lane.visible)
        .collect();
    for (lane_index, lane) in visible_automation_lanes.iter().enumerate() {
        let lane_top = track.automation_lane_top(lane_index);
        let lane_track_name = track_name_cloned.clone();
        let lane_target = lane.target;
        clips.push(
            pin(mouse_area(
                container("")
                    .width(Length::Fill)
                    .height(Length::Fixed(lane_height))
                    .style(|_theme| container::Style {
                        background: Some(Background::Color(Color {
                            r: 0.26,
                            g: 0.18,
                            b: 0.1,
                            a: 0.22,
                        })),
                        ..container::Style::default()
                    }),
            )
            .on_move(move |position| Message::TrackAutomationLaneHover {
                track_name: lane_track_name.clone(),
                target: lane_target,
                position,
            })
            .on_press(Message::TrackAutomationLaneInsertPoint {
                track_name: track_name_cloned.clone(),
                target: lane.target,
            }))
            .position(Point::new(0.0, lane_top))
            .into(),
        );
        clips.push(
            pin(
                container(text(format!("Automation {}", lane.target)).size(10))
                    .width(Length::Shrink)
                    .height(Length::Fixed(12.0))
                    .padding([1, 4])
                    .style(|_theme| container::Style {
                        background: Some(Background::Color(Color {
                            r: 0.35,
                            g: 0.24,
                            b: 0.12,
                            a: 0.45,
                        })),
                        ..container::Style::default()
                    }),
            )
            .position(Point::new(4.0, lane_top + 2.0))
            .into(),
        );

        let point_color = automation_point_color(lane.target);
        let mut sorted_indices: Vec<usize> = (0..lane.points.len()).collect();
        sorted_indices.sort_unstable_by_key(|&idx| lane.points[idx].sample);
        for pair in sorted_indices.windows(2) {
            let left = &lane.points[pair[0]];
            let right = &lane.points[pair[1]];
            let left_x = left.sample as f32 * pixels_per_sample;
            let right_x = right.sample as f32 * pixels_per_sample;
            let left_y =
                lane_top + 3.0 + (lane_clip_height - 2.0) * (1.0 - left.value.clamp(0.0, 1.0));
            let right_y =
                lane_top + 3.0 + (lane_clip_height - 2.0) * (1.0 - right.value.clamp(0.0, 1.0));
            let width = (right_x - left_x).abs().max(1.0);
            let min_y = left_y.min(right_y);
            clips.push(
                pin(container("")
                    .width(Length::Fixed(width))
                    .height(Length::Fixed(1.0))
                    .style(move |_theme| container::Style {
                        background: Some(Background::Color(point_color)),
                        ..container::Style::default()
                    }))
                .position(Point::new(left_x.min(right_x), min_y))
                .into(),
            );
        }
        for point in &lane.points {
            let clamped_value = point.value.clamp(0.0, 1.0);
            let x = point.sample as f32 * pixels_per_sample;
            let y = lane_top + 3.0 + (lane_clip_height - 2.0) * (1.0 - clamped_value);
            let point_track_name = track_name_cloned.clone();
            let point_target = lane.target;
            let point_sample = point.sample;
            clips.push(
                pin(mouse_area(
                    container("")
                        .width(Length::Fixed(5.0))
                        .height(Length::Fixed(5.0))
                        .style(move |_theme| container::Style {
                            background: Some(Background::Color(point_color)),
                            border: Border {
                                color: Color::from_rgba(0.1, 0.1, 0.1, 0.9),
                                width: 1.0,
                                radius: 2.5.into(),
                            },
                            ..container::Style::default()
                        }),
                )
                .on_right_press(Message::TrackAutomationLaneDeletePoint {
                    track_name: point_track_name,
                    target: point_target,
                    sample: point_sample,
                }))
                .position(Point::new((x - 2.0).max(0.0), y.max(lane_top + 2.0)))
                .into(),
            );
        }
    }

    if !track.is_master {
        for (index, clip) in track.audio.clips.iter().enumerate() {
            let clip_name = clip.name.clone();
            let clip_label = format!(
                "{}{}{}",
                AudioClipWidget::<Message>::clean_name(&clip_name),
                if clip.take_lane_pinned { " [P]" } else { "" },
                if clip.take_lane_locked { " [L]" } else { "" }
            );
            let is_selected = selected_audio_indices.contains(&index);
            let active_drag = active_clip_drag.filter(|d| {
                d.kind == Kind::Audio && d.track_index == track_name_cloned && d.index == index
            });
            let group_drag = active_clip_drag.filter(|d| {
                d.kind == Kind::Audio
                    && d.track_index == track_name_cloned
                    && selected_audio_indices.contains(&d.index)
                    && selected_audio_count > 1
                    && is_selected
            });
            let drag_for_clip = group_drag.or(active_drag);
            let dragged_to_other_track = drag_for_clip.is_some_and(|d| {
                !d.copy
                    && active_target_valid
                    && active_target_track
                        .is_some_and(|target| target != track_name_cloned.as_str())
            });
            let show_preview_in_this_track = drag_for_clip.is_some_and(|_| {
                active_target_track.is_some_and(|target| target == track_name_cloned.as_str())
            });
            let dragged_start = drag_for_clip
                .filter(|d| !d.copy && !show_preview_in_this_track)
                .map(|d| {
                    let delta_samples = (d.end.x - d.start.x) / pixels_per_sample.max(1.0e-6);
                    snap_sample(clip.start as f32 + delta_samples, delta_samples)
                        + active_clip_snap_adjust_samples
                })
                .unwrap_or(clip.start as f32);

            let _lane = 0;
            let lane_top = editor_lane_top(0) + 1.0;
            let clip_width = (clip.length as f32 * pixels_per_sample).max(12.0);
            let clip_height = (editor_lane_height - 2.0).max(8.0);
            let display_clip_label =
                AudioClipWidget::<Message>::label_for_width(&clip_label, clip_width);
            let audio_left_handle_hovered = state.hovered_clip_resize_handle.as_ref().is_some_and(
                |(track_idx, clip_idx, kind, is_right_side)| {
                    track_idx == &track_name_cloned
                        && *clip_idx == index
                        && *kind == Kind::Audio
                        && !*is_right_side
                },
            );
            let audio_right_handle_hovered = state.hovered_clip_resize_handle.as_ref().is_some_and(
                |(track_idx, clip_idx, kind, is_right_side)| {
                    track_idx == &track_name_cloned
                        && *clip_idx == index
                        && *kind == Kind::Audio
                        && *is_right_side
                },
            );

            let stretch_ratio = match &state.resizing {
                Some(crate::state::Resizing::Clip {
                    kind: maolan_engine::kind::Kind::Audio,
                    track_name: t,
                    index: i,
                    stretch_mode: true,
                    initial_length,
                    ..
                }) if t == &track_name_cloned && *i == index => {
                    (clip.length as f32 / initial_length.max(1.0)).max(0.01)
                }
                _ => 1.0,
            };

            if !dragged_to_other_track {
                let track_name_for_drag = track_name_cloned.clone();
                clips.push(
                    pin(
                        AudioClipWidget::new(widget_audio_clip_data(clip, stretch_ratio))
                            .with_session_root(session_root)
                            .with_pixels_per_sample(pixels_per_sample)
                            .with_size(clip_width, clip_height)
                            .with_label(display_clip_label.clone())
                            .selected(is_selected)
                            .hovered_handles(audio_left_handle_hovered, audio_right_handle_hovered)
                            .interactive(WidgetAudioClipInteraction {
                                on_select: Message::SelectClip {
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    kind: Kind::Audio,
                                },
                                on_open: Message::OpenClipPlugins {
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                },
                                on_drag: (!clip.take_lane_locked).then_some(
                                    Arc::new(move |point| {
                                        let mut clip_data = DraggedClip::new(
                                            Kind::Audio,
                                            index,
                                            track_name_for_drag.clone(),
                                        );
                                        clip_data.start = point;
                                        Message::ClipDrag(clip_data)
                                    })
                                        as Arc<dyn Fn(Point) -> Message + Send + Sync + 'static>,
                                ),
                                edges: WidgetClipEdgeMessages {
                                    left_hover_enter: Message::ClipResizeHandleHover {
                                        kind: Kind::Audio,
                                        track_idx: track_name_cloned.clone(),
                                        clip_idx: index,
                                        is_right_side: false,
                                        hovered: true,
                                    },
                                    left_hover_exit: Message::ClipResizeHandleHover {
                                        kind: Kind::Audio,
                                        track_idx: track_name_cloned.clone(),
                                        clip_idx: index,
                                        is_right_side: false,
                                        hovered: false,
                                    },
                                    left_press: Message::ClipResizeStart(
                                        Kind::Audio,
                                        track_name_cloned.clone(),
                                        index,
                                        false,
                                    ),
                                    right_hover_enter: Message::ClipResizeHandleHover {
                                        kind: Kind::Audio,
                                        track_idx: track_name_cloned.clone(),
                                        clip_idx: index,
                                        is_right_side: true,
                                        hovered: true,
                                    },
                                    right_hover_exit: Message::ClipResizeHandleHover {
                                        kind: Kind::Audio,
                                        track_idx: track_name_cloned.clone(),
                                        clip_idx: index,
                                        is_right_side: true,
                                        hovered: false,
                                    },
                                    right_press: Message::ClipResizeStart(
                                        Kind::Audio,
                                        track_name_cloned.clone(),
                                        index,
                                        true,
                                    ),
                                },
                                fade_in_press: Some(Message::FadeResizeStart {
                                    kind: Kind::Audio,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_fade_out: false,
                                }),
                                fade_out_press: Some(Message::FadeResizeStart {
                                    kind: Kind::Audio,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_fade_out: true,
                                }),
                            })
                            .into_element(),
                    )
                    .position(Point::new(dragged_start * pixels_per_sample, lane_top))
                    .into(),
                );
            }

            if let Some(drag) = drag_for_clip.filter(|_| show_preview_in_this_track) {
                let delta_samples = (drag.end.x - drag.start.x) / pixels_per_sample.max(1.0e-6);
                let preview_start = snap_sample(clip.start as f32 + delta_samples, delta_samples)
                    + active_clip_snap_adjust_samples;
                let preview_fill = if active_target_valid {
                    Background::Color(Color {
                        r: 0.72,
                        g: 0.86,
                        b: 1.0,
                        a: 0.7,
                    })
                } else {
                    Background::Color(Color {
                        r: 0.92,
                        g: 0.32,
                        b: 0.32,
                        a: 0.55,
                    })
                };
                let preview_border = if active_target_valid {
                    Color {
                        r: 0.98,
                        g: 0.98,
                        b: 0.98,
                        a: 0.9,
                    }
                } else {
                    Color {
                        r: 1.0,
                        g: 0.45,
                        b: 0.45,
                        a: 0.95,
                    }
                };
                clips.push(
                    pin(AudioClipWidget::new(widget_audio_clip_data(clip, 1.0))
                        .with_session_root(session_root)
                        .with_size(clip_width, clip_height)
                        .with_label(display_clip_label.clone())
                        .preview(preview_fill, preview_border)
                        .into_element())
                    .position(Point::new(preview_start * pixels_per_sample, lane_top))
                    .into(),
                );
            }
            if active_clip_snap_targets.contains(&crate::state::ClipId {
                track_idx: track_name_cloned.clone(),
                clip_idx: index,
                kind: Kind::Audio,
            }) {
                clips.push(
                    pin(clip_snap_outline(clip_width, clip_height, 8.0))
                        .position(Point::new(clip.start as f32 * pixels_per_sample, lane_top))
                        .into(),
                );
            }
        }
    }
    if !track.is_master {
        for (index, clip) in track.midi.clips.iter().enumerate() {
            let clip_name = clip.name.clone();
            let clip_label = format!(
                "{}{}{}",
                MIDIClipWidget::<Message>::clean_name(&clip_name),
                if clip.take_lane_pinned { " [P]" } else { "" },
                if clip.take_lane_locked { " [L]" } else { "" }
            );
            let is_selected = selected_midi_indices.contains(&index);
            let active_drag = active_clip_drag.filter(|d| {
                d.kind == Kind::MIDI && d.track_index == track_name_cloned && d.index == index
            });
            let group_drag = active_clip_drag.filter(|d| {
                d.kind == Kind::MIDI
                    && d.track_index == track_name_cloned
                    && selected_midi_indices.contains(&d.index)
                    && selected_midi_count > 1
                    && is_selected
            });
            let drag_for_clip = group_drag.or(active_drag);
            let dragged_to_other_track = drag_for_clip.is_some_and(|d| {
                !d.copy
                    && active_target_valid
                    && active_target_track
                        .is_some_and(|target| target != track_name_cloned.as_str())
            });
            let show_preview_in_this_track = drag_for_clip.is_some_and(|_| {
                active_target_track.is_some_and(|target| target == track_name_cloned.as_str())
            });
            let dragged_start = drag_for_clip
                .filter(|d| !d.copy && !show_preview_in_this_track)
                .map(|d| {
                    let delta_samples = (d.end.x - d.start.x) / pixels_per_sample.max(1.0e-6);
                    snap_sample(clip.start as f32 + delta_samples, delta_samples)
                        + active_clip_snap_adjust_samples
                })
                .unwrap_or(clip.start as f32);
            let lane = clip.input_channel.min(track.midi.ins.saturating_sub(1));
            let lane_top = editor_lane_top(layout.audio_lanes + lane) + 1.0;
            let clip_width = (clip.length as f32 * pixels_per_sample).max(12.0);
            let clip_height = (editor_lane_height - 2.0).max(8.0);
            let display_clip_label =
                MIDIClipWidget::<Message>::label_for_width(&clip_label, clip_width);
            let midi_left_handle_hovered = state.hovered_clip_resize_handle.as_ref().is_some_and(
                |(track_idx, clip_idx, kind, is_right_side)| {
                    track_idx == &track_name_cloned
                        && *clip_idx == index
                        && *kind == Kind::MIDI
                        && !*is_right_side
                },
            );
            let midi_right_handle_hovered = state.hovered_clip_resize_handle.as_ref().is_some_and(
                |(track_idx, clip_idx, kind, is_right_side)| {
                    track_idx == &track_name_cloned
                        && *clip_idx == index
                        && *kind == Kind::MIDI
                        && *is_right_side
                },
            );
            let midi_notes_for_clip = midi_clip_previews
                .and_then(|map| map.get(&(track_name_cloned.clone(), index)))
                .cloned();

            if !dragged_to_other_track {
                let track_name_for_drag = track_name_cloned.clone();
                clips.push(
                    pin(MIDIClipWidget::new(widget_midi_clip_data(clip))
                        .with_size(clip_width, clip_height)
                        .with_label(display_clip_label.clone())
                        .selected(is_selected)
                        .hovered_handles(midi_left_handle_hovered, midi_right_handle_hovered)
                        .with_notes(midi_notes_for_clip.clone())
                        .interactive(WidgetMIDIClipInteraction {
                            on_select: Message::SelectClip {
                                track_idx: track_name_cloned.clone(),
                                clip_idx: index,
                                kind: Kind::MIDI,
                            },
                            on_open: Message::OpenMidiPiano {
                                track_idx: track_name_cloned.clone(),
                                clip_idx: index,
                            },
                            on_drag: (!clip.take_lane_locked).then_some(Arc::new(move |point| {
                                let mut clip_data = DraggedClip::new(
                                    Kind::MIDI,
                                    index,
                                    track_name_for_drag.clone(),
                                );
                                clip_data.start = point;
                                Message::ClipDrag(clip_data)
                            })
                                as Arc<dyn Fn(Point) -> Message + Send + Sync + 'static>),
                            edges: WidgetClipEdgeMessages {
                                left_hover_enter: Message::ClipResizeHandleHover {
                                    kind: Kind::MIDI,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_right_side: false,
                                    hovered: true,
                                },
                                left_hover_exit: Message::ClipResizeHandleHover {
                                    kind: Kind::MIDI,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_right_side: false,
                                    hovered: false,
                                },
                                left_press: Message::ClipResizeStart(
                                    Kind::MIDI,
                                    track_name_cloned.clone(),
                                    index,
                                    false,
                                ),
                                right_hover_enter: Message::ClipResizeHandleHover {
                                    kind: Kind::MIDI,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_right_side: true,
                                    hovered: true,
                                },
                                right_hover_exit: Message::ClipResizeHandleHover {
                                    kind: Kind::MIDI,
                                    track_idx: track_name_cloned.clone(),
                                    clip_idx: index,
                                    is_right_side: true,
                                    hovered: false,
                                },
                                right_press: Message::ClipResizeStart(
                                    Kind::MIDI,
                                    track_name_cloned.clone(),
                                    index,
                                    true,
                                ),
                            },
                        })
                        .into_element())
                    .position(Point::new(dragged_start * pixels_per_sample, lane_top))
                    .into(),
                );
            }

            if let Some(drag) = drag_for_clip.filter(|_| show_preview_in_this_track) {
                let delta_samples = (drag.end.x - drag.start.x) / pixels_per_sample.max(1.0e-6);
                let preview_start = snap_sample(clip.start as f32 + delta_samples, delta_samples)
                    + active_clip_snap_adjust_samples;
                let preview_fill = if active_target_valid {
                    MIDIClipWidget::<Message>::two_edge_gradient(
                        MIDI_CLIP_SELECTED_BASE,
                        0.66,
                        0.66,
                        false,
                    )
                } else {
                    MIDIClipWidget::<Message>::two_edge_gradient(
                        Color {
                            r: 0.72,
                            g: 0.18,
                            b: 0.18,
                            a: 1.0,
                        },
                        0.72,
                        0.72,
                        false,
                    )
                };
                let preview_border = if active_target_valid {
                    Color {
                        r: 0.88,
                        g: 1.0,
                        b: 0.78,
                        a: 0.92,
                    }
                } else {
                    Color {
                        r: 1.0,
                        g: 0.45,
                        b: 0.45,
                        a: 0.95,
                    }
                };
                clips.push(
                    pin(MIDIClipWidget::new(widget_midi_clip_data(clip))
                        .with_size(clip_width, clip_height)
                        .with_label(display_clip_label.clone())
                        .with_notes(midi_notes_for_clip.clone())
                        .preview(preview_fill, preview_border, 8.0)
                        .into_element())
                    .position(Point::new(preview_start * pixels_per_sample, lane_top))
                    .into(),
                );
            }
            if active_clip_snap_targets.contains(&crate::state::ClipId {
                track_idx: track_name_cloned.clone(),
                clip_idx: index,
                kind: Kind::MIDI,
            }) {
                clips.push(
                    pin(clip_snap_outline(clip_width, clip_height, 8.0))
                        .position(Point::new(clip.start as f32 * pixels_per_sample, lane_top))
                        .into(),
                );
            }
        }
    }

    if !track.is_master
        && let Some(drag) = active_clip_drag
        && let Some(target) = active_target_track
        && target == track_name_cloned.as_str()
        && drag.track_index != track_name_cloned
    {
        let delta_samples = (drag.end.x - drag.start.x) / pixels_per_sample.max(1.0e-6);
        if let Some(source_track) = state.tracks.iter().find(|t| t.name == drag.track_index) {
            match drag.kind {
                Kind::Audio => {
                    let mut preview_indices: Vec<usize> = state
                        .selected_clips
                        .iter()
                        .filter(|id| {
                            id.kind == Kind::Audio
                                && id.track_idx == drag.track_index
                                && id.clip_idx < source_track.audio.clips.len()
                        })
                        .map(|id| id.clip_idx)
                        .collect();
                    preview_indices.sort_unstable();
                    preview_indices.dedup();
                    if preview_indices.len() <= 1 || !preview_indices.contains(&drag.index) {
                        preview_indices = vec![drag.index];
                    }
                    for clip_index in preview_indices {
                        let Some(source_clip) = source_track.audio.clips.get(clip_index) else {
                            continue;
                        };
                        let preview_fill = if active_target_valid {
                            AudioClipWidget::<Message>::two_edge_gradient(
                                AUDIO_CLIP_SELECTED_BASE,
                                0.7,
                                0.7,
                                true,
                            )
                        } else {
                            AudioClipWidget::<Message>::two_edge_gradient(
                                Color {
                                    r: 0.72,
                                    g: 0.18,
                                    b: 0.18,
                                    a: 1.0,
                                },
                                0.72,
                                0.72,
                                true,
                            )
                        };
                        let preview_border = if active_target_valid {
                            Color {
                                r: 0.98,
                                g: 0.98,
                                b: 0.98,
                                a: 0.9,
                            }
                        } else {
                            Color {
                                r: 1.0,
                                g: 0.45,
                                b: 0.45,
                                a: 0.95,
                            }
                        };
                        let clip_width = (source_clip.length as f32 * pixels_per_sample).max(12.0);
                        let clip_height = (editor_lane_height - 2.0).max(8.0);
                        let lane_top = editor_lane_top(0) + 1.0;
                        let preview_start =
                            snap_sample(source_clip.start as f32 + delta_samples, delta_samples)
                                + active_clip_snap_adjust_samples;
                        let display_clip_label = AudioClipWidget::<Message>::label_for_width(
                            &AudioClipWidget::<Message>::clean_name(&source_clip.name),
                            clip_width,
                        );
                        clips.push(
                            pin(
                                AudioClipWidget::new(widget_audio_clip_data(source_clip, 1.0))
                                    .with_session_root(session_root)
                                    .with_size(clip_width, clip_height)
                                    .with_label(display_clip_label)
                                    .preview(
                                        if active_target_valid {
                                            preview_fill
                                        } else {
                                            Background::Color(Color::TRANSPARENT)
                                        },
                                        preview_border,
                                    )
                                    .into_element(),
                            )
                            .position(Point::new(preview_start * pixels_per_sample, lane_top))
                            .into(),
                        );
                    }
                }
                Kind::MIDI => {
                    let mut preview_indices: Vec<usize> = state
                        .selected_clips
                        .iter()
                        .filter(|id| {
                            id.kind == Kind::MIDI
                                && id.track_idx == drag.track_index
                                && id.clip_idx < source_track.midi.clips.len()
                        })
                        .map(|id| id.clip_idx)
                        .collect();
                    preview_indices.sort_unstable();
                    preview_indices.dedup();
                    if preview_indices.len() <= 1 || !preview_indices.contains(&drag.index) {
                        preview_indices = vec![drag.index];
                    }
                    for clip_index in preview_indices {
                        let Some(source_clip) = source_track.midi.clips.get(clip_index) else {
                            continue;
                        };
                        let preview_fill = if active_target_valid {
                            MIDIClipWidget::<Message>::two_edge_gradient(
                                MIDI_CLIP_SELECTED_BASE,
                                0.7,
                                0.7,
                                false,
                            )
                        } else {
                            MIDIClipWidget::<Message>::two_edge_gradient(
                                Color {
                                    r: 0.72,
                                    g: 0.18,
                                    b: 0.18,
                                    a: 1.0,
                                },
                                0.72,
                                0.72,
                                false,
                            )
                        };
                        let preview_border = if active_target_valid {
                            Color {
                                r: 0.98,
                                g: 0.98,
                                b: 0.98,
                                a: 0.9,
                            }
                        } else {
                            Color {
                                r: 1.0,
                                g: 0.45,
                                b: 0.45,
                                a: 0.95,
                            }
                        };
                        let clip_width = (source_clip.length as f32 * pixels_per_sample).max(12.0);
                        let lane_top = editor_lane_top(0) + 1.0;
                        let preview_start =
                            snap_sample(source_clip.start as f32 + delta_samples, delta_samples)
                                + active_clip_snap_adjust_samples;
                        let display_clip_label = MIDIClipWidget::<Message>::label_for_width(
                            &MIDIClipWidget::<Message>::clean_name(&source_clip.name),
                            clip_width,
                        );
                        clips.push(
                            pin(MIDIClipWidget::new(widget_midi_clip_data(source_clip))
                                .with_size(clip_width, (editor_lane_height - 2.0).max(8.0))
                                .with_label(display_clip_label)
                                .preview(
                                    if active_target_valid {
                                        preview_fill
                                    } else {
                                        Background::Color(Color::TRANSPARENT)
                                    },
                                    preview_border,
                                    8.0,
                                )
                                .into_element())
                            .position(Point::new(preview_start * pixels_per_sample, lane_top))
                            .into(),
                        );
                    }
                }
            }
        }
    }

    if track.armed
        && let Some((preview_start, preview_current)) = recording_preview_bounds
        && preview_current > preview_start
    {
        let preview_width =
            ((preview_current - preview_start) as f32 * pixels_per_sample).max(12.0);
        let preview_top = editor_lane_top(0) + 1.0;
        let preview_peaks = recording_preview_peaks
            .and_then(|map| map.get(&track.name))
            .cloned()
            .unwrap_or_default();
        let preview_length = preview_current - preview_start;
        let preview_clip = container(
            container(Stack::with_children(vec![
                AudioClipWidget::<Message>::waveform_overlay(
                    preview_peaks,
                    None,
                    0,
                    preview_length,
                    preview_length,
                    preview_length,
                ),
                container(text("REC").size(12))
                    .width(Length::Fill)
                    .height(Length::Fill)
                    .padding(5)
                    .into(),
            ]))
            .width(Length::Fill)
            .height(Length::Fill)
            .padding(0)
            .style(|_theme| container::Style {
                background: Some(Background::Color(Color {
                    r: 0.85,
                    g: 0.25,
                    b: 0.25,
                    a: 0.35,
                })),
                ..container::Style::default()
            }),
        )
        .width(Length::Fixed(preview_width))
        .height(Length::Fill)
        .style(|_theme| container::Style {
            background: None,
            border: Border {
                color: Color {
                    r: 0.9,
                    g: 0.3,
                    b: 0.3,
                    a: 0.9,
                },
                width: 1.0,
                radius: 8.0.into(),
            },
            ..container::Style::default()
        });
        clips.push(
            pin(preview_clip)
                .position(Point::new(
                    preview_start as f32 * pixels_per_sample,
                    preview_top,
                ))
                .into(),
        );
    }
    container(
        Stack::from_vec(clips)
            .height(Length::Fill)
            .width(Length::Fill),
    )
    .id(track_name_cloned)
    .width(Length::Fill)
    .height(Length::Fixed(height))
    .style(|_theme| container::Style {
        background: Some(Background::Color(Color {
            r: 0.0,
            g: 0.0,
            b: 0.0,
            a: 0.0,
        })),
        border: Border {
            color: Color {
                r: 0.0,
                g: 0.0,
                b: 0.0,
                a: 1.0,
            },
            width: 1.0,
            radius: 0.0.into(),
        },
        ..container::Style::default()
    })
    .into()
}

pub(super) fn clip_context_menu_overlay(
    state: &StateData,
    transport_active: bool,
) -> Option<(Point, Element<'static, Message>)> {
    fn can_group_selected_clips(state: &StateData, menu_clip: &crate::state::ClipId) -> bool {
        let mut selected: Vec<_> = state.selected_clips.iter().collect();
        if selected.len() < 2 {
            return false;
        }
        if !selected.contains(&menu_clip) {
            return false;
        }
        selected.sort_by_key(|clip| clip.clip_idx);
        let Some(first) = selected.first() else {
            return false;
        };
        if selected
            .iter()
            .any(|clip| clip.track_idx != first.track_idx || clip.kind != first.kind)
        {
            return false;
        }
        let Some(track) = state
            .tracks
            .iter()
            .find(|track| track.name == first.track_idx)
        else {
            return false;
        };
        match first.kind {
            Kind::Audio => selected.iter().all(|clip| {
                track
                    .audio
                    .clips
                    .get(clip.clip_idx)
                    .is_some_and(|clip| !clip.is_group())
            }),
            Kind::MIDI => selected.iter().all(|clip| {
                track
                    .midi
                    .clips
                    .get(clip.clip_idx)
                    .is_some_and(|clip| !clip.is_group())
            }),
        }
    }

    let menu = state.clip_context_menu.as_ref()?;
    let clip = &menu.clip;
    let track = state.tracks.iter().find(|t| t.name == clip.track_idx)?;

    let content: Element<'static, Message> = match clip.kind {
        Kind::Audio => {
            let clip_ref = track.audio.clips.get(clip.clip_idx)?;
            let fade_enabled = clip_ref.fade_enabled;
            let muted = clip_ref.muted;
            let is_group = clip_ref.is_group();
            let track_idx = clip.track_idx.clone();
            let clip_idx = clip.clip_idx;
            column![
                crate::menu::menu_item_maybe(
                    if is_group { "Ungroup" } else { "Group" },
                    if is_group {
                        Some(Message::UngroupClip {
                            track_idx: track_idx.clone(),
                            clip_idx,
                            kind: Kind::Audio,
                        })
                    } else {
                        can_group_selected_clips(state, clip).then_some(Message::GroupSelectedClips)
                    },
                ),
                crate::menu::menu_item(
                    "Rename",
                    Message::ClipRenameShow {
                        track_idx: track_idx.clone(),
                        clip_idx,
                        kind: Kind::Audio,
                    },
                ),
                crate::menu::menu_item(
                    if muted { "Unmute" } else { "Mute" },
                    Message::ClipSetMuted {
                        track_idx: track_idx.clone(),
                        clip_idx,
                        kind: Kind::Audio,
                        muted: !muted,
                    },
                ),
                crate::menu::menu_item_maybe(
                    "Pitch Correction",
                    (!transport_active).then_some(Message::ClipOpenPitchCorrection {
                        track_idx: track_idx.clone(),
                        clip_idx,
                    }),
                ),
                crate::menu::menu_item(
                    if fade_enabled {
                        "Disable Fade"
                    } else {
                        "Enable Fade"
                    },
                    Message::ClipToggleFade {
                        track_idx,
                        clip_idx,
                        kind: Kind::Audio,
                    },
                ),
            ]
            .spacing(2)
            .into()
        }
        Kind::MIDI => {
            let clip_ref = track.midi.clips.get(clip.clip_idx)?;
            let muted = clip_ref.muted;
            let is_group = clip_ref.is_group();
            let track_idx = clip.track_idx.clone();
            let clip_idx = clip.clip_idx;
            column![
                crate::menu::menu_item_maybe(
                    if is_group { "Ungroup" } else { "Group" },
                    if is_group {
                        Some(Message::UngroupClip {
                            track_idx: track_idx.clone(),
                            clip_idx,
                            kind: Kind::MIDI,
                        })
                    } else {
                        can_group_selected_clips(state, clip).then_some(Message::GroupSelectedClips)
                    },
                ),
                crate::menu::menu_item(
                    "Rename",
                    Message::ClipRenameShow {
                        track_idx: track_idx.clone(),
                        clip_idx,
                        kind: Kind::MIDI,
                    },
                ),
                crate::menu::menu_item(
                    if muted { "Unmute" } else { "Mute" },
                    Message::ClipSetMuted {
                        track_idx: track_idx.clone(),
                        clip_idx,
                        kind: Kind::MIDI,
                        muted: !muted,
                    },
                ),
            ]
            .spacing(2)
            .into()
        }
    };

    let panel = container(content)
        .width(Length::Fixed(210.0))
        .padding(6)
        .style(|theme| {
            let palette = theme.extended_palette();
            container::Style {
                background: Some(Background::Color(palette.background.weak.color)),
                border: Border {
                    color: palette.background.strong.color,
                    width: 1.0,
                    radius: 6.0.into(),
                },
                ..container::Style::default()
            }
        })
        .into();

    Some((menu.anchor, panel))
}

#[derive(Debug, Clone)]
pub struct Editor {
    state: State,
}

pub struct EditorViewArgs<'a> {
    pub session_root: Option<&'a PathBuf>,
    pub pixels_per_sample: f32,
    pub samples_per_bar: f32,
    pub snap_mode: SnapMode,
    pub samples_per_beat: f64,
    pub active_clip_drag: Option<&'a DraggedClip>,
    pub active_target_track: Option<&'a str>,
    pub active_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 visible_track_window: VisibleTrackWindow,
}

#[derive(Clone)]
pub struct OwnedEditorViewArgs {
    pub session_root: Option<PathBuf>,
    pub pixels_per_sample: f32,
    pub samples_per_bar: f32,
    pub snap_mode: SnapMode,
    pub samples_per_beat: f64,
    pub active_clip_drag: Option<DraggedClip>,
    pub active_target_track: Option<String>,
    pub active_target_valid: bool,
    pub active_clip_snap_adjust_samples: f32,
    pub active_clip_snap_targets: Vec<crate::state::ClipId>,
    pub recording_preview_bounds: Option<(usize, usize)>,
    pub recording_preview_peaks: Option<HashMap<String, ClipPeaks>>,
    pub midi_clip_previews: Option<MidiClipPreviewMap>,
    pub visible_track_window: VisibleTrackWindow,
}

impl Editor {
    pub fn new(state: State) -> Self {
        Self { state }
    }

    pub fn render_hash(&self, args: &EditorViewArgs<'_>) -> u64 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        let state = self.state.blocking_read();

        args.session_root.hash(&mut hasher);
        args.pixels_per_sample.to_bits().hash(&mut hasher);
        args.samples_per_bar.to_bits().hash(&mut hasher);
        args.samples_per_beat.to_bits().hash(&mut hasher);
        std::mem::discriminant(&args.snap_mode).hash(&mut hasher);
        args.recording_preview_bounds.hash(&mut hasher);
        args.active_target_track.hash(&mut hasher);
        args.active_target_valid.hash(&mut hasher);
        args.active_clip_snap_adjust_samples
            .to_bits()
            .hash(&mut hasher);
        args.active_clip_snap_targets.hash(&mut hasher);
        args.visible_track_window.start_index.hash(&mut hasher);
        args.visible_track_window.end_index.hash(&mut hasher);
        args.visible_track_window
            .top_padding
            .to_bits()
            .hash(&mut hasher);
        args.visible_track_window
            .bottom_padding
            .to_bits()
            .hash(&mut hasher);
        if let Some(drag) = args.active_clip_drag {
            std::mem::discriminant(&drag.kind).hash(&mut hasher);
            drag.index.hash(&mut hasher);
            drag.track_index.hash(&mut hasher);
            drag.start.x.to_bits().hash(&mut hasher);
            drag.start.y.to_bits().hash(&mut hasher);
            drag.end.x.to_bits().hash(&mut hasher);
            drag.end.y.to_bits().hash(&mut hasher);
            drag.copy.hash(&mut hasher);
        }

        let mut selected_clips: Vec<_> = state.selected_clips.iter().collect();
        selected_clips.sort_by(|a, b| {
            a.track_idx
                .cmp(&b.track_idx)
                .then_with(|| {
                    let ak = match a.kind {
                        Kind::Audio => 0_u8,
                        Kind::MIDI => 1_u8,
                    };
                    let bk = match b.kind {
                        Kind::Audio => 0_u8,
                        Kind::MIDI => 1_u8,
                    };
                    ak.cmp(&bk)
                })
                .then_with(|| a.clip_idx.cmp(&b.clip_idx))
        });
        for clip in selected_clips {
            clip.track_idx.hash(&mut hasher);
            clip.clip_idx.hash(&mut hasher);
            std::mem::discriminant(&clip.kind).hash(&mut hasher);
        }

        state.hovered_clip_resize_handle.hash(&mut hasher);
        state
            .clip_marquee_start
            .map(|p| (p.x.to_bits(), p.y.to_bits()))
            .hash(&mut hasher);
        state
            .clip_marquee_end
            .map(|p| (p.x.to_bits(), p.y.to_bits()))
            .hash(&mut hasher);
        state
            .midi_clip_create_start
            .map(|p| (p.x.to_bits(), p.y.to_bits()))
            .hash(&mut hasher);
        state
            .midi_clip_create_end
            .map(|p| (p.x.to_bits(), p.y.to_bits()))
            .hash(&mut hasher);
        state
            .cut_indicator
            .map(|(x, y, h)| (x.to_bits(), y.to_bits(), h.to_bits()))
            .hash(&mut hasher);

        if let Some(menu) = state.clip_context_menu.as_ref() {
            menu.clip.track_idx.hash(&mut hasher);
            menu.clip.clip_idx.hash(&mut hasher);
            std::mem::discriminant(&menu.clip.kind).hash(&mut hasher);
            menu.anchor.x.to_bits().hash(&mut hasher);
            menu.anchor.y.to_bits().hash(&mut hasher);
        }

        for track in state
            .tracks
            .iter()
            .filter(|track| track.name != METRONOME_TRACK_ID)
            .skip(args.visible_track_window.start_index)
            .take(
                args.visible_track_window
                    .end_index
                    .saturating_sub(args.visible_track_window.start_index),
            )
        {
            track.name.hash(&mut hasher);
            track.height.to_bits().hash(&mut hasher);
            track.armed.hash(&mut hasher);
            track.audio.ins.hash(&mut hasher);
            track.midi.ins.hash(&mut hasher);
            track.midi_lane_channels.hash(&mut hasher);
            std::mem::discriminant(&track.automation_mode).hash(&mut hasher);

            for lane in &track.automation_lanes {
                lane.visible.hash(&mut hasher);
                std::mem::discriminant(&lane.target).hash(&mut hasher);
                lane.points.len().hash(&mut hasher);
                if let Some(first) = lane.points.first() {
                    first.sample.hash(&mut hasher);
                    first.value.to_bits().hash(&mut hasher);
                }
                if let Some(last) = lane.points.last() {
                    last.sample.hash(&mut hasher);
                    last.value.to_bits().hash(&mut hasher);
                }
            }

            for clip in &track.audio.clips {
                clip.name.hash(&mut hasher);
                clip.start.hash(&mut hasher);
                clip.length.hash(&mut hasher);
                clip.offset.hash(&mut hasher);
                clip.input_channel.hash(&mut hasher);
                clip.muted.hash(&mut hasher);
                clip.fade_enabled.hash(&mut hasher);
                clip.fade_in_samples.hash(&mut hasher);
                clip.fade_out_samples.hash(&mut hasher);
                clip.take_lane_override.hash(&mut hasher);
                clip.take_lane_pinned.hash(&mut hasher);
                clip.take_lane_locked.hash(&mut hasher);
                clip.peaks.len().hash(&mut hasher);
                for channel in clip.peaks.iter() {
                    channel.len().hash(&mut hasher);
                    if channel.is_empty() {
                        continue;
                    }
                    for i in 0..CHECKPOINTS {
                        let idx = (i * channel.len()) / CHECKPOINTS;
                        let sample = channel[idx.min(channel.len() - 1)];
                        sample[0].to_bits().hash(&mut hasher);
                        sample[1].to_bits().hash(&mut hasher);
                    }
                }
            }

            for clip in &track.midi.clips {
                clip.name.hash(&mut hasher);
                clip.start.hash(&mut hasher);
                clip.length.hash(&mut hasher);
                clip.offset.hash(&mut hasher);
                clip.input_channel.hash(&mut hasher);
                clip.muted.hash(&mut hasher);
                clip.take_lane_override.hash(&mut hasher);
                clip.take_lane_pinned.hash(&mut hasher);
                clip.take_lane_locked.hash(&mut hasher);
            }
        }

        if let Some(peaks_by_track) = args.recording_preview_peaks {
            let mut keys: Vec<_> = peaks_by_track.keys().collect();
            keys.sort_unstable();
            for key in keys {
                key.hash(&mut hasher);
                if let Some(peaks) = peaks_by_track.get(key) {
                    peaks.len().hash(&mut hasher);
                    for channel in peaks.iter() {
                        channel.len().hash(&mut hasher);
                        if let Some(first) = channel.first() {
                            first[0].to_bits().hash(&mut hasher);
                            first[1].to_bits().hash(&mut hasher);
                        }
                        if let Some(last) = channel.last() {
                            last[0].to_bits().hash(&mut hasher);
                            last[1].to_bits().hash(&mut hasher);
                        }
                    }
                }
            }
        }

        if let Some(previews) = args.midi_clip_previews {
            let mut keys: Vec<_> = previews.keys().collect();
            keys.sort_unstable();
            for (track_name, clip_index) in keys {
                track_name.hash(&mut hasher);
                clip_index.hash(&mut hasher);
                if let Some(notes) = previews.get(&(track_name.clone(), *clip_index)) {
                    notes.len().hash(&mut hasher);
                    if let Some(first) = notes.first() {
                        first.start_sample.hash(&mut hasher);
                        first.length_samples.hash(&mut hasher);
                        first.pitch.hash(&mut hasher);
                        first.velocity.hash(&mut hasher);
                    }
                    if let Some(last) = notes.last() {
                        last.start_sample.hash(&mut hasher);
                        last.length_samples.hash(&mut hasher);
                        last.pitch.hash(&mut hasher);
                        last.velocity.hash(&mut hasher);
                    }
                }
            }
        }

        hasher.finish()
    }

    pub fn into_view_owned(self, args: OwnedEditorViewArgs) -> Element<'static, Message> {
        let OwnedEditorViewArgs {
            session_root,
            pixels_per_sample,
            samples_per_bar,
            snap_mode,
            samples_per_beat,
            active_clip_drag,
            active_target_track,
            active_target_valid,
            active_clip_snap_adjust_samples,
            active_clip_snap_targets,
            recording_preview_bounds,
            recording_preview_peaks,
            midi_clip_previews,
            visible_track_window,
        } = args;
        let state_handle = self.state;
        let session_root_ref = session_root.as_ref();
        let active_clip_drag_ref = active_clip_drag.as_ref();
        let active_target_track_ref = active_target_track.as_deref();
        let recording_preview_peaks_ref = recording_preview_peaks.as_ref();
        let midi_clip_previews_ref = midi_clip_previews.as_ref();

        let mut result = column![];
        if visible_track_window.top_padding > 0.0 {
            result =
                result.push(Space::new().height(Length::Fixed(visible_track_window.top_padding)));
        }
        let state = state_handle.blocking_read();
        for track in state
            .tracks
            .iter()
            .filter(|track| track.name != METRONOME_TRACK_ID)
            .skip(visible_track_window.start_index)
            .take(
                visible_track_window
                    .end_index
                    .saturating_sub(visible_track_window.start_index),
            )
        {
            result = result.push(view_track_elements(TrackElementViewArgs {
                state: &state,
                track,
                session_root: session_root_ref,
                pixels_per_sample,
                samples_per_bar,
                snap_mode,
                samples_per_beat,
                active_clip_drag: active_clip_drag_ref,
                active_target_track: active_target_track_ref,
                active_target_valid,
                active_clip_snap_adjust_samples,
                active_clip_snap_targets: &active_clip_snap_targets,
                recording_preview_bounds,
                recording_preview_peaks: recording_preview_peaks_ref,
                midi_clip_previews: midi_clip_previews_ref,
            }));
        }
        if visible_track_window.bottom_padding > 0.0 {
            result = result
                .push(Space::new().height(Length::Fixed(visible_track_window.bottom_padding)));
        }
        let mut layers: Vec<Element<'static, Message>> =
            vec![result.width(Length::Fill).height(Length::Fill).into()];
        if let (Some(start), Some(end)) = (state.clip_marquee_start, state.clip_marquee_end) {
            let mut x = start.x.min(end.x);
            let mut y = start.y.min(end.y);
            let mut w = (start.x - end.x).abs();
            let mut h = (start.y - end.y).abs();
            if w > 1.0 || h > 1.0 {
                w = w.max(2.0);
                h = h.max(2.0);
                x = x.max(0.0);
                y = y.max(0.0);
                layers.push(
                    pin(container("")
                        .width(Length::Fixed(w))
                        .height(Length::Fixed(h))
                        .style(|_theme| container::Style {
                            background: Some(Background::Color(Color {
                                r: 0.45,
                                g: 0.75,
                                b: 1.0,
                                a: 0.12,
                            })),
                            border: Border {
                                color: Color {
                                    r: 0.65,
                                    g: 0.85,
                                    b: 1.0,
                                    a: 0.95,
                                },
                                width: 1.0,
                                radius: 0.0.into(),
                            },
                            ..container::Style::default()
                        }))
                    .position(Point::new(x, y))
                    .into(),
                );
            }
        }
        if let (Some(start), Some(end)) = (state.midi_clip_create_start, state.midi_clip_create_end)
        {
            let x = start.x.min(end.x).max(0.0);
            let y = start.y.min(end.y).max(0.0);
            let w = (start.x - end.x).abs().max(2.0);
            let h = (start.y - end.y).abs().max(2.0);
            layers.push(
                pin(container("")
                    .width(Length::Fixed(w))
                    .height(Length::Fixed(h))
                    .style(|_theme| container::Style {
                        background: Some(Background::Color(Color {
                            r: 0.5,
                            g: 0.9,
                            b: 0.55,
                            a: 0.18,
                        })),
                        border: Border {
                            color: Color {
                                r: 0.7,
                                g: 1.0,
                                b: 0.72,
                                a: 0.95,
                            },
                            width: 1.0,
                            radius: 8.0.into(),
                        },
                        ..container::Style::default()
                    }))
                .position(Point::new(x, y))
                .into(),
            );
        }
        if let Some((x, y, h)) = state.cut_indicator {
            layers.push(
                pin(container("")
                    .width(Length::Fixed(1.0))
                    .height(Length::Fixed(h))
                    .style(|_theme| container::Style {
                        background: Some(Background::Color(Color {
                            r: 1.0,
                            g: 1.0,
                            b: 1.0,
                            a: 0.3,
                        })),
                        ..container::Style::default()
                    }))
                .position(Point::new(x.max(0.0), y))
                .into(),
            );
        }
        container(
            mouse_area(
                Stack::from_vec(layers)
                    .width(Length::Fill)
                    .height(Length::Fill),
            )
            .on_move(Message::EditorMouseMoved)
            .on_press(Message::DeselectClips)
            .on_enter(Message::ShortcutsHint(Some("Marquee select".to_string())))
            .on_exit(Message::ShortcutsHint(None)),
        )
        .style(|_theme| crate::style::app_background())
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }
}

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

    #[test]
    fn automation_point_color_returns_correct_colors() {
        use crate::message::TrackAutomationTarget;

        let volume_color = automation_point_color(TrackAutomationTarget::Volume);
        let balance_color = automation_point_color(TrackAutomationTarget::Balance);
        let mute_color = automation_point_color(TrackAutomationTarget::Mute);

        assert!(volume_color.a > 0.0);
        assert!(balance_color.a > 0.0);
        assert!(mute_color.a > 0.0);

        assert_ne!(volume_color.r, balance_color.r);
    }

    #[test]
    fn automation_point_color_plugin_targets_have_colors() {
        use crate::message::TrackAutomationTarget;

        let lv2_color = automation_point_color(TrackAutomationTarget::Lv2Parameter {
            instance_id: 1,
            index: 0,
            min: 0.0,
            max: 1.0,
        });
        let vst3_color = automation_point_color(TrackAutomationTarget::Vst3Parameter {
            instance_id: 1,
            param_id: 0,
        });
        let clap_color = automation_point_color(TrackAutomationTarget::ClapParameter {
            instance_id: 1,
            param_id: 0,
            min: 0.0,
            max: 1.0,
        });

        assert!(lv2_color.a > 0.0);
        assert!(vst3_color.a > 0.0);
        assert!(clap_color.a > 0.0);
    }
}
