//! Selection of changes to include or exclude in the next record.
/// Selection can be picked at 3 levels (order matters, from more coarse to
/// more fine):
///
/// - overall
/// - per file
/// - per change in a file
///
/// To determine a pick state for anything from any of these levels, the
/// high-levels (first in the list above) take precedence over the more
/// fine-grained levels. This is so that fine-grained partial selection is
/// preserved even if a state at higher-level is changed to be all included
/// or excluded so that it's possible to return back to the partial
/// selection.
use libpijul::HashMap;

use crate::{diff, file};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pick {
    Include,
    Exclude,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PickSet {
    #[default]
    Include,
    Exclude,
    Partial,
}

#[derive(Clone, Debug, Default)]
pub struct State {
    /// State of all files
    pub overall: PickSet,
    /// Map from file name to its changes state. This is preserved even when
    /// `overall` selection changes.
    pub files: HashMap<file::Path, PickSet>,
    /// Map from file name to its individual change state. This is preserved
    /// even when `overall` and `files` selection changes
    pub changes: HashMap<file::Path, PartialFile>,
}

#[derive(Clone, Debug, Default)]
pub struct PartialFile {
    /// Map from file name to its state
    // NOTE: When it's possible to select individual lines/chars to record,
    // then the value would be `PickSet` and the inner-most value of
    // selected ranges would be `Pick`
    pub changes: HashMap<diff::IdHash, Pick>,
}

/// Determine the default value for a file that is not explicitly set.
pub fn default_pick_set(state: &State) -> PickSet {
    match default_pick(state) {
        Pick::Include => PickSet::Include,
        Pick::Exclude => PickSet::Exclude,
    }
}

/// Determine the default value for a change that is not explicitly set.
pub fn default_pick(state: &State) -> Pick {
    let State {
        overall,
        files,
        changes: _,
    } = state;

    if matches!(overall, PickSet::Partial)
        || files
            .values()
            .any(|pick| matches!(pick, PickSet::Partial | PickSet::Exclude))
    {
        Pick::Exclude
    } else {
        Pick::Include
    }
}

pub fn determine_file(state: &State, file: &file::Path) -> PickSet {
    match state.overall {
        PickSet::Include => PickSet::Include,
        PickSet::Exclude => PickSet::Exclude,
        PickSet::Partial => {
            if let Some(pick) = state.files.get(file) {
                *pick
            } else if file_has_any_partial_change(file, &state.changes) {
                PickSet::Partial
            } else {
                default_pick_set(state)
            }
        }
    }
}

pub fn determine_change(
    file: &file::Path,
    diff_id: diff::IdHash,
    state: &State,
) -> Pick {
    match state.overall {
        PickSet::Include => return Pick::Include,
        PickSet::Exclude => return Pick::Exclude,
        PickSet::Partial => {}
    }
    if let Some(pick) = state.files.get(file) {
        match pick {
            PickSet::Include => return Pick::Include,
            PickSet::Exclude => return Pick::Exclude,
            PickSet::Partial => {}
        }
    }
    let explicit = state
        .changes
        .get(file)
        .and_then(|file| file.changes.get(&diff_id))
        .copied();
    explicit.unwrap_or_else(|| default_pick(state))
}

pub fn file_has_any_partial_change(
    file: &file::Path,
    changes: &HashMap<file::Path, PartialFile>,
) -> bool {
    let (mut has_include, mut has_exclude) = (false, false);
    changes
        .get(file)
        .map(|file| {
            // We need to have both exclude and include to be partial
            file.changes.values().any(|pick| {
                has_exclude |= matches!(pick, Pick::Exclude);
                has_include |= matches!(pick, Pick::Include);
                has_include && has_exclude
            })
        })
        .unwrap_or_default()
}