use std::borrow::Cow;
use std::convert::TryFrom;
use std::ffi::{CStr, CString, OsStr, OsString};
use std::mem::MaybeUninit;
use std::num::NonZeroI32;
use std::ops::Deref;
use std::os::raw::{c_int, c_void};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::ptr::null_mut;
use std::slice;
use std::str;

use crate::{
    ffi, AnyLuaString, AsLua, LuaRead, Push, PushGuard, PushInto, PushOne, PushOneInto, ReadResult,
    Void, WrongType,
};

macro_rules! numeric_impl {
    ($t:ident, $push:path, $read:path $(, coerce: $coerce:expr)?) => {
        impl<L> Push<L> for $t
        where
            L: AsLua,
        {
            type Err = Void;      // TODO: use `!` instead (https://github.com/rust-lang/rust/issues/35121)

            #[inline(always)]
            fn push_to_lua(&self, lua: L) -> Result<PushGuard<L>, (Void, L)> {
                Self::push_into_lua(*self, lua)
            }
        }

        impl<L> PushOne<L> for $t
        where
            L: AsLua,
        {
        }

        impl<L> PushInto<L> for $t
        where
            L: AsLua,
        {
            type Err = Void;      // TODO: use `!` instead (https://github.com/rust-lang/rust/issues/35121)

            #[inline(always)]
            fn push_into_lua(self, lua: L) -> Result<PushGuard<L>, (Void, L)> {
                unsafe {
                    $push(lua.as_lua(), self as _);
                    Ok(PushGuard::new(lua, 1))
                }
            }
        }

        impl<L> PushOneInto<L> for $t
        where
            L: AsLua,
        {
        }

        impl<L> LuaRead<L> for $t
        where
            L: AsLua,
        {
            #[inline(always)]
            fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<$t, L> {
                return if let Some(v) = unsafe { read_numeric(lua.as_lua(), index.into()) } {
                    Ok(v as _)
                } else {
                    let e = WrongType::default()
                        .expected_type::<Self>()
                        .actual_single_lua(&lua, index);
                    Err((lua, e))
                };

                #[inline(always)]
                pub unsafe fn read_numeric(l: *mut ffi::lua_State, idx: c_int) -> Option<$t> {
                    match ffi::lua_type(l, idx) {
                        ffi::LUA_TNUMBER => {
                            let number = $read(l, idx);
                            $(
                                let number = $coerce(number);
                            )?
                            Some(number as _)
                        }
                        ffi::LUA_TCDATA => {
                            let mut ctypeid = std::mem::MaybeUninit::uninit();
                            let cdata = ffi::luaL_checkcdata(l, idx, ctypeid.as_mut_ptr());
                            match ctypeid.assume_init() {
                                ffi::CTID_CCHAR => Some(*cdata.cast::<std::os::raw::c_char>() as _),
                                ffi::CTID_INT8 => Some(*cdata.cast::<i8>() as _),
                                ffi::CTID_INT16 => Some(*cdata.cast::<i16>() as _),
                                ffi::CTID_INT32 => Some(*cdata.cast::<i32>() as _),
                                ffi::CTID_INT64 => Some(*cdata.cast::<i64>() as _),
                                ffi::CTID_UINT8 => Some(*cdata.cast::<u8>() as _),
                                ffi::CTID_UINT16 => Some(*cdata.cast::<u16>() as _),
                                ffi::CTID_UINT32 => Some(*cdata.cast::<u32>() as _),
                                ffi::CTID_UINT64 => Some(*cdata.cast::<u64>() as _),
                                ffi::CTID_FLOAT => Some(*cdata.cast::<f32>() as _),
                                ffi::CTID_DOUBLE => Some(*cdata.cast::<f64>() as _),
                                _ => None,
                            }
                        }
                        _ => None,
                    }
                }
            }
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// integers and floats
////////////////////////////////////////////////////////////////////////////////

numeric_impl! {isize, ffi::luaL_pushint64, ffi::lua_tonumber}
numeric_impl! {i64, ffi::luaL_pushint64, ffi::lua_tonumber}
numeric_impl! {i32, ffi::lua_pushinteger, ffi::lua_tointeger}
numeric_impl! {i16, ffi::lua_pushinteger, ffi::lua_tointeger}
numeric_impl! {i8, ffi::lua_pushinteger, ffi::lua_tointeger}

numeric_impl! {usize, ffi::luaL_pushuint64, ffi::lua_tonumber,
    coerce: |n| {
        if n >= 0. {
            n as usize
        } else {
            n as isize as usize
        }
    }
}
numeric_impl! {u64, ffi::luaL_pushuint64, ffi::lua_tonumber,
    coerce: |n| {
        if n >= 0. {
            n as u64
        } else {
            n as i64 as u64
        }
    }
}
numeric_impl! {u32, ffi::lua_pushinteger, ffi::lua_tointeger}
numeric_impl! {u16, ffi::lua_pushinteger, ffi::lua_tointeger}
numeric_impl! {u8, ffi::lua_pushinteger, ffi::lua_tointeger}

numeric_impl! {f64, ffi::lua_pushnumber, ffi::lua_tonumber}
numeric_impl! {f32, ffi::lua_pushnumber, ffi::lua_tonumber}

macro_rules! strict_numeric_impl {
    (@is_valid int $num:tt $t:ty) => {
        $num.is_finite() && $num.fract() == 0.0 &&
        $num >= <$t>::MIN as _ && $num <= <$t>::MAX as _
    };
    (@is_valid float $num:tt $t:ty) => {
        !$num.is_finite() || $num >= <$t>::MIN as _ && $num <= <$t>::MAX as _
    };
    ($k:tt $t:ty) => {
        impl<L> LuaRead<L> for Strict<$t>
        where
            L: AsLua,
        {
            #[inline(always)]
            fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<Self, L> {
                let l = lua.as_lua();
                let idx = index.into();
                let res = unsafe {
                    match ffi::lua_type(l, idx) {
                        ffi::LUA_TNUMBER => {
                            let num = ffi::lua_tonumber(l, idx);
                            let is_valid = strict_numeric_impl!(@is_valid $k num $t);
                            if is_valid {
                                Some(num as $t)
                            } else {
                                None
                            }
                        }
                        _ => None,
                    }
                };
                res.map(Strict).ok_or_else(|| {
                    let e = WrongType::default()
                        .expected_type::<Self>()
                        .actual_single_lua(&lua, index);
                    (lua, e)
                })
            }
        }
    };
}

////////////////////////////////////////////////////////////////////////////////
// Strict<T>
////////////////////////////////////////////////////////////////////////////////

/// A wrapper type for reading lua numbers of concrete precisions without
/// implicit coercions.
///
/// By default when reading a numeric type (int or float) from a lua number
/// (i.e. calling [`LuaRead::lua_read_at_position`]) the resulting number will
/// be implicitly coerced into the target type. For example if the lua number
/// has a non zero fractional part it will be discarded when reading the number
/// as integer.
/// ```no_run
/// use tlua::Lua;
/// let lua = tlua::Lua::new();
/// let i: Option<i32> = lua.eval("return 3.14").ok();
/// assert_eq!(i, Some(3));
/// ```
///
/// If you don't want the implicit coercision, you can use the `Strict` wrapper:
/// ```no_run
/// # use tlua::Lua;
/// use tlua::Strict;
/// # let lua = Lua::new();
/// let i: Option<Strict<i32>> = lua.eval("return 3.14").ok();
/// assert_eq!(i, None); // would result in loss of data
///
/// let f: Option<Strict<f64>> = lua.eval("return 3.14").ok();
/// assert_eq!(f, Some(Strict(3.14))); // ok
/// ```
///
/// This *strictness* also applies in terms of number sizes:
/// ```no_run
/// # use tlua::{Lua, Strict};
/// # let lua = Lua::new();
/// let i: Option<u8> = lua.eval("return 256").ok();
/// assert_eq!(i, Some(0)); // non-strict => data loss
///
/// let i: Option<Strict<u8>> = lua.eval("return 256").ok();
/// assert_eq!(i, None); // strict => must not lose data
///
/// let i: Option<Strict<u16>> = lua.eval("return 256").ok();
/// assert_eq!(i, Some(Strict(256))); // strict => no data loss
/// ```
///
/// # Reading cdata
///
/// This type only supports regular lua numbers. If you want to read a luajit
/// cdata type, use the [`CData`](crate::CData) wrapper instead.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct Strict<T>(pub T);

strict_numeric_impl! {int i8}
strict_numeric_impl! {int i16}
strict_numeric_impl! {int i32}
strict_numeric_impl! {int i64}
strict_numeric_impl! {int isize}
strict_numeric_impl! {int u8}
strict_numeric_impl! {int u16}
strict_numeric_impl! {int u32}
strict_numeric_impl! {int u64}
strict_numeric_impl! {int usize}
strict_numeric_impl! {float f32}
strict_numeric_impl! {float f64}

impl<T> From<T> for Strict<T> {
    fn from(v: T) -> Self {
        Self(v)
    }
}

macro_rules! impl_push_read {
    (
        $t:ty,
        $(push_to_lua(&$self1:ident, $lua1:ident) { $($push:tt)* })?
        $(push_into_lua($self2:ident, $lua2:ident) { $($push_into:tt)* })?
        $(
            read_at_position($lua3:ident, $index1:ident) { $($read:tt)* }
            $(read_at_maybe_zero_position($lua4:ident, $index2:ident) { $($read_mz:tt)* })?
        )?
    ) => {
        $(
            impl<L> Push<L> for $t
            where
                L: AsLua,
            {
                type Err = Void;      // TODO: use `!` instead (https://github.com/rust-lang/rust/issues/35121)

                #[inline(always)]
                fn push_to_lua(&$self1, $lua1: L) -> Result<PushGuard<L>, (Void, L)> {
                    $($push)*
                }
            }

            impl<L> PushOne<L> for $t
            where
                L: AsLua,
            {
            }
        )?

        $(
            impl<L> PushInto<L> for $t
            where
                L: AsLua,
            {
                type Err = Void;      // TODO: use `!` instead (https://github.com/rust-lang/rust/issues/35121)

                #[inline(always)]
                fn push_into_lua($self2, $lua2: L) -> Result<PushGuard<L>, (Void, L)> {
                    $($push_into)?
                }
            }

            impl<L> PushOneInto<L> for $t
            where
                L: AsLua,
            {
            }
        )?

        $(
            impl<L> LuaRead<L> for $t
            where
                L: AsLua,
            {
                #[inline(always)]
                fn lua_read_at_position($lua3: L, $index1: NonZeroI32) -> $crate::ReadResult<Self, L> {
                    $($read)*
                }

                $(
                    #[inline(always)]
                    fn lua_read_at_maybe_zero_position($lua4: L, $index2: i32) -> $crate::ReadResult<Self, L> {
                        $($read_mz)*
                    }
                )?
            }
        )?
    }
}

////////////////////////////////////////////////////////////////////////////////
// helpers for strings
////////////////////////////////////////////////////////////////////////////////

#[inline]
pub unsafe fn push_lua_string(lua: crate::LuaState, s: &[u8]) {
    // SAFETY:
    // - `lua` msut be valid
    // - may crash if stack overflows
    unsafe {
        ffi::lua_pushlstring(lua.as_lua(), s.as_ptr() as _, s.len() as _);
    }
}

#[inline]
pub fn push_lua_string_and_guard<L>(lua: L, s: &[u8]) -> PushGuard<L>
where
    L: AsLua,
{
    unsafe {
        push_lua_string(lua.as_lua(), s);
    }
    // SAFETY: just pushed a value onto stack
    unsafe { PushGuard::new(lua, 1) }
}

pub fn read_lua_string<'a>(lua: crate::LuaState, index: i32) -> Option<&'a [u8]> {
    // SAFETY: `lua` must be valid
    unsafe {
        let type_code = ffi::lua_type(lua, index);
        // lua_tolstring will silently convert a number to string and return
        // a success. But because this function may be called while
        // iterating over a table such silent conversion will break the
        // iterator. So instead we must check for the type explicitly
        if type_code != ffi::LUA_TSTRING {
            return None;
        }

        let mut size = 0;
        let c_ptr = ffi::lua_tolstring(lua, index, &mut size);
        if c_ptr.is_null() {
            return None;
        }

        let slice = slice::from_raw_parts(c_ptr as _, size);
        Some(slice)
    }
}

pub fn read_lua_string_or_err<'a, T, L>(
    lua: L,
    index: NonZeroI32,
) -> Result<(&'a [u8], L), (L, WrongType)>
where
    L: AsLua,
{
    let res = read_lua_string(lua.as_lua(), index.into());
    let Some(slice) = res else {
        let e = crate::WrongType::default()
            .expected_type::<T>()
            .actual_single_lua(&lua, index);
        return Err((lua, e));
    };

    Ok((slice, lua))
}

////////////////////////////////////////////////////////////////////////////////
// strings
////////////////////////////////////////////////////////////////////////////////

impl_push_read! { String,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
    push_into_lua(self, lua) {
        self.push_to_lua(lua)
    }
    read_at_position(lua, index) {
        let (slice, lua) = read_lua_string_or_err::<Self, _>(lua, index)?;

        let res = String::from_utf8(slice.to_vec());
        let s = match res {
            Ok(v) => v,
            Err(e) => {
                let e = crate::WrongType::default()
                    .expected_type::<Self>()
                    .actual(format!("non-utf8 string: {e}"));
                return Err((lua, e));
            }
        };

        Ok(s)
    }
}

impl_push_read! { CString,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
    push_into_lua(self, lua) {
        self.push_to_lua(lua)
    }
    read_at_position(lua, index) {
        let (slice, lua) = read_lua_string_or_err::<Self, _>(lua, index)?;

        let res = CString::new(slice.to_vec());
        let s = match res {
            Ok(v) => v,
            Err(e) => {
                let e = crate::WrongType::default()
                    .expected_type::<Self>()
                    .actual(format!("string with nul-byte: {e}"));
                return Err((lua, e));
            }
        };

        Ok(s)
    }
}

impl_push_read! { AnyLuaString,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
    push_into_lua(self, lua) {
        self.push_to_lua(lua)
    }
    read_at_position(lua, index) {
        let (slice, _) = read_lua_string_or_err::<Self, _>(lua, index)?;

        let s = AnyLuaString(slice.to_vec());
        Ok(s)
    }
}

impl_push_read! { str,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
}

impl_push_read! { CStr,
    push_to_lua(&self, lua) {
        unsafe {
            ffi::lua_pushlstring(
                lua.as_lua(),
                self.as_ptr() as _,
                self.to_bytes().len() as _,
            );
            Ok(PushGuard::new(lua, 1))
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// OsStr
////////////////////////////////////////////////////////////////////////////////

impl_push_read! { OsString,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
    push_into_lua(self, lua) {
        self.push_to_lua(lua)
    }
    read_at_position(lua, index) {
        let (slice, _) = read_lua_string_or_err::<Self, _>(lua, index)?;

        let s = OsString::from_vec(slice.to_vec());
        Ok(s)
    }
}

impl_push_read! { OsStr,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_bytes()))
    }
}

////////////////////////////////////////////////////////////////////////////////
// Path
////////////////////////////////////////////////////////////////////////////////

impl_push_read! { PathBuf,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_os_str().as_bytes()))
    }
    push_into_lua(self, lua) {
        self.push_to_lua(lua)
    }
    read_at_position(lua, index) {
        OsString::lua_read_at_position(lua, index).map(Into::into)
    }
}

impl_push_read! { Path,
    push_to_lua(&self, lua) {
        Ok(push_lua_string_and_guard(lua, self.as_os_str().as_bytes()))
    }
}

////////////////////////////////////////////////////////////////////////////////
// StringInLua
////////////////////////////////////////////////////////////////////////////////

/// String on the Lua stack.
///
/// It is faster -but less convenient- to read a `StringInLua` rather than a `String` because you
/// avoid any allocation.
///
/// The `StringInLua` derefs to `str`.
///
/// # Example
///
/// ```no_run
/// let mut lua = tlua::Lua::new();
/// lua.set("a", "hello");
///
/// let s: tlua::StringInLua<_> = lua.get("a").unwrap();
/// println!("{}", &*s);    // Prints "hello".
/// ```
#[derive(Debug, Eq, Ord, Hash)]
pub struct StringInLua<'a, L: 'a> {
    lua: L,
    str_ref: &'a str,
}

impl<L> StringInLua<'_, L> {
    pub fn into_inner(self) -> L {
        self.lua
    }
}

impl<L> std::cmp::PartialEq for StringInLua<'_, L> {
    fn eq(&self, other: &Self) -> bool {
        self.str_ref.eq(other.str_ref)
    }
}

impl<L> std::cmp::PartialOrd for StringInLua<'_, L> {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.str_ref.partial_cmp(other.str_ref)
    }
}

impl<L> std::cmp::PartialEq<&'_ str> for StringInLua<'_, L> {
    fn eq(&self, other: &&str) -> bool {
        self.str_ref.eq(*other)
    }
}

impl<'a, L> LuaRead<L> for StringInLua<'a, L>
where
    L: 'a + AsLua,
{
    fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<Self, L> {
        let (slice, lua) = read_lua_string_or_err::<Self, _>(lua, index)?;

        let res = str::from_utf8(slice);
        let str_ref = match res {
            Ok(v) => v,
            Err(e) => {
                let e = crate::WrongType::default()
                    .expected_type::<Self>()
                    .actual(format!("non-utf8 string: {e}"));
                return Err((lua, e));
            }
        };
        Ok(StringInLua { lua, str_ref })
    }
}

impl<L> Deref for StringInLua<'_, L> {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        self.str_ref
    }
}

////////////////////////////////////////////////////////////////////////////////
// BytesInLua
////////////////////////////////////////////////////////////////////////////////

/// Helper struct for reading lua string without extra memory allocations.
///
/// The `BytesInLua` derefs to `[u8]`.
///
/// # Example
///
/// ```no_run
/// let mut lua = tlua::Lua::new();
/// lua.set("a", "\x01\x02\x03");
///
/// let s: tlua::BytesInLua<_> = lua.get("a").unwrap();
/// println!("{:?}", &*s);    // Prints [1, 2, 3].
/// ```
#[derive(Debug, Eq, Ord, Hash)]
pub struct BytesInLua<'a, L: 'a> {
    lua: L,
    slice: &'a [u8],
}

impl<L> BytesInLua<'_, L> {
    #[inline]
    pub fn into_inner(self) -> L {
        self.lua
    }
}

impl<L> std::cmp::PartialEq for BytesInLua<'_, L> {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.slice.eq(other.slice)
    }
}

impl<L> std::cmp::PartialOrd for BytesInLua<'_, L> {
    #[inline]
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.slice.partial_cmp(other.slice)
    }
}

impl<'a, L> LuaRead<L> for BytesInLua<'a, L>
where
    L: 'a + AsLua,
{
    #[inline]
    fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<Self, L> {
        let (slice, lua) = read_lua_string_or_err::<Self, _>(lua, index)?;

        Ok(BytesInLua { lua, slice })
    }
}

impl<L> Deref for BytesInLua<'_, L> {
    type Target = [u8];

    #[inline]
    fn deref(&self) -> &[u8] {
        self.slice
    }
}

////////////////////////////////////////////////////////////////////////////////
// bool
////////////////////////////////////////////////////////////////////////////////

impl_push_read! { bool,
    push_to_lua(&self, lua) {
        Self::push_into_lua(*self, lua)
    }
    push_into_lua(self, lua) {
        unsafe {
            ffi::lua_pushboolean(lua.as_lua(), self as _);
            Ok(PushGuard::new(lua, 1))
        }
    }
    read_at_position(lua, index) {
        if !unsafe { ffi::lua_isboolean(lua.as_lua(), index.into()) } {
            let e = WrongType::default()
                .expected_type::<Self>()
                .actual_single_lua(&lua, index);
            return Err((lua, e));
        }

        Ok(unsafe { ffi::lua_toboolean(lua.as_lua(), index.into()) != 0 })
    }
}

////////////////////////////////////////////////////////////////////////////////
// ()
////////////////////////////////////////////////////////////////////////////////

impl_push_read! { (),
    push_to_lua(&self, lua) {
        ().push_into_lua(lua)
    }
    push_into_lua(self, lua) {
        unsafe { Ok(PushGuard::new(lua, 0)) }
    }
    read_at_position(_lua, _index) {
        Ok(())
    }
    read_at_maybe_zero_position(_lua, _index) {
        Ok(())
    }
}

////////////////////////////////////////////////////////////////////////////////
// Option<T>
////////////////////////////////////////////////////////////////////////////////

impl<L, T> Push<L> for Option<T>
where
    T: Push<L>,
    L: AsLua,
{
    type Err = T::Err;

    #[inline]
    fn push_to_lua(&self, lua: L) -> Result<PushGuard<L>, (Self::Err, L)> {
        match self {
            Some(val) => val.push_to_lua(lua),
            None => Ok(Nil.push_into_no_err(lua)),
        }
    }
}

impl<L, T> PushOne<L> for Option<T>
where
    T: PushOne<L>,
    L: AsLua,
{
}

impl<L, T> PushInto<L> for Option<T>
where
    T: PushInto<L>,
    L: AsLua,
{
    type Err = T::Err;

    #[inline]
    fn push_into_lua(self, lua: L) -> Result<PushGuard<L>, (Self::Err, L)> {
        match self {
            Some(val) => val.push_into_lua(lua),
            None => Ok(Nil.push_into_no_err(lua)),
        }
    }
}

impl<L, T> PushOneInto<L> for Option<T>
where
    T: PushOneInto<L>,
    L: AsLua,
{
}

impl<L, T> LuaRead<L> for Option<T>
where
    L: AsLua,
    T: LuaRead<L>,
{
    fn lua_read_at_maybe_zero_position(lua: L, index: i32) -> ReadResult<Option<T>, L> {
        if let Some(index) = NonZeroI32::new(index) {
            Self::lua_read_at_position(lua, index)
        } else {
            Ok(None)
        }
    }

    fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<Option<T>, L> {
        if unsafe { is_null_or_nil(lua.as_lua(), index.get()) } {
            return Ok(None);
        }
        T::lua_read_at_position(lua, index).map(Some)
    }
}

////////////////////////////////////////////////////////////////////////////////
// Result<A, B>
////////////////////////////////////////////////////////////////////////////////

impl<L, A, B> LuaRead<L> for Result<A, B>
where
    L: AsLua,
    A: for<'a> LuaRead<&'a L>,
    B: for<'b> LuaRead<&'b L>,
{
    fn lua_read_at_position(lua: L, index: NonZeroI32) -> ReadResult<Result<A, B>, L> {
        if let Ok(a) = A::lua_read_at_position(&lua, index) {
            return Ok(Ok(a));
        }
        if let Ok(b) = B::lua_read_at_position(&lua, index) {
            return Ok(Err(b));
        }
        let e = WrongType::default()
            .expected_type::<Self>()
            .actual_multiple_lua_at(&lua, index, Self::n_values_expected() as _);
        Err((lua, e))
    }
}

////////////////////////////////////////////////////////////////////////////////
// Nil
////////////////////////////////////////////////////////////////////////////////

/// A helper struct for pushing and reading simple lua `nil` values.
///
/// Reading from lua with return `Ok(Nil)` if there is a value on the stack and
/// value is of type `nil`, or if there is no value on the stack (e.g. no arguments
/// passed to lua function).
///
/// See also [`Null`].
#[derive(
    Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct Nil;

impl_push_read! {Nil,
    push_to_lua(&self, lua) {
        Self::push_into_lua(*self, lua)
    }
    push_into_lua(self, lua) {
        unsafe {
            ffi::lua_pushnil(lua.as_lua());
            Ok(PushGuard::new(lua, 1))
        }
    }
    read_at_position(lua, index) {
        if unsafe { ffi::lua_isnil(lua.as_lua(), index.into()) } {
            Ok(Nil)
        } else {
            let e = WrongType::default()
                .expected_type::<Self>()
                .actual_single_lua(&lua, index);
            Err((lua, e))
        }
    }
    read_at_maybe_zero_position(lua, index) {
        if let Some(index) = NonZeroI32::new(index) {
            Self::lua_read_at_position(lua, index)
        } else {
            Ok(Nil)
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// Null
////////////////////////////////////////////////////////////////////////////////

/// A helper struct for pushing and reading special `box.NULL` values.
///
/// See <https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_null/>
/// for more details about `box.NULL`.
///
/// Note that reading this type from lua will work with both regular `nil` and
/// `box.NULL`, as well as if there's no value at all (e.g. no arguments to the
/// lua function). But when pushing this type onto the lua stack the `box.NULL`
/// value will always be pushed.
///
/// See also `Nil`.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Null;

impl Null {
    unsafe fn is_null(lua: crate::LuaState, index: i32) -> bool {
        if ffi::lua_type(lua, index) == ffi::LUA_TCDATA {
            let mut ctypeid = MaybeUninit::uninit();
            let cdata = ffi::luaL_checkcdata(lua, index, ctypeid.as_mut_ptr());
            if ctypeid.assume_init() == ffi::CTID_P_VOID {
                return (*cdata.cast::<*const c_void>()).is_null();
            }
        }
        false
    }
}

pub unsafe fn is_null_or_nil(lua: crate::LuaState, index: i32) -> bool {
    ffi::lua_isnil(lua, index) || Null::is_null(lua, index)
}

impl_push_read! {Null,
    push_to_lua(&self, lua) {
        Self::push_into_lua(*self, lua)
    }
    push_into_lua(self, lua) {
        unsafe {
            let cdata = ffi::luaL_pushcdata(lua.as_lua(), ffi::CTID_P_VOID);
            *cdata.cast::<*mut c_void>() = null_mut();
            Ok(PushGuard::new(lua, 1))
        }
    }
    read_at_position(lua, index) {
        if unsafe { Null::is_null(lua.as_lua(), index.into()) } {
            Ok(Null)
        } else {
            let e = WrongType::default()
                .expected_type::<Self>()
                .actual_single_lua(&lua, index);
            Err((lua, e))
        }
    }
    read_at_maybe_zero_position(lua, index) {
        if let Some(index) = NonZeroI32::new(index) {
            Null::lua_read_at_position(lua, index)
        } else {
            Ok(Null)
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// True
////////////////////////////////////////////////////////////////////////////////

#[derive(
    Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(try_from = "bool", into = "bool")]
pub struct True;

impl From<True> for bool {
    fn from(_: True) -> Self {
        true
    }
}

impl TryFrom<bool> for True {
    type Error = False;
    fn try_from(v: bool) -> Result<Self, False> {
        if v {
            Ok(True)
        } else {
            Err(False)
        }
    }
}

impl_push_read! {True,
    push_to_lua(&self, lua) {
        Self::push_into_lua(*self, lua)
    }
    push_into_lua(self, lua) {
        true.push_into_lua(lua)
    }
    read_at_position(lua, index) {
        match bool::lua_read_at_position(&lua, index) {
            Ok(v) if v => Ok(True),
            _ => {
                let e = WrongType::default()
                    .expected_type::<Self>()
                    .actual_single_lua(&lua, index);
                Err((lua, e))
            }
        }
    }
}

impl std::fmt::Display for True {
    #[inline(always)]
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        bool::from(*self).fmt(f)
    }
}

////////////////////////////////////////////////////////////////////////////////
// False
////////////////////////////////////////////////////////////////////////////////

#[derive(
    Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(try_from = "bool", into = "bool")]
pub struct False;

impl From<False> for bool {
    fn from(_: False) -> Self {
        false
    }
}

impl TryFrom<bool> for False {
    type Error = True;
    fn try_from(v: bool) -> Result<Self, True> {
        if !v {
            Ok(False)
        } else {
            Err(True)
        }
    }
}

impl_push_read! {False,
    push_to_lua(&self, lua) {
        Self::push_into_lua(*self, lua)
    }
    push_into_lua(self, lua) {
        false.push_into_lua(lua)
    }
    read_at_position(lua, index) {
        match bool::lua_read_at_position(&lua, index) {
            Ok(v) if !v => Ok(False),
            _ => {
                let e = WrongType::default()
                    .expected_type::<Self>()
                    .actual_single_lua(&lua, index);
                Err((lua, e))
            }
        }
    }
}

impl std::fmt::Display for False {
    #[inline(always)]
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        bool::from(*self).fmt(f)
    }
}

////////////////////////////////////////////////////////////////////////////////
// Typename
////////////////////////////////////////////////////////////////////////////////

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Typename(pub &'static str);

impl Typename {
    pub fn get(&self) -> &'static str {
        self.0
    }
}

impl_push_read! {Typename,
    read_at_position(lua, index) {
        Ok(Self(
            match crate::typename(lua.as_lua(), index.into()).to_string_lossy() {
                Cow::Borrowed(s) => s,
                _ => unreachable!("lua typename is a valid unicode string"),
            }
        ))
    }
}

////////////////////////////////////////////////////////////////////////////////
// ToString
////////////////////////////////////////////////////////////////////////////////

/// String wrapper struct that can be used to read a lua value by converting it
/// to string possibly using `__tostring` metamethod.
#[derive(Debug, Clone)]
pub struct ToString(pub String);

impl From<ToString> for String {
    fn from(other: ToString) -> Self {
        other.0
    }
}

impl From<ToString> for Cow<'_, str> {
    fn from(other: ToString) -> Self {
        Cow::Owned(other.0)
    }
}

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

impl_push_read! {ToString,
    read_at_position(lua, index) {
        unsafe {
            let mut size = MaybeUninit::uninit();
            let c_ptr = ffi::luaT_tolstring(
                lua.as_lua(), index.into(), size.as_mut_ptr()
            );
            // the newly created string needs to be popped
            let _new_string = PushGuard::new(lua.as_lua(), 1);
            if c_ptr.is_null() {
                let e = WrongType::default()
                    .expected_type::<Self>()
                    .actual_single_lua(&lua, index);
                return Err((lua, e));
            }
            let slice = slice::from_raw_parts(c_ptr as _, size.assume_init());
            Ok(Self(String::from_utf8_lossy(slice).into()))
        }
    }
}
