///
//
// icey
// Copyright (c) 2005, icey <https://0state.com>
//
// SPDX-License-Identifier: LGPL-2.1+
//
/// @addtogroup http
/// @{

#pragma once

#include "icy/collection.h"
#include "icy/http/http.h"
#include "icy/packetstream.h"
#include "icy/thread.h"

#include <deque>
#include <memory>


namespace icy {
namespace http {


class HTTP_API Request;
class HTTP_API ConnectionStream;
class HTTP_API FormPart;


//
// HTML Form Writer
//

/// @ingroup http
/// FormWriter is an HTTP client connection adapter for writing HTML forms.
///
/// This class runs in its own thread so as not to block the event loop
/// while uploading big files. Class members are not synchronized hence
/// they should not be accessed while the form is sending, not that there
/// would be any reason to do so.
class HTTP_API FormWriter : public NVCollection
    , public PacketSource
    , public basic::Startable
{
public:
    /// Creates a FormWriter for the given connection and encoding.
    ///
    /// Encoding must be either "application/x-www-form-urlencoded"
    /// (which is the default) or "multipart/form-data".
    /// @param conn The HTTP connection stream to write form data to.
    /// @param encoding MIME encoding type.
    /// @return Heap-allocated FormWriter. The caller owns the returned pointer.
    static FormWriter* create(ConnectionStream& conn, const std::string& encoding = FormWriter::ENCODING_URL);

    /// Destroys the FormWriter.
    virtual ~FormWriter();

    /// Adds a part or file attachment to the multipart form.
    ///
    /// The FormWriter takes ownership of `part` and deletes it when done.
    /// Parts are only sent when the encoding is "multipart/form-data".
    /// @param name Form field name for this part.
    /// @param part Part to add. Ownership is transferred.
    void addPart(const std::string& name, FormPart* part);

    /// Starts the sending thread.
    void start();

    /// Stops the sending thread.
    void stop();

    /// Returns true if the request is complete.
    [[nodiscard]] bool complete() const;

    /// Returns true if the request is cancelled.
    [[nodiscard]] bool cancelled() const;

    /// Prepares the outgoing HTTP request object for submitting the form.
    void prepareSubmit();

    /// Processes the entire form body and computes its total byte length.
    /// Only meaningful for multipart/form-data when not using chunked encoding.
    /// @return Total content length in bytes.
    uint64_t calculateMultipartContentLength();

    /// Writes the complete "application/x-www-form-urlencoded" encoded body to `ostr`.
    /// All key-value pairs from the NVCollection base are percent-encoded and joined with '&'.
    /// @param ostr Output stream to write to.
    void writeUrl(std::ostream& ostr);

    /// Writes the next pending multipart chunk to the connection.
    /// Non-blocking; intended to be called repeatedly from the event loop
    /// until all parts have been sent.
    void writeMultipartChunk();

    /// Writes the next message chunk from the background runner thread.
    /// Called by the Runner; do not call directly.
    void writeAsync();

    /// Sets the MIME encoding used for submitting the form.
    /// Must be set before prepareSubmit() is called.
    /// @param encoding MIME type: ENCODING_URL or ENCODING_MULTIPART_FORM.
    void setEncoding(const std::string& encoding);

    /// Returns the encoding used for posting the form.
    [[nodiscard]] const std::string& encoding() const;

    /// Sets the MIME boundary string used to delimit multipart form parts.
    /// If not set, a random boundary is generated by prepareSubmit().
    /// Must be set before prepareSubmit() is called.
    /// @param boundary Boundary string (without leading "--").
    void setBoundary(const std::string& boundary);

    /// Returns the MIME boundary used for writing multipart form data.
    [[nodiscard]] const std::string& boundary() const;

    /// The associated HTTP client connection.
    [[nodiscard]] ConnectionStream& connection();

    /// The outgoing packet emitter.
    PacketSignal emitter;

    static const char* ENCODING_URL;               ///< "application/x-www-form-urlencoded"
    static const char* ENCODING_MULTIPART_FORM;    ///< "multipart/form-data"
    static const char* ENCODING_MULTIPART_RELATED; ///< "multipart/related" http://tools.ietf.org/html/rfc2387

protected:
    /// Creates the FormWriter that uses the given encoding.
    FormWriter(ConnectionStream& conn, std::shared_ptr<Runner> runner,
               const std::string& encoding = FormWriter::ENCODING_URL);

    FormWriter(const FormWriter&) = delete;
    FormWriter& operator=(const FormWriter&) = delete;
    FormWriter(FormWriter&&) = delete;
    FormWriter& operator=(FormWriter&&) = delete;

    /// Writes the message boundary std::string, followed
    /// by the message header to the output stream.
    void writePartHeader(const NVCollection& header, std::ostream& ostr);

    /// Writes the final boundary std::string to the output stream.
    void writeEnd(std::ostream& ostr);

    /// Creates a random boundary std::string.
    static std::string createBoundary();

    /// Updates the upload progress via the associated
    /// ConnectionStream object.
    virtual void updateProgress(int nread);

    friend class FormPart;
    friend class FilePart;
    friend class StringPart;

    /// Individual part within a multipart form submission
    struct Part
    {
        std::string name;
        std::unique_ptr<FormPart> part;
    };

    using PartQueue = std::deque<Part>;

    ConnectionStream& _stream;
    std::shared_ptr<Runner> _runner;
    std::string _encoding;
    std::string _boundary;
    PartQueue _parts;
    uint64_t _filesLength;
    int _writeState;
    bool _initial;
    bool _complete;
};


//
// Form Part
//

/// @ingroup http
/// An implementation of FormPart.
class HTTP_API FormPart
{
public:
    /// Creates the FormPart with the given MIME content type.
    /// @param contentType MIME type for this part (default: "application/octet-stream").
    FormPart(const std::string& contentType = "application/octet-stream");

    /// Destroys the FormPart.
    virtual ~FormPart();

    /// Resets the internal state and write position to the beginning.
    /// Called by FormWriter when retrying or recalculating content length.
    virtual void reset();

    /// Writes the next chunk of data to the FormWriter.
    /// @param writer The FormWriter to send the chunk through.
    /// @return true if more data remains to be written; false when complete.
    virtual bool writeChunk(FormWriter& writer) = 0;

    /// Writes the entire part data to the FormWriter in one call.
    /// @param writer The FormWriter to send data through.
    virtual void write(FormWriter& writer) = 0;

    /// Writes the entire part data to an output stream (used for content-length calculation).
    /// @param ostr Output stream to write to.
    virtual void write(std::ostream& ostr) = 0;

    /// Returns the extra MIME headers for this part (e.g. Content-Disposition).
    [[nodiscard]] NVCollection& headers();

    /// Returns true if this is the first write call since construction or reset().
    [[nodiscard]] virtual bool initialWrite() const;

    /// Returns the MIME content type for this part.
    [[nodiscard]] const std::string& contentType() const;

    /// Returns the total byte length of the part data.
    [[nodiscard]] virtual uint64_t length() const = 0;

protected:
    std::string _contentType;
    uint64_t _length;
    NVCollection _headers;
    bool _initialWrite;
};


//
// File Part
//

/// @ingroup http
/// Form part backed by a file on disk.
class HTTP_API FilePart : public FormPart
{
public:
    /// Creates the FilePart for the given path.
    ///
    /// The MIME type is set to application/octet-stream.
    ///
    /// Throws an FileException if the file cannot be opened.
    FilePart(const std::string& path);

    /// Creates the FilePart for the given
    /// path and MIME type.
    ///
    /// Throws an FileException if the file cannot be opened.
    FilePart(const std::string& path, const std::string& contentType);

    /// Creates the FilePart for the given
    /// path and MIME type. The given filename is
    /// used as part filename (see filename()) only.
    ///
    /// Throws an FileException if the file cannot be opened.
    FilePart(const std::string& path, const std::string& filename,
             const std::string& contentType);

    /// Destroys the FilePart.
    virtual ~FilePart();

    /// Opens the file for reading.
    /// @throws std::runtime_error if the file cannot be opened.
    virtual void open();

    /// Resets the file stream to the beginning and clears initial-write state.
    void reset() override;

    /// Writes the next chunk of the file to the FormWriter.
    /// @param writer The FormWriter to send the chunk through.
    /// @return true if more data remains; false when the file is fully sent.
    bool writeChunk(FormWriter& writer) override;

    /// Writes the entire file content to the FormWriter.
    /// @param writer The FormWriter to send data through.
    void write(FormWriter& writer) override;

    /// Writes the entire file content to an output stream (used for content-length calculation).
    /// @param ostr Output stream to write to.
    void write(std::ostream& ostr) override;

    /// Returns the filename component of the file path (not the full path).
    [[nodiscard]] const std::string& filename() const;

    /// Returns a reference to the underlying file input stream.
    [[nodiscard]] std::ifstream& stream();

    /// Returns the total file size in bytes.
    [[nodiscard]] uint64_t length() const override;

protected:
    std::string _path;
    std::string _filename;
    std::ifstream _istr;
    uint64_t _fileSize;
};


//
// String Part
//

/// @ingroup http
/// Form part backed by an in-memory string payload.
class HTTP_API StringPart : public FormPart
{
public:
    /// Creates a StringPart with the given data and default content type "application/octet-stream".
    /// @param data String data to send as this part.
    StringPart(const std::string& data);

    /// Creates a StringPart with the given data and MIME content type.
    /// @param data String data to send as this part.
    /// @param contentType MIME type for this part.
    StringPart(const std::string& data, const std::string& contentType);

    /// Destroys the StringPart.
    virtual ~StringPart();

    /// Writes the string data as a single chunk to the FormWriter.
    /// @param writer The FormWriter to send data through.
    /// @return false always (string data is sent in a single chunk).
    bool writeChunk(FormWriter& writer) override;

    /// Writes the string data to the FormWriter.
    /// @param writer The FormWriter to send data through.
    void write(FormWriter& writer) override;

    /// Writes the string data to an output stream.
    /// @param ostr Output stream to write to.
    void write(std::ostream& ostr) override;

    /// Returns the byte length of the string data.
    [[nodiscard]] uint64_t length() const override;

protected:
    std::string _data;
};


} // namespace http
} // namespace icy


/// @}
