pub(crate) mod shader;
pub(crate) mod video;

use crate::player::{FrameAction, Player, PlayerState};
use crate::ui::shared::screensaver::ScreensaverInhibitor;
use crate::ui::shared::styles::{
    self, ACCENT_RED, ACCENT_RED_HOVER, BG_CONTROL_BAR, GRAY_30, GRAY_70,
};
use iced::{Element, Task};
use std::sync::Arc;
use std::time::{Duration, Instant};

const CURSOR_HIDE_DELAY: Duration = Duration::from_secs(2);

#[derive(Debug, Clone)]
pub(crate) enum Message {
    ToggleFullscreen,
    MouseMoved,
    #[allow(dead_code)]
    SpaceKey(iced::window::Id),
    Seek(f64),
    SwitchAudioStream(AudioStreamOption),
    SetVolume(f32),
    CommitVolume,
    ToggleMute,
    #[allow(dead_code)]
    MKey(iced::window::Id),
}

/// Side effects the root reacts to (persisting audio settings).
#[derive(Debug, Clone, Copy)]
pub(crate) enum Event {
    VolumeCommitted { volume: f32, muted: bool },
    MuteToggled { muted: bool },
}

pub(crate) fn update(state: &mut State, msg: Message) -> (Task<crate::ui::Message>, Option<Event>) {
    match msg {
        Message::ToggleFullscreen => {
            state.is_fullscreen = !state.is_fullscreen;
            // Cursor and controls start out visible when entering fullscreen.
            state.last_mouse_move = Instant::now();
            let mode = if state.is_fullscreen {
                iced::window::Mode::Fullscreen
            } else {
                iced::window::Mode::Windowed
            };
            (
                iced::window::oldest().then(move |id| match id {
                    Some(id) => iced::window::set_mode(id, mode),
                    None => Task::none(),
                }),
                None,
            )
        }
        Message::MouseMoved => {
            state.last_mouse_move = Instant::now();
            (Task::none(), None)
        }
        Message::SpaceKey(_) => {
            if !state.paused {
                log::debug!("player paused");
                if let Err(e) = state.ctx.pause() {
                    log::error!("failed to pause player: {e}");
                    return (iced::exit(), None);
                }
            } else {
                log::debug!("player resumed");
                if let Err(e) = state.ctx.resume() {
                    log::error!("failed to resume player: {e}");
                    return (iced::exit(), None);
                }
            }
            state.paused = !state.paused;
            (Task::none(), None)
        }
        Message::Seek(secs) => {
            if let Err(e) = state.ctx.seek(secs) {
                log::error!("seek failed: {e}");
                return (iced::exit(), None);
            }
            state.paused = false;
            (Task::none(), None)
        }
        Message::SwitchAudioStream(option) => {
            if let Err(e) = state.ctx.switch_audio_stream(option.index) {
                log::error!("failed to switch audio stream: {e}");
            }
            state.selected_audio_stream = Some(option);
            (Task::none(), None)
        }
        Message::SetVolume(v) => {
            let v = v.clamp(0.0, 1.0);
            state.volume = v;
            state.ctx.set_volume(v);
            if state.muted {
                state.muted = false;
                state.ctx.set_muted(false);
            }
            (Task::none(), None)
        }
        Message::CommitVolume => (
            Task::none(),
            Some(Event::VolumeCommitted {
                volume: state.volume,
                muted: state.muted,
            }),
        ),
        Message::ToggleMute | Message::MKey(_) => {
            state.muted = !state.muted;
            state.ctx.set_muted(state.muted);
            (
                Task::none(),
                Some(Event::MuteToggled { muted: state.muted }),
            )
        }
    }
}

pub(crate) enum TickOutcome {
    /// Keep rendering; a frame may have been produced.
    Running,
    /// Playback reached the end of the file.
    Finished,
    /// The decoder failed irrecoverably.
    FatalError,
}

/// Drive the video pipeline by one frame tick.
pub(crate) fn tick(state: &mut State) -> TickOutcome {
    if state.ctx.video_ctx.is_finished() && !state.ctx.video_ctx.has_frames().unwrap_or(false) {
        state.ctx.video_ctx.set_state(PlayerState::Stopped);
        return TickOutcome::Finished;
    }

    if state.paused {
        return TickOutcome::Running;
    }

    let media = state.ctx.media_info();
    let (width, height) = (media.width, media.height);
    let frame_res = match video::process_frame(&state.ctx, width, height, &mut state.yuv_buffer) {
        Ok(res) => res,
        Err(e) => {
            log::error!("failed to process the next frame: {e}");
            return TickOutcome::FatalError;
        }
    };

    let video::FrameResult {
        frame_data,
        clock,
        action,
        buf_len,
    } = frame_res;

    state.frames_processed += 1;
    if matches!(action, FrameAction::DropFrame) {
        state.frames_dropped += 1;
    }

    if frame_data.is_some() {
        state.current_frame = frame_data;
    }

    if clock > 0.0 {
        state.current_time = clock;
    }

    log::trace!(
        "[video]: frame={} clock={:.3} video_buffer={} dropped={} action={}",
        state.frames_processed,
        clock,
        buf_len,
        state.frames_dropped,
        action
    );

    TickOutcome::Running
}

pub(crate) struct State {
    pub(crate) ctx: Player,
    pub(crate) current_frame: Option<Arc<shader::YuvFrameData>>,
    pub(crate) frames_dropped: usize,
    pub(crate) frames_processed: usize,
    pub(crate) last_mouse_move: Instant,
    pub(crate) is_fullscreen: bool,
    pub(crate) current_time: f64,
    pub(crate) total_duration: f64,
    pub(crate) paused: bool,
    pub(crate) audio_streams: Vec<AudioStreamOption>,
    pub(crate) selected_audio_stream: Option<AudioStreamOption>,
    pub(crate) volume: f32,
    pub(crate) muted: bool,
    pub(crate) yuv_buffer: Option<video::YuvScratch>,
    pub(crate) _screensaver_inhibitor: Option<ScreensaverInhibitor>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AudioStreamOption {
    pub(crate) index: usize,
    pub(crate) label: String,
}

impl std::fmt::Display for AudioStreamOption {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.label)
    }
}

pub(crate) fn view(state: &State) -> Element<'_, crate::ui::Message> {
    let video_content: Element<_> = if state.current_frame.is_some() {
        iced::widget::Shader::new(shader::VideoProgram {
            frame: state.current_frame.clone(),
            video_width: state.ctx.media_info().width,
            video_height: state.ctx.media_info().height,
        })
        .width(iced::Length::Fill)
        .height(iced::Length::Fill)
        .into()
    } else {
        iced::widget::text("buffering...").into()
    };

    let video_area = iced::widget::container(video_content)
        .width(iced::Length::Fill)
        .height(iced::Length::Fill)
        .center_x(iced::Length::Fill)
        .center_y(iced::Length::Fill)
        .style(|_theme| iced::widget::container::Style {
            background: Some(iced::Background::Color(iced::Color::BLACK)),
            ..Default::default()
        });

    let seek_slider = iced::widget::slider(0.0..=state.total_duration, state.current_time, |v| {
        crate::ui::Message::Player(Message::Seek(v))
    })
    .step(0.1)
    .style(|_theme, status| red_slider_style(status));

    let time_label = iced::widget::text(format!(
        "{} / {}",
        format_time(state.current_time),
        format_time(state.total_duration)
    ))
    .size(12)
    .color(GRAY_70);

    let mut info_row = iced::widget::row![time_label]
        .spacing(styles::SPACING_DEFAULT)
        .align_y(iced::Alignment::Center);

    info_row = info_row.push(iced::widget::Space::new().width(iced::Length::Fill));

    if state.audio_streams.len() > 1 {
        let audio_picker = iced::widget::pick_list(
            state.audio_streams.as_slice(),
            state.selected_audio_stream.as_ref(),
            |opt| crate::ui::Message::Player(Message::SwitchAudioStream(opt)),
        )
        .text_size(12);

        info_row = info_row.push(audio_picker);
    }

    let mute_glyph = if state.muted { "MUTE" } else { "VOL" };
    let mute_button = iced::widget::button(iced::widget::text(mute_glyph).size(16))
        .on_press(crate::ui::Message::Player(Message::ToggleMute))
        .padding(iced::Padding::new(styles::PADDING_CARD))
        .style(|_theme, status| iced::widget::button::Style {
            background: match status {
                iced::widget::button::Status::Hovered => Some(iced::Background::Color(GRAY_30)),
                _ => None,
            },
            text_color: GRAY_70,
            border: iced::border::rounded(styles::RADIUS_STANDARD),
            ..Default::default()
        });

    let volume_slider = iced::widget::slider(0.0..=1.0, state.volume, |v| {
        crate::ui::Message::Player(Message::SetVolume(v))
    })
    .on_release(crate::ui::Message::Player(Message::CommitVolume))
    .step(0.01_f32)
    .width(120)
    .style(|_theme, status| red_slider_style(status));

    info_row = info_row.push(mute_button).push(volume_slider);

    let control_bar = iced::widget::container(
        iced::widget::column![seek_slider, info_row].spacing(styles::SPACING_TIGHT),
    )
    .width(iced::Length::Fill)
    .padding(iced::Padding::new(styles::PADDING_SMALL))
    .style(|_theme| iced::widget::container::Style {
        background: Some(iced::Background::Color(BG_CONTROL_BAR)),
        ..Default::default()
    });

    if state.is_fullscreen {
        let idle = state.last_mouse_move.elapsed() > CURSOR_HIDE_DELAY;
        let cursor = if idle {
            iced::mouse::Interaction::Hidden
        } else {
            iced::mouse::Interaction::Idle
        };

        let mut layers = iced::widget::stack![video_area];
        if !idle {
            layers = layers.push(
                iced::widget::container(control_bar)
                    .width(iced::Length::Fill)
                    .height(iced::Length::Fill)
                    .align_bottom(iced::Length::Fill),
            );
        }

        // Presses captured by the control bar widgets never reach this area,
        // so double-clicking a slider cannot toggle fullscreen.
        iced::widget::mouse_area(layers)
            .on_move(|_| crate::ui::Message::Player(Message::MouseMoved))
            .on_double_click(crate::ui::Message::Player(Message::ToggleFullscreen))
            .interaction(cursor)
            .into()
    } else {
        let video_area: Element<_> = iced::widget::mouse_area(video_area)
            .on_double_click(crate::ui::Message::Player(Message::ToggleFullscreen))
            .into();
        iced::widget::column![video_area, control_bar].into()
    }
}

fn red_slider_style(status: iced::widget::slider::Status) -> iced::widget::slider::Style {
    let handle_color = match status {
        iced::widget::slider::Status::Hovered => ACCENT_RED_HOVER,
        _ => ACCENT_RED,
    };
    iced::widget::slider::Style {
        rail: iced::widget::slider::Rail {
            backgrounds: (ACCENT_RED.into(), GRAY_30.into()),
            width: 4.0,
            border: iced::border::rounded(2),
        },
        handle: iced::widget::slider::Handle {
            shape: iced::widget::slider::HandleShape::Circle { radius: 6.0 },
            background: handle_color.into(),
            border_width: 0.0,
            border_color: iced::Color::TRANSPARENT,
        },
    }
}

fn format_time(seconds: f64) -> String {
    let total_secs = seconds.max(0.0) as u64;
    let hours = total_secs / 3600;
    let mins = (total_secs % 3600) / 60;
    let secs = total_secs % 60;
    if hours > 0 {
        format!("{hours}:{mins:02}:{secs:02}")
    } else {
        format!("{mins:02}:{secs:02}")
    }
}
