Documentation
// License: see LICENSE file at root directory of `master` branch

//! # Request

use {
    alloc::string::String,
    crate::{HeaderMap, Method, Version},
};

#[cfg(feature="std")]
use {
    alloc::vec::Vec,
    core::convert::TryFrom,
    std::{
        io::{Error, ErrorKind, Read, Write},
    },
    crate::{
        IoResult, LINE_BREAK,
        io,
    },
};

/// # Request
///
/// ## References
///
/// - <https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages>
#[derive(Debug, Clone)]
pub struct Request {

    /// # Version
    pub version: Version,

    /// # Method
    pub method: Method,

    /// # URL
    pub url: String,

    /// # Headers
    pub headers: HeaderMap,

}

impl Request {

    /// # Sends this request with optional data
    ///
    /// If you don't have data to send, you can use [`empty()`][std::io/empty()].
    ///
    /// Result: total bytes sent.
    ///
    /// [std::io/empty()]: https://doc.rust-lang.org/std/io/fn.empty.html
    #[cfg(feature="std")]
    pub fn send<R, W>(&self, data: &mut R, target: &mut W) -> IoResult<u64> where R: Read, W: Write {
        let mut buf = format!("{method} {url} {version}{lb}", method=self.method, url=self.url, version=self.version, lb=LINE_BREAK);
        self.headers.iter().for_each(|(k, v)| buf += &format!("{k}: {v}{lb}", k=k, v=v, lb=LINE_BREAK));
        buf += LINE_BREAK;
        target.write_all(buf.as_bytes())?;

        let mut result = u64::try_from(buf.len()).map_err(|_| Error::new(ErrorKind::Other, __!("Failed converting `{}` to u64", buf.len())))?;

        let copied = std::io::copy(data, target)?;
        result = result.checked_add(copied).ok_or_else(|| Error::new(ErrorKind::InvalidData, __!("Overflow: {a} + {b}", a=result, b=copied)))?;

        target.flush().map(|()| result)
    }

    /// # Receives a request
    ///
    /// - `src` is used to read data.
    /// - `data` is for writing request's data to.
    #[cfg(feature="std")]
    pub fn recv<R, W>(src: &mut R, data: &mut W) -> IoResult<Self> where R: Read, W: Write {
        const BUF_SIZE: usize = 2048;

        let mut line_buffer = [0; BUF_SIZE];
        let (method, url, version, bytes) = read_start_line(src, &mut line_buffer)?;
        let (headers, bytes) = io::read_headers(src, &mut line_buffer, bytes)?;

        if let Some(bytes) = bytes {
            data.write_all(&bytes)?;
        }
        std::io::copy(src, data)?;
        data.flush()?;

        Ok(Self {
            version,
            method,
            url,
            headers,
        })
    }

}

/// # Reads start line
#[cfg(feature="std")]
fn read_start_line<R>(r: &mut R, line_buffer: &mut [u8]) -> IoResult<(Method, String, Version, Option<Vec<u8>>)> where R: Read {
    let lf = LINE_BREAK.as_bytes().last();

    let mut total_read = 0;
    let start_line = loop {
        let read = match r.read(&mut line_buffer[total_read..])? {
            0 => return Err(Error::new(ErrorKind::UnexpectedEof, __!("Invalid start line"))),
            other => other,
        };
        let last_idx = total_read;
        total_read += read;
        match line_buffer[last_idx..total_read].iter().position(|b| lf.map(|lf| lf == b).unwrap_or(false)).map(|p| p + last_idx) {
            Some(p) => match p > LINE_BREAK.len() && &line_buffer[p - LINE_BREAK.len() + 1 ..= p] == LINE_BREAK.as_bytes() {
                true => break &line_buffer[..=p],
                false => return Err(Error::new(ErrorKind::InvalidData, __!("Start line: invalid line break"))),
            },
            None => if total_read >= line_buffer.len() {
                return Err(Error::new(ErrorKind::InvalidData, __!("Start line too long")));
            },
        };
    };
    let start_line_len = start_line.len();

    let mut parts = start_line[..start_line_len - LINE_BREAK.len()].split(|b| b == &b' ');
    match (parts.next(), parts.next(), parts.next(), parts.next()) {
        (Some(method), Some(url), Some(version), None) => Ok((
            Method::try_from(method)?,
            String::from_utf8(url.to_vec()).map_err(|_| Error::new(ErrorKind::InvalidData, __!("Start line: invalid URL")))?,
            Version::try_from(version)?,
            match total_read > start_line_len {
                true => Some(line_buffer[start_line_len..total_read].to_vec()),
                false => None,
            },
        )),
        _ => Err(Error::new(ErrorKind::InvalidData, __!("Invalid start line"))),
    }
}