use super::*;
use crate::app::git::{GitChangeKind, GitDiffLine, GitDiffLineKind, GitDiffRow};
use crate::app::{git_diff_new_scroll_id, git_diff_old_scroll_id};
use crate::config::GitDiffLayout;
use iced::widget::column;

impl App {
    pub(super) fn git_detail(&self) -> Element<'_, Message> {
        let Some(project) = self.selected_project() else {
            return git_empty(
                "No project selected",
                "Choose a project to inspect its worktree.",
            );
        };
        let Some(state) = self.git_states.get(&project.project.id) else {
            return git_empty(
                "Not a Git worktree",
                "The selected project is not inside a Git repository.",
            );
        };
        let Some(worktree) = self.git_worktrees.get(&project.project.id) else {
            return git_empty("Reading worktree", "Git status will appear here shortly.");
        };

        let counts = [
            (GitChangeKind::Added, "Added"),
            (GitChangeKind::Modified, "Changed"),
            (GitChangeKind::Deleted, "Deleted"),
        ];
        let mut file_list = column![].spacing(0);
        for (kind, label) in counts {
            let files = worktree
                .files
                .iter()
                .filter(|file| file.kind() == kind)
                .collect::<Vec<_>>();
            if files.is_empty() {
                continue;
            }
            file_list = file_list.push(
                container(
                    row![
                        text(label).size(11).color(muted_text()),
                        Space::new().width(Length::Fill),
                        text(files.len()).size(11).color(muted_text()),
                    ]
                    .align_y(Alignment::Center),
                )
                .padding([8, 12])
                .width(Length::Fill),
            );
            for file in files {
                let path = file.path.clone();
                let selected = self.git_selected_project == Some(project.project.id)
                    && self.git_selected_files.contains(&file.path);
                let staged = file.staged_kind.map(|kind| format!("S {}", kind.marker()));
                let worktree = file
                    .worktree_kind
                    .map(|kind| format!("W {}", kind.marker()));
                file_list = file_list.push(rule::horizontal(1)).push(
                    container(
                        row![
                            toggle(selected).on_toggle({
                                let path = path.clone();
                                move |_| Message::ToggleGitFile(path.clone())
                            }),
                            text(kind.marker()).size(12).color(git_change_color(kind)),
                            button(text(&file.display_path).size(13))
                                .padding(0)
                                .style(button::text)
                                .on_press(Message::OpenGitDiff(path)),
                            Space::new().width(Length::Fill),
                            staged.map(|label| text(label).size(11).color(staged_color())),
                            worktree.map(|label| text(label).size(11).color(muted_text())),
                        ]
                        .spacing(8)
                        .align_y(Alignment::Center),
                    )
                    .padding([9, 12])
                    .width(Length::Fill),
                );
            }
        }
        if worktree.files.is_empty() {
            file_list = file_list.push(
                container(
                    column![
                        text("Worktree clean").size(18),
                        text("There are no staged or unstaged changes.")
                            .size(13)
                            .color(muted_text()),
                    ]
                    .spacing(6)
                    .align_x(Alignment::Center),
                )
                .padding(36)
                .center_x(Length::Fill)
                .width(Length::Fill),
            );
        }

        let selected = self.git_selected_project == Some(project.project.id);
        let can_stage = selected
            && worktree.files.iter().any(|file| {
                self.git_selected_files.contains(&file.path) && file.worktree_kind.is_some()
            });
        let can_unstage = selected
            && worktree.files.iter().any(|file| {
                self.git_selected_files.contains(&file.path) && file.staged_kind.is_some()
            });
        let idle = self.git_operation.is_none();
        let commit_ready =
            idle && !worktree.files.is_empty() && !self.git_commit_message.trim().is_empty();
        let commit_input: Element<'_, Message> = if self.modal_open() {
            container(
                text(if self.git_commit_message.is_empty() {
                    "Commit message"
                } else {
                    &self.git_commit_message
                })
                .size(13)
                .color(muted_text()),
            )
            .padding(9)
            .width(Length::Fill)
            .style(preference_group_style)
            .into()
        } else {
            text_input("Commit message", &self.git_commit_message)
                .on_input(Message::GitCommitMessageChanged)
                .on_submit(Message::GitCommit)
                .padding(9)
                .size(13)
                .into()
        };
        let controls = column![
            row![
                git_icon_action(
                    "＋",
                    "Stage selected files",
                    (idle && can_stage).then_some(Message::GitStageSelected)
                ),
                git_icon_action(
                    "−",
                    "Unstage selected files",
                    (idle && can_unstage).then_some(Message::GitUnstageSelected)
                ),
                commit_input,
                git_icon_action("●", "Commit", commit_ready.then_some(Message::GitCommit)),
                Space::new().width(6),
                git_icon_action("↻", "Fetch from origin", idle.then_some(Message::GitFetch)),
                git_icon_action("↓", "Pull from origin", idle.then_some(Message::GitPull)),
                git_icon_action("↑", "Push to origin", idle.then_some(Message::GitPush)),
            ]
            .spacing(7)
            .align_y(Alignment::Center),
            self.git_operation
                .as_ref()
                .map(|operation| text(format!("{}…", operation.label))
                    .size(12)
                    .color(muted_text())),
        ]
        .spacing(6);

        let summary = format!(
            "{} change{}",
            worktree.files.len(),
            if worktree.files.len() == 1 { "" } else { "s" }
        );
        container(
            column![
                row![
                    column![
                        text(&project.project.name).size(20),
                        text(worktree.root.display().to_string())
                            .size(11)
                            .color(muted_text()),
                    ]
                    .spacing(4),
                    Space::new().width(Length::Fill),
                    column![
                        text(&state.label).size(13),
                        text(summary).size(11).color(muted_text()),
                    ]
                    .spacing(4)
                    .align_x(Alignment::End),
                ]
                .align_y(Alignment::Center),
                container(scrollable(file_list).height(Length::Fill))
                    .height(Length::Fill)
                    .width(Length::Fill)
                    .style(overview_style),
                container(controls)
                    .padding(12)
                    .width(Length::Fill)
                    .style(overview_style),
            ]
            .spacing(12),
        )
        .padding(Padding::new(28.0).top(22.0))
        .height(Length::Fill)
        .width(Length::Fill)
        .into()
    }

    pub(super) fn git_commit_all_panel(&self) -> Element<'_, Message> {
        let dialog = container(
            column![
                text("Commit all changes?").size(24),
                text("No files are selected and no changes are staged. All worktree changes will be staged and committed.")
                    .size(14),
                row![
                    Space::new().width(Length::Fill),
                    action_button("Cancel").on_press(Message::DismissPanel),
                    action_button("Commit all changes").on_press(Message::ConfirmGitCommitAll),
                ]
                .spacing(8),
            ]
            .spacing(12),
        )
        .padding(22)
        .width(500)
        .style(overview_style);
        opaque(
            container(dialog)
                .center_x(Length::Fill)
                .center_y(Length::Fill)
                .style(modal_backdrop_style),
        )
    }

    pub(super) fn git_diff_panel(&self) -> Element<'_, Message> {
        let diff = self.git_diff.as_ref().unwrap();
        let unified_active = self.git_diff_layout == GitDiffLayout::Unified;
        let split_active = self.git_diff_layout == GitDiffLayout::Split;
        let mode = container(
            row![
                button(text("1 column").size(12))
                    .padding([4, 10])
                    .on_press(Message::SetGitDiffLayout(GitDiffLayout::Unified))
                    .style(move |theme, status| segmented_button_style(
                        theme,
                        status,
                        unified_active
                    )),
                button(text("2 columns").size(12))
                    .padding([4, 10])
                    .on_press(Message::SetGitDiffLayout(GitDiffLayout::Split))
                    .style(move |theme, status| segmented_button_style(
                        theme,
                        status,
                        split_active
                    )),
            ]
            .spacing(2),
        )
        .padding(2)
        .style(segmented_control_style);
        let content = if unified_active {
            unified_diff(&diff.lines)
        } else {
            split_diff(diff.split_rows())
        };
        let diff_scroll = scrollable(content).height(Length::Fill).width(Length::Fill);
        let dialog = container(
            column![
                row![
                    column![text("File diff").size(22), text(&diff.path).size(13)].spacing(4),
                    Space::new().width(Length::Fill),
                    mode,
                    action_button("Close").on_press(Message::DismissPanel),
                ]
                .spacing(10)
                .align_y(Alignment::Center),
                container(diff_scroll)
                    .height(Length::Fill)
                    .width(Length::Fill)
                    .style(overview_style),
            ]
            .spacing(12),
        )
        .padding(18)
        .max_width(1_240)
        .max_height(840)
        .width(Length::Fill)
        .height(Length::Fill)
        .style(overview_style);
        opaque(
            container(dialog)
                .padding([42, 32])
                .center_x(Length::Fill)
                .center_y(Length::Fill)
                .style(modal_backdrop_style),
        )
    }
}

fn git_empty<'a>(title: &'a str, description: &'a str) -> Element<'a, Message> {
    container(
        column![
            text(title).size(24),
            text(description).size(14).color(muted_text()),
        ]
        .spacing(8)
        .align_x(Alignment::Center),
    )
    .center_x(Length::Fill)
    .center_y(Length::Fill)
    .into()
}

fn git_icon_action<'a>(
    symbol: &'a str,
    label: &'a str,
    message: Option<Message>,
) -> Element<'a, Message> {
    let mut action = action_button(text(symbol).size(17)).padding([5, 9]);
    if let Some(message) = message {
        action = action.on_press(message);
    }
    tooltip(
        action,
        container(text(label).size(12))
            .padding(8)
            .style(preference_group_style),
        tooltip::Position::Top,
    )
    .gap(5)
    .into()
}

fn unified_diff(lines: &[GitDiffLine]) -> Element<'_, Message> {
    let mut content = column![].spacing(0).width(Length::Shrink);
    for line in lines {
        content = content.push(diff_line(line, false));
    }
    content.into()
}

fn split_diff(rows: Vec<GitDiffRow>) -> Element<'static, Message> {
    let width = split_diff_width(&rows);
    let old = scrollable(split_diff_half(&rows, true, width))
        .id(git_diff_old_scroll_id())
        .direction(scrollable::Direction::Horizontal(
            scrollable::Scrollbar::new(),
        ))
        .on_scroll(Message::GitDiffScrolled)
        .width(Length::Fill);
    let new = scrollable(split_diff_half(&rows, false, width))
        .id(git_diff_new_scroll_id())
        .direction(scrollable::Direction::Horizontal(
            scrollable::Scrollbar::new(),
        ))
        .on_scroll(Message::GitDiffScrolled)
        .width(Length::Fill);
    row![old, rule::vertical(1), new].into()
}

fn split_diff_width(rows: &[GitDiffRow]) -> f32 {
    rows.iter()
        .flat_map(|row| match row {
            GitDiffRow::Header(line) => [Some(line), None],
            GitDiffRow::Pair { old, new } => [old.as_ref(), new.as_ref()],
        })
        .flatten()
        .map(|line| {
            line.text
                .chars()
                .map(|character| if character == '\t' { 4 } else { 1 })
                .sum::<usize>()
        })
        .max()
        .unwrap_or_default() as f32
        * 8.0
        + 66.0
}

fn split_diff_half(rows: &[GitDiffRow], old: bool, width: f32) -> Element<'static, Message> {
    let mut content = column![].spacing(0).width(width);
    for row in rows {
        content = content.push(match row {
            GitDiffRow::Header(line) => diff_line(line, old),
            GitDiffRow::Pair {
                old: old_line,
                new: new_line,
            } => diff_half(
                if old {
                    old_line.as_ref()
                } else {
                    new_line.as_ref()
                },
                old,
            ),
        });
    }
    content.into()
}

fn diff_half(line: Option<&GitDiffLine>, old: bool) -> Element<'static, Message> {
    line.map_or_else(
        || {
            container(Space::new().width(Length::Fill))
                .height(24)
                .width(Length::Fill)
                .into()
        },
        |line| diff_line(line, old),
    )
}

fn diff_line(line: &GitDiffLine, old: bool) -> Element<'static, Message> {
    let kind = line.kind;
    let number = if old {
        line.old_number
    } else {
        line.new_number.or(line.old_number)
    };
    container(
        row![
            text(number.map_or_else(String::new, |number| number.to_string()))
                .font(iced::Font::MONOSPACE)
                .size(11)
                .color(muted_text())
                .width(42),
            text(line.text.clone())
                .font(iced::Font::MONOSPACE)
                .size(12)
                .wrapping(iced::widget::text::Wrapping::None),
        ]
        .spacing(8)
        .align_y(Alignment::Center),
    )
    .padding([3, 8])
    .height(24)
    .width(Length::Fill)
    .style(move |_| diff_line_style(kind))
    .into()
}

fn diff_line_style(kind: GitDiffLineKind) -> container::Style {
    let background = match kind {
        GitDiffLineKind::Addition => Some(Color::from_rgba8(45, 120, 72, 0.24)),
        GitDiffLineKind::Deletion => Some(Color::from_rgba8(170, 55, 64, 0.24)),
        GitDiffLineKind::Hunk => Some(Color::from_rgba8(55, 92, 150, 0.22)),
        GitDiffLineKind::Section => Some(Color::from_rgba8(255, 255, 255, 0.08)),
        GitDiffLineKind::Header | GitDiffLineKind::Context => None,
    };
    match background {
        Some(background) => container::Style::default().background(background),
        None => container::Style::default(),
    }
}

fn git_change_color(kind: GitChangeKind) -> Color {
    match kind {
        GitChangeKind::Added => Color::from_rgb8(93, 186, 112),
        GitChangeKind::Modified => Color::from_rgb8(224, 174, 82),
        GitChangeKind::Deleted => Color::from_rgb8(224, 96, 104),
    }
}

fn staged_color() -> Color {
    Color::from_rgb8(93, 186, 112)
}

fn modal_backdrop_style(_: &Theme) -> container::Style {
    container::Style::default().background(Color::from_rgba8(0, 0, 0, 0.72))
}

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

    #[test]
    fn split_diff_width_covers_the_longest_side_and_tabs() {
        let rows = [GitDiffRow::Pair {
            old: Some(GitDiffLine {
                kind: GitDiffLineKind::Deletion,
                old_number: Some(1),
                new_number: None,
                text: "short".into(),
            }),
            new: Some(GitDiffLine {
                kind: GitDiffLineKind::Addition,
                old_number: None,
                new_number: Some(1),
                text: "\tlong".into(),
            }),
        }];

        assert_eq!(split_diff_width(&rows), 130.0);
    }
}
