////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2026 OVITO GmbH, Germany
//
//  This file is part of OVITO (Open Visualization Tool).
//
//  OVITO is free software; you can redistribute it and/or modify it either under the
//  terms of the GNU General Public License version 3 as published by the Free Software
//  Foundation (the "GPL") or, at your option, under the terms of the MIT License.
//  If you do not alter this notice, a recipient may use your version of this
//  file under either the GPL or the MIT License.
//
//  You should have received a copy of the GPL along with this program in a
//  file LICENSE.GPL.txt.  You should have received a copy of the MIT License along
//  with this program in a file LICENSE.MIT.txt
//
//  This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
//  either express or implied. See the GPL or the MIT License for the specific language
//  governing rights and limitations.
//
////////////////////////////////////////////////////////////////////////////////////////

#pragma once


#include <ovito/stdobj/StdObj.h>
#include <ovito/stdobj/properties/Property.h>
#include <ovito/stdobj/properties/PropertyReference.h>
#include <ovito/stdobj/properties/PropertyContainer.h>
#include <ovito/stdobj/properties/PropertyContainerClass.h>

namespace Ovito {

/**
 * \brief Defines the mapping of a column in an imported data file to a target property in OVITO.
 *
 * An InputColumnMapping is composed of a list of these structures, one for each data file column.
 */
class OVITO_STDOBJ_EXPORT InputColumnInfo
{
public:

    /// Default constructor mapping the column to no property.
    InputColumnInfo() = default;

    /// Constructor mapping the column to a standard property.
    InputColumnInfo(PropertyContainerClassPtr pclass, int typeId, int vectorComponent = -1) {
        mapToStandardProperty(pclass, typeId, vectorComponent);
    }

    /// Constructor mapping the column to a user-defined property.
    InputColumnInfo(const QString& propertyName, int dataType, int vectorComponent = -1) {
        mapToUserProperty(propertyName, dataType, vectorComponent);
    }

    /// Maps this column to a user-defined property.
    void mapToUserProperty(const QString& propertyName, int dataType, int vectorComponent = -1) {
        this->property = PropertyReference(vectorComponent < 0 ? propertyName : QStringLiteral("%1.%2").arg(propertyName).arg(vectorComponent + 1));
        this->dataType = dataType;
    }

    /// Maps this column to a standard property.
    void mapToStandardProperty(PropertyContainerClassPtr pclass, int typeId, int vectorComponent = -1) {
        OVITO_ASSERT(pclass);
        OVITO_ASSERT(typeId != Property::GenericUserProperty);
        this->property = PropertyReference(pclass, typeId, vectorComponent);
        this->dataType = pclass->standardPropertyDataType(typeId);
    }

    /// Maps this column to a property.
    void mapToProperty(const PropertyReference& property, int dataType) {
        this->property = property;
        this->dataType = dataType;
    }

    /// Returns true if the file column is mapped to a target property; false otherwise (file column will be ignored during import).
    bool isMapped() const { return dataType != QMetaType::Void && property; }

    /// Removes the mapping of this file column to a target property.
    void unmap() {
        property = {};
        dataType = QMetaType::Void;
    }

    /// Compares two column records for equality.
    bool operator==(const InputColumnInfo& other) const {
        return property == other.property && dataType == other.dataType && columnName == other.columnName;
    }

    /// The target property this column is mapped to.
    PropertyReference property;

    /// The data type of the property if this column is mapped to a user-defined property.
    /// This field can be set to QMetaType::Void to indicate that the column should be ignored during file import.
    int dataType = QMetaType::Void;

    /// The name of the column in the input file. This information is read from the input file (if available).
    QString columnName;
};

/**
 * \brief Defines a mapping of data columns of an input file to OVITO properties.
 */
class OVITO_STDOBJ_EXPORT InputColumnMapping : public std::vector<InputColumnInfo>
{
public:

    /// Constructor.
    explicit InputColumnMapping(PropertyContainerClassPtr containerClass = nullptr) : _containerClass(containerClass) {}

    /// Return the class of the property container.
    PropertyContainerClassPtr containerClass() const { return _containerClass; }

    /// Converts the mapping to a different target container class.
    void convertToContainerClass(PropertyContainerClassPtr newClass);

    /// Saves the mapping into a byte array.
    [[nodiscard]] QByteArray toByteArray() const;

    /// Loads the mapping from a byte array.
    void fromByteArray(const QByteArray& array);

    /// Checks if the mapping is valid; throws an exception if not.
    void validate(const QString& fileFormatName = {}) const;

    /// Returns the first few lines of the file, which can help the user to figure out
    /// the column mapping.
    [[nodiscard]] const QString& fileExcerpt() const { return _fileExcerpt; }

    /// Stores the first few lines of the file, which can help the user to figure out
    /// the column mapping.
    void setFileExcerpt(const QString& text) { _fileExcerpt = text; }

    /// Returns whether at least some of the file columns have names.
    [[nodiscard]] bool hasFileColumnNames() const
    {
        return std::any_of(begin(), end(), [](const InputColumnInfo& column) { return !column.columnName.isEmpty(); });
    }

    /// \brief Maps a file column to a standard property unless there is already another column mapped to the same property.
    /// \param column The file column index.
    /// \param typeId Specifies the standard property.
    /// \param vectorComponent The component index if the target property is a vector property.
    bool mapColumnToStandardProperty(int column, int typeId, int vectorComponent = -1);

    /// \brief Maps this column to a user-defined property unless there is already another column mapped to the same property.
    /// \param column The file column index.
    /// \param propertyName The name of target particle property.
    /// \param dataType The data type of the property to create.
    /// \param vectorComponent The component index if the target property is a vector property.
    bool mapColumnToUserProperty(int column, const QString& propertyName, int dataType, int vectorComponent = -1);

    /// Returns the callback function that can be provided to set the numeric ID of a new element type.
    [[nodiscard]] const std::function<int(const QLatin1String&)>& typeIdConverter() const { return _typeIdConverter; }

    /// Sets the callback function that can be provided to set the numeric ID of a new element type.
    void setTypeIdConverter(std::function<int(const QLatin1String&)>&& typeIdConverter) { _typeIdConverter = std::move(typeIdConverter); }

    /// Compares two mapping for equality.
    bool operator==(const InputColumnMapping& other) const {
        if(containerClass() != other.containerClass()) return false;
        if(size() != other.size()) return false;
        if(!std::equal(begin(), end(), other.begin())) return false;
        return fileExcerpt() == other.fileExcerpt();
    }

    /// Compares two mappings for inequality.
    bool operator!=(const InputColumnMapping& other) const { return !(*this == other); }

    friend OVITO_STDOBJ_EXPORT SaveStream& operator<<(SaveStream& stream, const InputColumnMapping& m);
    friend OVITO_STDOBJ_EXPORT LoadStream& operator>>(LoadStream& stream, InputColumnMapping& m);

private:

    /// A string with the first few lines of the file, which is meant as a hint for the user to figure out
    /// the correct column mapping.
    QString _fileExcerpt;

    /// The property container type.
    PropertyContainerClassPtr _containerClass = nullptr;

    /// Callback function that can be provided to set the numeric ID of a new element type.
    /// This can be used to automatically set atomic numbers for known element names
    std::function<int(const QLatin1String&)> _typeIdConverter;
};

/**
 * Encapsulates a column mapping for a specific type of property container.
 */
template<class PropertyContainerType>
class TypedInputColumnMapping : public InputColumnMapping
{
public:

    /// Default constructor.
    TypedInputColumnMapping() : InputColumnMapping(&PropertyContainerType::OOClass()) {}

    /// Constructor for conversion from a generic InputColumnMapping.
    TypedInputColumnMapping(const InputColumnMapping& m) : InputColumnMapping(&PropertyContainerType::OOClass()) {
        OVITO_ASSERT(m.containerClass() == containerClass());
        InputColumnMapping::operator=(m);
    }

    friend SaveStream& operator<<(SaveStream& stream, const TypedInputColumnMapping& m) {
        return stream << static_cast<const InputColumnMapping&>(m);
    }

    friend LoadStream& operator>>(LoadStream& stream, TypedInputColumnMapping& m) {
        return stream >> static_cast<InputColumnMapping&>(m);
    }
};

/**
 * \brief Helper class that reads column-based data from an input file and
 *        stores the parsed values in a set of target properties as specified by an InputColumnMapping.
 */
class OVITO_STDOBJ_EXPORT InputColumnReader : public QObject
{
public:

    /// \brief Initializes the object.
    /// \param frameLoader The file reader that created this column reader object.
    /// \param mapping Defines the mapping of columns of the input file
    ///        to the target properties.
    /// \param container The property container where the parsed data will be stored in.
    /// \throws Exception if the mapping is not valid.
    InputColumnReader(StandardFrameLoader& frameLoader, const InputColumnMapping& mapping, PropertyContainer* container, bool removeExistingProperties = true, bool validateMapping = true);

    /// \brief Tells the parser to read the names of element types from the given file column
    void readTypeNamesFromColumn(int nameColumn, int numericIdColumn);

    /// \brief Parses the string tokens from one line of the input file and stores the values
    ///        in the target properties.
    /// \param elementIndex The line index starting at 0 that specifies the data element whose properties
    ///                  are read from the input file.
    /// \param dataLine The text line read from the input file containing the field values.
    void readElement(size_t elementIndex, const char* dataLine);

    /// \brief Parses the string tokens from one line of the input file and stores the values
    ///        in the target properties.
    /// \param elementIndex The line index starting at 0 that specifies the data element whose properties
    ///                  are read from the input file.
    /// \param dataLine The text line read from the input file containing the field values.
    const char* readElement(size_t elementIndex, const char* dataLine, const char* dataLineEnd);

    /// Processes the values from one line of the input file and stores them in the target properties.
    void readElement(size_t elementIndex, const double* values, int nvalues);

    /// Parse a single field from a text line.
    void parseField(size_t elementIndex, int columnIndex, const char* token, const char* token_end);

    /// Sorts the created element types either by numeric ID or by name, depending on how they were stored in the input file.
    void sortElementTypes();

    /// Returns a list of properties that have been parsed.
    std::set<Property*> parsedProperties() const;

    /// Explicitly release the target properties written to by this class.
    void reset() { _properties.clear(); }

    /// Assigns textual names, read from separate file columns, to numeric element types.
    void assignTypeNamesFromSeparateColumns();

    /// Indicates that the element names of at least one typed property are read from
    /// a separate file column.
    bool readingTypeNamesFromSeparateColumns() const { return _readingTypeNamesFromSeparateColumns; }

private:

    /// Specifies the mapping of file columns to target properties.
    InputColumnMapping _mapping;

    /// The container that receives the parsed data.
    PropertyContainer* _container;

    /// The file reader that called this column reader.
    StandardFrameLoader& _frameLoader;

    struct TargetPropertyRecord {
        Property* property = nullptr;
        RawBufferAccess<access_mode::read_write> propertyArray;
        uint8_t* data;
        size_t stride;
        size_t count;
        int vectorComponent;
        int dataType;
        ElementTypeClassPtr elementTypeClass = nullptr;
        bool numericElementTypes;
        int nameOfNumericTypeColumn = -1;
        std::pair<const char*, const char*> typeName{nullptr, nullptr};
        int lastTypeId = -1;
    };

    /// Mapping of input file columns to target memory.
    std::vector<TargetPropertyRecord> _properties;

    /// Indicates that the element names of at least one typed property are read from
    /// a separate file column.
    bool _readingTypeNamesFromSeparateColumns = false;
};

}   // End of namespace

Q_DECLARE_METATYPE(Ovito::InputColumnInfo);
Q_DECLARE_METATYPE(Ovito::InputColumnMapping);
