use std::collections::BTreeMap;
use std::thread;
use anyhow::anyhow;
use anyhow::Context;
use base::error;
#[cfg(feature = "video-encoder")]
use base::info;
use base::AsRawDescriptor;
use base::Error as SysError;
use base::Event;
use base::RawDescriptor;
use base::Tube;
use data_model::Le32;
use remain::sorted;
use thiserror::Error;
use vm_memory::GuestMemory;
use zerocopy::IntoBytes;
use crate::virtio::copy_config;
use crate::virtio::virtio_device::VirtioDevice;
use crate::virtio::DeviceType;
use crate::virtio::Interrupt;
use crate::virtio::Queue;
#[macro_use]
mod macros;
mod async_cmd_desc_map;
mod command;
mod control;
#[cfg(feature = "video-decoder")]
pub mod decoder;
pub mod device;
#[cfg(feature = "video-encoder")]
mod encoder;
mod error;
mod event;
pub mod format;
mod params;
mod protocol;
pub mod resource;
mod response;
mod utils;
pub mod worker;
#[cfg(all(
    feature = "video-decoder",
    not(any(feature = "libvda", feature = "ffmpeg", feature = "vaapi"))
))]
compile_error!("The \"video-decoder\" feature requires at least one of \"ffmpeg\", \"libvda\" or \"vaapi\" to also be enabled.");
#[cfg(all(
    feature = "video-encoder",
    not(any(feature = "libvda", feature = "ffmpeg"))
))]
compile_error!("The \"video-encoder\" feature requires at least one of \"ffmpeg\" or \"libvda\" to also be enabled.");
#[cfg(feature = "ffmpeg")]
mod ffmpeg;
#[cfg(feature = "libvda")]
mod vda;
use command::ReadCmdError;
use device::Device;
use worker::Worker;
use super::device_constants::video::backend_supported_virtio_features;
use super::device_constants::video::virtio_video_config;
use super::device_constants::video::VideoBackendType;
use super::device_constants::video::VideoDeviceType;
const CMD_QUEUE_SIZE: u16 = 1024;
const EVENT_QUEUE_SIZE: u16 = 256;
const QUEUE_SIZES: &[u16] = &[CMD_QUEUE_SIZE, EVENT_QUEUE_SIZE];
#[sorted]
#[derive(Error, Debug)]
pub enum Error {
    #[error("failed to clone a descriptor: {0}")]
    CloneDescriptorFailed(SysError),
    #[error("no available descriptor in which an event is written to")]
    DescriptorNotAvailable,
    #[cfg(feature = "video-decoder")]
    #[error("failed to create a video device: {0}")]
    DeviceCreationFailed(String),
    #[error("failed to create an EventAsync: {0}")]
    EventAsyncCreationFailed(cros_async::AsyncError),
    #[error("failed to read a command from the guest: {0}")]
    ReadFailure(ReadCmdError),
    #[error("failed to create WaitContext: {0}")]
    WaitContextCreationFailed(SysError),
    #[error("failed to wait for events: {0}")]
    WaitError(SysError),
    #[error("failed to write an event {event:?} into event queue: {error}")]
    WriteEventFailure {
        event: event::VideoEvt,
        error: std::io::Error,
    },
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct VideoDevice {
    device_type: VideoDeviceType,
    backend: VideoBackendType,
    kill_evt: Option<Event>,
    resource_bridge: Option<Tube>,
    base_features: u64,
}
impl VideoDevice {
    pub fn new(
        base_features: u64,
        device_type: VideoDeviceType,
        backend: VideoBackendType,
        resource_bridge: Option<Tube>,
    ) -> VideoDevice {
        VideoDevice {
            device_type,
            backend,
            kill_evt: None,
            resource_bridge,
            base_features,
        }
    }
}
impl Drop for VideoDevice {
    fn drop(&mut self) {
        if let Some(kill_evt) = self.kill_evt.take() {
            let _ = kill_evt.signal();
        }
    }
}
pub fn build_config(backend: VideoBackendType) -> virtio_video_config {
    let mut device_name = [0u8; 32];
    match backend {
        #[cfg(feature = "libvda")]
        VideoBackendType::Libvda => device_name[0..6].copy_from_slice("libvda".as_bytes()),
        #[cfg(feature = "libvda")]
        VideoBackendType::LibvdaVd => device_name[0..8].copy_from_slice("libvdavd".as_bytes()),
        #[cfg(feature = "ffmpeg")]
        VideoBackendType::Ffmpeg => device_name[0..6].copy_from_slice("ffmpeg".as_bytes()),
        #[cfg(feature = "vaapi")]
        VideoBackendType::Vaapi => device_name[0..5].copy_from_slice("vaapi".as_bytes()),
    };
    virtio_video_config {
        version: Le32::from(0),
        max_caps_length: Le32::from(1024), max_resp_length: Le32::from(1024), device_name,
    }
}
impl VirtioDevice for VideoDevice {
    fn keep_rds(&self) -> Vec<RawDescriptor> {
        let mut keep_rds = Vec::new();
        if let Some(resource_bridge) = &self.resource_bridge {
            keep_rds.push(resource_bridge.as_raw_descriptor());
        }
        keep_rds
    }
    fn device_type(&self) -> DeviceType {
        match &self.device_type {
            VideoDeviceType::Decoder => DeviceType::VideoDecoder,
            VideoDeviceType::Encoder => DeviceType::VideoEncoder,
        }
    }
    fn queue_max_sizes(&self) -> &[u16] {
        QUEUE_SIZES
    }
    fn features(&self) -> u64 {
        self.base_features | backend_supported_virtio_features(self.backend)
    }
    fn read_config(&self, offset: u64, data: &mut [u8]) {
        let mut cfg = build_config(self.backend);
        copy_config(data, 0, cfg.as_mut_bytes(), offset);
    }
    fn activate(
        &mut self,
        mem: GuestMemory,
        _interrupt: Interrupt,
        mut queues: BTreeMap<usize, Queue>,
    ) -> anyhow::Result<()> {
        if queues.len() != QUEUE_SIZES.len() {
            return Err(anyhow!(
                "wrong number of queues are passed: expected {}, actual {}",
                queues.len(),
                QUEUE_SIZES.len()
            ));
        }
        let (self_kill_evt, kill_evt) = Event::new()
            .and_then(|e| Ok((e.try_clone()?, e)))
            .context("failed to create kill Event pair")?;
        self.kill_evt = Some(self_kill_evt);
        let cmd_queue = queues.pop_first().unwrap().1;
        let event_queue = queues.pop_first().unwrap().1;
        let backend = self.backend;
        let resource_bridge = self
            .resource_bridge
            .take()
            .context("no resource bridge is passed")?;
        let mut worker = Worker::new(cmd_queue, event_queue);
        let worker_result = match &self.device_type {
            #[cfg(feature = "video-decoder")]
            VideoDeviceType::Decoder => thread::Builder::new()
                .name("v_video_decoder".to_owned())
                .spawn(move || {
                    let device: Box<dyn Device> =
                        match create_decoder_device(backend, resource_bridge, mem) {
                            Ok(value) => value,
                            Err(e) => {
                                error!("{}", e);
                                return;
                            }
                        };
                    if let Err(e) = worker.run(device, &kill_evt) {
                        error!("Failed to start decoder worker: {}", e);
                    };
                    }),
            #[cfg(feature = "video-encoder")]
            VideoDeviceType::Encoder => thread::Builder::new()
                .name("v_video_encoder".to_owned())
                .spawn(move || {
                    let device: Box<dyn Device> = match backend {
                        #[cfg(feature = "libvda")]
                        VideoBackendType::Libvda => {
                            let vda = match encoder::backend::vda::LibvdaEncoder::new() {
                                Ok(vda) => vda,
                                Err(e) => {
                                    error!("Failed to initialize VDA for encoder: {}", e);
                                    return;
                                }
                            };
                            match encoder::EncoderDevice::new(vda, resource_bridge, mem) {
                                Ok(encoder) => Box::new(encoder),
                                Err(e) => {
                                    error!("Failed to create encoder device: {}", e);
                                    return;
                                }
                            }
                        }
                        #[cfg(feature = "libvda")]
                        VideoBackendType::LibvdaVd => {
                            error!("Invalid backend for encoder");
                            return;
                        }
                        #[cfg(feature = "ffmpeg")]
                        VideoBackendType::Ffmpeg => {
                            let ffmpeg = encoder::backend::ffmpeg::FfmpegEncoder::new();
                            match encoder::EncoderDevice::new(ffmpeg, resource_bridge, mem) {
                                Ok(encoder) => Box::new(encoder),
                                Err(e) => {
                                    error!("Failed to create encoder device: {}", e);
                                    return;
                                }
                            }
                        }
                        #[cfg(feature = "vaapi")]
                        VideoBackendType::Vaapi => {
                            error!("The VA-API encoder is not supported yet");
                            return;
                        }
                    };
                    if let Err(e) = worker.run(device, &kill_evt) {
                        error!("Failed to start encoder worker: {}", e);
                    }
                }),
            #[allow(unreachable_patterns)]
            device_type => unreachable!("Not compiled with {:?} enabled", device_type),
        };
        worker_result.with_context(|| {
            format!(
                "failed to spawn virtio_video worker for {:?}",
                &self.device_type
            )
        })?;
        Ok(())
    }
}
#[cfg(feature = "video-decoder")]
pub fn create_decoder_device(
    backend: VideoBackendType,
    resource_bridge: Tube,
    mem: GuestMemory,
) -> Result<Box<dyn Device>> {
    use decoder::backend::DecoderBackend;
    let backend = match backend {
        #[cfg(feature = "libvda")]
        VideoBackendType::Libvda => {
            decoder::backend::vda::LibvdaDecoder::new(libvda::decode::VdaImplType::Gavda)
                .map_err(|e| {
                    Error::DeviceCreationFailed(format!(
                        "Failed to initialize VDA for decoder: {}",
                        e
                    ))
                })?
                .into_trait_object()
        }
        #[cfg(feature = "libvda")]
        VideoBackendType::LibvdaVd => {
            decoder::backend::vda::LibvdaDecoder::new(libvda::decode::VdaImplType::Gavd)
                .map_err(|e| {
                    Error::DeviceCreationFailed(format!(
                        "Failed to initialize VD for decoder: {}",
                        e
                    ))
                })?
                .into_trait_object()
        }
        #[cfg(feature = "ffmpeg")]
        VideoBackendType::Ffmpeg => {
            decoder::backend::ffmpeg::FfmpegDecoder::new().into_trait_object()
        }
        #[cfg(feature = "vaapi")]
        VideoBackendType::Vaapi => decoder::backend::vaapi::VaapiDecoder::new()
            .map_err(|e| {
                Error::DeviceCreationFailed(format!(
                    "Failed to initialize VA-API driver for decoder: {}",
                    e
                ))
            })?
            .into_trait_object(),
    };
    Ok(Box::new(decoder::Decoder::new(
        backend,
        resource_bridge,
        mem,
    )))
}
#[cfg(feature = "video-encoder")]
struct EosBufferManager {
    stream_id: u32,
    eos_buffer: Option<u32>,
    client_awaits_eos: bool,
    responses: Vec<device::VideoEvtResponseType>,
}
#[cfg(feature = "video-encoder")]
impl EosBufferManager {
    fn new(stream_id: u32) -> Self {
        Self {
            stream_id,
            eos_buffer: None,
            client_awaits_eos: false,
            responses: Default::default(),
        }
    }
    fn try_reserve_eos_buffer(&mut self, buffer_id: u32) -> bool {
        let is_none = self.eos_buffer.is_none();
        if is_none {
            info!(
                "stream {}: keeping buffer {} aside to signal EOS.",
                self.stream_id, buffer_id
            );
            self.eos_buffer = Some(buffer_id);
        }
        is_none
    }
    fn try_complete_eos(
        &mut self,
        responses: Vec<device::VideoEvtResponseType>,
    ) -> Option<Vec<device::VideoEvtResponseType>> {
        let eos_buffer_id = self.eos_buffer.take().or_else(|| {
            info!("stream {}: no EOS resource available on successful flush response, waiting for next buffer to be queued.", self.stream_id);
            self.client_awaits_eos = true;
            if !self.responses.is_empty() {
                error!("stream {}: EOS requested while one is already in progress. This is a bug!", self.stream_id);
            }
            self.responses = responses;
            None
        })?;
        let eos_tag = device::AsyncCmdTag::Queue {
            stream_id: self.stream_id,
            queue_type: command::QueueType::Output,
            resource_id: eos_buffer_id,
        };
        let eos_response = response::CmdResponse::ResourceQueue {
            timestamp: 0,
            flags: protocol::VIRTIO_VIDEO_BUFFER_FLAG_EOS,
            size: 0,
        };
        self.client_awaits_eos = false;
        info!(
            "stream {}: signaling EOS using buffer {}.",
            self.stream_id, eos_buffer_id
        );
        let mut responses = std::mem::take(&mut self.responses);
        responses.insert(
            0,
            device::VideoEvtResponseType::AsyncCmd(device::AsyncCmdResponse::from_response(
                eos_tag,
                eos_response,
            )),
        );
        Some(responses)
    }
    fn reset(&mut self) {
        self.eos_buffer = None;
        self.client_awaits_eos = false;
        self.responses.clear();
    }
}