/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK 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 version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#ifndef ANATOMICALORIENTATION_H
#define ANATOMICALORIENTATION_H

#include <array>
#include <QVariantList>
#include <QVariantMap>
#include <QVariant>
#include <QString>
#include <QUuid>


namespace camitk {


/**
 * AnatomicalOrientation describes the relationship between 3D axes of a FrameOfReference and
 * medical image anatomical orientation labels
 *
 * For most methods, each axis is referred to by its index:
 * - 0 corresponds to the x-axis
 * - 1 corresponds to the y-axis
 * - 2 corresponds to the z-axis
 *
 * For each axis, the two possible directions are
 * - positive direction points towards increasing/maximum values
 * - negative direction points towards decreasing/minimum values
 *
 * The label of the positive direction is called "maxLabel".
 * The label of the negative direction is called "minLabel".
 *
 * Three letter codes are supported, e.g. "RAI", "LAS", or its "+"" variant "LPS+".
 *
 * Supported letters are R (right), L (left), A (anterior),
 * P (posterior), I (inferior) or F (foot), S (superior) or H (head).
 *
 * \example
 * 3 letter code "RAI" means
 * - axis 0 (X) goes from Right (negative direction) to left (positive direction)
 * - axis 1 (Y) goes from Anterior to posterior
 * - axis 2 (Z) goes from Inferior to superior.
 *
 * The "+" variant reverses this convention, so LPS+ is equivalent to RAI
 *
 * Using custom labels is also supported (this is incompatible with three letter code convention).
 * In this case you can set positive and/or negative direction labels for any axis.
 * Custom labels should only be used for specialized context / specific organ anatomy description
 * (e.g. heart imaging along apex/base axis), as the standard viewer can only deal with the
 * standard axial/coronal/sagittal orientation.
 * Using standard 3-letter orientation is recommended.
 *
 *
 * \note Indirect frame of references are supported.
 *
 * This replaces and extend ImageOrientationHelper.
 */
class AnatomicalOrientation: public InterfacePersistence {

public:
    /**
     * Default constructor
     *
     * Anatomical orientation is unknown
     */
    AnatomicalOrientation() {
        setUnkown();
    }

    /**
     * Constructor from a 3-letter code (e.g. "RAI")
     */
    explicit AnatomicalOrientation(QString threeLetterCode) {
        setOrientation(threeLetterCode);
    }

    /**
     * Constructor setting a custom label for each axis negative and positive directions
     */
    AnatomicalOrientation(QString minXLabel, QString maxXLabel, QString minYLabel, QString maxYLabel, QString minZLabel, QString maxZLabel) {
        setOrientation(minXLabel, maxXLabel, minYLabel, maxYLabel, minZLabel, maxZLabel);
    }

    /**
     * Reset anatomical information to unknown
     */
    void setUnkown() {
        known = false;
        code = "";
        minLabels.fill("");
        maxLabels.fill("");
    }

    /**
     * Check if anatomical information is unknown
     */
    bool isUnknown() const {
        return !known;
    }

    /**
     * Sets the orientation using the standard 3-letter code
     */
    void setOrientation(QString threeLetterCode) {
        if (this->set3LetterCode(threeLetterCode)) {
            known = true;
        }
        else {
            known = false;
        }

    }

    /**
     * Set a custom label for the negative and positive directions of all axes
     */
    void setOrientation(QString minXLabel, QString maxXLabel, QString minYLabel, QString maxYLabel, QString minZLabel, QString maxZLabel) {
        code = "";
        minLabels = {minXLabel, minYLabel, minZLabel};
        maxLabels = {maxXLabel, maxYLabel, maxZLabel};
        known = true;
    }

    /**
     * Set the label of the corresponding axis negative direction
     */
    void setMinLabel(int axis, QString minLabel) {
        if (axis >= 0 && axis < 3) {
            code = "";
            minLabels[axis] = minLabel;
            known = true;
        }
    }

    /**
     * Set the label of the corresponding axis positive direction
     */
    void setMaxLabel(int axis, QString maxLabel) {
        if (axis >= 0 && axis < 3) {
            code = "";
            maxLabels[axis] = maxLabel;
            known = true;
        }
    }

    /**
     * Set both negative and positive direction labels for an axis
     * @param minLabel The label towards the negative/minimum values in the chosen axis
     * @param maxLabel The label towards the positive/maximum values in the chosen axis
     */
    void setLabels(int axis, QString minLabel, QString maxLabel) {
        setMinLabel(axis, minLabel);
        setMaxLabel(axis, maxLabel);
    };

    /**
     * Get the label of the negative/minimum direction of the specified axis
     */
    QString getMinLabel(int axis) const {
        if (axis >= 0 && axis < 3) {
            return minLabels[axis];
        }
        else {
            return "";
        }
    }

    /**
     * Get the label of the positive/maximum direction of the specified axis
     */
    QString getMaxLabel(int axis) const {
        if (axis >= 0 && axis < 3) {
            return maxLabels[axis];
        }
        else {
            return "";
        }
    }

    /**
     * @returns a pair<axisIndex, directionIsMin> where
     * - axisIndex is the axis index (0,1,2)
     * that matches the given label name (-1 is returned if there is no match),
     * - directionIsMin is true if the given label name matches the negative/minimum direction
     * of axisIndex and false if it matches the positive/maximum direction
     *
     * Example:
     * getAxisFromName("R") -> (0, true) for RAI data, (0, false) for LAI,
     * getAxisFromName("A") -> (1, true) for RAI data.
     * getAxisFromName("Apex") -> (1, false) if "Apex" is the label of the positive/maximum direction for the y-axis
     */
    std::pair<int, bool> getAxisFromName(QString name) const {
        for (unsigned int ax = 0; ax < 3; ++ax) {
            if (minLabels[ax] == name) {
                return {ax, true};
            }
            else if (maxLabels[ax] == name) {
                return {ax, false};
            }
        }
        return {-1, false};
    }

    /**
     * Returns the inverted 3-letter code
     * (e.g. RAI becomes LPS)
     */
    static QString invert3LetterCode(const QString& code) {
        QString invertedCode = code;
        for (int i = 0; i < code.size(); ++i) {
            if (code[i] == 'R') {
                invertedCode[i] = 'L';
            }
            else if (code[i] == 'L') {
                invertedCode[i] = 'R';
            }
            else if (code[i] == 'A') {
                invertedCode[i] = 'P';
            }
            else if (code[i] == 'P') {
                invertedCode[i] = 'A';
            }
            else if (code[i] == 'I') {
                invertedCode[i] = 'S';
            }
            else if (code[i] == 'S') {
                invertedCode[i] = 'I';
            }
            else if (code[i] == 'F') {
                invertedCode[i] = 'H';
            }
            else if (code[i] == 'H') {
                invertedCode[i] = 'F';
            }
        }
        return invertedCode;
    }

    /**
     * Return the current 3 letter code or an empty string if it is unknown or custom
     *
     * @param plus if true, the "+" variant is returned instead
     *
     * @see setOrientation(QString threeLetterCode) for more detail
     */
    QString get3LetterCode(bool plus = false) const {
        if (!known || code.isEmpty()) {
            return "";
        }
        if (plus) { // Invert the axis letters and add a '+' at the end
            QString plusCode = AnatomicalOrientation::invert3LetterCode(code);
            plusCode.append('+');
            return plusCode;
        }
        else {
            return code;
        }
    }

    /**
     * Returns the label of the corresponding axis/direction (or empty string if there is no label)
     *
     * @param axis The axis index (0,1,2)
     * @param minDirection if true, return the label of the minimum/negative direction, otherwise return the label of the maximum/position direction for the given axis
     */
    QString getLabel(int axis, bool minDirection) const {
        if (axis >= 0 && axis < 3) {
            return minDirection ? minLabels[axis] : maxLabels[axis];
        }
        return "";
    }

    /// @name InterfacePersistence implementation
    /// @{

    /**
     * Convert all data from the object to a QVariantMap
    */
    QVariant toVariant() const override {
        if (known) {
            return QVariantMap {
                {"code", code},
                {"minLabels", QVariantList{minLabels[0], minLabels[1], minLabels[2]}},
                {"maxLabels", QVariantList{maxLabels[0], maxLabels[1], maxLabels[2]}},
            };
        }
        else {
            return QVariantMap();
        }
    }

    /**
     * Load data from a QVariant to initialize the current object
    */
    void fromVariant(const QVariant& variant) override {
        known = false;
        code = "";
        QVariantMap map = variant.toMap();
        QVariantList list;
        if (map.contains("code")) { // It is known
            known = true;
            code = map.value("code").toString();
        }

        if (map.contains("minLabels") && map.contains("maxLabels")) {
            known = true;

            list = map.value("minLabels").toList();
            if (list.size() == 3) {
                minLabels[0] = list[0].toString();
                minLabels[1] = list[1].toString();
                minLabels[2] = list[2].toString();
            }
            else {
                minLabels.fill("");
            }

            list = map.value("maxLabels").toList();
            if (list.size() == 3) {
                maxLabels[0] = list[0].toString();
                maxLabels[1] = list[1].toString();
                maxLabels[2] = list[2].toString();
            }
            else {
                maxLabels.fill("");
            }
        }
        else { // Unknown
            if (code.isEmpty()) {
                minLabels.fill("");
                maxLabels.fill("");
            }
            else {
                set3LetterCode(code); // Ensure minLabels and maxLabels are filled from the code
            }

        }
    }

    /**
     * SetUuid does nothing
     * It does not store the uuid, but is needed to implement InterfacePersistence
     */
    bool setUuid(QUuid newid) override {
        return false;
    }

    /**
     * Returns an invalid uuid because AnatomicalOrientation is part of a FrameOfReference
     *  which has its own uuid, but this method is needed to implement InterfacePersistence
     */
    QUuid getUuid() const override {
        return QUuid();
    }
    ///@}

private:
    /// Labels of the minimum direction of the axes
    std::array<QString, 3> minLabels;

    /// Labels of the maximum direction of the axes
    std::array<QString, 3> maxLabels;

    /// Whether the orientation has been set
    bool known;

    /// 3-letter code if available, custom otherwise
    QString code;

    /**
     * Sets the orientation from a 3-letter code.
     *
     * Orientation can be set as "RAI", "LPS" or any other 3-letter convention
     * in which each letter is the anatomical label for the minimum value of the relevant axis.
     *
     * This function also supports the alternative convention in which each letter
     * is the anatomical label for the maximum value of the relevant axis, using "RAI+" or "LPS+".
     *
     * You can also force the use of the '+' convention setting the "forcePlus" arg to true.
     */
    bool set3LetterCode(QString code = "", bool forcePlus = false) {
        if (code.size() < 3 || code.size() > 4) { // Minimum 3 letters, maximum 4 (in case there is a '+' at the end)
            return false;
        }
        if ((code.size() > 3 &&  code[3] == '+') || forcePlus) {
            this->code = AnatomicalOrientation::invert3LetterCode(code);
        }
        else {
            this->code = code;
        }
        this->code.truncate(3); // in case there is an unwanted trailing '+'
        QString invertedCode = AnatomicalOrientation::invert3LetterCode(this->code);
        minLabels[0] = code[0];
        minLabels[1] = code[1];
        minLabels[2] = code[2];
        maxLabels[0] = invertedCode[0];
        maxLabels[1] = invertedCode[1];
        maxLabels[2] = invertedCode[2];
        known = true;
        return true;
    }

};

} // namespace camitk

#endif // ANATOMICALORIENTATION_H
