//! multipart/form-data
use std::borrow::Cow;
use std::fmt;
use std::pin::Pin;

use bytes::Bytes;
use http::HeaderMap;
use mime_guess::Mime;
use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};

use futures_core::Stream;
use futures_util::{future, stream, StreamExt};

use super::Body;

/// An async multipart/form-data request.
pub struct Form {
    inner: FormParts<Part>,
}

/// A field in a multipart form.
pub struct Part {
    meta: PartMetadata,
    value: Body,
    body_length: Option<u64>,
}

pub(crate) struct FormParts<P> {
    pub(crate) boundary: String,
    pub(crate) computed_headers: Vec<Vec<u8>>,
    pub(crate) fields: Vec<(Cow<'static, str>, P)>,
    pub(crate) percent_encoding: PercentEncoding,
}

pub(crate) struct PartMetadata {
    mime: Option<Mime>,
    file_name: Option<Cow<'static, str>>,
    pub(crate) headers: HeaderMap,
}

pub(crate) trait PartProps {
    fn value_len(&self) -> Option<u64>;
    fn metadata(&self) -> &PartMetadata;
}

// ===== impl Form =====

impl Default for Form {
    fn default() -> Self {
        Self::new()
    }
}

impl Form {
    /// Creates a new async Form without any content.
    pub fn new() -> Form {
        Form {
            inner: FormParts::new(),
        }
    }

    /// Get the boundary that this form will use.
    #[inline]
    pub fn boundary(&self) -> &str {
        self.inner.boundary()
    }

    /// Add a data field with supplied name and value.
    ///
    /// # Examples
    ///
    /// ```
    /// let form = reqwest::multipart::Form::new()
    ///     .text("username", "seanmonstar")
    ///     .text("password", "secret");
    /// ```
    pub fn text<T, U>(self, name: T, value: U) -> Form
    where
        T: Into<Cow<'static, str>>,
        U: Into<Cow<'static, str>>,
    {
        self.part(name, Part::text(value))
    }

    /// Adds a customized Part.
    pub fn part<T>(self, name: T, part: Part) -> Form
    where
        T: Into<Cow<'static, str>>,
    {
        self.with_inner(move |inner| inner.part(name, part))
    }

    /// Configure this `Form` to percent-encode using the `path-segment` rules.
    pub fn percent_encode_path_segment(self) -> Form {
        self.with_inner(|inner| inner.percent_encode_path_segment())
    }

    /// Configure this `Form` to percent-encode using the `attr-char` rules.
    pub fn percent_encode_attr_chars(self) -> Form {
        self.with_inner(|inner| inner.percent_encode_attr_chars())
    }

    /// Configure this `Form` to skip percent-encoding
    pub fn percent_encode_noop(self) -> Form {
        self.with_inner(|inner| inner.percent_encode_noop())
    }

    /// Consume this instance and transform into an instance of Body for use in a request.
    pub(crate) fn stream(mut self) -> Body {
        if self.inner.fields.is_empty() {
            return Body::empty();
        }

        // create initial part to init reduce chain
        let (name, part) = self.inner.fields.remove(0);
        let start = Box::pin(self.part_stream(name, part))
            as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;

        let fields = self.inner.take_fields();
        // for each field, chain an additional stream
        let stream = fields.into_iter().fold(start, |memo, (name, part)| {
            let part_stream = self.part_stream(name, part);
            Box::pin(memo.chain(part_stream))
                as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
        });
        // append special ending boundary
        let last = stream::once(future::ready(Ok(
            format!("--{}--\r\n", self.boundary()).into()
        )));
        Body::stream(stream.chain(last))
    }

    /// Generate a hyper::Body stream for a single Part instance of a Form request.
    pub(crate) fn part_stream<T>(
        &mut self,
        name: T,
        part: Part,
    ) -> impl Stream<Item = Result<Bytes, crate::Error>>
    where
        T: Into<Cow<'static, str>>,
    {
        // start with boundary
        let boundary = stream::once(future::ready(Ok(
            format!("--{}\r\n", self.boundary()).into()
        )));
        // append headers
        let header = stream::once(future::ready(Ok({
            let mut h = self
                .inner
                .percent_encoding
                .encode_headers(&name.into(), &part.meta);
            h.extend_from_slice(b"\r\n\r\n");
            h.into()
        })));
        // then append form data followed by terminating CRLF
        boundary
            .chain(header)
            .chain(part.value.into_stream())
            .chain(stream::once(future::ready(Ok("\r\n".into()))))
    }

    pub(crate) fn compute_length(&mut self) -> Option<u64> {
        self.inner.compute_length()
    }

    fn with_inner<F>(self, func: F) -> Self
    where
        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
    {
        Form {
            inner: func(self.inner),
        }
    }
}

impl fmt::Debug for Form {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.inner.fmt_fields("Form", f)
    }
}

// ===== impl Part =====

impl Part {
    /// Makes a text parameter.
    pub fn text<T>(value: T) -> Part
    where
        T: Into<Cow<'static, str>>,
    {
        let body = match value.into() {
            Cow::Borrowed(slice) => Body::from(slice),
            Cow::Owned(string) => Body::from(string),
        };
        Part::new(body, None)
    }

    /// Makes a new parameter from arbitrary bytes.
    pub fn bytes<T>(value: T) -> Part
    where
        T: Into<Cow<'static, [u8]>>,
    {
        let body = match value.into() {
            Cow::Borrowed(slice) => Body::from(slice),
            Cow::Owned(vec) => Body::from(vec),
        };
        Part::new(body, None)
    }

    /// Makes a new parameter from an arbitrary stream.
    pub fn stream<T: Into<Body>>(value: T) -> Part {
        Part::new(value.into(), None)
    }

    /// Makes a new parameter from an arbitrary stream with a known length. This is particularly
    /// useful when adding something like file contents as a stream, where you can know the content
    /// length beforehand.
    pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
        Part::new(value.into(), Some(length))
    }

    fn new(value: Body, body_length: Option<u64>) -> Part {
        Part {
            meta: PartMetadata::new(),
            value,
            body_length,
        }
    }

    /// Tries to set the mime of this part.
    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
    }

    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
    fn mime(self, mime: Mime) -> Part {
        self.with_inner(move |inner| inner.mime(mime))
    }

    /// Sets the filename, builder style.
    pub fn file_name<T>(self, filename: T) -> Part
    where
        T: Into<Cow<'static, str>>,
    {
        self.with_inner(move |inner| inner.file_name(filename))
    }

    fn with_inner<F>(self, func: F) -> Self
    where
        F: FnOnce(PartMetadata) -> PartMetadata,
    {
        Part {
            meta: func(self.meta),
            ..self
        }
    }
}

impl fmt::Debug for Part {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut dbg = f.debug_struct("Part");
        dbg.field("value", &self.value);
        self.meta.fmt_fields(&mut dbg);
        dbg.finish()
    }
}

impl PartProps for Part {
    fn value_len(&self) -> Option<u64> {
        if self.body_length.is_some() {
            self.body_length
        } else {
            self.value.content_length()
        }
    }

    fn metadata(&self) -> &PartMetadata {
        &self.meta
    }
}

// ===== impl FormParts =====

impl<P: PartProps> FormParts<P> {
    pub(crate) fn new() -> Self {
        FormParts {
            boundary: gen_boundary(),
            computed_headers: Vec::new(),
            fields: Vec::new(),
            percent_encoding: PercentEncoding::PathSegment,
        }
    }

    pub(crate) fn boundary(&self) -> &str {
        &self.boundary
    }

    /// Adds a customized Part.
    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
    where
        T: Into<Cow<'static, str>>,
    {
        self.fields.push((name.into(), part));
        self
    }

    /// Configure this `Form` to percent-encode using the `path-segment` rules.
    pub(crate) fn percent_encode_path_segment(mut self) -> Self {
        self.percent_encoding = PercentEncoding::PathSegment;
        self
    }

    /// Configure this `Form` to percent-encode using the `attr-char` rules.
    pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
        self.percent_encoding = PercentEncoding::AttrChar;
        self
    }

    /// Configure this `Form` to skip percent-encoding
    pub(crate) fn percent_encode_noop(mut self) -> Self {
        self.percent_encoding = PercentEncoding::NoOp;
        self
    }

    // If predictable, computes the length the request will have
    // The length should be preditable if only String and file fields have been added,
    // but not if a generic reader has been added;
    pub(crate) fn compute_length(&mut self) -> Option<u64> {
        let mut length = 0u64;
        for &(ref name, ref field) in self.fields.iter() {
            match field.value_len() {
                Some(value_length) => {
                    // We are constructing the header just to get its length. To not have to
                    // construct it again when the request is sent we cache these headers.
                    let header = self.percent_encoding.encode_headers(name, field.metadata());
                    let header_length = header.len();
                    self.computed_headers.push(header);
                    // The additions mimic the format string out of which the field is constructed
                    // in Reader. Not the cleanest solution because if that format string is
                    // ever changed then this formula needs to be changed too which is not an
                    // obvious dependency in the code.
                    length += 2
                        + self.boundary().len() as u64
                        + 2
                        + header_length as u64
                        + 4
                        + value_length
                        + 2
                }
                _ => return None,
            }
        }
        // If there is a at least one field there is a special boundary for the very last field.
        if !self.fields.is_empty() {
            length += 2 + self.boundary().len() as u64 + 4
        }
        Some(length)
    }

    /// Take the fields vector of this instance, replacing with an empty vector.
    fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
        std::mem::replace(&mut self.fields, Vec::new())
    }
}

impl<P: fmt::Debug> FormParts<P> {
    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct(ty_name)
            .field("boundary", &self.boundary)
            .field("parts", &self.fields)
            .finish()
    }
}

// ===== impl PartMetadata =====

impl PartMetadata {
    pub(crate) fn new() -> Self {
        PartMetadata {
            mime: None,
            file_name: None,
            headers: HeaderMap::default(),
        }
    }

    pub(crate) fn mime(mut self, mime: Mime) -> Self {
        self.mime = Some(mime);
        self
    }

    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
    where
        T: Into<Cow<'static, str>>,
    {
        self.file_name = Some(filename.into());
        self
    }
}

impl PartMetadata {
    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
        &self,
        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
        debug_struct
            .field("mime", &self.mime)
            .field("file_name", &self.file_name)
            .field("headers", &self.headers)
    }
}

// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'<')
    .add(b'>')
    .add(b'`');

// https://url.spec.whatwg.org/#path-percent-encode-set
const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');

const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');

// https://tools.ietf.org/html/rfc8187#section-3.2.1
const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
    .remove(b'!')
    .remove(b'#')
    .remove(b'$')
    .remove(b'&')
    .remove(b'+')
    .remove(b'-')
    .remove(b'.')
    .remove(b'^')
    .remove(b'_')
    .remove(b'`')
    .remove(b'|')
    .remove(b'~');

pub(crate) enum PercentEncoding {
    PathSegment,
    AttrChar,
    NoOp,
}

impl PercentEncoding {
    pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
        let s = format!(
            "Content-Disposition: form-data; {}{}{}",
            self.format_parameter("name", name),
            match field.file_name {
                Some(ref file_name) => format!("; {}", self.format_filename(file_name)),
                None => String::new(),
            },
            match field.mime {
                Some(ref mime) => format!("\r\nContent-Type: {}", mime),
                None => "".to_string(),
            },
        );
        field
            .headers
            .iter()
            .fold(s.into_bytes(), |mut header, (k, v)| {
                header.extend_from_slice(b"\r\n");
                header.extend_from_slice(k.as_str().as_bytes());
                header.extend_from_slice(b": ");
                header.extend_from_slice(v.as_bytes());
                header
            })
    }

    // According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
    // See https://github.com/seanmonstar/reqwest/issues/419.
    fn format_filename(&self, filename: &str) -> String {
        let legal_filename = filename
            .replace("\\", "\\\\")
            .replace("\"", "\\\"")
            .replace("\r", "\\\r")
            .replace("\n", "\\\n");
        format!("filename=\"{}\"", legal_filename)
    }

    fn format_parameter(&self, name: &str, value: &str) -> String {
        let legal_value = match *self {
            PercentEncoding::PathSegment => {
                percent_encoding::utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET).to_string()
            }
            PercentEncoding::AttrChar => {
                percent_encoding::utf8_percent_encode(value, ATTR_CHAR_ENCODE_SET).to_string()
            }
            PercentEncoding::NoOp => value.to_string(),
        };
        if value.len() == legal_value.len() {
            // nothing has been percent encoded
            format!("{}=\"{}\"", name, value)
        } else {
            // something has been percent encoded
            format!("{}*=utf-8''{}", name, legal_value)
        }
    }
}

fn gen_boundary() -> String {
    use crate::util::fast_random as random;

    let a = random();
    let b = random();
    let c = random();
    let d = random();

    format!("{:016x}-{:016x}-{:016x}-{:016x}", a, b, c, d)
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures_util::TryStreamExt;
    use futures_util::{future, stream};
    use tokio::{self, runtime};

    #[test]
    fn form_empty() {
        let form = Form::new();

        let rt = runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("new rt");
        let body = form.stream().into_stream();
        let s = body.map_ok(|try_c| try_c.to_vec()).try_concat();

        let out = rt.block_on(s);
        assert!(out.unwrap().is_empty());
    }

    #[test]
    fn stream_to_end() {
        let mut form = Form::new()
            .part(
                "reader1",
                Part::stream(Body::stream(stream::once(future::ready::<
                    Result<String, crate::Error>,
                >(Ok(
                    "part1".to_owned()
                ))))),
            )
            .part("key1", Part::text("value1"))
            .part("key2", Part::text("value2").mime(mime::IMAGE_BMP))
            .part(
                "reader2",
                Part::stream(Body::stream(stream::once(future::ready::<
                    Result<String, crate::Error>,
                >(Ok(
                    "part2".to_owned()
                ))))),
            )
            .part("key3", Part::text("value3").file_name("filename"));
        form.inner.boundary = "boundary".to_string();
        let expected = "--boundary\r\n\
             Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
             part1\r\n\
             --boundary\r\n\
             Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
             value1\r\n\
             --boundary\r\n\
             Content-Disposition: form-data; name=\"key2\"\r\n\
             Content-Type: image/bmp\r\n\r\n\
             value2\r\n\
             --boundary\r\n\
             Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
             part2\r\n\
             --boundary\r\n\
             Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
             value3\r\n--boundary--\r\n";
        let rt = runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("new rt");
        let body = form.stream().into_stream();
        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();

        let out = rt.block_on(s).unwrap();
        // These prints are for debug purposes in case the test fails
        println!(
            "START REAL\n{}\nEND REAL",
            std::str::from_utf8(&out).unwrap()
        );
        println!("START EXPECTED\n{}\nEND EXPECTED", expected);
        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
    }

    #[test]
    fn stream_to_end_with_header() {
        let mut part = Part::text("value2").mime(mime::IMAGE_BMP);
        part.meta.headers.insert("Hdr3", "/a/b/c".parse().unwrap());
        let mut form = Form::new().part("key2", part);
        form.inner.boundary = "boundary".to_string();
        let expected = "--boundary\r\n\
                        Content-Disposition: form-data; name=\"key2\"\r\n\
                        Content-Type: image/bmp\r\n\
                        hdr3: /a/b/c\r\n\
                        \r\n\
                        value2\r\n\
                        --boundary--\r\n";
        let rt = runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("new rt");
        let body = form.stream().into_stream();
        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();

        let out = rt.block_on(s).unwrap();
        // These prints are for debug purposes in case the test fails
        println!(
            "START REAL\n{}\nEND REAL",
            std::str::from_utf8(&out).unwrap()
        );
        println!("START EXPECTED\n{}\nEND EXPECTED", expected);
        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
    }

    #[test]
    fn correct_content_length() {
        // Setup an arbitrary data stream
        let stream_data = b"just some stream data";
        let stream_len = stream_data.len();
        let stream_data = stream_data
            .chunks(3)
            .map(|c| Ok::<_, std::io::Error>(Bytes::from(c)));
        let the_stream = futures_util::stream::iter(stream_data);

        let bytes_data = b"some bytes data".to_vec();
        let bytes_len = bytes_data.len();

        let stream_part = Part::stream_with_length(Body::stream(the_stream), stream_len as u64);
        let body_part = Part::bytes(bytes_data);

        // A simple check to make sure we get the configured body length
        assert_eq!(stream_part.value_len().unwrap(), stream_len as u64);

        // Make sure it delegates to the underlying body if length is not specified
        assert_eq!(body_part.value_len().unwrap(), bytes_len as u64);
    }

    #[test]
    fn header_percent_encoding() {
        let name = "start%'\"\r\nßend";
        let field = Part::text("");

        assert_eq!(
            PercentEncoding::PathSegment.encode_headers(name, &field.meta),
            &b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
        );

        assert_eq!(
            PercentEncoding::AttrChar.encode_headers(name, &field.meta),
            &b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
        );
    }
}
