// Copyright 2026 PingCAP, Inc.

use anyhow::{Result, bail};
use tantivy::tokenizer::{Token, TokenStream, Tokenizer};

/// Emits Lucene-compatible hierarchy prefixes, including the complete input.
///
/// For example, `/src/pkg/main.go` with delimiter `/` emits `/src`,
/// `/src/pkg`, and `/src/pkg/main.go`.
///
/// This tokenizer defines search semantics over UTF-8 text. It deliberately
/// does not apply TiDB collation rules. Query-side prefix filters must build an
/// exact hierarchy term instead of running this tokenizer, which would also
/// emit broader parents. The hierarchy prefix `/src/` is represented by term
/// `/src`; SQL translation must separately preserve boundary and collation
/// semantics.
#[derive(Clone)]
pub struct PathHierarchyTokenizer {
    delimiter: String,
    token: Token,
}

impl PathHierarchyTokenizer {
    pub fn new(delimiter: impl Into<String>) -> Result<Self> {
        let delimiter = delimiter.into();
        if delimiter.is_empty() {
            bail!("path hierarchy tokenizer requires a non-empty delimiter");
        }
        Ok(Self {
            delimiter,
            token: Token::default(),
        })
    }
}

pub struct PathHierarchyTokenStream<'a> {
    text: &'a str,
    delimiter: &'a str,
    next_offset: usize,
    position: usize,
    token: &'a mut Token,
    exhausted: bool,
}

impl Tokenizer for PathHierarchyTokenizer {
    type TokenStream<'a> = PathHierarchyTokenStream<'a>;

    fn token_stream<'a>(&'a mut self, text: &'a str) -> Self::TokenStream<'a> {
        self.token.reset();
        PathHierarchyTokenStream {
            text,
            delimiter: &self.delimiter,
            next_offset: 0,
            position: 0,
            token: &mut self.token,
            exhausted: false,
        }
    }
}

impl TokenStream for PathHierarchyTokenStream<'_> {
    fn advance(&mut self) -> bool {
        if self.exhausted || self.text.is_empty() {
            self.exhausted = true;
            return false;
        }

        loop {
            let Some(relative_offset) = self.text[self.next_offset..].find(self.delimiter) else {
                self.exhausted = true;
                return self.emit(self.text.len());
            };
            let delimiter_offset = self.next_offset + relative_offset;
            self.next_offset = delimiter_offset + self.delimiter.len();

            // A leading delimiter belongs to every token, but is not a token
            // by itself. This matches Lucene PathHierarchyTokenizer.
            if delimiter_offset == 0 {
                continue;
            }
            return self.emit(delimiter_offset);
        }
    }

    fn token(&self) -> &Token {
        self.token
    }

    fn token_mut(&mut self) -> &mut Token {
        self.token
    }
}

impl PathHierarchyTokenStream<'_> {
    fn emit(&mut self, end: usize) -> bool {
        self.token.offset_from = 0;
        self.token.offset_to = end;
        self.token.position = self.position;
        self.token.position_length = 1;
        self.token.text.clear();
        self.token.text.push_str(&self.text[..end]);

        self.position += 1;
        true
    }
}

#[cfg(test)]
mod tests {
    use tantivy::tokenizer::{TokenStream, Tokenizer};

    use super::*;

    fn tokens(text: &str, delimiter: &str) -> Vec<String> {
        let mut tokenizer = PathHierarchyTokenizer::new(delimiter).unwrap();
        let mut stream = tokenizer.token_stream(text);
        let mut tokens = Vec::new();
        stream.process(&mut |token| tokens.push(token.text.clone()));
        tokens
    }

    #[test]
    fn test_path_hierarchy_tokenizer() {
        assert_eq!(
            tokens("/src/pkg/main.go", "/"),
            ["/src", "/src/pkg", "/src/pkg/main.go"]
        );
        assert_eq!(
            tokens("src.pkg.main.go", "."),
            ["src", "src.pkg", "src.pkg.main", "src.pkg.main.go"]
        );
        assert_eq!(
            tokens("database::table::column", "::"),
            ["database", "database::table", "database::table::column"]
        );
    }

    #[test]
    fn test_path_hierarchy_tokenizer_boundaries() {
        assert_eq!(tokens("/src/pkg/", "/"), ["/src", "/src/pkg", "/src/pkg/"]);
        assert_eq!(tokens("main.go", "/"), ["main.go"]);
        assert_eq!(tokens("/", "/"), ["/"]);
        assert_eq!(tokens("//", "/"), ["/", "//"]);
        assert!(tokens("", "/").is_empty());
    }

    #[test]
    fn test_path_hierarchy_tokenizer_rejects_empty_delimiter() {
        assert!(PathHierarchyTokenizer::new("").is_err());
    }
}
