//! Support for cbor tags
use core::fmt;
use core::marker::PhantomData;
use serde::de::{
    Deserialize, Deserializer, EnumAccess, IntoDeserializer, MapAccess, SeqAccess, Visitor,
};
use serde::forward_to_deserialize_any;
use serde::ser::{Serialize, Serializer};

/// signals that a newtype is from a CBOR tag
pub(crate) const CBOR_NEWTYPE_NAME: &str = "\0cbor_tag";

/// A value that is optionally tagged with a cbor tag
///
/// this only serves as an intermediate helper for tag serialization or deserialization
pub struct Tagged<T> {
    /// cbor tag
    pub tag: Option<u64>,
    /// value
    pub value: T,
}

impl<T> Tagged<T> {
    /// Create a new tagged value
    pub fn new(tag: Option<u64>, value: T) -> Self {
        Self { tag, value }
    }
}

impl<T: Serialize> Serialize for Tagged<T> {
    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        set_tag(self.tag);
        let r = s.serialize_newtype_struct(CBOR_NEWTYPE_NAME, &self.value);
        set_tag(None);
        r
    }
}

fn untagged<T>(value: T) -> Tagged<T> {
    Tagged::new(None, value)
}

macro_rules! delegate {
    ($name: ident, $type: ty) => {
        fn $name<E: serde::de::Error>(self, v: $type) -> Result<Self::Value, E> {
            T::deserialize(v.into_deserializer()).map(untagged)
        }
    };
}

struct EnumDeserializer<A>(A);

impl<'de, A> Deserializer<'de> for EnumDeserializer<A>
where
    A: EnumAccess<'de>,
{
    type Error = A::Error;

    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
        visitor.visit_enum(self.0)
    }

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf option unit unit_struct newtype_struct seq tuple
        tuple_struct map struct enum identifier ignored_any
    }
}

struct NoneDeserializer<E>(PhantomData<E>);

impl<'de, E> Deserializer<'de> for NoneDeserializer<E>
where
    E: serde::de::Error,
{
    type Error = E;

    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
        visitor.visit_none()
    }

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf option unit unit_struct newtype_struct seq tuple
        tuple_struct map struct enum identifier ignored_any
    }
}

struct BytesDeserializer<'a, E>(&'a [u8], PhantomData<E>);

impl<'de, E> Deserializer<'de> for BytesDeserializer<'_, E>
where
    E: serde::de::Error,
{
    type Error = E;

    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
        visitor.visit_bytes(self.0)
    }

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf option unit unit_struct newtype_struct seq tuple
        tuple_struct map struct enum identifier ignored_any
    }
}

/// A visitor that intercepts *just* visit_newtype_struct and passes through everything else.
struct MaybeTaggedVisitor<T>(PhantomData<T>);

impl<'de, T: Deserialize<'de>> Visitor<'de> for MaybeTaggedVisitor<T> {
    type Value = Tagged<T>;

    fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt.write_str("a cbor tag newtype")
    }

    delegate!(visit_bool, bool);

    delegate!(visit_i8, i8);
    delegate!(visit_i16, i16);
    delegate!(visit_i32, i32);
    delegate!(visit_i64, i64);

    delegate!(visit_u8, u8);
    delegate!(visit_u16, u16);
    delegate!(visit_u32, u32);
    delegate!(visit_u64, u64);

    delegate!(visit_f32, f32);
    delegate!(visit_f64, f64);

    delegate!(visit_char, char);
    delegate!(visit_str, &str);
    delegate!(visit_borrowed_str, &'de str);

    #[cfg(feature = "std")]
    delegate!(visit_byte_buf, Vec<u8>);

    #[cfg(feature = "std")]
    delegate!(visit_string, String);

    fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
        T::deserialize(BytesDeserializer(value, PhantomData)).map(untagged)
    }

    fn visit_borrowed_bytes<E: serde::de::Error>(self, value: &'de [u8]) -> Result<Self::Value, E> {
        T::deserialize(serde::de::value::BorrowedBytesDeserializer::new(value)).map(untagged)
    }

    fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
        T::deserialize(().into_deserializer()).map(untagged)
    }

    fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
        T::deserialize(NoneDeserializer(PhantomData)).map(untagged)
    }

    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
        T::deserialize(deserializer).map(untagged)
    }

    fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
        T::deserialize(serde::de::value::SeqAccessDeserializer::new(seq)).map(untagged)
    }

    fn visit_map<V: MapAccess<'de>>(self, map: V) -> Result<Self::Value, V::Error> {
        T::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(untagged)
    }

    fn visit_enum<A: EnumAccess<'de>>(self, data: A) -> Result<Self::Value, A::Error> {
        T::deserialize(EnumDeserializer(data)).map(untagged)
    }

    fn visit_newtype_struct<D: serde::Deserializer<'de>>(
        self,
        deserializer: D,
    ) -> Result<Self::Value, D::Error> {
        let t = get_tag();
        T::deserialize(deserializer).map(|v| Tagged::new(t, v))
    }
}

impl<'de, T: serde::de::Deserialize<'de>> serde::de::Deserialize<'de> for Tagged<T> {
    fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_any(MaybeTaggedVisitor::<T>(PhantomData))
    }
}

/// function to get the current cbor tag
///
/// The only place where it makes sense to call this function is within visit_newtype_struct of a serde visitor.
/// This is a low level API. In most cases it is preferable to use Tagged
pub fn current_cbor_tag() -> Option<u64> {
    get_tag()
}

#[cfg(feature = "tags")]
pub(crate) fn set_tag(value: Option<u64>) {
    CBOR_TAG.with(|f| *f.borrow_mut() = value);
}

#[cfg(feature = "tags")]
pub(crate) fn get_tag() -> Option<u64> {
    CBOR_TAG.with(|f| *f.borrow())
}

#[cfg(not(feature = "tags"))]
pub(crate) fn set_tag(_value: Option<u64>) {}

#[cfg(not(feature = "tags"))]
pub(crate) fn get_tag() -> Option<u64> {
    None
}

#[cfg(feature = "tags")]
use std::cell::RefCell;

#[cfg(feature = "tags")]
thread_local!(static CBOR_TAG: RefCell<Option<u64>> = const { RefCell::new(None) });
