// Copyright (C) 2025  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

use std::borrow::Cow;
use std::hash::{Hash, Hasher};

use anyhow::Result;
use imara_diff::TokenSource;
use swh_contents::diff::contents::FileContents;
use swh_contents::diff::tokenizers::{FileLines, LineIterator, Tokenizer};

#[derive(Clone, Debug)]
pub struct StrippedAsciiWhitespaceTokenizer;

impl Tokenizer for StrippedAsciiWhitespaceTokenizer {
    type Tokens<'a> = StrippedAsciiWhitespaceFileLines<'a>;

    fn tokenize<'a>(&self, input: &'a FileContents) -> Result<Option<Self::Tokens<'a>>> {
        Ok(input.lines().map(StrippedAsciiWhitespaceFileLines))
    }
}

fn strip_ascii_whitespace(bytes: &[u8]) -> Cow<'_, [u8]> {
    if bytes.iter().any(|b| b.is_ascii_whitespace()) {
        Cow::Owned(
            bytes
                .iter()
                .copied()
                .filter(|b| !b.is_ascii_whitespace())
                .collect(),
        )
    } else {
        Cow::Borrowed(bytes)
    }
}

/// A line wrapper that compares and hashes by content stripped of ASCII whitespaces.
#[derive(Clone, Debug)]
pub struct StrippedAsciiWhitespaceLine<'a>(&'a [u8]);

impl Hash for StrippedAsciiWhitespaceLine<'_> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        strip_ascii_whitespace(self.0).hash(state);
    }
}

impl PartialEq for StrippedAsciiWhitespaceLine<'_> {
    fn eq(&self, other: &Self) -> bool {
        strip_ascii_whitespace(self.0) == strip_ascii_whitespace(other.0)
    }
}

impl Eq for StrippedAsciiWhitespaceLine<'_> {}

/// A [`FileLines`] wrapper whose tokens are [`StrippedAsciiWhitespaceFileLine`]s,
/// so that whitespace-only changes are invisible to the diff algorithm.
#[derive(Clone, Debug)]
pub struct StrippedAsciiWhitespaceFileLines<'a>(FileLines<'a>);

impl<'a> TokenSource for StrippedAsciiWhitespaceFileLines<'a> {
    type Token = StrippedAsciiWhitespaceLine<'a>;
    type Tokenizer = StrippedAsciiWhitespaceLineIterator<'a>;

    fn tokenize(&self) -> Self::Tokenizer {
        StrippedAsciiWhitespaceLineIterator {
            inner: self.0.tokenize(),
        }
    }

    fn estimate_tokens(&self) -> u32 {
        self.0.estimate_tokens()
    }
}
pub struct StrippedAsciiWhitespaceLineIterator<'a> {
    inner: LineIterator<'a>,
}

impl<'a> Iterator for StrippedAsciiWhitespaceLineIterator<'a> {
    type Item = StrippedAsciiWhitespaceLine<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(StrippedAsciiWhitespaceLine)
    }
}
