// Public domain - https://gist.github.com/nolanw/dff7cc5d5570b030d6ba385698348b7c

import Foundation

extension URLRequest {

    /**
     Configures the URL request for `multipart/form-data`. The request's `httpBody` is set, and a value is set for the HTTP header field `Content-Type`.

     - Parameter parameters: The form data to set.
     - Parameter encoding: The encoding to use for the keys and values.

     - Throws: `MultipartFormDataEncodingError` if any keys or values in `parameters` are not entirely in `encoding`.

     - Note: The default `httpMethod` is `GET`, and `GET` requests do not typically have a response body. Remember to set the `httpMethod` to e.g. `POST` before sending the request.
     - Seealso: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data
     */
    public mutating func setMultipartFormData(_ parameters: [String: String], encoding: String.Encoding, files: [MultipartFormFile]) throws {

        let makeRandom = { UInt32.random(in: (.min)...(.max)) }
        let boundary = String(format: "------------------------%08X%08X", makeRandom(), makeRandom())

        let contentType: String = try {
            guard let charset = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(encoding.rawValue)) else {
                throw MultipartFormDataEncodingError.characterSetName
            }
            return "multipart/form-data; charset=\(charset); boundary=\(boundary)"
        }()
        addValue(contentType, forHTTPHeaderField: "Content-Type")

        httpBody = try {
            var body = Data()

            for (rawName, rawValue) in parameters {
                if !body.isEmpty {
                    body.append("\r\n".data(using: .utf8)!)
                }

                body.append("--\(boundary)\r\n".data(using: .utf8)!)

                guard rawName.canBeConverted(to: encoding) else {
                    throw MultipartFormDataEncodingError.name(rawName)
                }

                if let data = rawValue.data(using: encoding) {
                    if let disposition = "Content-Disposition: form-data; name=\"\(rawName)\"\r\n".data(using: encoding) {
                        body.append(contentsOf: disposition)
                    }
                    body.append("\r\n".data(using: .utf8)!)
                    body.append(data)
                } else {
                    throw MultipartFormDataEncodingError.value("\(rawValue)", name: rawName)
                }
            }

            for file in files {
                if !body.isEmpty {
                    body.append("\r\n".data(using: .utf8)!)
                }

                body.append("--\(boundary)\r\n".data(using: .utf8)!)

                body.append("Content-Disposition: form-data; name=\"\(file.formKey)\"; filename=\"\(file.filename)\"\r\n".data(using: .utf8)!)
                body.append("Content-Type: \(file.contentType)\r\n\r\n".data(using: .utf8)!)
                body.append(file.data)
                body.append("\r\n".data(using: .utf8)!)
            }

            body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)

            return body
        }()
    }
}

public struct MultipartFormFile {
    public var formKey: String
    public var filename: String
    public var contentType: String
    public var data: Data
}

public enum MultipartFormDataEncodingError: Error {
    case characterSetName
    case name(String)
    case value(String, name: String)
}