// SPDX-License-Identifier: MPL-2.0
use crate::error::{Error, Result};
use std::{
	collections::HashMap,
	fmt,
	ops::{Deref, DerefMut},
};
use time::{Duration, OffsetDateTime};
use zbus::zvariant::{OwnedObjectPath, Value as ZValue};

#[derive(Debug, Clone, PartialEq)]
pub struct Metadata {
	inner: HashMap<String, MetadataValue>,
}

impl Metadata {
	/// `xesam:album`: The track artist(s).
	pub fn album(&self) -> Option<String> {
		self.inner
			.get("xesam:album")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
	}

	/// `xesam:artist`: The track artist(s).
	pub fn artists(&self) -> Option<Vec<String>> {
		self.inner
			.get("xesam:artist")
			.cloned()
			.and_then(|artists| artists.try_into_array().ok())
			.map(|artists| {
				artists
					.into_iter()
					.filter_map(|v| v.try_into_string().ok())
					.collect()
			})
	}

	/// `xesam:asText`: The track lyrics.
	pub fn lyrics(&self) -> Option<String> {
		self.inner
			.get("xesam:asText")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
	}

	/// `xesam:albumArtist`: The album artist(s).
	pub fn album_artists(&self) -> Option<Vec<String>> {
		self.inner
			.get("xesam:albumArtist")
			.cloned()
			.and_then(|artists| artists.try_into_array().ok())
			.map(|artists| {
				artists
					.into_iter()
					.filter_map(|v| v.try_into_string().ok())
					.collect()
			})
	}

	/// `xesam:audioBPM`: The speed of the music, in beats per minute.
	pub fn bpm(&self) -> Option<u64> {
		self.inner
			.get("xesam:audioBPM")
			.cloned()
			.and_then(|v| v.try_into_uint().ok())
	}

	/// `xesam:autoRating`: An automatically-generated rating, based on things such as how often it has been played.
	/// This should be in the range 0.0 to 1.0.
	pub fn auto_rating(&self) -> Option<f64> {
		self.inner
			.get("xesam:autoRating")
			.cloned()
			.and_then(|v| v.try_into_double().ok())
	}

	/// `xesam:composer`: The composer(s) of the track.
	pub fn composer(&self) -> Option<Vec<String>> {
		self.inner
			.get("xesam:composer")
			.cloned()
			.and_then(|artists| artists.try_into_array().ok())
			.map(|artists| {
				artists
					.into_iter()
					.filter_map(|v| v.try_into_string().ok())
					.collect()
			})
	}

	/// `xesam:contentCreated`: When the track was created. Usually only the year component will be useful.
	pub fn created(&self) -> Option<OffsetDateTime> {
		self.inner
			.get("xesam:contentCreated")
			.cloned()
			.and_then(|v| v.try_into_date().ok())
	}

	/// `xesam:discNumber`: The disc number on the album that this track is from.
	pub fn disc_number(&self) -> Option<u64> {
		self.inner
			.get("xesam:discNumber")
			.cloned()
			.and_then(|v| v.try_into_uint().ok())
	}

	/// `xesam:firstUsed`: When the track was first played.
	pub fn first_played(&self) -> Option<OffsetDateTime> {
		self.inner
			.get("xesam:firstUsed")
			.cloned()
			.and_then(|v| v.try_into_date().ok())
	}

	/// `xesam:genre`: The genre(s) of the track.
	pub fn genre(&self) -> Option<Vec<String>> {
		self.inner
			.get("xesam:genre")
			.cloned()
			.and_then(|artists| artists.try_into_array().ok())
			.map(|artists| {
				artists
					.into_iter()
					.filter_map(|v| v.try_into_string().ok())
					.collect()
			})
	}

	/// `xesam:lastUsed`: When the track was last played.
	pub fn last_played(&self) -> Option<OffsetDateTime> {
		self.inner
			.get("xesam:lastUsed")
			.cloned()
			.and_then(|v| v.try_into_date().ok())
	}

	/// `xesam:lyricist`: The lyricist(s) of the track.
	pub fn lyricist(&self) -> Option<Vec<String>> {
		self.inner
			.get("xesam:lyricist")
			.cloned()
			.and_then(|artists| artists.try_into_array().ok())
			.map(|artists| {
				artists
					.into_iter()
					.filter_map(|v| v.try_into_string().ok())
					.collect()
			})
	}

	/// `xesam:title`: The track title.
	pub fn title(&self) -> Option<String> {
		self.inner
			.get("xesam:title")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
	}

	/// `xesam:trackNumber`: The track number on the album that this track is from.
	pub fn track_number(&self) -> Option<u64> {
		self.inner
			.get("xesam:trackNumber")
			.cloned()
			.and_then(|v| v.try_into_uint().ok())
	}

	/// `xesam:url`: The location of the media file.
	pub fn url(&self) -> Option<String> {
		self.inner
			.get("xesam:url")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
	}

	/// `xesam:useCount`: The number of times the track has been played.
	pub fn use_count(&self) -> Option<u64> {
		self.inner
			.get("xesam:useCount")
			.cloned()
			.and_then(|v| v.try_into_uint().ok())
	}

	/// `xesam:userRating`: The user's rating of the track.
	pub fn user_rating(&self) -> Option<f64> {
		self.inner
			.get("xesam:userRating")
			.cloned()
			.and_then(|v| v.try_into_double().ok())
	}

	/// `mpris:trackid`: D-Bus path: A unique identity for this track within the context of an MPRIS object (eg: tracklist).
	pub fn track_id(&self) -> Option<OwnedObjectPath> {
		self.inner
			.get("mpris:trackid")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
			.and_then(|path| OwnedObjectPath::try_from(path).ok())
	}

	/// `mpris:length`: The length of the track in microseconds.
	pub fn length(&self) -> Option<Duration> {
		self.inner
			.get("mpris:length")
			.cloned()
			.and_then(|v| match &v {
				MetadataValue::Int(i) => Some(*i),
				MetadataValue::UInt(u) => Some(*u as i64),
				MetadataValue::Str(s) => s.parse().ok(),
				_ => None,
			})
			.map(Duration::microseconds)
	}

	/// `mpris:artUrl`: The location of an image representing the track or album.
	/// Clients should not assume this will continue to exist when the media player stops giving out the URL.
	pub fn art_url(&self) -> Option<String> {
		self.inner
			.get("mpris:artUrl")
			.cloned()
			.and_then(|v| v.try_into_string().ok())
	}
}

impl Deref for Metadata {
	type Target = HashMap<String, MetadataValue>;

	fn deref(&self) -> &Self::Target {
		&self.inner
	}
}

impl DerefMut for Metadata {
	fn deref_mut(&mut self) -> &mut Self::Target {
		&mut self.inner
	}
}

impl fmt::Display for Metadata {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		write!(f, "{{")?;
		let mut iter = self.inner.iter().peekable();
		while let Some((k, v)) = iter.next() {
			if iter.peek().is_some() {
				write!(f, "{}: {}, ", k, v)?;
			} else {
				write!(f, "{}: {}", k, v)?;
			}
		}
		write!(f, "}}")
	}
}

impl<'a, V: Into<ZValue<'a>>> From<HashMap<String, V>> for Metadata {
	fn from(map: HashMap<String, V>) -> Self {
		Self {
			inner: map
				.into_iter()
				.map(|(k, v)| (k, MetadataValue::from(&v.into())))
				.collect(),
		}
	}
}

#[derive(Clone, PartialEq)]
pub enum MetadataValue {
	Str(String),
	Double(f64),
	Int(i64),
	UInt(u64),
	Bool(bool),
	Array(Vec<MetadataValue>),
	Dict(HashMap<String, MetadataValue>),
	__Unsupported,
}

impl MetadataValue {
	fn variant(&self) -> &'static str {
		match self {
			MetadataValue::Str(_) => "Str",
			MetadataValue::Double(_) => "Double",
			MetadataValue::Int(_) => "Int",
			MetadataValue::UInt(_) => "UInt",
			MetadataValue::Bool(_) => "Bool",
			MetadataValue::Array(_) => "Array",
			MetadataValue::Dict(_) => "Dict",
			MetadataValue::__Unsupported => "Unsupported",
		}
	}

	/// Tries to extract a string from the variant,
	/// returning an error if the variant is not a string.
	pub fn try_into_string(self) -> Result<String> {
		match self {
			MetadataValue::Str(s) => Ok(s),
			_ => Err(Error::IncorrectVariant {
				wanted: "Str",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract a string from the variant,
	/// panicking if the variant is not a string.
	pub fn into_string(self) -> String {
		self.try_into_string()
			.unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract a date/time from the variant,
	/// returning an error if the variant is not a date/time.
	pub fn try_into_date(self) -> Result<OffsetDateTime> {
		let variant = self.variant();
		match self {
			MetadataValue::Str(s) => {
				OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err(
					|_| Error::IncorrectVariant {
						wanted: "String (DateTime)",
						actual: variant,
					},
				)
			}
			_ => Err(Error::IncorrectVariant {
				wanted: "String (DateTime)",
				actual: variant,
			}),
		}
	}

	/// Tries to extract a date/time from the variant,
	/// panicking if the variant is not a date/time.
	pub fn into_date(self) -> OffsetDateTime {
		self.try_into_date().unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract a double from the variant,
	/// returning an error if the variant is not a double.
	pub fn try_into_double(self) -> Result<f64> {
		match self {
			MetadataValue::Double(d) => Ok(d),
			_ => Err(Error::IncorrectVariant {
				wanted: "Double",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract a double from the variant,
	/// panicking if the variant is not a double.
	pub fn into_double(self) -> f64 {
		self.try_into_double()
			.unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract an integer from the variant,
	/// returning an error if the variant is not an integer.
	pub fn try_into_int(self) -> Result<i64> {
		match self {
			MetadataValue::Int(i) => Ok(i),
			_ => Err(Error::IncorrectVariant {
				wanted: "Int",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract an integer from the variant,
	/// panicking if the variant is not an integer.
	pub fn into_int(self) -> i64 {
		self.try_into_int().unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract an unsigned integer from the variant,
	/// returning an error if the variant is not an unsigned integer.
	pub fn try_into_uint(self) -> Result<u64> {
		match self {
			MetadataValue::UInt(u) => Ok(u),
			_ => Err(Error::IncorrectVariant {
				wanted: "UInt",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract an unsigned integer from the variant,
	/// panicking if the variant is not an unsigned integer.
	pub fn into_uint(self) -> u64 {
		self.try_into_uint().unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract a boolean from the variant,
	/// returning an error if the variant is not a boolean.
	pub fn try_into_bool(self) -> Result<bool> {
		match self {
			MetadataValue::Bool(b) => Ok(b),
			_ => Err(Error::IncorrectVariant {
				wanted: "Bool",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract a boolean from the variant,
	/// panicking if the variant is not a boolean.
	pub fn into_bool(self) -> bool {
		self.try_into_bool().unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract an array from the variant,
	/// returning an error if the variant is not an array.
	pub fn try_into_array(self) -> Result<Vec<MetadataValue>> {
		match self {
			MetadataValue::Array(a) => Ok(a),
			_ => Err(Error::IncorrectVariant {
				wanted: "Array",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract an array from the variant,
	/// panicking if the variant is not an array.
	pub fn into_array(self) -> Vec<MetadataValue> {
		self.try_into_array()
			.unwrap_or_else(|err| panic!("{}", err))
	}

	/// Tries to extract a dictionary from the variant,
	/// returning an error if the variant is not a dictionary.
	/// The dictionary is returned as a map from string keys to values.
	pub fn try_into_dict(self) -> Result<HashMap<String, MetadataValue>> {
		match self {
			MetadataValue::Dict(d) => Ok(d),
			_ => Err(Error::IncorrectVariant {
				wanted: "Dict",
				actual: self.variant(),
			}),
		}
	}

	/// Tries to extract a dictionary from the variant,
	/// panicking if the variant is not a dictionary.
	/// The dictionary is returned as a map from string keys to values.
	pub fn into_dict(self) -> HashMap<String, MetadataValue> {
		self.try_into_dict().unwrap_or_else(|err| panic!("{}", err))
	}
}

impl<'a> From<&ZValue<'a>> for MetadataValue {
	fn from(value: &ZValue) -> Self {
		match value {
			ZValue::U8(u) => Self::UInt(*u as u64),
			ZValue::Bool(b) => Self::Bool(*b),
			ZValue::I16(i) => Self::Int(*i as i64),
			ZValue::U16(u) => Self::UInt(*u as u64),
			ZValue::I32(i) => Self::Int(*i as i64),
			ZValue::U32(u) => Self::UInt(*u as u64),
			ZValue::I64(i) => Self::Int(*i),
			ZValue::U64(u) => Self::UInt(*u),
			ZValue::F64(f) => Self::Double(*f),
			ZValue::Str(s) => Self::Str(s.to_string()),
			ZValue::ObjectPath(path) => Self::Str(path.to_string()),
			ZValue::Array(a) => Self::Array(a.iter().map(|v| v.into()).collect()),
			ZValue::Dict(d) => Self::Dict(
				HashMap::<String, ZValue>::try_from(d.to_owned())
					.unwrap()
					.into_iter()
					.map(|(k, v)| (k, (&v).into()))
					.collect(),
			),
			ZValue::Value(value) => Self::from(&**value),
			_ => Self::__Unsupported,
		}
	}
}

impl fmt::Debug for MetadataValue {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::__Unsupported => write!(f, "__Unsupported"),
			Self::Int(i) => write!(f, "{}", i),
			Self::UInt(u) => write!(f, "{}", u),
			Self::Double(d) => write!(f, "{}", d),
			Self::Str(s) => write!(f, "{}", s),
			Self::Bool(b) => write!(f, "{}", b),
			Self::Array(a) => write!(f, "{:?}", a),
			Self::Dict(d) => {
				let mut debug_struct = f.debug_struct("Dict");
				for (k, v) in d {
					debug_struct.field(k, &v);
				}
				debug_struct.finish()
			}
		}
	}
}

impl fmt::Display for MetadataValue {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::__Unsupported => write!(f, "__Unsupported"),
			Self::Int(i) => write!(f, "{}", i),
			Self::UInt(u) => write!(f, "{}", u),
			Self::Double(d) => write!(f, "{}", d),
			Self::Str(s) => write!(f, "\"{}\"", s),
			Self::Bool(b) => write!(f, "{}", b),
			Self::Array(a) => {
				write!(f, "[")?;
				let mut iter = a.iter().peekable();
				while let Some(value) = iter.next() {
					if iter.peek().is_some() {
						write!(f, "{}, ", value)?;
					} else {
						write!(f, "{}", value)?;
					}
				}
				write!(f, "]")
			}
			Self::Dict(d) => {
				write!(f, "{{")?;
				let mut iter = d.iter().peekable();
				while let Some((k, v)) = iter.next() {
					if iter.peek().is_some() {
						write!(f, "{}: {}, ", k, v)?;
					} else {
						write!(f, "{}: {}", k, v)?;
					}
				}
				write!(f, "}}")
			}
		}
	}
}
