// ===== External Includes ===== //
#include <pugixml.hpp>

// ===== OpenXLSX Includes ===== //
#include "XLCell.hpp"
#include "XLCellIterator.hpp"
#include "XLCellReference.hpp"
#include "XLRow.hpp"
#include "XLStyles.hpp"    // XLDefaultCellFormat

#include "XLUtilities.hpp"

namespace OpenXLSX
{
    XLRow::XLRow() : m_rowNode(nullptr), m_sharedStrings(XLSharedStringsDefaulted), m_rowDataProxy(this, m_rowNode.get()) {}

    /**
     * @details Constructs a new XLRow object from information in the underlying XML file. A pointer to the corresponding
     * node in the underlying XML file must be provided.
     */
    XLRow::XLRow(const XMLNode& rowNode, const XLSharedStrings& sharedStrings)
        : m_rowNode(std::make_unique<XMLNode>(rowNode)),
          m_sharedStrings(sharedStrings),
          m_rowDataProxy(this, m_rowNode.get())
    {}

    XLRow::XLRow(const XLRow& other)
        : m_rowNode(other.m_rowNode ? std::make_unique<XMLNode>(*other.m_rowNode) : nullptr),
          m_sharedStrings(other.m_sharedStrings),
          m_rowDataProxy(this, m_rowNode.get())
    {}

    /**
     * @details Because the m_rowDataProxy variable is tied to an exact XLRow object, the move operation is
     * not a 'pure' move, as a new XLRowDataProxy has to be constructed.
     */
    XLRow::XLRow(XLRow&& other) noexcept
        : m_rowNode(std::move(other.m_rowNode)),
          m_sharedStrings(std::move(other.m_sharedStrings)),
          m_rowDataProxy(this, m_rowNode.get())
    {}

    XLRow::~XLRow() = default;

    XLRow& XLRow::operator=(const XLRow& other)
    {
        if (&other != this) {
            auto temp = XLRow(other);
            std::swap(*this, temp);
        }
        return *this;
    }

    /**
     * @details Because the m_rowDataProxy variable is tied to an exact XLRow object, the move operation is
     * not a 'pure' move, as a new XLRowDataProxy has to be constructed.
     */
    XLRow& XLRow::operator=(XLRow&& other) noexcept
    {
        if (&other != this) {
            m_rowNode       = std::move(other.m_rowNode);
            m_sharedStrings = std::move(other.m_sharedStrings);
            m_rowDataProxy  = XLRowDataProxy(this, m_rowNode.get());
        }
        return *this;
    }

    bool XLRow::empty() const { return (!m_rowNode) or m_rowNode->empty(); }

    XLRow::operator bool() const { return m_rowNode and (not m_rowNode->empty()); }

    /**
     * @details Retrieves the explicitly assigned height of the row. If no custom height is set, it defaults to 15.0.
     */
    double XLRow::height() const
    {
        return m_rowNode->attribute("ht").as_double(15.0);    // NOLINT
    }

    /**
     * @details Modifies the visual height of the row. The 'customHeight' attribute must be explicitly set to 1, 
     *          otherwise MS Excel will ignore the 'ht' value and auto-fit the row height based on cell contents.
     */
    void XLRow::setHeight(float height)    // NOLINT
    {
        // Set the 'ht' attribute for the Cell. If it does not exist, create it.
        if (m_rowNode->attribute("ht").empty())
            m_rowNode->append_attribute("ht") = height;
        else
            m_rowNode->attribute("ht").set_value(height);

        // Set the 'customHeight' attribute. If it does not exist, create it.
        if (m_rowNode->attribute("customHeight").empty())
            m_rowNode->append_attribute("customHeight") = 1;
        else
            m_rowNode->attribute("customHeight").set_value(1);
    }

    /**
     * @details Retrieves the row's vertical descent value. This is used by Excel's rendering engine 
     *          to appropriately position text baselines for certain fonts.
     */
    float XLRow::descent() const
    {
        return m_rowNode->attribute("x14ac:dyDescent").as_float(0.25);    // NOLINT
    }

    /**
     * @details Sets the typography descent attribute. This fine-tunes how far characters drop below 
     *          the text baseline in this specific row, maintaining exact layout fidelity.
     */
    void XLRow::setDescent(float descent)
    {
        // Set the 'x14ac:dyDescent' attribute. If it does not exist, create it.
        if (m_rowNode->attribute("x14ac:dyDescent").empty())
            m_rowNode->append_attribute("x14ac:dyDescent") = descent;
        else
            m_rowNode->attribute("x14ac:dyDescent") = descent;
    }

    /**
     * @details Retrieves the row's visibility state. Hidden rows remain in the document and participate in calculations,
     *          but are not rendered in the spreadsheet UI.
     */
    bool XLRow::isHidden() const { return m_rowNode->attribute("hidden").as_bool(false); }

    /**
     * @details Modifies the visual state of the row. Typically used for applying filters or manually hiding 
     *          rows without deleting their underlying data or breaking formulas.
     */
    void XLRow::setHidden(bool state)    // NOLINT
    {
        // Set the 'hidden' attribute. If it does not exist, create it.
        if (m_rowNode->attribute("hidden").empty())
            m_rowNode->append_attribute("hidden") = static_cast<int>(state);
        else
            m_rowNode->attribute("hidden").set_value(static_cast<int>(state));
    }

    /**
     * @details Retrieves the row's outline level (0-7), which determines its nesting depth within a hierarchical grouping structure.
     */
    uint8_t XLRow::outlineLevel() const
    {
        return static_cast<uint8_t>(m_rowNode->attribute("outlineLevel").as_uint(0));
    }

    /**
     * @details Applies a grouping depth to the row. Values > 7 are capped, as MS Excel only supports up to 8 levels (0-7).
     *          Setting the level to 0 completely removes the outline grouping attribute.
     */
    void XLRow::setOutlineLevel(uint8_t level)
    {
        if (level > 7) level = 7;
        if (level == 0) {
            m_rowNode->remove_attribute("outlineLevel");
        } else {
            if (m_rowNode->attribute("outlineLevel").empty())
                m_rowNode->append_attribute("outlineLevel") = level;
            else
                m_rowNode->attribute("outlineLevel").set_value(level);
        }
    }

    /**
     * @details Checks if the row carries the collapsed marker. In Excel, this flag is typically placed on the 
     *          summary row adjacent to a hidden group to render the '+' expansion UI button.
     */
    bool XLRow::isCollapsed() const
    {
        return m_rowNode->attribute("collapsed").as_bool(false);
    }

    /**
     * @details Controls the expansion UI marker ('+' or '-') for grouped rows. This must be set on the summary row
     *          that remains visible when the corresponding detailed rows are hidden.
     */
    void XLRow::setCollapsed(bool state)
    {
        if (!state) {
            m_rowNode->remove_attribute("collapsed");
        } else {
            if (m_rowNode->attribute("collapsed").empty())
                m_rowNode->append_attribute("collapsed") = static_cast<int>(state);
            else
                m_rowNode->attribute("collapsed").set_value(static_cast<int>(state));
        }
    }

    /**
     * @note 2024-08-18: changed return type of rowNumber to uint32_t
     *       CAUTION: there is no validity check on the underlying XML (nor was there ever one in case a value was inconsistent with
     * OpenXLSX::MAX_ROWS)
     */
    uint32_t XLRow::rowNumber() const { return static_cast<uint32_t>(m_rowNode->attribute("r").as_ullong()); }

    /**
     * @details Get the number of cells in the row, by returning the size of the m_cells vector.
     */
    uint16_t XLRow::cellCount() const
    {
        const auto node = m_rowNode->last_child_of_type(pugi::node_element);
        if (node.empty()) return 0;
        // Performance optimization: use lightweight column extraction instead of XLCellReference object creation
        return extractColumnFromCellRef(node.attribute("r").value());
    }

    XLRowDataProxy& XLRow::values() { return m_rowDataProxy; }

    const XLRowDataProxy& XLRow::values() const { return m_rowDataProxy; }

    XLRowDataRange XLRow::cells() const
    {
        const XMLNode node = m_rowNode->last_child_of_type(pugi::node_element);
        if (node.empty()) return XLRowDataRange();    // empty range
        // Performance optimization: use lightweight column extraction instead of XLCellReference object creation
        return XLRowDataRange(*m_rowNode, 1, extractColumnFromCellRef(node.attribute("r").value()), m_sharedStrings.get());
    }

    XLRowDataRange XLRow::cells(uint16_t cellCount) const { return XLRowDataRange(*m_rowNode, 1, cellCount, m_sharedStrings.get()); }

    XLRowDataRange XLRow::cells(uint16_t firstCell, uint16_t lastCell) const
    { return XLRowDataRange(*m_rowNode, firstCell, lastCell, m_sharedStrings.get()); }

    /**
     * @details Find & return a cell in indicated column
     */
    XLCell XLRow::findCell(uint16_t columnNumber)
    {
        if (m_rowNode->empty()) return XLCell{};

        XMLNode cellNode = m_rowNode->last_child_of_type(pugi::node_element);

        // ===== If there are no cells in the current row, or the requested cell is beyond the last cell in the row...
        // Performance optimization: use lightweight column extraction instead of XLCellReference object creation
        uint16_t lastColNo = cellNode.empty() ? 0 : extractColumnFromCellRef(cellNode.attribute("r").value());
        if (cellNode.empty() or (lastColNo < columnNumber)) return XLCell{};    // fail

        // ===== If the requested node is closest to the end, start from the end and search backwards...
        if (lastColNo - columnNumber < columnNumber) {
            uint16_t colNo = lastColNo;
            while (not cellNode.empty() and (colNo > columnNumber)) {
                cellNode = cellNode.previous_sibling_of_type(pugi::node_element);
                if (not cellNode.empty()) colNo = extractColumnFromCellRef(cellNode.attribute("r").value());
            }
            // ===== If the backwards search failed to locate the requested cell
            if (cellNode.empty() or (colNo < columnNumber)) return XLCell{};    // fail
        }
        // ===== Otherwise, start from the beginning
        else {
            // ===== At this point, it is guaranteed that there is at least one node_element in the row that is not empty.
            cellNode = m_rowNode->first_child_of_type(pugi::node_element);

            // ===== It has been verified above that the requested columnNumber is <= the column number of the last node_element, therefore
            // this loop will halt:
            uint16_t colNo = extractColumnFromCellRef(cellNode.attribute("r").value());
            while (colNo < columnNumber) {
                cellNode = cellNode.next_sibling_of_type(pugi::node_element);
                colNo    = extractColumnFromCellRef(cellNode.attribute("r").value());
            }
            // ===== If the forwards search failed to locate the requested cell
            if (colNo > columnNumber) return XLCell{};    // fail
        }
        return XLCell(cellNode, m_sharedStrings.get());
    }

    /**
     * @details Determine the value of the style attribute "s" - if attribute does not exist, return default value
     */
    XLStyleIndex XLRow::format() const { return m_rowNode->attribute("s").as_uint(XLDefaultCellFormat); }

    /**
     * @brief Set the row style as a reference to the array index of xl/styles.xml:<styleSheet>:<cellXfs>
     *        If the style attribute "s" does not exist, create it
     */
    bool XLRow::setFormat(XLStyleIndex cellFormatIndex)
    {
        XMLAttribute customFormatAtt = m_rowNode->attribute("customFormat");
        if (cellFormatIndex != XLDefaultCellFormat) {
            if (customFormatAtt.empty()) {
                customFormatAtt = m_rowNode->append_attribute("customFormat");
                if (customFormatAtt.empty()) return false;    // fail if missing customFormat attribute could not be created
            }
            customFormatAtt.set_value("true");
        }
        else {    // cellFormatIndex is XLDefaultCellFormat
            if (not customFormatAtt.empty())
                m_rowNode->remove_attribute(customFormatAtt);    // an existing customFormat attribute should be deleted
        }

        XMLAttribute styleAtt = m_rowNode->attribute("s");
        if (styleAtt.empty()) {
            styleAtt = m_rowNode->append_attribute("s");
            if (styleAtt.empty()) return false;
        }
        styleAtt.set_value(cellFormatIndex);

        return true;
    }

    // ---------- Private Member Functions ---------- //

    bool XLRow::isEqual(const XLRow& lhs, const XLRow& rhs)
    {
        // 2024-05-28 BUGFIX: (!lhs.m_rowNode and rhs.m_rowNode) was not evaluated, triggering a segmentation fault on dereferencing
        if (static_cast<bool>(lhs.m_rowNode) != static_cast<bool>(rhs.m_rowNode)) return false;
        // ===== If execution gets here, row nodes are BOTH valid or BOTH invalid / empty
        if (not lhs.m_rowNode) return true;    // checking one for being empty is enough to know both are empty
        return *lhs.m_rowNode == *rhs.m_rowNode;
    }

    bool XLRow::isLessThan(const XLRow& lhs, const XLRow& rhs) { return *lhs.m_rowNode < *rhs.m_rowNode; }

}    // namespace OpenXLSX

namespace OpenXLSX
{
    XLRowIterator::XLRowIterator(const XLRowRange& rowRange, XLIteratorLocation loc)
        : m_dataNode(std::make_unique<XMLNode>(*rowRange.m_dataNode)),
          m_firstRow(rowRange.m_firstRow),
          m_lastRow(rowRange.m_lastRow),
          m_currentRow(),
          m_sharedStrings(rowRange.m_sharedStrings),
          m_endReached(false),
          m_hintRow(),
          m_hintRowNumber(0),
          m_currentRowStatus(XLNotLoaded),
          m_currentRowNumber(0)
    {
        if (loc == XLIteratorLocation::End)
            m_endReached = true;
        else {
            m_currentRowNumber = m_firstRow;
        }
    }

    XLRowIterator::~XLRowIterator() = default;

    XLRowIterator::XLRowIterator(const XLRowIterator& other)
        : m_dataNode(std::make_unique<XMLNode>(*other.m_dataNode)),
          m_firstRow(other.m_firstRow),
          m_lastRow(other.m_lastRow),
          m_currentRow(other.m_currentRow),
          m_sharedStrings(other.m_sharedStrings),
          m_endReached(other.m_endReached),
          m_hintRow(other.m_hintRow),
          m_hintRowNumber(other.m_hintRowNumber),
          m_currentRowStatus(other.m_currentRowStatus),
          m_currentRowNumber(other.m_currentRowNumber)
    {}

    XLRowIterator::XLRowIterator(XLRowIterator&& other) noexcept = default;

    XLRowIterator& XLRowIterator::operator=(const XLRowIterator& other)
    {
        if (&other != this) {
            auto temp = XLRowIterator(other);
            std::swap(*this, temp);
        }

        return *this;
    }

    XLRowIterator& XLRowIterator::operator=(XLRowIterator&& other) noexcept = default;

    /**
     * @brief update m_currentRow by fetching (or inserting) a row at m_currentRowNumber
     */
    void XLRowIterator::updateCurrentRow(bool createIfMissing)
    {
        // ===== Quick exit checks - can't be true when m_endReached
        if (m_currentRowStatus == XLLoaded) return;                           // nothing to do, row is already loaded
        if (!createIfMissing and m_currentRowStatus == XLNoSuchRow) return;    // nothing to do, row has already been determined as missing

        // ===== At this stage, m_currentRowStatus is XLUnloaded or XLNoSuchRow and createIfMissing == true

        if (m_endReached)
            throw XLInputError("XLRowIterator updateCurrentRow: iterator should not be dereferenced when endReached() == true");

        // ===== Row needs to be updated

        if (m_hintRow.empty()) {    // no hint has been established: fetch first row node the "tedious" way
            if (createIfMissing)    // getRowNode creates missing rows
                m_currentRow = XLRow(getRowNode(*m_dataNode, m_currentRowNumber), m_sharedStrings.get());
            else    // findRowNode returns an empty row for missing rows
                m_currentRow = XLRow(findRowNode(*m_dataNode, m_currentRowNumber), m_sharedStrings.get());
        }
        else {
            // ===== Find or create, and fetch an XLRow at m_currentRowNumber
            if (m_currentRowNumber > m_hintRowNumber) {
                // ===== Start from m_hintRow and search forwards...
                XMLNode  rowNode = m_hintRow.next_sibling_of_type(pugi::node_element);
                uint32_t rowNo   = 0;
                while (not rowNode.empty()) {
                    rowNo = static_cast<uint32_t>(rowNode.attribute("r").as_ullong());
                    if (rowNo >= m_currentRowNumber) break;    // if desired row was reached / passed, break before incrementing rowNode
                    rowNode = rowNode.next_sibling_of_type(pugi::node_element);
                }
                if (rowNo != m_currentRowNumber)
                    rowNode = XMLNode{};    // if a higher row number was found, set empty node (means: "missing")

                // ===== Create missing row node if createIfMissing == true
                if (createIfMissing and rowNode.empty()) {
                    rowNode = m_dataNode->insert_child_after("row", m_hintRow);
                    rowNode.append_attribute("r").set_value(m_currentRowNumber);
                }
                if (rowNode.empty())           // if row could not be found / created
                    m_currentRow = XLRow{};    // make sure m_currentRow is set to an empty cell
                else
                    m_currentRow = XLRow(rowNode, m_sharedStrings.get());
            }
            else
                throw XLInternalError("XLRowIterator::updateCurrentRow: an internal error occured (m_currentRowNumber <= m_hintRowNumber)");
        }

        if (m_currentRow.empty())                // if row is confirmed missing
            m_currentRowStatus = XLNoSuchRow;    // mark this status for further calls to updateCurrentRow()
        else {
            // ===== If the current row exists, update the hints
            m_hintRow          = *m_currentRow.m_rowNode;    // don't store a full XLRow, just the XMLNode, for better performance
            m_hintRowNumber    = m_currentRowNumber;
            m_currentRowStatus = XLLoaded;    // mark row status for further calls to updateCurrentRow()
        }
    }

    XLRowIterator& XLRowIterator::operator++()    // 2024-04-29: patched for whitespace
    {
        if (m_endReached) throw XLInputError("XLRowIterator: tried to increment beyond end operator");

        if (m_currentRowNumber < m_lastRow)
            ++m_currentRowNumber;
        else
            m_endReached = true;

        m_currentRowStatus = XLNotLoaded;    // trigger a new attempt to locate / create the row via updateRowCell

        return *this;
    }

    XLRowIterator XLRowIterator::operator++(int)    // NOLINT
    {
        auto oldIter(*this);
        ++(*this);
        return oldIter;
    }

    XLRow& XLRowIterator::operator*()
    {
        updateCurrentRow(XLCreateIfMissing);
        return m_currentRow;
    }

    XLRowIterator::pointer XLRowIterator::operator->()
    {
        updateCurrentRow(XLCreateIfMissing);
        return &m_currentRow;
    }

    bool XLRowIterator::operator==(const XLRowIterator& rhs) const
    {
        if (m_endReached and rhs.m_endReached) return true;    // If both iterators are end iterators

        if (m_currentRowNumber != rhs.m_currentRowNumber)    // If iterators point to a different row
            return false;                                    // that means no match

        // Check if iterators belong to the same underlying XML document node (same worksheet)
        if (m_dataNode && rhs.m_dataNode && *m_dataNode != *rhs.m_dataNode)
            return false;
        
        // Check if iterators were created with the same range boundaries
        if (m_firstRow != rhs.m_firstRow || m_lastRow != rhs.m_lastRow)
            return false;

        return true;
    }

    bool XLRowIterator::operator!=(const XLRowIterator& rhs) const { return !(*this == rhs); }

    XLRowIterator::operator bool() const { return false; }

    bool XLRowIterator::rowExists()
    {
        // ===== Update m_currentRow once so that rowExists will always test the correct cell (an empty row if current row doesn't exist)
        updateCurrentRow(XLDoNotCreateIfMissing);
        return not m_currentRow.empty();
    }

}    // namespace OpenXLSX

namespace OpenXLSX
{
    XLRowRange::XLRowRange(const XMLNode& dataNode, uint32_t first, uint32_t last, const XLSharedStrings& sharedStrings)
        : m_dataNode(std::make_unique<XMLNode>(dataNode)),
          m_firstRow(first),
          m_lastRow(last),
          m_sharedStrings(sharedStrings)
    {}

    XLRowRange::XLRowRange(const XLRowRange& other)
        : m_dataNode(std::make_unique<XMLNode>(*other.m_dataNode)),
          m_firstRow(other.m_firstRow),
          m_lastRow(other.m_lastRow),
          m_sharedStrings(other.m_sharedStrings)
    {}

    XLRowRange::XLRowRange(XLRowRange&& other) noexcept = default;

    XLRowRange::~XLRowRange() = default;

    XLRowRange& XLRowRange::operator=(const XLRowRange& other)
    {
        if (&other != this) {
            auto temp = XLRowRange(other);
            std::swap(*this, temp);
        }

        return *this;
    }

    XLRowRange& XLRowRange::operator=(XLRowRange&& other) noexcept = default;

    uint32_t XLRowRange::rowCount() const { return m_lastRow - m_firstRow + 1; }

    XLRowIterator XLRowRange::begin() { return XLRowIterator(*this, XLIteratorLocation::Begin); }

    XLRowIterator XLRowRange::end() { return XLRowIterator(*this, XLIteratorLocation::End); }

}    // namespace OpenXLSX
