//! A changed file's contents diff.

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

use inflorescence_iced_widget::nav_scrollable;
use libflorescence::repo;
use tracing::error;

use std::cmp;
use std::collections::HashSet;

pub fn init_file(
    file_content: FileContent<'_>,
    changed_file: Option<&repo::ChangedFile>,
) -> File {
    let (diffs_with_contents, diffs_without_contents) = changed_file
        .map(from_repo_changed_file)
        .unwrap_or((vec![], vec![]));

    match file_content {
        FileContent::Decoded(file_content) => {
            let combined =
                combine_decoded_contents(&file_content, diffs_with_contents);
            File::Decoded(DecodedFile {
                combined,
                diffs_without_contents,
            })
        }
        FileContent::UnknownEncoding => File::Undecodable(UndecodableFile {
            diffs_with_contents,
            diffs_without_contents,
        }),
        FileContent::Image(bytes) => {
            // TODO: get image with changes unapplied
            File::Image(bytes)
        }
    }
}

pub fn from_repo_changed_file(
    changed_file: &repo::ChangedFile,
) -> (Vec<(DiffWithContents, IdHash)>, Vec<DiffWithoutContents>) {
    changed_file.iter().fold((vec![], vec![]), |(mut with, mut without), diff| {
         match diff {
            repo::ChangedFileDiff::Move{ old_path } => {
                without.push(DiffWithoutContents::Move { old_path: old_path.clone() });
            },
            repo::ChangedFileDiff::MoveEdge => {
                without.push(DiffWithoutContents::MoveEdge);
            },
            repo::ChangedFileDiff::Del { contents } => {
                let contents = match contents {
                    Some(repo::Contents::Decoded(lines) | repo::Contents::ShortBase64(lines)) =>  Some(lines.clone()),
                    Some(repo::Contents::UnknownEncoding(_)) | None => None
                };
                without.push(DiffWithoutContents::Del);
                with.push((DiffWithContents::Del{ contents }, id_parts_hash(diff)));
            },
            repo::ChangedFileDiff::Undel => {
                with.push((DiffWithContents::Undel, id_parts_hash(diff)));
            },
            repo::ChangedFileDiff::Add { contents } => {
                let contents = match contents {
                    Some(repo::Contents::Decoded(lines) | repo::Contents::ShortBase64(lines)) =>  Some(lines.clone()),
                    Some(repo::Contents::UnknownEncoding(_)) | None => None
                };
                with.push((DiffWithContents::Add { contents }, id_parts_hash(diff)));
            },
            repo::ChangedFileDiff::SolveNameConflict => {
                without.push(DiffWithoutContents::SolveNameConflict);
            },
            repo::ChangedFileDiff::UnsolveNameConflict => {
                without.push(DiffWithoutContents::UnsolveNameConflict);
            },
            repo::ChangedFileDiff::Edit { line, deleted, contents } => match contents{
                repo::Contents::Decoded(lines) => {
                    with.push((DiffWithContents::Edit {
                        line: *line,
                        deleted: *deleted,
                        contents: lines.clone(),
                    }, id_parts_hash(diff)));
                },
                repo::Contents::ShortBase64(short) => {
                    without.push(DiffWithoutContents::Edit {
                        line: *line,
                        deleted: *deleted,
                        contents: UndecodableContents::ShortBase64(short.clone()),
                    });
                },
                repo::Contents::UnknownEncoding(_bytes) => {
                    without.push(DiffWithoutContents::Edit {
                        line: *line,
                        deleted: *deleted,
                        contents: UndecodableContents::UnknownEncoding,
                    });
                },
            },
            repo::ChangedFileDiff::Replacement { line, change_contents, replacement_contents } => match (change_contents, replacement_contents) {
                (repo::Contents::Decoded(change), repo::Contents::Decoded(replacement)) => {
                    with.push((DiffWithContents::Replacement {
                        line: *line,
                        change_contents: change.clone(),
                        replacement_contents: replacement.clone(),
                    }, id_parts_hash(diff)));
                },
                (repo::Contents::ShortBase64(change), repo::Contents::ShortBase64(replacement)) => {
                    without.push(DiffWithoutContents::Replacement {
                        line: *line,
                        change_contents: UndecodableContents::ShortBase64(change.clone()),
                        replacement_contents: UndecodableContents::ShortBase64(replacement.clone()),
                    });
                },
                (_, repo::Contents::UnknownEncoding(_)) |
                (repo::Contents::UnknownEncoding(_), _) => {
                    without.push(DiffWithoutContents::Replacement {
                        line: *line,
                        change_contents: UndecodableContents::UnknownEncoding,
                        replacement_contents: UndecodableContents::UnknownEncoding,
                    });
                },
                _ => {
                    error!("The change and replacement have different encoding! Change: {change_contents:?}, replacement: {replacement_contents:?}");
                    without.push(DiffWithoutContents::Replacement {
                        line: *line,
                        change_contents: UndecodableContents::UnknownEncoding,
                        replacement_contents: UndecodableContents::UnknownEncoding,
                    });
                }
            },
            repo::ChangedFileDiff::SolveOrderConflict => {
                without.push(DiffWithoutContents::SolveOrderConflict);
            },
            repo::ChangedFileDiff::UnsolveOrderConflict => {
                without.push(DiffWithoutContents::UnsolveOrderConflict);
            },
            repo::ChangedFileDiff::ResurrectZombines => {
                without.push(DiffWithoutContents::ResurrectZombines);
            },
            repo::ChangedFileDiff::AddRoot => {
                without.push(DiffWithoutContents::AddRoot);
            },
            repo::ChangedFileDiff::DelRoot => {
                without.push(DiffWithoutContents::DelRoot);
            },
        };
        (with, without)
    })
}

/// Returns true if from the diffs imply that the file should exist.
pub fn should_file_exist(changed_file: &repo::ChangedFile) -> bool {
    for change in changed_file {
        match change {
            repo::ChangedFileDiff::Del { .. }
            | repo::ChangedFileDiff::AddRoot
            | repo::ChangedFileDiff::DelRoot => return false,
            repo::ChangedFileDiff::Move { .. }
            | repo::ChangedFileDiff::MoveEdge
            | repo::ChangedFileDiff::Undel
            | repo::ChangedFileDiff::Add { .. }
            | repo::ChangedFileDiff::SolveNameConflict
            | repo::ChangedFileDiff::UnsolveNameConflict
            | repo::ChangedFileDiff::Edit { .. }
            | repo::ChangedFileDiff::Replacement { .. }
            | repo::ChangedFileDiff::SolveOrderConflict
            | repo::ChangedFileDiff::UnsolveOrderConflict
            | repo::ChangedFileDiff::ResurrectZombines => {}
        }
    }
    true
}

/// Get indices of unchanged sections that are less relevant than the actual
/// changes
pub fn unchanged_sections(diff: &File) -> HashSet<usize> {
    match diff {
        File::Decoded(file) => file
            .combined
            .sections
            .iter()
            .enumerate()
            .filter_map(|(ix, section)| match section {
                Section::Unchanged(_) => Some(ix),
                Section::Changed { .. } => None,
            })
            .collect(),
        File::Undecodable(_) | File::Image(_) => HashSet::new(),
    }
}

pub fn file_diff_needs_scrolling(files_diffs: &FilesState) -> bool {
    nav_scrollable::needs_scrolling(&files_diffs.diffs_nav)
}

fn combine_decoded_contents(
    file_content: &str,
    diffs_with_contents: Vec<(DiffWithContents, IdHash)>,
) -> Combined {
    let changes_len = diffs_with_contents.len();
    let mut file_lines = trim_line_break_suffix(file_content).split('\n');
    let mut sections = Vec::with_capacity(diffs_with_contents.len());
    let mut current_line: usize = 1;
    let mut max_line_num: usize = 1;

    for (change, diff_id) in diffs_with_contents {
        match change {
            DiffWithContents::Add { contents } => {
                let added = contents
                    .as_deref()
                    .map(trim_line_break_suffix)
                    .map(contents_to_lines)
                    .unwrap_or_default();
                let max_line_num = added.len();
                sections.push(Section::Changed {
                    deleted: vec![],
                    added,
                    diff_id,
                });
                return Combined {
                    sections,
                    max_line_num,
                };
            }
            DiffWithContents::Undel => {
                debug_assert_eq!(changes_len, 1);
                let added: Vec<_> = file_lines.map(str::to_string).collect();
                let max_line_num = added.len();
                sections.push(Section::Changed {
                    deleted: vec![],
                    added,
                    diff_id,
                });
                return Combined {
                    sections,
                    max_line_num,
                };
            }
            DiffWithContents::Del { contents } => {
                let deleted = contents
                    .as_deref()
                    .map(trim_line_break_suffix)
                    .map(contents_to_lines)
                    .unwrap_or_default();
                let max_line_num = deleted.len();
                sections.push(Section::Changed {
                    deleted,
                    added: vec![],
                    diff_id,
                });
                return Combined {
                    sections,
                    max_line_num,
                };
            }
            DiffWithContents::Edit {
                line,
                deleted,
                contents,
            } => {
                let lines_before = line - current_line;
                if lines_before != 0 {
                    current_line = line;
                    sections.push(Section::Unchanged(take_n_lines(
                        &mut file_lines,
                        lines_before,
                    )));
                }

                if deleted {
                    let deleted =
                        contents_to_lines(trim_line_break_suffix(&contents));
                    max_line_num = current_line + deleted.len();

                    sections.push(Section::Changed {
                        deleted,
                        added: vec![],
                        diff_id,
                    });
                } else {
                    let added =
                        contents_to_lines(trim_line_break_suffix(&contents));
                    current_line += added.len();
                    max_line_num = current_line;

                    drop_n_lines(&mut file_lines, added.len());
                    sections.push(Section::Changed {
                        deleted: vec![],
                        added,
                        diff_id,
                    });
                }
            }
            DiffWithContents::Replacement {
                line,
                change_contents,
                replacement_contents,
            } => {
                let lines_before = line - current_line;
                if lines_before != 0 {
                    current_line = line;
                    sections.push(Section::Unchanged(take_n_lines(
                        &mut file_lines,
                        lines_before,
                    )));
                }

                let added = contents_to_lines(trim_line_break_suffix(
                    &replacement_contents,
                ));
                let deleted =
                    contents_to_lines(trim_line_break_suffix(&change_contents));
                max_line_num =
                    current_line + cmp::max(added.len(), deleted.len());
                current_line += added.len();

                drop_n_lines(&mut file_lines, added.len());
                sections.push(Section::Changed {
                    deleted,
                    added,
                    diff_id,
                });
            }
        }
    }
    let rest: Lines = file_lines.map(str::to_string).collect();
    if !rest.is_empty() {
        max_line_num = current_line + rest.len();
        sections.push(Section::Unchanged(rest));
    }

    Combined {
        sections,
        max_line_num,
    }
}

fn take_n_lines(file_lines: &mut std::str::Split<'_, char>, n: usize) -> Lines {
    let mut lines = Vec::with_capacity(n);
    for _ in 0..n {
        lines.push(file_lines.next().unwrap().to_string());
    }
    lines
}

fn drop_n_lines(file_lines: &mut std::str::Split<'_, char>, n: usize) {
    for _ in 0..n {
        file_lines.next().unwrap();
    }
}

fn trim_line_break_suffix(text: &str) -> &str {
    trim_suffix(text, '\n')
}

// This is a nightly-only experimental API. (trim_prefix_suffix https://github.com/rust-lang/rust/issues/142312)
fn trim_suffix(text: &str, suffix: char) -> &str {
    text.strip_suffix(suffix).unwrap_or(text)
}