#[doc(inline)]
pub use inflorescence_model::diff::{
    contents_to_lines, Combined, DecodedFile, DiffWithContents,
    DiffWithoutContents, File, FileAndState, IdHash, Lines, Section, State,
    UndecodableContents, UndecodableFile,
};

use crate::view::Msg;
use crate::{checkbox, el, theme, Theme};
use inflorescence_iced_widget::{nav_scrollable, nav_selectable};
use inflorescence_model::to_record;
use libflorescence::file;

use iced::widget::{column, container, image, row, text};
use iced::{alignment, Element, Font, Length};

use std::cmp;

pub fn view<'a>(
    state: &'a State,
    nav: &'a nav_scrollable::State,
    file: &'a File,
    path: Option<&'a file::Path>,
    diff_selected: bool,
    select_sections: bool,
    to_record: &to_record::State,
) -> Element<'a, Msg, Theme> {
    match file {
        File::Decoded(decoded_file) => view_decoded(
            state,
            nav,
            decoded_file,
            path,
            diff_selected,
            select_sections,
            to_record,
        ),
        File::Undecodable(undecodable_file) => {
            view_undecodable(state, nav, undecodable_file, diff_selected)
        }
        File::Image(bytes) => {
            el(image(image::Handle::from_bytes(bytes.clone())))
        }
    }
}

fn view_decoded<'a>(
    _state: &'a State,
    nav: &'a nav_scrollable::State,
    file: &'a DecodedFile,
    path: Option<&'a file::Path>,
    diff_selected: bool,
    select_sections: bool,
    to_record: &to_record::State,
) -> Element<'a, Msg, Theme> {
    let DecodedFile {
        combined,
        diffs_without_contents,
    } = file;

    let line_num_digits = combined.max_line_num.to_string().len();

    // TODO use state to display selection, and control section expansion
    let mut current_line = 1;
    const CHECKBOX_SPACING: u32 = 4;
    let sections_view = combined.sections.iter().map(|section| match section {
        Section::Unchanged(lines) => {
            let res = lines.iter().enumerate().map(move |(ix, line)| {
                line_view(
                    LineKind::Unchanged,
                    current_line + ix,
                    line_num_digits,
                    line,
                )
            });

            current_line += lines.len();

            if path.is_some() {
                // Add space to align these with toggle in `Section::Changed`
                let toggle_space =
                    el(container(text("")).width(checkbox::WIDTH));

                el(row([toggle_space, el(column(res))])
                    .spacing(CHECKBOX_SPACING))
            } else {
                el(column(res))
            }
        }
        Section::Changed {
            deleted,
            added,
            diff_id,
        } => {
            let res = deleted
                .iter()
                .enumerate()
                .map(move |(ix, line)| {
                    line_view(
                        LineKind::Deleted,
                        current_line + ix,
                        line_num_digits,
                        line,
                    )
                })
                .chain(added.iter().enumerate().map(move |(ix, line)| {
                    line_view(
                        LineKind::Added,
                        current_line + ix,
                        line_num_digits,
                        line,
                    )
                }));

            current_line += added.len();

            if let Some(path) = path {
                let to_record_pick =
                    to_record::determine_change(path, *diff_id, to_record);
                let to_record_toggle = el(checkbox::two_way(to_record_pick)
                    .on_press_with(|| {
                        Msg::ToRecord(to_record::Msg::ToggleChange {
                            path: path.clone(),
                            diff_id: *diff_id,
                        })
                    }));

                el(row([to_record_toggle, el(column(res))])
                    .spacing(CHECKBOX_SPACING))
            } else {
                el(column(res))
            }
        }
    });

    let nav_view = if diff_selected && select_sections {
        nav_selectable(
            nav,
            sections_view,
            || theme::Container::NavNonSelectedSection,
            || theme::Container::NavSelectedSection,
        )
    } else {
        nav_scrollable(nav, sections_view)
    }
    .class(if diff_selected {
        theme::Scrollable::Selected
    } else {
        theme::Scrollable::Normal
    });
    let sections = el(nav_view);

    let nav_with_sections = if diffs_without_contents.is_empty() {
        sections
    } else {
        let diffs_without_contents_view = diffs_without_contents
            .iter()
            .map(view_diff_without_contents);

        el(column([el(column(diffs_without_contents_view)), sections])
            .spacing(10))
    };

    if select_sections {
        let selected_hunk_ix = nav.get_selected_section_ix().unwrap_or(0) + 1;
        let hunk_count = combined
            .sections
            .iter()
            .filter(|section| matches!(section, Section::Changed { .. }))
            .count();
        el(column([
            el(text(format!(
                "Selected hunk: {}/{}",
                selected_hunk_ix, hunk_count
            ))),
            nav_with_sections,
        ])
        .spacing(10))
    } else {
        nav_with_sections
    }
}

fn view_undecodable<'a>(
    _state: &'a State,
    nav: &'a nav_scrollable::State,
    file: &'a UndecodableFile,
    diff_selected: bool,
) -> Element<'a, Msg, Theme> {
    let UndecodableFile {
        diffs_with_contents,
        diffs_without_contents,
    } = file;

    let diffs = diffs_with_contents
        .iter()
        .map(view_diff_with_contents)
        .chain(
            diffs_without_contents
                .iter()
                .map(view_diff_without_contents),
        );

    el(column([
        el(text("Cannot display contents due to unknown encoding")),
        el(nav_scrollable(nav, diffs).class(if diff_selected {
            theme::Scrollable::Selected
        } else {
            theme::Scrollable::Normal
        })),
    ]))
}

/// View diffs without context (the file contents)
fn view_diff_with_contents<'a>(
    (diff, _id): &(DiffWithContents, IdHash),
) -> Element<'a, Msg, Theme> {
    match diff {
        DiffWithContents::Add { contents } => {
            if let Some(contents) = contents {
                let line_num = 1;
                let lines = contents_to_lines(contents);
                let max_line_num = line_num + lines.len();
                let line_num_digits = max_line_num.to_string().len();

                let lines_view =
                    lines.into_iter().enumerate().map(|(ix, line)| {
                        line_view(
                            LineKind::Added,
                            line_num + ix,
                            line_num_digits,
                            line,
                        )
                    });
                el(column(lines_view))
            } else {
                el(text("Added"))
            }
        }
        DiffWithContents::Edit {
            line,
            deleted,
            contents,
        } => {
            let line_num = *line;
            let lines = contents_to_lines(contents);
            let max_line_num = line_num + lines.len();
            let line_num_digits = max_line_num.to_string().len();

            let lines_view = lines.into_iter().enumerate().map(|(ix, line)| {
                line_view(
                    if *deleted {
                        LineKind::Deleted
                    } else {
                        LineKind::Added
                    },
                    line_num + ix,
                    line_num_digits,
                    line,
                )
            });
            el(column(lines_view))
        }
        DiffWithContents::Replacement {
            line,
            change_contents,
            replacement_contents,
        } => {
            let line_num = *line;
            let change_lines = contents_to_lines(change_contents);
            let replacement_lines = contents_to_lines(replacement_contents);
            let max_line_num = line_num
                + cmp::max(change_lines.len(), replacement_lines.len());
            let line_num_digits = max_line_num.to_string().len();

            let lines_view = change_lines
                .into_iter()
                .enumerate()
                .map(|(ix, line)| {
                    line_view(
                        LineKind::Deleted,
                        line_num + ix,
                        line_num_digits,
                        line,
                    )
                })
                .chain(replacement_lines.into_iter().enumerate().map(
                    |(ix, line)| {
                        line_view(
                            LineKind::Added,
                            line_num + ix,
                            line_num_digits,
                            line,
                        )
                    },
                ));
            el(column(lines_view))
        }
        DiffWithContents::Del { contents } => {
            if let Some(contents) = contents {
                let line_num = 1;
                let lines = contents_to_lines(contents);
                let max_line_num = line_num + lines.len();
                let line_num_digits = max_line_num.to_string().len();

                let lines_view =
                    lines.into_iter().enumerate().map(|(ix, line)| {
                        line_view(
                            LineKind::Deleted,
                            line_num + ix,
                            line_num_digits,
                            line,
                        )
                    });
                el(column(lines_view))
            } else {
                el(text("Deleted"))
            }
        }
        DiffWithContents::Undel => el(text("Revived")),
    }
}

/// View diffs without context (the file contents)
fn view_diff_without_contents<'a>(
    diff: &'a DiffWithoutContents,
) -> Element<'a, Msg, Theme> {
    match diff {
        DiffWithoutContents::SolveNameConflict => {
            el(text("Solve name conflict"))
        }
        DiffWithoutContents::UnsolveNameConflict => {
            el(text("Unsolve name conflict"))
        }
        DiffWithoutContents::SolveOrderConflict => {
            el(text("Solve order conflict"))
        }
        DiffWithoutContents::UnsolveOrderConflict => {
            el(text("Unsolve order conflict"))
        }
        DiffWithoutContents::ResurrectZombines => el(text("Resurrect zombies")),
        DiffWithoutContents::AddRoot => el(text("Add root")),
        DiffWithoutContents::DelRoot => el(text("Delete root")),
        DiffWithoutContents::Edit {
            line,
            deleted,
            contents,
        } => {
            let line_num = *line;
            let line = undecodable_contents_to_str(contents);

            line_view(
                if *deleted {
                    LineKind::Deleted
                } else {
                    LineKind::Added
                },
                line_num,
                1,
                line,
            )
        }
        DiffWithoutContents::Replacement {
            line,
            change_contents,
            replacement_contents,
        } => {
            let line_num = *line;
            let change_line = undecodable_contents_to_str(change_contents);
            let replacement_line =
                undecodable_contents_to_str(replacement_contents);

            el(column([
                line_view(LineKind::Deleted, line_num, 1, change_line),
                line_view(LineKind::Added, line_num, 1, replacement_line),
            ]))
        }
        DiffWithoutContents::Move { old_path } => {
            el(text(format!("Moved from {old_path}")))
        }
        DiffWithoutContents::MoveEdge => el(text("Moved".to_string())),
        DiffWithoutContents::Del => el(text("Deleted file")),
    }
}

fn mono_text<'a>(
    txt: impl text::IntoFragment<'a>,
) -> iced::widget::Text<'a, Theme> {
    text(txt)
        .font(Font::MONOSPACE)
        .wrapping(text::Wrapping::WordOrGlyph)
        .align_y(alignment::Vertical::Top)
}

fn line_num_view<'a>(
    num: usize,
    digits: usize,
) -> iced::widget::Text<'a, Theme> {
    // Fill the string to the number of digits
    let txt = format!("{num:digits$} ");
    mono_text(txt)
        .font(Font::MONOSPACE)
        // TODO replace with class
        // .style(move |theme| {
        //     let palette = theme.extended_palette();
        //     text::Style {
        //         color: Some(palette.background.base.text.scale_alpha(0.61)),
        //     }
        // })
        .align_y(alignment::Vertical::Top)
}

#[derive(Debug, Clone, Copy)]
enum LineKind {
    Unchanged,
    Added,
    Deleted,
}

fn line_view<'a>(
    kind: LineKind,
    line_num: usize,
    line_num_digits: usize,
    line: impl text::IntoFragment<'a>,
) -> Element<'a, Msg, Theme> {
    let line = container(row([
        el(mono_text(match kind {
            LineKind::Unchanged => "  ",
            LineKind::Added => "+ ",
            LineKind::Deleted => "- ",
        })),
        el(line_num_view(line_num, line_num_digits)),
        el(mono_text(line)
            .shaping(text::Shaping::Advanced)
            .width(Length::Fill)),
    ]));

    el(match kind {
        LineKind::Unchanged => line,
        LineKind::Added => line.class(theme::Container::DiffAddition),
        LineKind::Deleted => line.class(theme::Container::DiffDeletion),
    })
}

fn undecodable_contents_to_str(contents: &UndecodableContents) -> &str {
    match contents {
        UndecodableContents::ShortBase64(short) => short,
        UndecodableContents::UnknownEncoding => "Unknown encoding",
    }
}