//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#ifndef MATERIALX_GEOM_H
#define MATERIALX_GEOM_H

/// @file
/// Geometric element subclasses

#include <MaterialXCore/Export.h>

#include <MaterialXCore/Element.h>

MATERIALX_NAMESPACE_BEGIN

extern MX_CORE_API const string GEOM_PATH_SEPARATOR;
extern MX_CORE_API const string UNIVERSAL_GEOM_NAME;
extern MX_CORE_API const string UDIM_TOKEN;
extern MX_CORE_API const string UV_TILE_TOKEN;
extern MX_CORE_API const string UDIM_SET_PROPERTY;

class GeomElement;
class GeomInfo;
class GeomProp;
class GeomPropDef;
class Collection;
class CollectionAdd;
class CollectionRemove;

/// A shared pointer to a GeomElement
using GeomElementPtr = shared_ptr<GeomElement>;
/// A shared pointer to a const GeomElement
using ConstGeomElementPtr = shared_ptr<const GeomElement>;

/// A shared pointer to a GeomInfo
using GeomInfoPtr = shared_ptr<GeomInfo>;
/// A shared pointer to a const GeomInfo
using ConstGeomInfoPtr = shared_ptr<const GeomInfo>;

/// A shared pointer to a GeomProp
using GeomPropPtr = shared_ptr<GeomProp>;
/// A shared pointer to a const GeomProp
using ConstGeomPropPtr = shared_ptr<const GeomProp>;

/// A shared pointer to a GeomPropDef
using GeomPropDefPtr = shared_ptr<GeomPropDef>;
/// A shared pointer to a const GeomPropDef
using ConstGeomPropDefPtr = shared_ptr<const GeomPropDef>;

/// A shared pointer to a Collection
using CollectionPtr = shared_ptr<Collection>;
/// A shared pointer to a const Collection
using ConstCollectionPtr = shared_ptr<const Collection>;

/// @class GeomPath
/// A MaterialX geometry path, representing the hierarchical location
/// expressed by a geometry name.
class MX_CORE_API GeomPath
{
  public:
    GeomPath() :
        _empty(true)
    {
    }
    ~GeomPath() = default;

    bool operator==(const GeomPath& rhs) const
    {
        return _vec == rhs._vec &&
               _empty == rhs._empty;
    }
    bool operator!=(const GeomPath& rhs) const
    {
        return !(*this == rhs);
    }

    /// Construct a path from a geometry name string.
    explicit GeomPath(const string& geom) :
        _vec(splitString(geom, GEOM_PATH_SEPARATOR)),
        _empty(geom.empty())
    {
    }

    /// Convert a path to a geometry name string.
    operator string() const
    {
        if (_vec.empty())
        {
            return _empty ? EMPTY_STRING : UNIVERSAL_GEOM_NAME;
        }
        return GEOM_PATH_SEPARATOR + joinStrings(_vec, GEOM_PATH_SEPARATOR);
    }

    /// Return true if there is any geometry in common between the two paths.
    /// @param rhs A second geometry path to be compared with this one
    /// @param contains If true, then we require that the first path completely
    ///    contains the second one.
    bool isMatching(const GeomPath& rhs, bool contains = false) const
    {
        if (_empty || rhs._empty)
        {
            return false;
        }
        if (contains && _vec.size() > rhs._vec.size())
        {
            return false;
        }
        size_t minSize = std::min(_vec.size(), rhs._vec.size());
        for (size_t i = 0; i < minSize; i++)
        {
            if (_vec[i] != rhs._vec[i])
            {
                return false;
            }
        }
        return true;
    }

    /// Return true if this geometry path is empty.  An empty path matches
    /// no other geometry paths.
    bool isEmpty() const
    {
        return _empty;
    }

    /// Return true if this geometry path is universal.  A universal path
    /// matches all non-empty geometry paths.
    bool isUniversal() const
    {
        return _vec.empty() && !_empty;
    }

  private:
    StringVec _vec;
    bool _empty;
};

/// @class GeomElement
/// The base class for geometric elements, which support bindings to geometries
/// and geometric collections.
class MX_CORE_API GeomElement : public Element
{
  protected:
    GeomElement(ElementPtr parent, const string& category, const string& name) :
        Element(parent, category, name)
    {
    }

  public:
    virtual ~GeomElement() { }

    /// @name Geometry
    /// @{

    /// Set the geometry string of this element.
    void setGeom(const string& geom)
    {
        setAttribute(GEOM_ATTRIBUTE, geom);
    }

    /// Return true if this element has a geometry string.
    bool hasGeom() const
    {
        return hasAttribute(GEOM_ATTRIBUTE);
    }

    /// Return the geometry string of this element.
    const string& getGeom() const
    {
        return getAttribute(GEOM_ATTRIBUTE);
    }

    /// Return the active geometry string of this element, taking all geometry
    /// string substitutions at this scope into account.
    string getActiveGeom() const
    {
        return hasGeom() ?
               createStringResolver()->resolve(getGeom(), GEOMNAME_TYPE_STRING) :
               EMPTY_STRING;
    }

    /// @}
    /// @name Collection
    /// @{

    /// Set the collection string of this element.
    void setCollectionString(const string& collection)
    {
        setAttribute(COLLECTION_ATTRIBUTE, collection);
    }

    /// Return true if this element has a collection string.
    bool hasCollectionString() const
    {
        return hasAttribute(COLLECTION_ATTRIBUTE);
    }

    /// Return the collection string of this element.
    const string& getCollectionString() const
    {
        return getAttribute(COLLECTION_ATTRIBUTE);
    }

    /// Assign a Collection to this element.
    void setCollection(ConstCollectionPtr collection);

    /// Return the Collection that is assigned to this element.
    CollectionPtr getCollection() const;

    /// @}
    /// @name Validation
    /// @{

    /// Validate that the given element tree, including all descendants, is
    /// consistent with the MaterialX specification.
    bool validate(string* message = nullptr) const override;

    /// @}

  public:
    static const string GEOM_ATTRIBUTE;
    static const string COLLECTION_ATTRIBUTE;
};

/// @class GeomInfo
/// A geometry info element within a Document.
class MX_CORE_API GeomInfo : public GeomElement
{
  public:
    GeomInfo(ElementPtr parent, const string& name) :
        GeomElement(parent, CATEGORY, name)
    {
    }
    virtual ~GeomInfo() { }

    /// @name GeomProp Elements
    /// @{

    /// Add a GeomProp to this element.
    /// @param name The name of the new GeomProp.
    ///     If no name is specified, then a unique name will automatically be
    ///     generated.
    /// @return A shared pointer to the new GeomProp.
    GeomPropPtr addGeomProp(const string& name = EMPTY_STRING)
    {
        return addChild<GeomProp>(name);
    }

    /// Return the GeomProp, if any, with the given name.
    GeomPropPtr getGeomProp(const string& name) const
    {
        return getChildOfType<GeomProp>(name);
    }

    /// Return a vector of all GeomProp elements.
    vector<GeomPropPtr> getGeomProps() const
    {
        return getChildrenOfType<GeomProp>();
    }

    /// Remove the GeomProp, if any, with the given name.
    void removeGeomProp(const string& name)
    {
        removeChildOfType<GeomProp>(name);
    }

    /// @}
    /// @name Tokens
    /// @{

    /// Add a Token to this element.
    /// @param name The name of the new Token.
    ///     If no name is specified, then a unique name will automatically be
    ///     generated.
    /// @return A shared pointer to the new Token.
    TokenPtr addToken(const string& name = EMPTY_STRING)
    {
        return addChild<Token>(name);
    }

    /// Return the Token, if any, with the given name.
    TokenPtr getToken(const string& name) const
    {
        return getChildOfType<Token>(name);
    }

    /// Return a vector of all Token elements.
    vector<TokenPtr> getTokens() const
    {
        return getChildrenOfType<Token>();
    }

    /// Remove the Token, if any, with the given name.
    void removeToken(const string& name)
    {
        removeChildOfType<Token>(name);
    }

    /// @}
    /// @name Values
    /// @{

    /// Set the value of a GeomProp by its name, creating a child element
    /// to hold the GeomProp if needed.
    template <class T> GeomPropPtr setGeomPropValue(const string& name,
                                                    const T& value,
                                                    const string& type = EMPTY_STRING);

    /// Set the string value of a Token by its name, creating a child element
    /// to hold the Token if needed.
    TokenPtr setTokenValue(const string& name, const string& value)
    {
        TokenPtr token = getToken(name);
        if (!token)
            token = addToken(name);
        token->setValue<string>(value);
        return token;
    }

    /// @}

  public:
    static const string CATEGORY;
};

/// @class GeomProp
/// A geometric property element within a GeomInfo.
class MX_CORE_API GeomProp : public ValueElement
{
  public:
    GeomProp(ElementPtr parent, const string& name) :
        ValueElement(parent, CATEGORY, name)
    {
    }
    virtual ~GeomProp() { }

  public:
    static const string CATEGORY;
};

/// @class GeomPropDef
/// An element representing a declaration of geometric property data.
///
/// A GeomPropDef element contains a reference to a geometric node and a set of
/// modifiers for that node.  For example, a world-space normal can be declared
/// as a reference to the "normal" geometric node with a space setting of
/// "world", or a specific set of texture coordinates can be declared as a
/// reference to the "texcoord" geometric node with an index setting of "1".
class MX_CORE_API GeomPropDef : public TypedElement
{
  public:
    GeomPropDef(ElementPtr parent, const string& name) :
        TypedElement(parent, CATEGORY, name)
    {
    }
    virtual ~GeomPropDef() { }

    /// @name Geometric Property
    /// @{

    /// Set the geometric property string of this element.
    void setGeomProp(const string& node)
    {
        setAttribute(GEOM_PROP_ATTRIBUTE, node);
    }

    /// Return true if this element has a geometric property string.
    bool hasGeomProp() const
    {
        return hasAttribute(GEOM_PROP_ATTRIBUTE);
    }

    /// Return the geometric property string of this element.
    const string& getGeomProp() const
    {
        return getAttribute(GEOM_PROP_ATTRIBUTE);
    }

    /// @}
    /// @name Geometric Space
    /// @{

    /// Set the geometric space string of this element.
    void setSpace(const string& space)
    {
        setAttribute(SPACE_ATTRIBUTE, space);
    }

    /// Return true if this element has a geometric space string.
    bool hasSpace() const
    {
        return hasAttribute(SPACE_ATTRIBUTE);
    }

    /// Return the geometric space string of this element.
    const string& getSpace() const
    {
        return getAttribute(SPACE_ATTRIBUTE);
    }

    /// @}
    /// @name Geometric Index
    /// @{

    /// Set the index string of this element.
    void setIndex(const string& space)
    {
        setAttribute(INDEX_ATTRIBUTE, space);
    }

    /// Return true if this element has an index string.
    bool hasIndex() const
    {
        return hasAttribute(INDEX_ATTRIBUTE);
    }

    /// Return the index string of this element.
    const string& getIndex() const
    {
        return getAttribute(INDEX_ATTRIBUTE);
    }

    /// @}

  public:
    static const string CATEGORY;
    static const string GEOM_PROP_ATTRIBUTE;
    static const string SPACE_ATTRIBUTE;
    static const string INDEX_ATTRIBUTE;
};

/// @class Collection
/// A collection element within a Document.
class MX_CORE_API Collection : public Element
{
  public:
    Collection(ElementPtr parent, const string& name) :
        Element(parent, CATEGORY, name)
    {
    }
    virtual ~Collection() { }

    /// @name Include Geometry
    /// @{

    /// Set the include geometry string of this element.
    void setIncludeGeom(const string& geom)
    {
        setAttribute(INCLUDE_GEOM_ATTRIBUTE, geom);
    }

    /// Return true if this element has an include geometry string.
    bool hasIncludeGeom() const
    {
        return hasAttribute(INCLUDE_GEOM_ATTRIBUTE);
    }

    /// Return the include geometry string of this element.
    const string& getIncludeGeom() const
    {
        return getAttribute(INCLUDE_GEOM_ATTRIBUTE);
    }

    /// Return the active include geometry string of this element, taking all
    /// geometry string substitutions at this scope into account.
    string getActiveIncludeGeom() const
    {
        return hasIncludeGeom() ?
               createStringResolver()->resolve(getIncludeGeom(), GEOMNAME_TYPE_STRING) :
               EMPTY_STRING;
    }

    /// @}
    /// @name Exclude Geometry
    /// @{

    /// Set the exclude geometry string of this element.
    void setExcludeGeom(const string& geom)
    {
        setAttribute(EXCLUDE_GEOM_ATTRIBUTE, geom);
    }

    /// Return true if this element has an exclude geometry string.
    bool hasExcludeGeom() const
    {
        return hasAttribute(EXCLUDE_GEOM_ATTRIBUTE);
    }

    /// Return the exclude geometry string of this element.
    const string& getExcludeGeom() const
    {
        return getAttribute(EXCLUDE_GEOM_ATTRIBUTE);
    }

    /// Return the active exclude geometry string of this element, taking all
    /// geometry string substitutions at this scope into account.
    string getActiveExcludeGeom() const
    {
        return hasExcludeGeom() ?
               createStringResolver()->resolve(getExcludeGeom(), GEOMNAME_TYPE_STRING) :
               EMPTY_STRING;
    }

    /// @}
    /// @name Include Collection
    /// @{

    /// Set the include collection string of this element.
    void setIncludeCollectionString(const string& collection)
    {
        setAttribute(INCLUDE_COLLECTION_ATTRIBUTE, collection);
    }

    /// Return true if this element has an include collection string.
    bool hasIncludeCollectionString() const
    {
        return hasAttribute(INCLUDE_COLLECTION_ATTRIBUTE);
    }

    /// Return the include collection string of this element.
    const string& getIncludeCollectionString() const
    {
        return getAttribute(INCLUDE_COLLECTION_ATTRIBUTE);
    }

    /// Set the collection that is directly included by this element.
    void setIncludeCollection(ConstCollectionPtr collection);

    /// Set the vector of collections that are directly included by
    /// this element.
    void setIncludeCollections(const vector<ConstCollectionPtr>& collections);

    /// Return the vector of collections that are directly included by
    /// this element.
    vector<CollectionPtr> getIncludeCollections() const;

    /// Return true if the include chain for this element contains a cycle.
    bool hasIncludeCycle() const;

    /// @}
    /// @name Geometry Matching
    /// @{

    /// Return true if this collection and the given geometry string have any
    /// geometries in common.
    /// @throws ExceptionFoundCycle if a cycle is encountered.
    bool matchesGeomString(const string& geom) const;

    /// @}
    /// @name Validation
    /// @{

    /// Validate that the given element tree, including all descendants, is
    /// consistent with the MaterialX specification.
    bool validate(string* message = nullptr) const override;

    /// @}

  public:
    static const string CATEGORY;
    static const string INCLUDE_GEOM_ATTRIBUTE;
    static const string EXCLUDE_GEOM_ATTRIBUTE;
    static const string INCLUDE_COLLECTION_ATTRIBUTE;
};

template <class T> GeomPropPtr GeomInfo::setGeomPropValue(const string& name,
                                                          const T& value,
                                                          const string& type)
{
    GeomPropPtr geomProp = getChildOfType<GeomProp>(name);
    if (!geomProp)
        geomProp = addGeomProp(name);
    geomProp->setValue(value, type);
    return geomProp;
}

/// Given two geometry strings, each containing an array of geom names, return
/// true if they have any geometries in common.
///
/// An empty geometry string matches no geometries, while the universal geometry
/// string "/" matches all non-empty geometries.
///
/// If the contains argument is set to true, then we require that a geom path
/// in the first string completely contains a geom path in the second string.
///
/// @todo Geometry name expressions are not yet supported.
MX_CORE_API bool geomStringsMatch(const string& geom1, const string& geom2, bool contains = false);

MATERIALX_NAMESPACE_END

#endif
