//    Copyright (C) 2018-2020 Jakub Melka
//
//    This file is part of PdfForQt.
//
//    PdfForQt is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    PdfForQt is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with PDFForQt.  If not, see <https://www.gnu.org/licenses/>.


#ifndef PDFOBJECT_H
#define PDFOBJECT_H

#include "pdfglobal.h"

#include <QByteArray>

#include <memory>
#include <vector>
#include <variant>
#include <array>
#include <initializer_list>

namespace pdf
{
class PDFArray;
class PDFString;
class PDFStream;
class PDFDictionary;
class PDFAbstractVisitor;

/// This class represents a content of the PDF object. It can be
/// array of objects, dictionary, content stream data, or string data.
class PDFObjectContent
{
public:
    constexpr PDFObjectContent() = default;
    virtual ~PDFObjectContent() = default;

    /// Equals operator. Returns true, if content of this object is
    /// equal to the content of the other object.
    virtual bool equals(const PDFObjectContent* other) const = 0;

    /// Optimizes memory consumption of this object
    virtual void optimize() = 0;
};

/// This class represents inplace string in the PDF object. To avoid too much
/// memory allocation, we store small strings inplace as small objects, so
/// we do not use memory allocator, so this doesn't cause performance downgrade.
/// Very often, PDF document consists of large number of names and strings
/// objects, which will fit into this category.
struct PDFInplaceString
{
    static constexpr const int MAX_STRING_SIZE = sizeof(PDFObjectReference) - 1;

    constexpr PDFInplaceString() = default;

    inline PDFInplaceString(const char* data, int size)
    {
        Q_ASSERT(size <= MAX_STRING_SIZE);
        this->size = static_cast<uint8_t>(size);
        std::copy(data, data + size, string.data());
    }

    inline PDFInplaceString(const QByteArray& data)
    {
        Q_ASSERT(data.size() <= MAX_STRING_SIZE);
        size = static_cast<uint8_t>(data.size());
        std::copy(data.cbegin(), data.cend(), string.data());
    }

    inline bool operator==(const PDFInplaceString& other) const
    {
        if (size != other.size)
        {
            return false;
        }

        for (uint8_t i = 0; i < size; ++i)
        {
            if (string[i] != other.string[i])
            {
                return false;
            }
        }

        return true;
    }

    inline bool operator !=(const PDFInplaceString& other) const
    {
        return !(*this == other);
    }

    QByteArray getString() const
    {
        return (size > 0) ? QByteArray(string.data(), size) : QByteArray();
    }

    uint8_t size = 0;
    std::array<char, MAX_STRING_SIZE> string = { };
};

/// Reference to the string implementations
struct PDFStringRef
{
    const PDFInplaceString* inplaceString = nullptr;
    const PDFString* memoryString = nullptr;

    QByteArray getString() const;
};

/// This class represents string, which can be inplace string (no memory allocation),
/// or classic byte array string, if not enough space for embedded string.
class PDFFORQTLIBSHARED_EXPORT PDFInplaceOrMemoryString
{
public:
    constexpr PDFInplaceOrMemoryString() = default;
    explicit PDFInplaceOrMemoryString(const char* string);
    explicit PDFInplaceOrMemoryString(QByteArray string);

    // Default destructor should be OK
    inline ~PDFInplaceOrMemoryString() = default;

    // Enforce default copy constructor and default move constructor
    constexpr inline PDFInplaceOrMemoryString(const PDFInplaceOrMemoryString&) = default;
    constexpr inline PDFInplaceOrMemoryString(PDFInplaceOrMemoryString&&) = default;

    // Enforce default copy assignment operator and move assignment operator
    constexpr inline PDFInplaceOrMemoryString& operator=(const PDFInplaceOrMemoryString&) = default;
    constexpr inline PDFInplaceOrMemoryString& operator=(PDFInplaceOrMemoryString&&) = default;

    bool equals(const char* value, size_t length) const;

    inline bool operator==(const PDFInplaceOrMemoryString&) const = default;
    inline bool operator!=(const PDFInplaceOrMemoryString&) const = default;

    inline bool operator==(const QByteArray& value) const { return equals(value.constData(), value.size()); }
    inline bool operator==(const char* value) const { return equals(value, std::strlen(value)); }

    /// Returns true, if string is inplace (i.e. doesn't allocate memory)
    bool isInplace() const;

    /// Returns string. If string is inplace, byte array is constructed.
    QByteArray getString() const;

private:
    std::variant<typename std::monostate, PDFInplaceString, QByteArray> m_value;
};

class PDFFORQTLIBSHARED_EXPORT PDFObject
{
public:
    enum class Type : uint8_t
    {
        // Simple PDF objects
        Null,
        Bool,
        Int,
        Real,
        String,
        Name,

        // Complex PDF objects
        Array,
        Dictionary,
        Stream,
        Reference
    };

    static std::vector<Type> getTypes() { return { Type::Null, Type::Bool, Type::Int, Type::Real, Type::String, Type::Name, Type::Array, Type::Dictionary, Type::Stream, Type::Reference }; }

    typedef std::shared_ptr<PDFObjectContent> PDFObjectContentPointer;

    // Default constructor should be constexpr
    constexpr inline PDFObject() :
        m_type(Type::Null),
        m_data()
    {

    }

    // Default destructor should be OK
    inline ~PDFObject() = default;

    // Enforce default copy constructor and default move constructor
    constexpr inline PDFObject(const PDFObject&) = default;
    constexpr inline PDFObject(PDFObject&&) = default;

    // Enforce default copy assignment operator and move assignment operator
    constexpr inline PDFObject& operator=(const PDFObject&) = default;
    constexpr inline PDFObject& operator=(PDFObject&&) = default;

    inline Type getType() const { return m_type; }

    // Test operators
    inline bool isNull() const { return m_type == Type::Null; }
    inline bool isBool() const { return m_type == Type::Bool; }
    inline bool isInt() const { return m_type == Type::Int; }
    inline bool isReal() const { return m_type == Type::Real; }
    inline bool isString() const { return m_type == Type::String; }
    inline bool isName() const { return m_type == Type::Name; }
    inline bool isArray() const { return m_type == Type::Array; }
    inline bool isDictionary() const { return m_type == Type::Dictionary; }
    inline bool isStream() const { return m_type == Type::Stream; }
    inline bool isReference() const { return m_type == Type::Reference; }

    inline bool getBool() const { return std::get<bool>(m_data); }
    inline PDFInteger getInteger() const { return std::get<PDFInteger>(m_data); }
    inline PDFReal getReal() const { return std::get<PDFReal>(m_data); }
    QByteArray getString() const;
    const PDFDictionary* getDictionary() const;
    PDFObjectReference getReference() const { return std::get<PDFObjectReference>(m_data); }
    PDFStringRef getStringObject() const;
    const PDFStream* getStream() const;
    const PDFArray* getArray() const;

    bool operator==(const PDFObject& other) const;
    bool operator!=(const PDFObject& other) const { return !(*this == other); }

    /// Accepts the visitor
    void accept(PDFAbstractVisitor* visitor) const;

    /// Creates a null object
    static inline PDFObject createNull() { return PDFObject(); }

    /// Creates a boolean object
    static inline PDFObject createBool(bool value) { return PDFObject(Type::Bool, value); }

    /// Creates an integer object
    static inline PDFObject createInteger(PDFInteger value) { return PDFObject(Type::Int, value); }

    /// Creates an object with real number
    static inline PDFObject createReal(PDFReal value) { return PDFObject(Type::Real, value); }

    /// Creates a reference object
    static inline PDFObject createReference(const PDFObjectReference& reference) { return PDFObject(Type::Reference, reference); }

    /// Creates an array object
    static inline PDFObject createArray(PDFObjectContentPointer&& value) { value->optimize(); return PDFObject(Type::Array, std::move(value)); }

    /// Creates a dictionary object
    static inline PDFObject createDictionary(PDFObjectContentPointer&& value) { value->optimize(); return PDFObject(Type::Dictionary, std::move(value)); }

    /// Creates a stream object
    static inline PDFObject createStream(PDFObjectContentPointer&& value) { value->optimize(); return PDFObject(Type::Stream, std::move(value)); }

    /// Creates a name object
    static PDFObject createName(QByteArray name);

    /// Creates a string object
    static PDFObject createString(QByteArray name);

    /// Creates a name object
    static PDFObject createName(PDFStringRef name);

    /// Creates a string object
    static PDFObject createString(PDFStringRef name);

private:
    template<typename T>
    constexpr inline PDFObject(Type type, T&& value) :
        m_data(std::forward<T>(value)),
        m_type(type)
    {

    }


    std::variant<typename std::monostate, bool, PDFInteger, PDFReal, PDFObjectReference, PDFObjectContentPointer, PDFInplaceString> m_data;
    Type m_type;
};

/// Represents raw string in the PDF file. No conversions are performed, this is
/// reason, that we do not use QString, but QByteArray instead.
class PDFString : public PDFObjectContent
{
public:
    inline explicit PDFString() = default;
    inline explicit PDFString(QByteArray&& value) :
        m_string(std::move(value))
    {

    }

    virtual ~PDFString() override = default;

    virtual bool equals(const PDFObjectContent* other) const override;

    const QByteArray& getString() const { return m_string; }
    void setString(const QByteArray &getString);

    /// Optimizes the string for memory consumption
    virtual void optimize() override;

private:
    QByteArray m_string;
};

/// Represents an array of objects in the PDF file.
class PDFArray : public PDFObjectContent
{
public:
    inline constexpr PDFArray() = default;
    inline PDFArray(std::vector<PDFObject>&& objects) : m_objects(qMove(objects)) { }
    virtual ~PDFArray() override = default;

    virtual bool equals(const PDFObjectContent* other) const override;

    /// Returns item at the specified index. If index is invalid,
    /// then it throws an exception.
    const PDFObject& getItem(size_t index) const { return m_objects.at(index); }

    /// Returns size of the array (number of elements)
    size_t getCount() const { return m_objects.size(); }

    /// Returns capacity of the array (theoretical number of elements before reallocation)
    size_t getCapacity() const { return m_objects.capacity(); }

    /// Appends object to the end of object list
    void appendItem(PDFObject object);

    /// Optimizes the array for memory consumption
    virtual void optimize() override;

    auto begin() { return m_objects.begin(); }
    auto end() { return m_objects.end(); }

    auto begin() const { return m_objects.begin(); }
    auto end() const { return m_objects.end(); }

private:
    std::vector<PDFObject> m_objects;
};

/// Represents a dictionary of objects in the PDF file. Dictionary is
/// an array of pairs key-value, where key is name object and value is any
/// PDF object. For this reason, we use QByteArray for key. We do not use
/// map, because dictionaries are usually small.
class PDFFORQTLIBSHARED_EXPORT PDFDictionary : public PDFObjectContent
{
public:
    using DictionaryEntry = std::pair<PDFInplaceOrMemoryString, PDFObject>;

    inline constexpr PDFDictionary() = default;
    inline PDFDictionary(std::vector<DictionaryEntry>&& dictionary) : m_dictionary(qMove(dictionary)) { }
    virtual ~PDFDictionary() override = default;

    virtual bool equals(const PDFObjectContent* other) const override;

    /// Returns object for the key. If key is not found in the dictionary,
    /// then valid reference to the null object is returned.
    /// \param key Key
    const PDFObject& get(const QByteArray& key) const;

    /// Returns object for the key. If key is not found in the dictionary,
    /// then valid reference to the null object is returned.
    /// \param key Key
    const PDFObject& get(const char* key) const;

    /// Returns object for the key. If key is not found in the dictionary,
    /// then valid reference to the null object is returned.
    /// \param key Key
    const PDFObject& get(const PDFInplaceOrMemoryString& key) const;

    /// Returns true, if dictionary contains a particular key
    /// \param key Key to be found in the dictionary
    bool hasKey(const QByteArray& key) const { return find(key) != m_dictionary.cend(); }

    /// Returns true, if dictionary contains a particular key
    /// \param key Key to be found in the dictionary
    bool hasKey(const char* key) const { return find(key) != m_dictionary.cend(); }

    /// Adds a new entry to the dictionary.
    /// \param key Key
    /// \param value Value
    void addEntry(PDFInplaceOrMemoryString&& key, PDFObject&& value) { m_dictionary.emplace_back(std::move(key), std::move(value)); }

    /// Adds a new entry to the dictionary.
    /// \param key Key
    /// \param value Value
    void addEntry(const PDFInplaceOrMemoryString& key, PDFObject&& value) { m_dictionary.emplace_back(key, std::move(value)); }

    /// Sets entry value. If entry with given key doesn't exist,
    /// then it is created.
    /// \param key Key
    /// \param value Value
    void setEntry(const PDFInplaceOrMemoryString& key, PDFObject&& value);

    /// Returns count of items in the dictionary
    size_t getCount() const { return m_dictionary.size(); }

    /// Returns capacity of items in the dictionary
    size_t getCapacity() const { return m_dictionary.capacity(); }

    /// Returns n-th key of the dictionary
    /// \param index Zero-based index of key in the dictionary
    const PDFInplaceOrMemoryString& getKey(size_t index) const { return m_dictionary[index].first; }

    /// Returns n-th value of the dictionary
    /// \param index Zero-based index of value in the dictionary
    const PDFObject& getValue(size_t index) const { return m_dictionary[index].second; }

    /// Removes null objects from dictionary
    void removeNullObjects();

    /// Optimizes the dictionary for memory consumption
    virtual void optimize() override;

private:
    /// Finds an item in the dictionary array, if the item is not in the dictionary,
    /// then end iterator is returned.
    /// \param key Key to be found
    std::vector<DictionaryEntry>::const_iterator find(const QByteArray& key) const;

    /// Finds an item in the dictionary array, if the item is not in the dictionary,
    /// then end iterator is returned.
    /// \param key Key to be found
    std::vector<DictionaryEntry>::iterator find(const QByteArray& key);

    /// Finds an item in the dictionary array, if the item is not in the dictionary,
    /// then end iterator is returned.
    /// \param key Key to be found
    std::vector<DictionaryEntry>::const_iterator find(const char* key) const;

    /// Finds an item in the dictionary array, if the item is not in the dictionary,
    /// then end iterator is returned.
    /// \param key Key to be found
    std::vector<DictionaryEntry>::const_iterator find(const PDFInplaceOrMemoryString& key) const;

    /// Finds an item in the dictionary array, if the item is not in the dictionary,
    /// then end iterator is returned.
    /// \param key Key to be found
    std::vector<DictionaryEntry>::iterator find(const PDFInplaceOrMemoryString& key);

    std::vector<DictionaryEntry> m_dictionary;
};

/// Represents a stream object in the PDF file. Stream consists of dictionary
/// and stream content - byte array.
class PDFStream : public PDFObjectContent
{
public:
    inline explicit constexpr PDFStream() = default;
    inline explicit PDFStream(PDFDictionary&& dictionary, QByteArray&& content) :
        m_dictionary(std::move(dictionary)),
        m_content(std::move(content))
    {

    }

    virtual ~PDFStream() override = default;

    virtual bool equals(const PDFObjectContent* other) const override;

    /// Returns dictionary for this content stream
    const PDFDictionary* getDictionary() const { return &m_dictionary; }

    /// Optimizes the stream for memory consumption
    virtual void optimize() override { m_dictionary.optimize(); m_content.shrink_to_fit(); }

    /// Returns content of the stream
    const QByteArray* getContent() const { return &m_content; }

private:
    PDFDictionary m_dictionary;
    QByteArray m_content;
};

class PDFObjectManipulator
{
public:
    explicit PDFObjectManipulator() = delete;

    enum MergeFlag
    {
        NoFlag            = 0x0000,
        RemoveNullObjects = 0x0001, ///< Remove null object from dictionaries
        ConcatenateArrays = 0x0002, ///< Concatenate arrays instead of replace
    };
    Q_DECLARE_FLAGS(MergeFlags, MergeFlag)

    /// Merges two objects. If object type is different, then object from right is used.
    /// If both objects are dictionaries, then their content is merged, object \p right
    /// has precedence over object \p left. If both objects are arrays, and concatenating
    /// flag is turned on, then they are concatenated instead of replacing left array
    /// by right array. If remove null objects flag is turend on, then null objects
    /// are removed from dictionaries.
    /// \param left Left, 'slave' object
    /// \param right Right 'master' object, has priority over left
    /// \param flags Merge flags
    static PDFObject merge(PDFObject left, PDFObject right, MergeFlags flags);

    /// Remove null objects from all dictionaries
    /// \param object Object
    static PDFObject removeNullObjects(PDFObject object);
};

}   // namespace pdf

#endif // PDFOBJECT_H