use std::ops::Range;

use bstr::{ByteSlice, ByteVec};

use crate::clear_and_set_capacity;

/// Undo identifiers like `$Id:<hexsha>$` to `$Id$` in `src` and write to `buf`. Newlines between dollars are ignored.
/// Return `true` if `buf` was written or `false` if `src` was left unaltered (as there was nothing to do).
pub fn undo(src: &[u8], buf: &mut Vec<u8>) -> Result<bool, std::collections::TryReserveError> {
    fn find_range(input: &[u8]) -> Option<Range<usize>> {
        let mut ofs = 0;
        loop {
            let mut cursor = input.get(ofs..)?;
            let start = cursor.find(b"$Id:")?;
            cursor = cursor.get((start + 4)..)?;
            let maybe_end = cursor.find_byteset(b"$\n")?;
            if cursor[maybe_end] == b'\n' {
                ofs += start + 4 + maybe_end + 1;
                continue;
            } else {
                return Some((ofs + start)..(ofs + start + 4 + maybe_end + 1));
            }
        }
    }

    let mut ofs = 0;
    let mut initialized = false;
    while let Some(range) = find_range(&src[ofs..]) {
        if !initialized {
            clear_and_set_capacity(buf, src.len())?;
            initialized = true;
        }
        buf.push_str(&src[ofs..][..range.start]);
        buf.push_str(b"$Id$");
        ofs += range.end;
    }
    if initialized {
        buf.push_str(&src[ofs..]);
    }
    Ok(initialized)
}

///
pub mod apply {
    /// The error produced by [`super::apply()`].
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("Could not allocate buffer")]
        OutOfMemory(#[from] std::collections::TryReserveError),
        #[error("Could not hash blob")]
        Hasher(#[from] gix_hash::hasher::Error),
    }
}

/// Substitute all occurrences of `$Id$` with `$Id: <hexsha-of-input>$` if present in `src` and write all changes to `buf`,
/// with `object_hash` being used accordingly. Return `true` if `buf` was written to or `false` if no change was made
/// (as there was nothing to do).
///
/// ### Deviation
///
/// `Git` also tries to cleanup 'stray' substituted `$Id: <hex>$`, but we don't do that, sticking exactly to what ought to be done.
/// The respective code is up to 16 years old and one might assume that `git` by now handles checking and checkout filters correctly.
pub fn apply(src: &[u8], object_hash: gix_hash::Kind, buf: &mut Vec<u8>) -> Result<bool, apply::Error> {
    const HASH_LEN: usize = ": ".len() + gix_hash::Kind::longest().len_in_hex();
    let mut id = None;
    let mut ofs = 0;
    while let Some(pos) = src[ofs..].find(b"$Id$") {
        let id = match id {
            None => {
                let new_id = gix_object::compute_hash(object_hash, gix_object::Kind::Blob, src)?;
                id = new_id.into();
                clear_and_set_capacity(buf, src.len() + HASH_LEN)?; // pre-allocate for one ID
                new_id
            }
            Some(id) => id.to_owned(),
        };

        buf.push_str(&src[ofs..][..pos + 3]);
        buf.push_str(b": ");
        id.write_hex_to(&mut *buf).expect("writes to memory always work");
        buf.push(b'$');

        ofs += pos + 4;
    }
    if id.is_some() {
        buf.push_str(&src[ofs..]);
    }
    Ok(id.is_some())
}
