//
// This file is part of MorphoGraphX - http://www.MorphoGraphX.org
// Copyright (C) 2012-2015 Richard S. Smith and collaborators.
//
// If you use MorphoGraphX in your work, please cite:
//   http://dx.doi.org/10.7554/eLife.05864
//
// MorphoGraphX is free software, and is licensed under under the terms of the 
// GNU General (GPL) Public License version 2.0, http://www.gnu.org/licenses.
// 
#ifndef PLYFILE_HPP
#define PLYFILE_HPP

#include <Config.hpp>

#include <QHash>
#include <QList>
#include <QString>
#include <QStringList>
#include <stdint.h>
#include <vector>

class QFile;

#define FORALL_PLY_TYPES(macro) \
  macro(int8_t);                \
  macro(uint8_t);               \
  macro(int16_t);               \
  macro(uint16_t);              \
  macro(int32_t);               \
  macro(uint32_t);              \
  macro(float);                 \
  macro(double)

#define FORALL_PLY_TYPEIDS(macro)   \
  macro(int8_t, PlyFile::CHAR);     \
  macro(uint8_t, PlyFile::UCHAR);   \
  macro(int16_t, PlyFile::SHORT);   \
  macro(uint16_t, PlyFile::USHORT); \
  macro(int32_t, PlyFile::INT);     \
  macro(uint32_t, PlyFile::UINT);   \
  macro(float, PlyFile::FLOAT);     \
  macro(double, PlyFile::DOUBLE)

namespace mgx 
{
  /**
   * \class PlyFile PlyFile.hpp <PlyFile.hpp>
   *
   * Class representing the content of a PLY file.
   *
   * A PLY file is a collection of elements, with a format and a version number (that currently should be 1.0).
   *
   * Each element has a name, that must be unique in the file, a size (i.e. how many items they contain), and a set of
   * properties, shared by the elements.
   *
   * Each property has:
   * \li a name, that must be unique in the element
   * \li a kind: value or list
   * \li a memory type: the type of the property as held in memory
   * \li a file type: the type of the property as written in the file
   * \li a size type: which type to use to write the number of elements in the file
   */
  struct mgx_EXPORT PlyFile {
    /**
     * Enumeration for the possible types of the properties
     */
    enum TYPE {
      CHAR,         ///< 8 bits signed integer
      UCHAR,        ///< 8 bits unsigned integer
      SHORT,        ///< 16 bits signed integer
      USHORT,       ///< 16 bits unsigned integer
      INT,          ///< 32 bits signed integer
      UINT,         ///< 32 bits unsigned integer
      FLOAT,        ///< 32 bits floating point
      DOUBLE,       ///< 64 bits floating point
      NB_TYPES,     ///< Number of types, also used to mark an invalid type
      INVALID_TYPE = NB_TYPES
    };
  
  #define INLINE_TYPE_ASSOC(T, TYPEID)                     \
    static inline TYPE getType(const T &) { return TYPEID; \
    }
    FORALL_PLY_TYPEIDS(INLINE_TYPE_ASSOC)
  #undef INLINE_TYPE_ASSOC
  
    /**
     * Array holding the size in byte of the various types
     */
    static const unsigned int typeSizes[NB_TYPES];
    /**
     * Array of C-string representation of the types
     */
    static char const* const typeNames[NB_TYPES + 1][3];
  
    /**
     * Enumeration of the possible file formats
     */
    enum FORMAT_TYPES {
      UNSPECIFIED_FORMAT = 0,     ///< The format is not yet specified.
      ASCII,                      ///< The content is written in ASCII
      BINARY_LITTLE_ENDIAN,       ///< The content is written in binary with little endian representation of numbers
      BINARY_BIG_ENDIAN           ///< The content is written in binary with big endian representation of numbers
    };
  
    /**
     * Array of C-string representation of the formats
     */
    static char const* const formatNames[4];
  
    struct Element;
  
    /**
     * \class Property
     *
     * Class representing a property in an element
     */
    struct Property {
      /**
       * Kind of a property
       */
      enum KIND {
        VALUE,       ///< The property holds a single value per element
        LIST         ///< The property holds a variable number of values per element
      };
      /**
       * Constructor of a property
       * \param name Name of the property
       * \param el Element containing the property
       *
       * The name of the property must be unique in the element. If this is not the case, the property won't be
       * attached
       * to the element.
       *
       * \note Properties should usually be created using the Element::createValue or Element::createList methods.
       */
      Property(const QString& name, Element* el = 0);
  
      /**
       * The destructor takes charge to detach the property from any element that contains it.
       */
      ~Property();
  
      /**
       * Allocate the memory for the property, as long as \c memType is not INVALID_TYPE
       *
       * All the vectors are initialized with default-constructed values.
       */
      void allocate(size_t size);
  
      /**
       * Free the memory occupied by the property
       */
      void deallocate();
  
      /**
       * Return a pointer to the list help by the property as long as the property is a list and the type is correct.
       */
      template <typename T> std::vector<std::vector<T> >* list()
      {
        if(_kind != LIST)
          return 0;
        if(getType(T()) != _memType)
          return 0;
        return (std::vector<std::vector<T> >*)_content;
      }
  
      /**
       * Return a pointer to the values help by the property as long as the property is a value and the type is
       * correct.
       */
      template <typename T> std::vector<T>* value()
      {
        if(_kind != VALUE)
          return 0;
        if(getType(T()) != _memType)
          return 0;
        return (std::vector<T>*)_content;
      }
  
      /**
       * Return a pointer to the list help by the property as long as the property is a list and the type is correct.
       */
      template <typename T> const std::vector<std::vector<T> >* list() const
      {
        if(_kind != LIST)
          return 0;
        if(getType(T()) != _memType)
          return 0;
        return (const std::vector<std::vector<T> >*)_content;
      }
  
      /**
       * Return a pointer to the values help by the property as long as the property is a value and the type is
       * correct.
       */
      template <typename T> const std::vector<T>* value() const
      {
        if(_kind != VALUE)
          return 0;
        if(getType(T()) != _memType)
          return 0;
        return (const std::vector<T>*)_content;
      }
  
      /**
       * Print an error on the standard output and return false
       */
      bool error(const QString& str) const;
  
      /**
       * Name of the property
       */
      const QString& name() const {
        return _name;
      }
  
      /**
       * File type of the property value
       */
      TYPE fileType() const {
        return _fileType;
      }
  
      /**
       * Memory type of the property value
       */
      TYPE memType() const {
        return _memType;
      }
  
      /**
       * File type of the size of the property list
       */
      TYPE sizeType() const {
        return _sizeType;
      }
  
      /**
       * Kind of the property
       */
      KIND kind() const {
        return _kind;
      }
  
      /**
       * Size of the property, that is the number of elements stored in it.
       */
      size_t size() const {
        return _size;
      }
  
      /**
       * Change the name of the property, only if the new name doesn't conflict with one of the other properties in
       * the
       * parent element.
       */
      bool rename(const QString& n);
  
      /**
       * Change the file type of the property.
       */
      void setFileType(TYPE ft) {
        _fileType = ft;
      }
  
      /**
       * Change the memory type of the property.
       *
       * If the property is already allocated, the existing values will be converted to the new type.
       */
      void setMemType(TYPE mt);
  
      /**
       * Change the file type of the size of the property
       */
      void setSizeType(TYPE st) {
        _sizeType = st;
      }
  
      /**
       * Change the kind of the property.
       *
       * \warning If the property has already been allocated, this will erase any data held in it.
       */
      void setKind(KIND k);     // Changing the kind after allocation looses all data!
  
      /**
       * Get the element containing the property, if any.
       */
      Element* parent() {
        return _parent;
      }
  
      /**
       * Get the element containing the property, if any.
       */
      const Element* parent() const {
        return _parent;
      }
  
      /**
       * Change the parent, only if the new parent doesn't already have a property with the same name
       */
      bool setParent(Element* parent);
  
      /**
       * Resize the property.
       *
       * If the property is already allocated, it should leep the \c min(size,s) first property values.
       *
       * \warning You should never call this method if the property is already in an element
       */
      void resize(size_t s);
  
  protected:
      QString _name;        // Name of the property
      TYPE _fileType;       // type of the elements on the file
      TYPE _memType;        // type of the elements in memory (INVALID_TYPE if the property should not be loaded)
      TYPE _sizeType;       // type used to hold the number of elements (if any)
      KIND _kind;           // Single value or list (later .. vector?)
      Element* _parent;     // Element containing the property (if any)
      void* _content;       // Content of the property, if allocated
      size_t _size;         // Number of elements
    };
  
    /**
     * \class Element
     *
     * Class representing an element
     */
    struct Element {
      /**
       * Element constructor
       *
       * \param name Name of the lement
       * \param parent PLY file holding the element
       *
       * The name must be unique in the PLY file. If this is not the case, the element won't be attached to the PLY
       * file
       * and the parent won't be set.
       *
       * \note An element should be created using the PlyFile::createElement method.
       */
      Element(const QString& name, PlyFile* parent = 0);
  
      /**
       * The destructor takes care of detaching the element from the PLY file that contains it.
       */
      ~Element();
  
      /**
       * Allocate the memory for all the properties attached to the element
       */
      void allocate();
  
      /**
       * Remove any property attached to this element.
       *
       * The properties will be deleted.
       */
      void clear();
  
      /**
       * Number of properties in the element
       */
      size_t nbProperties() const {
        return _properties.size();
      }
  
      /**
       * Number of items in the element
       */
      size_t size() const {
        return _nbElements;
      }
  
      /**
       * Change the number of items in the element.
       *
       * This will call Property::resize on all the properties of the element.
       */
      void resize(size_t n);
  
      /**
       * Get the list of property names
       */
      QStringList properties() const;
  
      /**
       * Access a property by index number
       */
      Property* property(size_t pos);
      /**
       * Access a property by index number
       */
      const Property* property(size_t pos) const;
      /**
       * Access a property by name
       */
      Property* property(const QString& name);
      /**
       * Access a property by name
       */
      const Property* property(const QString& name) const;
  
      /**
       * Create a new value property if the name doesn't already exist
       *
       * \param name Name of the new property
       * \param file File type of the property
       * \param mem Memory type. If INVALID_TYPE is specified here, the memory type will be equal to the file type
       */
      Property* createValue(const QString& name, TYPE file, TYPE mem = INVALID_TYPE);
      /**
       * Create a new value property if the name doesn't already exist
       *
       * \param name Name of the new property
       * \param size File type for the size of the list
       * \param file File type of the property
       * \param mem Memory type. If INVALID_TYPE is specified here, the memory type will be equal to the file type
       */
      Property* createList(const QString& name, TYPE size, TYPE file, TYPE mem = INVALID_TYPE);
  
      /**
       * Write the error in the standard out and return false
       */
      bool error(const QString& str) const;
  
      /**
       * Returns true if the element has a property names \c name
       */
      bool hasProperty(const QString& name) const;
  
      /**
       * Attach a property to the element
       */
      bool attach(Property* prop);
  
      /**
       * Remove a property from the element
       */
      bool detach(Property* prop);
  
      /**
       * Remove a property from the element, and return the removed property
       */
      Property* detach(const QString& name);
  
      /**
       * Name of the element
       */
      const QString& name() const {
        return _name;
      }
  
      /**
       * Rename the element, only if the containing PLY file doesn't already contain an element with the new name
       */
      bool rename(const QString& n);
  
      /**
       * Return true if the element has been allocated
       */
      bool allocated() const {
        return _allocated;
      }
  
      /**
       * Change the parent of the element, only if the new parent doesn't have an element with the same name
       */
      bool setParent(PlyFile* p);
      /**
       * Get the parent of the element
       */
      PlyFile* parent() {
        return _parent;
      }
      /**
       * Get the parent of the element
       */
      const PlyFile* parent() const {
        return _parent;
      }
  
      /**
       * \internal
       * Inform the element a property has been renamed
       */
      void _rename_prop(Property* prop, const QString& new_name);     // Don't call this yourself!
                                                                      /**
                                                                       * \internal
                                                                       * Inform the element a property is to be attached
                                                                       */
      void _attach(Property* prop);
      /**
       * \internal
       * Inform the element a property is to be detached
       */
      void _detach(Property* prop);
  
  protected:
      QString _name;
      size_t _nbElements;
      QList<Property*> _properties;
      QHash<QString, int> _property_map;
      PlyFile* _parent;
      bool _allocated;
    };
  
    /**
     * Create a new file
     */
    PlyFile();
  
    /**
     * Remove any element from the file, and reset version number and format
     */
    void clear();
  
    /**
     * Initialize the file with a format and a version
     */
    bool init(FORMAT_TYPES format = BINARY_LITTLE_ENDIAN, const QString& version = "1.0");
  
    /**
     * Validate the content. This must be called before writing
     */
    bool validate();
  
    /**
     * Allocate all the properties of all the element in the file
     */
    void allocate();
  
    /**
     * Format of the file
     */
    FORMAT_TYPES format() const {
      return _format;
    }
    /**
     * Set the file format, checking the validity of the argument
     */
    bool setFormat(FORMAT_TYPES f)
    {
      if(f != UNSPECIFIED_FORMAT) {
        _format = f;
        return true;
      }
      return false;
    }
  
    /**
     * Set the format version, checking it's validity
     */
    bool setVersion(QString version);
    /**
     * Version fo the format
     */
    const QString& version() const {
      return _version;
    }
  
    /**
     * Access an element by index
     */
    Element* element(size_t idx);
    /**
     * Access an element by index
     */
    const Element* element(size_t idx) const;
    /**
     * Access an element by name
     */
    Element* element(const QString& name);
    /**
     * Access an element by name
     */
    const Element* element(const QString& name) const;
    /**
     * Create a element \c name with \c nb_elements items
     */
    Element* createElement(const QString& name, size_t nb_elements);
  
    /**
     * Get the current element (i.e. the last one created with createElement
     */
    Element* currentElement() {
      return current_element;
    }
    /**
     * Check if the element \c name exists
     */
    bool hasElement(const QString& name) const;
  
    /**
     * Return the number of elements in the file
     */
    size_t nbElements() const {
      return _elements.size();
    }
  
    /**
     * Attach an element to the file
     */
    bool attach(Element* el);
  
    /**
     * Detach an element from the file
     */
    bool detach(Element* el);
  
    /**
     * Parse the head of a PLY file.
     *
     * \returns True if the header has been correctly parsed
     */
    bool parseHeader(const QString& filename);
  
    /**
     * Parse the content of the current file.
     *
     * The file's header must have been parsed. This gives the user the opportunity to change the memory type of the
     * properties before reading. Also, any property with a \c INVALID_TYPE memory type will be skipped instead of read.
     */
    bool parseContent();   // Parse the content of the file whose header has been parsed last
  
    /**
     * Write the error on standard out, possibly with file and line number if they have been specified. Then, returns
     * false.
     */
    bool error(const QString err) const;
  
    /**
     * Save the file to \c filename
     */
    bool save(const QString& filename) const;
  
    /**
     * Get the position of the content in a file.
     *
     * This is set by PlyFile::parseHeader
     */
    qint64 contentPosition() const {
      return _contentPosition;
    }
  
    /**
     * Check if the last call to PlyFile::validate was successful of not
     */
    bool isValid() const {
      return is_valid;
    }
    /**
     * Check if the last call to PlyFile::validate was successful of not
     */
    operator bool() const { return is_valid; }
  
    /**
     * See the comments currently defined
     */
    const QStringList& comments() const {
      return _comments;
    }
  
    /**
     * Add a comment
     *
     * If the line contains new lines, it will be split
     */
    void addComment(QString line);
  
    /**
     * Remove all comments
     */
    void clearComments() {
      _comments.clear();
    }
  
    /**
     * \internal
     * Inform the file an element is being attached
     */
    void _attach(Element* el);
    /**
     * \internal
     * Inform the file an element is being detached
     */
    void _detach(Element* el);
  
  protected:
    bool parseAsciiContent(QFile& f);
    bool parseBinaryContent(QFile& f, bool little_endian);
  
    bool readFormat(const QStringList& fields);
    bool readElement(const QStringList& fields);
    bool readProperty(const QStringList& fields);
  
    bool writeHeader(QFile& f) const;
    bool writeAsciiContent(QFile& f) const;
    bool writeBinaryContent(QFile& f, bool little_endian) const;
  
    TYPE parseType(QString typeName) const;
  
    QList<Element*> _elements;
    QHash<QString, int> _element_map;
  
    Element* current_element;
  
    QString filename;
    int line_nb;
  
    FORMAT_TYPES _format;
    QString _version;
    QStringList _comments;
    int _version_major, _version_minor;
    bool is_valid;
    qint64 _contentPosition;
  };
}
#endif
