//! HTTP body representations (buffered + streaming)

use crate::errors::Result;
use bytes::Bytes;
use futures::stream::Stream;
use std::fmt;
use std::io::Read;
use std::path::PathBuf;

pub type AsyncBodyStream = Box<dyn Stream<Item = Result<Bytes>> + Send + Unpin + 'static>;

pub enum Body {
    Empty,
    Bytes(Bytes),
    SyncReader(Box<dyn Read + Send + 'static>),
    AsyncStream(AsyncBodyStream),
    File {
        path: PathBuf,
        offset: u64,
        len: Option<u64>,
    },
}

impl fmt::Debug for Body {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Body::Empty => f.write_str("Body::Empty"),
            Body::Bytes(bytes) => f.debug_tuple("Body::Bytes").field(&bytes.len()).finish(),
            Body::SyncReader(_) => f.write_str("Body::SyncReader(<reader>)"),
            Body::AsyncStream(_) => f.write_str("Body::AsyncStream(<stream>)"),
            Body::File { path, offset, len } => f
                .debug_struct("Body::File")
                .field("path", path)
                .field("offset", offset)
                .field("len", len)
                .finish(),
        }
    }
}

impl Body {
    pub fn empty() -> Self {
        Body::Empty
    }

    pub fn from_bytes(bytes: Bytes) -> Self {
        if bytes.is_empty() {
            Body::Empty
        } else {
            Body::Bytes(bytes)
        }
    }

    pub fn from_vec(bytes: Vec<u8>) -> Self {
        Self::from_bytes(Bytes::from(bytes))
    }

    pub fn from_file(path: impl Into<PathBuf>) -> Self {
        Body::File {
            path: path.into(),
            offset: 0,
            len: None,
        }
    }

    pub fn from_file_range(path: impl Into<PathBuf>, offset: u64, len: Option<u64>) -> Self {
        Body::File {
            path: path.into(),
            offset,
            len,
        }
    }

    pub fn from_async_chunks(chunks: Vec<Bytes>) -> Self {
        use futures::stream;
        let stream = stream::iter(chunks.into_iter().map(Ok));
        Body::AsyncStream(Box::new(stream))
    }

    pub fn from_sync_chunks(chunks: Vec<Bytes>) -> Self {
        Body::SyncReader(Box::new(ChunkReader::new(chunks)))
    }

    /// Known length for buffered bodies. Streaming bodies return None.
    pub fn len(&self) -> Option<usize> {
        match self {
            Body::Empty => Some(0),
            Body::Bytes(bytes) => Some(bytes.len()),
            Body::SyncReader(_) | Body::AsyncStream(_) => None,
            Body::File { len, .. } => len.and_then(|v| usize::try_from(v).ok()),
        }
    }

    pub fn is_empty(&self) -> bool {
        matches!(self, Body::Empty) || matches!(self, Body::Bytes(b) if b.is_empty())
    }

    pub fn as_bytes(&self) -> Option<&Bytes> {
        match self {
            Body::Bytes(bytes) => Some(bytes),
            _ => None,
        }
    }
}

struct ChunkReader {
    chunks: std::collections::VecDeque<Bytes>,
    offset: usize,
}

impl ChunkReader {
    fn new(chunks: Vec<Bytes>) -> Self {
        Self {
            chunks: std::collections::VecDeque::from(chunks),
            offset: 0,
        }
    }
}

impl Read for ChunkReader {
    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
        if out.is_empty() {
            return Ok(0);
        }

        loop {
            let front = match self.chunks.front() {
                Some(chunk) => chunk,
                None => return Ok(0),
            };

            if self.offset >= front.len() {
                self.chunks.pop_front();
                self.offset = 0;
                continue;
            }

            let remaining = &front[self.offset..];
            let n = remaining.len().min(out.len());
            out[..n].copy_from_slice(&remaining[..n]);
            self.offset += n;
            return Ok(n);
        }
    }
}
