//! atproto-static-web   A simple web viewer for AT Proto.
//! Copyright (C) 2025  AverageHelper
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program.  If not, see <https://www.gnu.org/licenses/>.

use bitvec::{order::Msb0, view::BitView};
use data_encoding::{BitOrder, Encoding, Specification};
use std::str::FromStr;

/// See https://atproto.com/specs/tid
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TimestampIdentifier {
	// Even tho the value is technically a 64-bit integer, bsky's source-of-truth is the string representation, and the string's noise bits get lost when we store as anything other than a string; using a char array since the size is known and fits on the stack.
	chars: [u8; 13],
}

impl FromStr for TimestampIdentifier {
	type Err = TidParseError;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		// TID Syntax: https://atproto.com/specs/tid#tid-syntax

		// Spec: "length is always 13 ASCII characters"
		if s.len() != 13 {
			return Err(TidParseError::WrongLength);
		}

		// Spec: "the first character must be one of `234567abcdefghij`"
		if !"234567abcdefghij".contains(s.chars().next().unwrap()) {
			return Err(TidParseError::IllegalStartCharacter);
		}

		let bytes = base32_decode(s).ok_or(TidParseError::IllegalCharacter)?;
		let bytes: [u8; 8] = bytes.try_into().expect("already checked length");
		// Spec: "64-bit integer; big-endian byte ordering"
		let integer = u64::from_be_bytes(bytes);
		let bits = integer.view_bits::<Msb0>();

		// Spec: "The top bit is always 0"
		if bits[0] {
			return Err(TidParseError::HighBit);
		}

		Ok(Self {
			chars: s.as_bytes().try_into().expect("already checked length"),
		})
	}
}

impl core::fmt::Display for TimestampIdentifier {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.as_string())
	}
}

impl TimestampIdentifier {
	pub fn from_ascii_str(s: &ascii::AsciiStr) -> Result<Self, TidParseError> {
		Self::from_str(s.into())
	}

	/// Returns the TID as a base32-encoded string.
	fn as_string(&self) -> &str {
		str::from_utf8(&self.chars).expect("ascii")
	}
}

static BASE32_ATPROTO: std::sync::OnceLock<Encoding> = std::sync::OnceLock::new();

fn base32_atproto() -> &'static Encoding {
	BASE32_ATPROTO.get_or_init(|| {
		// The base32 crate doesn't support custom alphabets, and the one we need is like Rfc4648Lower but reversing the digits and the alphabet.

		// Spec: "encoded as base32-sortable. That is, encoded with characters 234567abcdefghijklmnopqrstuvwxyz"
		// Spec: "no special padding characters (like `=`) are used"
		static ALPHABET: &str = "234567abcdefghijklmnopqrstuvwxyz";
		let mut spec = Specification::new();
		spec.symbols.push_str(ALPHABET);
		spec.bit_order = BitOrder::MostSignificantFirst;
		spec.check_trailing_bits = false;
		spec.encoding().unwrap()
	})
}

fn base32_decode(data: &str) -> Option<Vec<u8>> {
	base32_atproto().decode(data.as_bytes()).ok()
}

#[derive(Debug)]
pub struct HighBit(());

impl core::fmt::Display for HighBit {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "top bit must not be set")
	}
}

impl core::error::Error for HighBit {}

#[derive(Debug)]
pub enum TidParseError {
	/// The top bit was set.
	HighBit,

	/// String is composed of one or more non-ASCII characters,
	/// or characters outside of the set `234567abcdefghijklmnopqrstuvwxyz`.
	IllegalCharacter,

	/// First character of string is not one of `234567abcdefghij`.
	IllegalStartCharacter,

	/// The string length is not 13 characters.
	WrongLength,
}

impl core::fmt::Display for TidParseError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::HighBit => write!(f, "{}", HighBit(())),
			Self::IllegalCharacter => {
				write!(f, "the string contains characters that aren't in range")
			}
			Self::IllegalStartCharacter => {
				write!(f, "the string starts with a character that isn't in range")
			}
			Self::WrongLength => write!(f, "the string is not 13 characters long"),
		}
	}
}

impl core::error::Error for TidParseError {}

impl From<HighBit> for TidParseError {
	fn from(_: HighBit) -> Self {
		Self::HighBit
	}
}

// MARK: - Tests

#[cfg(test)]
mod tests {
	use super::*;

	impl TimestampIdentifier {
		fn as_integer(&self) -> u64 {
			// Spec: "64-bit integer; big-endian byte ordering"
			let s = self.as_string();
			let bytes = base32_decode(s).unwrap();
			let bytes: [u8; 8] = bytes.try_into().expect("already checked length");
			u64::from_be_bytes(bytes)
		}
	}

	#[test]
	fn test_parsing() {
		let cases = [
			("2222222222222", 0), // Spec: "The TID corresponding to integer zero is 2222222222222"
			("3zzzzzzzzzzzz", 1152921504606846975),
			("7777777777777", 2975281302211218002),
			("3jzfcijpj2z2a", 864326339526147587),
		];

		for (tid, i) in cases {
			let result: TimestampIdentifier = tid.parse().unwrap();
			let expected = TimestampIdentifier {
				chars: tid.as_bytes().try_into().unwrap(),
			};
			assert_eq!(result, expected);
			assert_eq!(result.as_integer(), i);
			assert_eq!(result.as_string(), expected.as_string());
		}
	}

	#[test]
	fn test_encoding() {
		let cases = [
			(0, "2222222222222"), // Spec: "The TID corresponding to integer zero is 2222222222222"
			(1152921504606846975, "3zzzzzzzzzzzz"),
			(1152921504606846975, "3zzzzzzzzzzzy"), // NOTE: different from the above '3zzzzzzzzzzzz', because of math and "noise bits"
			(2975281302211218002, "7777777777777"),
			(2975281302211218002, "7777777777776"), // NOTE: different from the above '7777777777777', because of math and "noise bits"
			(864326339526147587, "3jzfcijpj2z2a"),
		];

		for (i, expected_str) in cases {
			let result = TimestampIdentifier {
				chars: expected_str.as_bytes().try_into().unwrap(),
			};
			let expected = TimestampIdentifier::from_str(expected_str).unwrap();
			assert_eq!(result, expected); // integer is the same, but string may differ
			assert_eq!(result.as_integer(), i);

			let result_str = result.as_string();
			assert_eq!(result_str, expected_str);
			assert_eq!(result_str.len(), 13); // Spec: "length is always 13 ASCII characters"
		}
	}

	#[test]
	fn test_parse_not_base32() {
		// not base32
		// See https://atproto.com/specs/tid#examples
		assert!(matches!(
			"3jzfcijpj2z21".parse::<TimestampIdentifier>(),
			Err(TidParseError::IllegalCharacter)
		));
		assert!(matches!(
			"0000000000000".parse::<TimestampIdentifier>(),
			Err(TidParseError::IllegalStartCharacter)
		));
	}

	#[test]
	fn test_parse_case_sensitivity() {
		// case-sensitive
		// See https://atproto.com/specs/tid#examples
		assert!(matches!(
			"3JZFCIJPJ2Z2A".parse::<TimestampIdentifier>(),
			Err(TidParseError::IllegalCharacter)
		));
	}

	#[test]
	fn test_parse_wrong_length() {
		// too long/short
		// See https://atproto.com/specs/tid#examples
		let cases = ["3jzfcijpj2z2aa", "3jzfcijpj2z2", "222"];
		for case in cases {
			assert!(matches!(
				case.parse::<TimestampIdentifier>(),
				Err(TidParseError::WrongLength)
			));
		}
	}

	#[test]
	fn test_parse_legacy_dash_syntax() {
		// legacy dash syntax *not* supported (TTTT-TTT-TTTT-CC)
		// See https://atproto.com/specs/tid#examples
		let result = "3jzf-cij-pj2z-2a".parse::<TimestampIdentifier>();
		assert!(
			matches!(result, Err(TidParseError::WrongLength)),
			"{result:?}"
		);
	}

	#[test]
	fn test_high_bit() {
		// high bit can't be set
		// See https://atproto.com/specs/tid#examples
		let cases = ["zzzzzzzzzzzzz", "kjzfcijpj2z2a"];
		for case in cases {
			let result = case.parse::<TimestampIdentifier>();
			assert!(
				matches!(result, Err(TidParseError::IllegalStartCharacter)),
				"{result:?}"
			);
		}
	}
}
