// SPDX-License-Identifier: LGPL-2.1-or-later

/****************************************************************************
 *   Copyright (c) 2018-2022 Zheng, Lei (realthunder)                       *
 *   <realthunder.dev@gmail.com>                                            *
 *   Copyright (c) 2023 FreeCAD Project Association                         *
 *                                                                          *
 *   This file is part of FreeCAD.                                          *
 *                                                                          *
 *   FreeCAD 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 2.1 of the   *
 *   License, or (at your option) any later version.                        *
 *                                                                          *
 *   FreeCAD 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 FreeCAD. If not, see                                *
 *   <https://www.gnu.org/licenses/>.                                       *
 *                                                                          *
 ***************************************************************************/

#ifndef DATA_ELEMENTMAP_H
#define DATA_ELEMENTMAP_H

#include "FCGlobal.h"

#include "Application.h"
#include "MappedElement.h"
#include "StringHasher.h"

#include <cstring>
#include <deque>
#include <functional>
#include <map>
#include <memory>


namespace Data
{

class ElementMap;
using ElementMapPtr = std::shared_ptr<ElementMap>;

/** Element trace callback
 *
 * The callback has the following call signature
 *  (const std::string &name, size_t offset, long encodedTag, long tag) -> bool
 *
 * @param name: the current element name.
 * @param offset: the offset skipping the encoded element name for the next iteration.
 * @param encodedTag: the tag encoded inside the current element, which is usually the tag
 *                    of the previous step in the shape history.
 * @param tag: the tag of the current shape element.
 *
 * @sa traceElement()
 */
typedef std::function<bool(const MappedName&, int, long, long)> TraceCallback;

/* This class provides for ComplexGeoData's ability to provide proper naming.
 * Specifically, ComplexGeoData uses this class for it's `_id` property.
 * Most of the operations work with the `indexedNames` and `mappedNames` maps.
 * `indexedNames` maps a string to both a name queue and children.
 *   each of those children store an IndexedName, offset details, postfix, ids, and
 *   possibly a recursive elementmap
 * `mappedNames` maps a MappedName to a specific IndexedName.
 */
class AppExport ElementMap
    : public std::enable_shared_from_this<ElementMap>  // TODO can remove shared_from_this?
{
public:
    /** Default constructor: hooks internal functions to \c signalSaveDocument and
     * \c signalStartRestoreDocument. This is related to the save and restore process
     * of the map.
     */
    ElementMap();

    /** Ensures that naming is properly assigned. It then marks as "used" all the StringID
     * that are used to make up this particular map and are stored in the hasherRef passed
     * as a parameter. Finally do this recursively for all childEelementMaps as well.
     *
     * @param hasherRef where all the StringID needed to build the map are stored.
     */
    // FIXME this should be made part of \c save, to achieve symmetry with the restore method
    void beforeSave(const ::App::StringHasherRef& hasherRef) const;

    /** Serialize this map. Calls \c collectChildMaps to get \c childMapSet and
     * \c postfixMap, then calls the other (private) save function with those parameters.
     * @param stream: serialized stream
     */
    void save(std::ostream& stream) const;

    /** Deserialize and restore this map. This function restores \c childMaps and
     * \c postfixes from the stream, then calls the other (private) restore function with those
     * parameters.
     * @param hasherRef: where all the StringIDs are stored
     * @param stream: stream to deserialize
     */
    ElementMapPtr restore(::App::StringHasherRef hasherRef, std::istream& stream);


    /** Add a sub-element name mapping.
     *
     * @param element: the original \c Type + \c Index element name
     * @param name: the mapped sub-element name. May or may not start with
     * elementMapPrefix().
     * @param sid: in case you use a hasher to hash the element name, pass in
     * the string id reference using this parameter. You can have more than one
     * string id associated with the same name.
     * @param overwrite: if true, it will overwrite existing names
     *
     * @return Returns the stored mapped element name.
     *
     * An element can have multiple mapped names. However, a name can only be
     * mapped to one element
     *
     * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access,
     *   now you must pass in `long masterTag` explicitly.
     */
    MappedName setElementName(const IndexedName& element,
                              const MappedName& name,
                              long masterTag,
                              const ElementIDRefs* sid = nullptr,
                              bool overwrite = false);

    /* Generates a new MappedName from the current details.
     *
     * The result is streamed to `ss` and stored in `name`.
     *
     * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access,
     *   now you must pass in `long masterTag` explicitly.
     */
    void encodeElementName(char element_type,
                           MappedName& name,
                           std::ostringstream& ss,
                           ElementIDRefs* sids,
                           long masterTag,
                           const char* postfix = nullptr,
                           long tag = 0,
                           bool forceTag = false) const;

    /// Remove \c name from the map
    void erase(const MappedName& name);

    /// Remove \c idx and all the MappedNames associated with it
    void erase(const IndexedName& idx);

    unsigned long size() const;

    bool empty() const;

    IndexedName find(const MappedName& name, ElementIDRefs* sids = nullptr) const;

    MappedName find(const IndexedName& idx, ElementIDRefs* sids = nullptr) const;

    std::vector<std::pair<MappedName, ElementIDRefs>> findAll(const IndexedName& idx) const;

    // prefix searching is disabled, as TopoShape::getRelatedElement() is
    // deprecated in favor of GeoFeature::getRelatedElement(). Besides, there
    // is efficient way to support child element map if we were to implement
    // prefix search.
#if 0
    std::vector<MappedElement> findAllStartsWith(const char *prefix) const;
#endif

    bool hasChildElementMap() const;

    /* Ensures that for each IndexedName mapped to IndexedElements, that
     *  each child is properly hashed (cached).
     *
     * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access,
     *   now you must pass in `long masterTag` explicitly.
     */
    void hashChildMaps(long masterTag);

    struct AppExport MappedChildElements
    {
        IndexedName indexedName;
        int count;
        int offset;
        long tag;
        ElementMapPtr elementMap;
        QByteArray postfix;
        ElementIDRefs sids;

        // prefix() has been moved to ElementNamingUtils.h
    };

    /* Note: the original addChildElements passed `ComplexGeoData& master` for getting the `Tag`,
     *   now it just passes `long masterTag`.*/
    void addChildElements(long masterTag, const std::vector<MappedChildElements>& children);

    std::vector<MappedChildElements> getChildElements() const;

    std::vector<MappedElement> getAll() const;

    long getElementHistory(const MappedName& name,
                           long masterTag,
                           MappedName* original = nullptr,
                           std::vector<MappedName>* history = nullptr) const;

    /** Iterate through the history of the give element name with a given callback
     *
     * @param name: the input element name
     * @param cb: trace callback with call signature.
     * @sa TraceCallback
     */
    void traceElement(const MappedName& name, long masterTag, TraceCallback cb) const;


private:
    /** Serialize this map
     * @param stream: serialized stream
     * @param childMapSet: where all child element maps are stored
     * @param postfixMap. where all postfixes are stored
     */
    void save(std::ostream& stream,
              int index,
              const std::map<const ElementMap*, int>& childMapSet,
              const std::map<QByteArray, int>& postfixMap) const;

    /** Deserialize and restore this map.
     * @param hasherRef: where all the StringIDs are stored
     * @param stream: stream to deserialize
     * @param childMaps: where all child element maps are stored
     * @param postfixes. where all postfixes are stored
     */
    ElementMapPtr restore(::App::StringHasherRef hasherRef,
                          std::istream& stream,
                          std::vector<ElementMapPtr>& childMaps,
                          const std::vector<std::string>& postfixes);

    /** Associate the MappedName \c name with the IndexedName \c idx.
     * @param name: the name to add
     * @param idx: the indexed name that \c name will be bound to
     * @param sids: where StringIDs that make up the name are stored
     * @param overwrite: if true, all the names associated with \c idx will be discarded
     * @param existing: out variable: if not overwriting, and \c name is already
     * associated with another indexedName, set \c existing to that indexedname
     * @return the name just added, or an empty name if it wasn't added.
     */
    MappedName addName(MappedName& name,
                       const IndexedName& idx,
                       const ElementIDRefs& sids,
                       bool overwrite,
                       IndexedName* existing);

    /** Utility function that adds \c postfix to \c postfixMap, and to \c postfixes
     * if it was not present in the map.
     */
    static void addPostfix(const QByteArray& postfix,
                           std::map<QByteArray, int>& postfixMap,
                           std::vector<QByteArray>& postfixes);

    /* Note: the original proc passed `ComplexGeoData& master` for getting the `Tag`,
     *   now it just passes `long masterTag`.*/
    MappedName renameDuplicateElement(int index,
                                      const IndexedName& element,
                                      const IndexedName& element2,
                                      const MappedName& name,
                                      ElementIDRefs& sids,
                                      long masterTag) const;

    /** Convenience method to hash the main element name
     *
     * @param name: main element name
     * @param sid: store any output string ID references
     * @return the hashed element name;
     */
    MappedName hashElementName(const MappedName& name, ElementIDRefs& sids) const;

    /// Reverse hashElementName()
    MappedName dehashElementName(const MappedName& name) const;

    // FIXME duplicate code? as in copy/paste
    const MappedNameRef* findMappedRef(const IndexedName& idx) const;
    MappedNameRef* findMappedRef(const IndexedName& idx);

    MappedNameRef& mappedRef(const IndexedName& idx);

    void collectChildMaps(std::map<const ElementMap*, int>& childMapSet,
                          std::vector<const ElementMap*>& childMaps,
                          std::map<QByteArray, int>& postfixMap,
                          std::vector<QByteArray>& postfixes) const;

    struct CStringComp
    {
    public:
        bool operator()(const char* str1, const char* str2) const
        {
            return std::strcmp(str1, str2) < 0;
        }
    };

    struct IndexedElements
    {
        std::deque<MappedNameRef> names;
        std::map<int, MappedChildElements> children;
    };

    std::map<const char*, IndexedElements, CStringComp> indexedNames;

    std::map<MappedName, IndexedName, std::less<>> mappedNames;

    struct ChildMapInfo
    {
        int index = 0;
        MappedChildElements* childMap = nullptr;
        std::map<ElementMap*, int> mapIndices;
    };

    QHash<QByteArray, ChildMapInfo> childElements;
    std::size_t childElementSize = 0;

    mutable unsigned _id = 0;

    void init();

public:
    /// String hasher for element name shortening
    App::StringHasherRef hasher;
};


}  // namespace Data

#endif  // DATA_ELEMENTMAP_H
