projects involving the gemini protocol
//! Gemini Responses
//!
//! Gemini reponses consist of a `Header` and an optional response body
//! consisting of a stream of bytes.

use std::str;

use crate::{
    gemtext::Builder,
    header::Header,
    status::{Category, Status},
};

/// Gemini responses
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Response {
    /// Response header
    pub header: Header,
    body: Option<Vec<u8>>,
}

impl Response {
    /// Return the status of this `Response`.
    pub fn status(&self) -> Status {
        self.header.status
    }

    /// Return whether or not this `Response` has a body.
    pub fn has_body(&self) -> bool {
        self.body.is_some()
    }

    /// Construct a new response.
    ///
    /// This function maintains the invariant that only successful responses
    /// may include response bodies.
    pub fn new(header: Header, body: Option<Vec<u8>>) -> Option<Self> {
        match (header.status.category(), &body) {
            (Category::Success, Some(_)) | (_, None) => Some(Response { header, body }),
            _ => None,
        }
    }

    // TODO: add some built-in mime-types for more infallible constructors?

    /// Construct a new response with a body.
    pub fn with_body(mime_type: String, body: Vec<u8>) -> Option<Self> {
        let header = Header::new(Status::SUCCESS, mime_type)?;
        let body = Some(body);
        Some(Response { header, body })
    }

    /// Construct a new response with a gemtext body.
    pub fn with_gemtext(builder: Builder) -> Self {
        let header = Header::gemtext();
        let body = Some(builder.build().bytes().collect());
        Self::new_unchecked(header, body)
    }

    /// Construct a new response with no body.
    pub fn without_body(header: Header) -> Option<Self> {
        Self::new(header, None)
    }

    /// Construct a new response without maintaining invariants.
    ///
    /// This constructor may produce a `Response' which is invalid with respect
    /// to the Gemini spec. Users of this function should be careful to check
    /// that `header` has `Category::Success`.
    pub fn new_unchecked(header: Header, body: Option<Vec<u8>>) -> Self {
        Response { header, body }
    }

    /// Return the response body as a string, if present and valid utf-8.
    ///
    /// If you wish to allow invalid utf-8, call `body_bytes` and use the
    /// appropriate `String` function.
    pub fn body_text(&self) -> Option<&str> {
        let bytes = self.body.as_ref()?;
        str::from_utf8(bytes).ok()
    }

    /// Return the response body as raw bytes, if present.
    pub fn body_bytes(&self) -> Option<&[u8]> {
        self.body.as_deref()
    }
}

/// Parser
#[cfg(feature = "parsers")]
pub mod parse {
    use nom::{
        combinator::{map_opt, rest},
        error::context,
        sequence::pair,
        IResult,
    };

    use super::*;

    use crate::header::parse::header;

    /// A `nom` parser for a response. Any bytes after the CRLF that separates
    /// the header from the response body will be considered part of the body.
    pub fn response(input: &[u8]) -> IResult<&[u8], Response> {
        context(
            "response",
            map_opt(pair(header, rest), |t| {
                if t.1.is_empty() {
                    Response::new(t.0, None)
                } else {
                    let v = Vec::from(t.1);
                    Response::new(t.0, Some(v))
                }
            }),
        )(input)
    }

    #[cfg(test)]
    mod test {
        use super::*;

        #[test]
        fn test_response() {
            let bytes = b"20 text/gemini\r\n=> gemini://foo.bar.baz/ wow";
            let res = response(bytes).unwrap().1;
            assert_eq!(res.body_text().unwrap(), "=> gemini://foo.bar.baz/ wow")
        }

        #[test]
        fn test_response_no_body() {
            let bytes = b"60 owwwwwwww!\r\ni shouldn't be here";
            assert!(response(bytes).is_err());

            let bytes = b"61 this is fine\r\n";
            assert!(response(bytes).is_ok());
        }
    }
}