//! Safe wrapper for the `VIDIOC_(TRY_)DECODER_CMD` ioctls.

use bitflags::bitflags;
use nix::errno::Errno;
use std::convert::Infallible;
use std::convert::TryFrom;
use std::os::unix::io::AsRawFd;
use thiserror::Error;

use crate::bindings;
use crate::bindings::v4l2_decoder_cmd;
use crate::ioctl::ioctl_and_convert;
use crate::ioctl::IoctlConvertError;
use crate::ioctl::IoctlConvertResult;

bitflags! {
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub struct StartCmdFlags: u32 {
        const MUTE_AUDIO = bindings::V4L2_DEC_CMD_START_MUTE_AUDIO;
    }

    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub struct StopCmdFlags: u32 {
        const TO_BLACK = bindings::V4L2_DEC_CMD_STOP_TO_BLACK;
        const IMMEDIATELY = bindings::V4L2_DEC_CMD_STOP_IMMEDIATELY;
    }

    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub struct PauseCmdFlags: u32 {
        const TO_BLACK = bindings::V4L2_DEC_CMD_PAUSE_TO_BLACK;
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, enumn::N)]
#[repr(u32)]
pub enum DecoderStartCmdFormat {
    #[default]
    None = bindings::V4L2_DEC_START_FMT_NONE,
    Gop = bindings::V4L2_DEC_START_FMT_GOP,
}

/// Safe variant of `struct v4l2_decoder_cmd`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecoderCmd {
    Start {
        flags: StartCmdFlags,
        speed: i32,
        format: DecoderStartCmdFormat,
    },
    Stop {
        flags: StopCmdFlags,
        pts: u64,
    },
    Pause {
        flags: PauseCmdFlags,
    },
    Resume,
}

impl DecoderCmd {
    /// Returns a simple START command without any extra parameter.
    pub fn start() -> Self {
        DecoderCmd::Start {
            flags: StartCmdFlags::empty(),
            speed: Default::default(),
            format: Default::default(),
        }
    }

    /// Returns a simple STOP command without any extra parameter.
    pub fn stop() -> Self {
        DecoderCmd::Stop {
            flags: StopCmdFlags::empty(),
            pts: Default::default(),
        }
    }

    /// Returns a simple PAUSE command without any extra parameter.
    pub fn pause() -> Self {
        DecoderCmd::Pause {
            flags: PauseCmdFlags::empty(),
        }
    }

    /// Returns a RESUME command.
    pub fn resume() -> Self {
        DecoderCmd::Resume
    }
}

impl From<DecoderCmd> for v4l2_decoder_cmd {
    fn from(command: DecoderCmd) -> Self {
        match command {
            DecoderCmd::Start {
                flags,
                speed,
                format,
            } => v4l2_decoder_cmd {
                cmd: bindings::V4L2_DEC_CMD_START,
                flags: flags.bits(),
                __bindgen_anon_1: bindings::v4l2_decoder_cmd__bindgen_ty_1 {
                    start: bindings::v4l2_decoder_cmd__bindgen_ty_1__bindgen_ty_2 {
                        speed,
                        format: format as u32,
                    },
                },
            },
            DecoderCmd::Stop { flags, pts } => v4l2_decoder_cmd {
                cmd: bindings::V4L2_DEC_CMD_STOP,
                flags: flags.bits(),
                __bindgen_anon_1: bindings::v4l2_decoder_cmd__bindgen_ty_1 {
                    stop: bindings::v4l2_decoder_cmd__bindgen_ty_1__bindgen_ty_1 { pts },
                },
            },
            DecoderCmd::Pause { flags } => v4l2_decoder_cmd {
                cmd: bindings::V4L2_DEC_CMD_PAUSE,
                flags: flags.bits(),
                __bindgen_anon_1: Default::default(),
            },
            DecoderCmd::Resume => v4l2_decoder_cmd {
                cmd: bindings::V4L2_DEC_CMD_RESUME,
                flags: Default::default(),
                __bindgen_anon_1: Default::default(),
            },
        }
    }
}

#[derive(Debug, Error)]
pub enum BuildDecoderCmdError {
    #[error("invalid command code {0}")]
    InvalidCommandCode(u32),
    #[error("invalid start command flags 0x{0:x}")]
    InvalidStartFlags(u32),
    #[error("invalid start command format {0}")]
    InvalidStartFormat(u32),
    #[error("invalid stop command flags 0x{0:x}")]
    InvalidStopFlags(u32),
    #[error("invalid pause command flags 0x{0:x}")]
    InvalidPauseFlags(u32),
}

impl TryFrom<v4l2_decoder_cmd> for DecoderCmd {
    type Error = BuildDecoderCmdError;

    fn try_from(cmd: v4l2_decoder_cmd) -> Result<Self, Self::Error> {
        let cmd = match cmd.cmd {
            bindings::V4L2_DEC_CMD_START => {
                // SAFETY: safe because we confirmed we are dealing with a START command.
                let params = unsafe { cmd.__bindgen_anon_1.start };
                DecoderCmd::Start {
                    flags: StartCmdFlags::from_bits_truncate(cmd.flags),
                    speed: params.speed,
                    format: DecoderStartCmdFormat::n(params.format).unwrap_or_default(),
                }
            }
            bindings::V4L2_DEC_CMD_STOP => DecoderCmd::Stop {
                flags: StopCmdFlags::from_bits_truncate(cmd.flags),
                // SAFETY: safe because we confirmed we are dealing with a STOP command.
                pts: unsafe { cmd.__bindgen_anon_1.stop.pts },
            },
            bindings::V4L2_DEC_CMD_PAUSE => DecoderCmd::Pause {
                flags: PauseCmdFlags::from_bits_truncate(cmd.flags),
            },
            bindings::V4L2_DEC_CMD_RESUME => DecoderCmd::Resume,
            code => return Err(BuildDecoderCmdError::InvalidCommandCode(code)),
        };

        Ok(cmd)
    }
}

impl TryFrom<v4l2_decoder_cmd> for () {
    type Error = Infallible;

    fn try_from(_: v4l2_decoder_cmd) -> Result<Self, Self::Error> {
        Ok(())
    }
}

#[derive(Debug, Error)]
pub enum DecoderCmdIoctlError {
    #[error("drain already in progress")]
    DrainInProgress,
    #[error("command not supported by device")]
    UnsupportedCommand,
    #[error("unexpected ioctl error: {0}")]
    Other(Errno),
}

impl From<DecoderCmdIoctlError> for Errno {
    fn from(err: DecoderCmdIoctlError) -> Self {
        match err {
            DecoderCmdIoctlError::DrainInProgress => Errno::EBUSY,
            DecoderCmdIoctlError::UnsupportedCommand => Errno::EINVAL,
            DecoderCmdIoctlError::Other(e) => e,
        }
    }
}

impl From<Errno> for DecoderCmdIoctlError {
    fn from(error: Errno) -> Self {
        match error {
            Errno::EBUSY => DecoderCmdIoctlError::DrainInProgress,
            Errno::EINVAL => DecoderCmdIoctlError::UnsupportedCommand,
            e => DecoderCmdIoctlError::Other(e),
        }
    }
}

#[doc(hidden)]
mod ioctl {
    use crate::bindings::v4l2_decoder_cmd;
    nix::ioctl_readwrite!(vidioc_decoder_cmd, b'V', 96, v4l2_decoder_cmd);
    nix::ioctl_readwrite!(vidioc_try_decoder_cmd, b'V', 97, v4l2_decoder_cmd);
}

pub type DecoderCmdError<CE> = IoctlConvertError<DecoderCmdIoctlError, CE>;
pub type DecoderCmdResult<O, CE> = IoctlConvertResult<O, DecoderCmdIoctlError, CE>;

/// Safe wrapper around the `VIDIOC_DECODER_CMD` ioctl.
pub fn decoder_cmd<I, O>(fd: &impl AsRawFd, command: I) -> DecoderCmdResult<O, O::Error>
where
    I: Into<v4l2_decoder_cmd>,
    O: TryFrom<v4l2_decoder_cmd>,
    O::Error: std::fmt::Debug,
{
    let mut dec_cmd = command.into();

    ioctl_and_convert(
        unsafe { ioctl::vidioc_decoder_cmd(fd.as_raw_fd(), &mut dec_cmd) }
            .map(|_| dec_cmd)
            .map_err(Into::into),
    )
}

/// Safe wrapper around the `VIDIOC_TRY_DECODER_CMD` ioctl.
pub fn try_decoder_cmd<I, O>(fd: &impl AsRawFd, command: I) -> DecoderCmdResult<O, O::Error>
where
    I: Into<v4l2_decoder_cmd>,
    O: TryFrom<v4l2_decoder_cmd>,
    O::Error: std::fmt::Debug,
{
    let mut dec_cmd = command.into();

    ioctl_and_convert(
        unsafe { ioctl::vidioc_try_decoder_cmd(fd.as_raw_fd(), &mut dec_cmd) }
            .map(|_| dec_cmd)
            .map_err(Into::into),
    )
}

#[cfg(test)]
mod tests {
    use std::convert::TryFrom;

    use crate::{
        bindings,
        ioctl::{DecoderStartCmdFormat, PauseCmdFlags, StartCmdFlags, StopCmdFlags},
    };

    use super::DecoderCmd;

    #[test]
    fn build_decoder_cmd() {
        // Build START command and back.
        let cmd = bindings::v4l2_decoder_cmd {
            cmd: bindings::V4L2_DEC_CMD_START,
            flags: bindings::V4L2_DEC_CMD_START_MUTE_AUDIO,
            __bindgen_anon_1: bindings::v4l2_decoder_cmd__bindgen_ty_1 {
                start: bindings::v4l2_decoder_cmd__bindgen_ty_1__bindgen_ty_2 {
                    speed: 42,
                    format: bindings::V4L2_DEC_START_FMT_GOP,
                },
            },
        };
        let cmd_safe = DecoderCmd::try_from(cmd).unwrap();
        assert_eq!(
            cmd_safe,
            DecoderCmd::Start {
                flags: StartCmdFlags::MUTE_AUDIO,
                speed: 42,
                format: DecoderStartCmdFormat::Gop,
            }
        );
        let cmd_rebuilt: bindings::v4l2_decoder_cmd = cmd_safe.into();
        assert_eq!(cmd_safe, DecoderCmd::try_from(cmd_rebuilt).unwrap());

        // Build STOP command and back.
        let cmd = bindings::v4l2_decoder_cmd {
            cmd: bindings::V4L2_DEC_CMD_STOP,
            flags: bindings::V4L2_DEC_CMD_STOP_IMMEDIATELY | bindings::V4L2_DEC_CMD_STOP_TO_BLACK,
            __bindgen_anon_1: bindings::v4l2_decoder_cmd__bindgen_ty_1 {
                stop: bindings::v4l2_decoder_cmd__bindgen_ty_1__bindgen_ty_1 { pts: 496000 },
            },
        };
        let cmd_safe = DecoderCmd::try_from(cmd).unwrap();
        assert_eq!(
            cmd_safe,
            DecoderCmd::Stop {
                flags: StopCmdFlags::IMMEDIATELY | StopCmdFlags::TO_BLACK,
                pts: 496000,
            }
        );
        let cmd_rebuilt: bindings::v4l2_decoder_cmd = cmd_safe.into();
        assert_eq!(cmd_safe, DecoderCmd::try_from(cmd_rebuilt).unwrap());

        // Build PAUSE command and back.
        let cmd = bindings::v4l2_decoder_cmd {
            cmd: bindings::V4L2_DEC_CMD_PAUSE,
            flags: bindings::V4L2_DEC_CMD_PAUSE_TO_BLACK,
            __bindgen_anon_1: Default::default(),
        };
        let cmd_safe = DecoderCmd::try_from(cmd).unwrap();
        assert_eq!(
            cmd_safe,
            DecoderCmd::Pause {
                flags: PauseCmdFlags::TO_BLACK,
            }
        );
        let cmd_rebuilt: bindings::v4l2_decoder_cmd = cmd_safe.into();
        assert_eq!(cmd_safe, DecoderCmd::try_from(cmd_rebuilt).unwrap());

        // Build RESUME command and back.
        let cmd = bindings::v4l2_decoder_cmd {
            cmd: bindings::V4L2_DEC_CMD_RESUME,
            flags: Default::default(),
            __bindgen_anon_1: Default::default(),
        };
        let cmd_safe = DecoderCmd::try_from(cmd).unwrap();
        assert_eq!(cmd_safe, DecoderCmd::Resume);
        let cmd_rebuilt: bindings::v4l2_decoder_cmd = cmd_safe.into();
        assert_eq!(cmd_safe, DecoderCmd::try_from(cmd_rebuilt).unwrap());
    }
}