use crate::frame::FrameCtx;
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::StringKey;
use crate::theme::Step12;
use crate::widget_id::{WidgetId, WidgetKey};

use super::dialog::{Dialog, DialogButton, show_dialog};
use super::paint::{LabelText, WidgetPaint};
use super::table::{ListItem, ListView, ListViewState, show_list_view};
use super::text_input::{AlwaysValid, MemoryClipboard, TextInput, TextInputState, show_text_input};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FilePickerMode {
    Open,
    Save,
}

#[derive(Clone, Debug, PartialEq)]
pub struct FilePickerEntry {
    pub id: WidgetId,
    pub label: LabelText,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct FilePickerState {
    pub list: ListViewState,
    pub filename: TextInputState,
    pub clipboard: MemoryClipboard,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct FilePickerLabels {
    pub title: StringKey,
    pub confirm: StringKey,
    pub list: StringKey,
    pub filename_placeholder: StringKey,
}

#[derive(Debug, PartialEq)]
pub struct FilePickerDialog<'a, 'state> {
    pub id: WidgetId,
    pub viewport: LayoutRect,
    pub size: LayoutSize,
    pub mode: FilePickerMode,
    pub current_path: LabelText,
    pub entries: &'a [FilePickerEntry],
    pub state: &'state mut FilePickerState,
    pub labels: FilePickerLabels,
}

impl<'a, 'state> FilePickerDialog<'a, 'state> {
    #[must_use]
    pub fn new(
        id: WidgetId,
        viewport: LayoutRect,
        mode: FilePickerMode,
        current_path: impl Into<LabelText>,
        entries: &'a [FilePickerEntry],
        labels: FilePickerLabels,
        state: &'state mut FilePickerState,
    ) -> Self {
        Self {
            id,
            viewport,
            size: LayoutSize::new(LayoutPx::new(540.0), LayoutPx::new(420.0)),
            mode,
            current_path: current_path.into(),
            entries,
            state,
            labels,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum FilePickerOutcome {
    Cancelled,
    Open {
        folder: WidgetId,
    },
    Save {
        folder: Option<WidgetId>,
        filename: String,
    },
}

#[derive(Clone, Debug, PartialEq)]
pub struct FilePickerResponse {
    pub outcome: Option<FilePickerOutcome>,
    pub navigated_to: Option<WidgetId>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_file_picker(
    ctx: &mut FrameCtx<'_>,
    picker: FilePickerDialog<'_, '_>,
) -> FilePickerResponse {
    let FilePickerDialog {
        id,
        viewport,
        size,
        mode,
        current_path,
        entries,
        state,
        labels,
    } = picker;
    let confirm_id = id.child(WidgetKey::new("confirm"));
    let cancel_id = id.child(WidgetKey::new("cancel"));
    let trimmed_empty = state.filename.text.trim().is_empty();
    let any_text = !state.filename.text.is_empty();
    let confirm_disabled = match mode {
        FilePickerMode::Open => state.list.focused.is_none(),
        FilePickerMode::Save => trimmed_empty && (any_text || state.list.focused.is_none()),
    };
    let buttons = [
        DialogButton::secondary(cancel_id, StringKey::new("file_picker.cancel")),
        DialogButton {
            disabled: confirm_disabled,
            ..DialogButton::primary(confirm_id, labels.confirm)
        },
    ];
    let list_items: Vec<ListItem> = entries
        .iter()
        .map(|e| ListItem {
            id: e.id,
            label: e.label.clone(),
        })
        .collect();
    let (response, list_opened) = show_dialog(
        ctx,
        Dialog::new(id, viewport, size, labels.title, &buttons),
        |ctx, body_rect, paint| {
            paint.push(WidgetPaint::Label {
                rect: path_label_rect(body_rect),
                text: current_path.clone(),
                color: ctx.theme().colors.text_secondary(),
                role: ctx.theme().typography.caption,
            });
            let list_rect = list_rect_for(body_rect, mode);
            paint.push(WidgetPaint::Surface {
                rect: list_rect,
                fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0),
                border: Some(crate::theme::Border {
                    width: crate::theme::StrokeWidth::HAIRLINE,
                    color: ctx.theme().colors.neutral.step(Step12::SUBTLE_BORDER),
                }),
                radius: ctx.theme().radius.sm,
                elevation: None,
            });
            let list_response = show_list_view(
                ctx,
                ListView::new(
                    id.child(WidgetKey::new("list")),
                    list_rect,
                    labels.list,
                    &list_items,
                    &mut state.list,
                ),
            );
            paint.extend(list_response.paint);
            if mode == FilePickerMode::Save {
                let widget = TextInput {
                    id: id.child(WidgetKey::new("filename")),
                    rect: filename_rect(body_rect),
                    placeholder: labels.filename_placeholder,
                    state: &mut state.filename,
                    disabled: false,
                    validator: AlwaysValid,
                };
                let text_response = show_text_input(ctx, widget, &mut state.clipboard);
                paint.extend(text_response.paint);
            }
            list_response.opened
        },
    );
    let outcome = resolve_outcome(
        ResolveInputs {
            mode,
            confirm_id,
            cancel_id,
            opened: list_opened,
            list_focused: state.list.focused,
            filename: state.filename.text.as_str(),
        },
        &response,
    );
    FilePickerResponse {
        outcome,
        navigated_to: list_opened,
        paint: response.paint,
    }
}

#[derive(Copy, Clone)]
struct ResolveInputs<'a> {
    mode: FilePickerMode,
    confirm_id: WidgetId,
    cancel_id: WidgetId,
    opened: Option<WidgetId>,
    list_focused: Option<WidgetId>,
    filename: &'a str,
}

fn resolve_outcome(
    inputs: ResolveInputs<'_>,
    response: &super::dialog::DialogResponse,
) -> Option<FilePickerOutcome> {
    if response.dismissed || response.activated == Some(inputs.cancel_id) {
        return Some(FilePickerOutcome::Cancelled);
    }
    if inputs.mode == FilePickerMode::Open
        && let Some(folder) = inputs.opened
    {
        return Some(FilePickerOutcome::Open { folder });
    }
    if response.activated != Some(inputs.confirm_id) {
        return None;
    }
    match inputs.mode {
        FilePickerMode::Open => inputs
            .list_focused
            .map(|folder| FilePickerOutcome::Open { folder }),
        FilePickerMode::Save => Some(FilePickerOutcome::Save {
            folder: inputs.list_focused,
            filename: inputs.filename.to_owned(),
        }),
    }
}

fn path_label_rect(body: LayoutRect) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(body.origin.x.value() + 16.0),
            LayoutPx::new(body.origin.y.value() + 8.0),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(body.size.width.value() - 32.0),
            LayoutPx::new(20.0),
        ),
    )
}

fn list_rect_for(body: LayoutRect, mode: FilePickerMode) -> LayoutRect {
    let list_height = match mode {
        FilePickerMode::Open => body.size.height.value() - 36.0,
        FilePickerMode::Save => body.size.height.value() - 80.0,
    }
    .max(0.0);
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(body.origin.x.value() + 16.0),
            LayoutPx::new(body.origin.y.value() + 32.0),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(body.size.width.value() - 32.0),
            LayoutPx::new(list_height),
        ),
    )
}

fn filename_rect(body: LayoutRect) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(body.origin.x.value() + 16.0),
            LayoutPx::new(body.origin.y.value() + body.size.height.value() - 36.0),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(body.size.width.value() - 32.0),
            LayoutPx::new(28.0),
        ),
    )
}

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

    use super::{
        FilePickerDialog, FilePickerEntry, FilePickerMode, FilePickerOutcome, FilePickerState,
        LabelText, show_file_picker,
    };
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey, PointerButton,
        PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

    fn picker_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("picker"))
    }

    fn entries() -> Vec<FilePickerEntry> {
        vec![
            FilePickerEntry {
                id: picker_id().child(WidgetKey::new("doc1")),
                label: LabelText::Key(StringKey::new("doc.first")),
            },
            FilePickerEntry {
                id: picker_id().child(WidgetKey::new("doc2")),
                label: LabelText::Key(StringKey::new("doc.second")),
            },
        ]
    }

    fn viewport() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        )
    }

    const fn test_labels() -> super::FilePickerLabels {
        super::FilePickerLabels {
            title: StringKey::new("file_picker.title"),
            confirm: StringKey::new("file_picker.confirm"),
            list: StringKey::new("file_picker.list"),
            filename_placeholder: StringKey::new("file_picker.filename_placeholder"),
        }
    }

    fn run_picker(
        focus: &mut FocusManager,
        hits: &mut HitFrame,
        prev: &HitState,
        snap: &mut InputSnapshot,
        state: &mut FilePickerState,
        entries: &[FilePickerEntry],
        mode: FilePickerMode,
    ) -> super::FilePickerResponse {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = crate::a11y::AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            snap,
            focus,
            &table,
            StringTable::empty(),
            hits,
            prev,
            &mut a11y,
            &mut shaper,
        );
        show_file_picker(
            &mut ctx,
            FilePickerDialog::new(
                picker_id(),
                viewport(),
                mode,
                StringKey::new("path.home"),
                entries,
                test_labels(),
                state,
            ),
        )
    }

    fn row_center(entries: &[FilePickerEntry], idx: usize) -> LayoutPos {
        let body_y = viewport().origin.y.value() + viewport().size.height.value() * 0.5 - 210.0;
        let body_top = body_y + 32.0;
        let list_top = body_top + 32.0;
        let row_height = 24.0;
        let _ = entries;
        let center_x = viewport().origin.x.value() + viewport().size.width.value() * 0.5 - 200.0;
        #[allow(clippy::cast_precision_loss, reason = "row index fits f32 mantissa")]
        let center_y = list_top + (idx as f32 + 0.5) * row_height;
        LayoutPos::new(LayoutPx::new(center_x), LayoutPx::new(center_y))
    }

    #[test]
    fn double_click_row_in_open_mode_commits_open() {
        let entries = entries();
        let mut state = FilePickerState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = row_center(&entries, 1);
        let press = |pos: LayoutPos| {
            let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
            snap.pointer = Some(PointerSample::new(pos));
            snap.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
            snap
        };
        let release = |pos: LayoutPos| {
            let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
            snap.pointer = Some(PointerSample::new(pos));
            snap.buttons_released = PointerButtonMask::just(PointerButton::Primary);
            snap
        };
        let idle_pos = |pos: LayoutPos| {
            let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
            snap.pointer = Some(PointerSample::new(pos));
            snap
        };
        let frames = [
            press(click_pos),
            release(click_pos),
            press(click_pos),
            release(click_pos),
            idle_pos(click_pos),
        ];
        let mut last_outcome: Option<FilePickerOutcome> = None;
        frames.into_iter().for_each(|mut snap| {
            let mut hits = HitFrame::new();
            let response = run_picker(
                &mut focus,
                &mut hits,
                &prev,
                &mut snap,
                &mut state,
                &entries,
                FilePickerMode::Open,
            );
            last_outcome = response.outcome;
            prev = resolve(&prev, &hits, &snap, focus.focused());
        });
        assert_eq!(
            last_outcome,
            Some(FilePickerOutcome::Open {
                folder: entries[1].id,
            }),
        );
    }

    #[test]
    fn escape_cancels_picker() {
        let entries = entries();
        let mut state = FilePickerState::default();
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Escape),
            ModifierMask::NONE,
        ));
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut snap,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            show_file_picker(
                &mut ctx,
                FilePickerDialog::new(
                    picker_id(),
                    viewport(),
                    FilePickerMode::Open,
                    StringKey::new("path.home"),
                    &entries,
                    test_labels(),
                    &mut state,
                ),
            )
        };
        assert_eq!(response.outcome, Some(FilePickerOutcome::Cancelled));
    }

    #[test]
    fn save_mode_paints_filename_input_above_buttons() {
        let entries = entries();
        let mut state = FilePickerState::default();
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut snap,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            show_file_picker(
                &mut ctx,
                FilePickerDialog::new(
                    picker_id(),
                    viewport(),
                    FilePickerMode::Save,
                    StringKey::new("path.home"),
                    &entries,
                    test_labels(),
                    &mut state,
                ),
            )
        };
        let _next = resolve(&prev, &hits, &snap, focus.focused());
        let label_count = response
            .paint
            .iter()
            .filter(|p| matches!(p, super::WidgetPaint::Label { .. }))
            .count();
        assert!(label_count >= 2);
    }
}
