//! 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 super::too_long_error::{LengthKind, TooLongError};
use core::{fmt::Display, ops::Deref};
use serde::{Deserialize, de};
use unicode_segmentation::UnicodeSegmentation;

/// A `String` whose length and grapheme count are limited.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LimitedString<const MAX_LENGTH: usize, const MAX_GRAPHEMES: usize>(String);

impl<const L: usize, const G: usize> Default for LimitedString<L, G> {
	/// Creates an empty [`LimitedString`]
	#[inline]
	fn default() -> Self {
		Self(Default::default()) // empty string always fits the limits
	}
}

impl<const L: usize, const G: usize> Deref for LimitedString<L, G> {
	type Target = str;

	#[inline]
	fn deref(&self) -> &Self::Target {
		&self.0
	}
}

impl<const L: usize, const G: usize> AsRef<str> for LimitedString<L, G> {
	#[inline]
	fn as_ref(&self) -> &str {
		self.0.as_ref()
	}
}

impl<const L: usize, const G: usize> Display for LimitedString<L, G> {
	#[inline]
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.0)
	}
}

impl<const L: usize, const G: usize> PartialEq<String> for LimitedString<L, G> {
	#[inline]
	fn eq(&self, other: &String) -> bool {
		self.0.eq(other)
	}
}

impl<const L: usize, const G: usize> PartialEq<&str> for LimitedString<L, G> {
	#[inline]
	fn eq(&self, other: &&str) -> bool {
		self.0.as_str().eq(*other)
	}
}

impl<const MAX_LENGTH: usize, const MAX_GRAPHEMES: usize> TryFrom<String>
	for LimitedString<MAX_LENGTH, MAX_GRAPHEMES>
{
	type Error = TooLongError;

	fn try_from(value: String) -> Result<Self, Self::Error> {
		// Check that the value's byte length is within range
		let len = value.len();
		if len > MAX_LENGTH {
			return Err(TooLongError::new(len, MAX_LENGTH, LengthKind::Bytes));
		}

		// Check that the value fits the maximum number of Unicode Grapheme Clusters
		// See https://atproto.com/specs/lexicon#:~:text=maxGraphemes
		if len < MAX_GRAPHEMES {
			// If the Rust string length (bytes) is within the maximum limit,
			// its grapheme length will also be within.
			Ok(Self(value))
		} else {
			let graphemes = value.graphemes(true).count();
			if graphemes < MAX_GRAPHEMES {
				Ok(Self(value))
			} else {
				Err(TooLongError::new(
					graphemes,
					MAX_GRAPHEMES,
					LengthKind::Graphemes,
				))
			}
		}
	}
}

impl<const MAX_LENGTH: usize, const MAX_GRAPHEMES: usize> TryFrom<&str>
	for LimitedString<MAX_LENGTH, MAX_GRAPHEMES>
{
	type Error = TooLongError;

	#[inline]
	fn try_from(value: &str) -> Result<Self, Self::Error> {
		Self::try_from(value.to_string())
	}
}

impl<'de, const MAX_LENGTH: usize, const MAX_GRAPHEMES: usize> Deserialize<'de>
	for LimitedString<MAX_LENGTH, MAX_GRAPHEMES>
{
	#[inline]
	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		let value = String::deserialize(deserializer)?;

		Self::try_from(value).map_err(|err| de::Error::invalid_length(err.actual(), err.expected()))
	}
}

// MARK: - Tests

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

	#[test]
	fn test_deserializes_empty_string() {
		let data = r#""""#;
		let result: LimitedString<4, 4> = serde_json::from_str(data).unwrap();
		assert_eq!(result, "");
	}

	#[test]
	fn test_deserializes_short_string() {
		let data = r#""あ""#;
		let result: LimitedString<4, 4> = serde_json::from_str(data).unwrap();
		assert_eq!(result, "あ");
	}

	#[test]
	fn test_fails_on_too_many_graphemes() {
		let data = r#""あああああ""#;
		let result = serde_json::from_str::<LimitedString<48, 4>>(data);
		assert!(result.is_err());

		let error = result.err().unwrap();
		assert_eq!(
			format!("{error}"),
			"invalid length 5, expected length at most 4 grapheme clusters"
		);
	}

	#[test]
	fn test_fails_on_too_many_bytes() {
		let data = r#""ああああ""#;
		let result = serde_json::from_str::<LimitedString<4, 8>>(data);
		assert!(result.is_err());

		let error = result.err().unwrap();
		assert_eq!(
			format!("{error}"),
			"invalid length 12, expected length at most 4 bytes"
		);
	}
}
