//! Selection of changes to include or exclude in the next record.

#[cfg(test)]
mod test;

pub use libflorescence::to_record::{
    determine_change, determine_file, PartialFile, Pick, PickSet, State,
};

use libflorescence::prelude::*;
use libflorescence::to_record::{
    default_pick, default_pick_set, file_has_any_partial_change,
};
use libflorescence::{diff, file, repo};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Msg {
    ToggleOverall,
    ToggleFile {
        path: file::Path,
    },
    ToggleChange {
        path: file::Path,
        diff_id: diff::IdHash,
    },
}

pub fn update(state: &mut State, msg: Msg, changed_files: &repo::ChangedFiles) {
    match msg {
        Msg::ToggleOverall => {
            state.overall = next_overall_pick(state, changed_files);
            if let PickSet::Partial = state.overall {
                state.files.iter_mut().for_each(|(file, pick)| {
                    if file_has_any_partial_change(file, &state.changes) {
                        *pick = PickSet::Partial;
                    }
                });
            }
        }
        Msg::ToggleFile { path } => {
            // Set a pick for files that don't have it
            {
                let unset_file_pick = match state.overall {
                    PickSet::Include | PickSet::Exclude => state.overall,
                    PickSet::Partial => PickSet::Exclude,
                };
                for changed_file in changed_files.keys() {
                    if !state.files.contains_key(changed_file) {
                        state
                            .files
                            .insert(changed_file.clone(), unset_file_pick);
                    }
                }
            }

            let pick = next_file_pick(&path, state);
            state.files.insert(path, pick);

            state.overall = determine_overall_pick(state, changed_files);
        }
        Msg::ToggleChange {
            path: file,
            diff_id: change,
        } => {
            // Set a pick for files that don't have it
            {
                let unset_file_pick = match state.overall {
                    PickSet::Include | PickSet::Exclude => state.overall,
                    PickSet::Partial => PickSet::Exclude,
                };
                for changed_file in changed_files.keys() {
                    if !state.files.contains_key(changed_file) {
                        state
                            .files
                            .insert(changed_file.clone(), unset_file_pick);
                    }
                }
            }
            // Set a pick for this file's changes that don't have it
            {
                let unset_pick = match state
                    .files
                    .get(&file)
                    .copied()
                    .unwrap_or(state.overall)
                {
                    PickSet::Include => Pick::Include,
                    PickSet::Exclude | PickSet::Partial => Pick::Exclude,
                };
                let PartialFile { changes } =
                    state.changes.entry(file.clone()).or_default();
                for change in changed_files.get(&file).unwrap() {
                    let diff_id = diff::id_parts_hash(change);
                    changes.entry(diff_id).or_insert(unset_pick);
                }
            }

            let pick = next_file_change_pick(&file, change, state);

            let partial_file =
                state.changes.entry(file.clone()).or_insert_with(default);
            partial_file.changes.insert(change, pick);

            let file_pick = determine_file_pick(&file, state, changed_files)
                .unwrap_or_else(|| {
                    if changed_files.get(&file).unwrap().len() > 1 {
                        PickSet::Partial
                    } else {
                        match pick {
                            Pick::Include => PickSet::Include,
                            Pick::Exclude => PickSet::Exclude,
                        }
                    }
                });
            state.files.insert(file, file_pick);

            state.overall = determine_overall_pick(state, changed_files);
        }
    }
}

pub fn next_overall_pick(
    state: &State,
    changed_files: &repo::ChangedFiles,
) -> PickSet {
    let maybe_next = next_pick_set(state.overall);
    if let PickSet::Partial = maybe_next
        && !any_partial_change(state, changed_files)
    {
        next_pick_set(maybe_next)
    } else {
        maybe_next
    }
}

pub fn next_file_pick(file: &file::Path, state: &State) -> PickSet {
    let current = (match state.overall {
        pick @ (PickSet::Include | PickSet::Exclude) => Some(pick),
        PickSet::Partial => None,
    })
    .or_else(|| state.files.get(file).copied())
    .unwrap_or_else(|| default_pick_set(state));
    let maybe_next = next_pick_set(current);
    if let PickSet::Partial = maybe_next
        && !file_has_any_partial_change(file, &state.changes)
    {
        next_pick_set(maybe_next)
    } else {
        maybe_next
    }
}

pub fn next_file_change_pick(
    file: &file::Path,
    diff_id: diff::IdHash,
    state: &State,
) -> Pick {
    let current = (match state.overall {
        PickSet::Include => Some(Pick::Include),
        PickSet::Exclude => Some(Pick::Exclude),
        PickSet::Partial => None,
    })
    .or_else(|| {
        state.files.get(file).and_then(|pick| match pick {
            PickSet::Include => Some(Pick::Include),
            PickSet::Exclude => Some(Pick::Exclude),
            PickSet::Partial => None,
        })
    })
    .or_else(|| {
        state
            .changes
            .get(file)
            .and_then(|PartialFile { changes }| changes.get(&diff_id))
            .copied()
    })
    .unwrap_or_else(|| default_pick(state));
    next_pick(current)
}

/// Add an untracked file to explicitly include in to-record if the overall
/// state is partial or exclude
pub fn add_untracked_file_to_record(state: &mut State, path: file::Path) {
    if matches!(state.overall, PickSet::Exclude | PickSet::Partial) {
        state.files.insert(path, PickSet::Include);
        state.overall = PickSet::Partial;
    }
}

/// Remove an untracked file from to-record
pub fn rm_untracked_file_to_record(
    state: &mut State,
    path: &file::Path,
    changed_files: &repo::ChangedFiles,
) {
    state.files.remove(path);
    state.overall = determine_overall_pick(state, changed_files);
}

fn next_pick_set(pick: PickSet) -> PickSet {
    match pick {
        PickSet::Include => PickSet::Partial,
        PickSet::Partial => PickSet::Exclude,
        PickSet::Exclude => PickSet::Include,
    }
}

fn next_pick(pick: Pick) -> Pick {
    match pick {
        Pick::Include => Pick::Exclude,
        Pick::Exclude => Pick::Include,
    }
}

fn any_partial_change(
    state: &State,
    changed_files: &repo::ChangedFiles,
) -> bool {
    let files = changed_files.keys();

    if state.files.len() == files.len() {
        let (mut has_include, mut has_exclude) = (false, false);
        state.files.iter().any(|(file, pick)| {
            has_exclude |= matches!(pick, PickSet::Exclude);
            has_include |= matches!(pick, PickSet::Include);
            (has_include && has_exclude)
                || matches!(pick, PickSet::Partial)
                || file_has_any_partial_change(file, &state.changes)
        })
    } else {
        state.files.iter().any(|(file, pick)| {
            matches!(pick, PickSet::Exclude | PickSet::Partial)
                || file_has_any_partial_change(file, &state.changes)
        })
    }
}

fn determine_overall_pick(
    state: &State,
    changed_files: &repo::ChangedFiles,
) -> PickSet {
    let files = changed_files.keys();

    let mut pick = None;
    if state.files.len() != files.len() {
        pick = Some(default_pick_set(state));
    }
    for file_pick in state.files.values() {
        if let Some(pick) = &mut pick {
            *pick = match (*pick, *file_pick) {
                (PickSet::Include, PickSet::Include) => PickSet::Include,
                (PickSet::Exclude, PickSet::Exclude) => PickSet::Exclude,
                (PickSet::Include, PickSet::Exclude)
                | (PickSet::Include, PickSet::Partial)
                | (PickSet::Exclude, PickSet::Include)
                | (PickSet::Exclude, PickSet::Partial)
                | (PickSet::Partial, PickSet::Include)
                | (PickSet::Partial, PickSet::Exclude)
                | (PickSet::Partial, PickSet::Partial) => PickSet::Partial,
            };
            if let PickSet::Partial = pick {
                break;
            }
        } else {
            pick = Some(*file_pick);
        }
    }

    pick.unwrap_or(PickSet::Include)
}

fn determine_file_pick(
    file: &file::Path,
    state: &State,
    changed_files: &repo::ChangedFiles,
) -> Option<PickSet> {
    let changes = changed_files.get(file).unwrap();

    let mut pick = None;
    if let Some(file) = state.changes.get(file) {
        if file.changes.len() != changes.len() {
            return Some(PickSet::Partial);
        }
        for change_pick in file.changes.values() {
            if let Some(pick) = &mut pick {
                *pick = match (*pick, change_pick) {
                    (PickSet::Include, Pick::Include) => PickSet::Include,
                    (PickSet::Exclude, Pick::Exclude) => PickSet::Exclude,
                    (PickSet::Exclude, Pick::Include)
                    | (PickSet::Include, Pick::Exclude)
                    | (PickSet::Partial, Pick::Include)
                    | (PickSet::Partial, Pick::Exclude) => PickSet::Partial,
                };
                if let PickSet::Partial = pick {
                    break;
                }
            } else {
                pick = Some(match change_pick {
                    Pick::Include => PickSet::Include,
                    Pick::Exclude => PickSet::Exclude,
                });
            }
        }
    }
    pick
}