// xlsxworksheet.cpp

#include "xlsxworksheet.h"

#include "xlsxcell.h"
#include "xlsxcell_p.h"
#include "xlsxcellformula.h"
#include "xlsxcellformula_p.h"
#include "xlsxcelllocation.h"
#include "xlsxcellrange.h"
#include "xlsxcellreference.h"
#include "xlsxchart.h"
#include "xlsxconditionalformatting_p.h"
#include "xlsxdrawing_p.h"
#include "xlsxdrawinganchor_p.h"
#include "xlsxformat.h"
#include "xlsxformat_p.h"
#include "xlsxrichstring.h"
#include "xlsxsharedstrings_p.h"
#include "xlsxstyles_p.h"
#include "xlsxutility_p.h"
#include "xlsxworkbook.h"
#include "xlsxworksheet_p.h"

#include <cmath>

#include <QBuffer>
#include <QDate>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QMap>
#include <QMapIterator>
#include <QPoint>
#include <QTextDocument>
#include <QTime>
#include <QUrl>
#include <QVariant>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QtGlobal>

QT_BEGIN_NAMESPACE_XLSX

WorksheetPrivate::WorksheetPrivate(Worksheet *p, Worksheet::CreateFlag flag)
    : AbstractSheetPrivate(p, flag)
    , windowProtection(false)
    , showFormulas(false)
    , showGridLines(true)
    , showRowColHeaders(true)
    , showZeros(true)
    , rightToLeft(false)
    , tabSelected(false)
    , showRuler(false)
    , showOutlineSymbols(true)
    , showWhiteSpace(true)
    , urlPattern(QStringLiteral("^([fh]tt?ps?://)|(mailto:)|(file://)"))
{
}

WorksheetPrivate::~WorksheetPrivate()
{
}

/*
  Calculate the "spans" attribute of the <row> tag. This is an
  XLSX optimisation and isn't strictly required. However, it
  makes comparing files easier. The span is the same for each
  block of 16 rows.
 */
void WorksheetPrivate::calculateSpans() const
{
    row_spans.clear();
    int span_min = XLSX_COLUMN_MAX + 1;
    int span_max = -1;

    for (int row_num = dimension.firstRow(); row_num <= dimension.lastRow(); row_num++) {
        auto it = cellTable.constFind(row_num);
        if (it != cellTable.constEnd()) {
            for (int col_num = dimension.firstColumn(); col_num <= dimension.lastColumn();
                 col_num++) {
                if (it->contains(col_num)) {
                    if (span_max == -1) {
                        span_min = col_num;
                        span_max = col_num;
                    } else {
                        if (col_num < span_min)
                            span_min = col_num;
                        else if (col_num > span_max)
                            span_max = col_num;
                    }
                }
            }
        }
        auto cIt = comments.constFind(row_num);
        if (cIt != comments.constEnd()) {
            for (int col_num = dimension.firstColumn(); col_num <= dimension.lastColumn();
                 col_num++) {
                if (cIt->contains(col_num)) {
                    if (span_max == -1) {
                        span_min = col_num;
                        span_max = col_num;
                    } else {
                        if (col_num < span_min)
                            span_min = col_num;
                        else if (col_num > span_max)
                            span_max = col_num;
                    }
                }
            }
        }

        if (row_num % 16 == 0 || row_num == dimension.lastRow()) {
            if (span_max != -1) {
                row_spans[row_num / 16] = QStringLiteral("%1:%2").arg(span_min).arg(span_max);
                span_min                = XLSX_COLUMN_MAX + 1;
                span_max                = -1;
            }
        }
    }
}

QString WorksheetPrivate::generateDimensionString() const
{
    if (!dimension.isValid())
        return QStringLiteral("A1");
    else
        return dimension.toString();
}

/*
  Check that row and col are valid and store the max and min
  values for use in other methods/elements. The ignore_row /
  ignore_col flags is used to indicate that we wish to perform
  the dimension check without storing the value. The ignore
  flags are use by setRow() and dataValidate.
*/
int WorksheetPrivate::checkDimensions(int row, int col, bool ignore_row, bool ignore_col)
{
    Q_ASSERT_X(row != 0, "checkDimensions", "row should start from 1 instead of 0");
    Q_ASSERT_X(col != 0, "checkDimensions", "column should start from 1 instead of 0");

    if (row > XLSX_ROW_MAX || row < 1 || col > XLSX_COLUMN_MAX || col < 1)
        return -1;

    if (!ignore_row) {
        if (row < dimension.firstRow() || dimension.firstRow() == -1)
            dimension.setFirstRow(row);
        if (row > dimension.lastRow())
            dimension.setLastRow(row);
    }
    if (!ignore_col) {
        if (col < dimension.firstColumn() || dimension.firstColumn() == -1)
            dimension.setFirstColumn(col);
        if (col > dimension.lastColumn())
            dimension.setLastColumn(col);
    }

    return 0;
}

/*!
  \class Worksheet
  \inmodule QtXlsx
  \brief Represent one worksheet in the workbook.
*/

/*!
 * \internal
 */
Worksheet::Worksheet(const QString &name, int id, Workbook *workbook, CreateFlag flag)
    : AbstractSheet(name, id, workbook, new WorksheetPrivate(this, flag))
{
    if (!workbook) // For unit test propose only. Ignore the memory leak.
        d_func()->workbook = new Workbook(flag);
}

/*!
 * \internal
 *
 * Make a copy of this sheet.
 */

Worksheet *Worksheet::copy(const QString &distName, int distId) const
{
    Q_D(const Worksheet);
    Worksheet *sheet          = new Worksheet(distName, distId, d->workbook, F_NewFromScratch);
    WorksheetPrivate *sheet_d = sheet->d_func();

    sheet_d->dimension = d->dimension;

    QMapIterator<int, QMap<int, std::shared_ptr<Cell>>> it(d->cellTable);
    while (it.hasNext()) {
        it.next();
        int row = it.key();
        QMapIterator<int, std::shared_ptr<Cell>> it2(it.value());
        while (it2.hasNext()) {
            it2.next();
            int col = it2.key();

            auto cell           = std::make_shared<Cell>(it2.value().get());
            cell->d_ptr->parent = sheet;

            if (cell->cellType() == Cell::SharedStringType)
                d->workbook->sharedStrings()->addSharedString(cell->d_ptr->richString);

            sheet_d->cellTable[row][col] = cell;
        }
    }

    sheet_d->merges = d->merges;
    //    sheet_d->rowsInfo = d->rowsInfo;
    //    sheet_d->colsInfo = d->colsInfo;
    //    sheet_d->colsInfoHelper = d->colsInfoHelper;
    //    sheet_d->dataValidationsList = d->dataValidationsList;
    //    sheet_d->conditionalFormattingList = d->conditionalFormattingList;

    return sheet;
}

/*!
 * Destroys this workssheet.
 */
Worksheet::~Worksheet()
{
}

/*!
 * Returns whether sheet is protected.
 */
bool Worksheet::isWindowProtected() const
{
    Q_D(const Worksheet);
    return d->windowProtection;
}

/*!
 * Protects/unprotects the sheet based on \a protect.
 */
void Worksheet::setWindowProtected(bool protect)
{
    Q_D(Worksheet);
    d->windowProtection = protect;
}

/*!
 * Return whether formulas instead of their calculated results shown in cells
 */
bool Worksheet::isFormulasVisible() const
{
    Q_D(const Worksheet);
    return d->showFormulas;
}

/*!
 * Show formulas in cells instead of their calculated results when \a visible is true.
 */
void Worksheet::setFormulasVisible(bool visible)
{
    Q_D(Worksheet);
    d->showFormulas = visible;
}

/*!
 * Return whether gridlines is shown or not.
 */
bool Worksheet::isGridLinesVisible() const
{
    Q_D(const Worksheet);
    return d->showGridLines;
}

/*!
 * Show or hide the gridline based on \a visible
 */
void Worksheet::setGridLinesVisible(bool visible)
{
    Q_D(Worksheet);
    d->showGridLines = visible;
}

/*!
 * Return whether is row and column headers is vislbe.
 */
bool Worksheet::isRowColumnHeadersVisible() const
{
    Q_D(const Worksheet);
    return d->showRowColHeaders;
}

/*!
 * Show or hide the row column headers based on \a visible
 */
void Worksheet::setRowColumnHeadersVisible(bool visible)
{
    Q_D(Worksheet);
    d->showRowColHeaders = visible;
}

/*!
 * Return whether the sheet is shown right-to-left or not.
 */
bool Worksheet::isRightToLeft() const
{
    Q_D(const Worksheet);
    return d->rightToLeft;
}

/*!
 * Enable or disable the right-to-left based on \a enable.
 */
void Worksheet::setRightToLeft(bool enable)
{
    Q_D(Worksheet);
    d->rightToLeft = enable;
}

/*!
 * Return whether is cells that have zero value show a zero.
 */
bool Worksheet::isZerosVisible() const
{
    Q_D(const Worksheet);
    return d->showZeros;
}

/*!
 * Show a zero in cells that have zero value if \a visible is true.
 */
void Worksheet::setZerosVisible(bool visible)
{
    Q_D(Worksheet);
    d->showZeros = visible;
}

/*!
 * Return whether this tab is selected.
 */
bool Worksheet::isSelected() const
{
    Q_D(const Worksheet);
    return d->tabSelected;
}

/*!
 * Select this sheet if \a select is true.
 */
void Worksheet::setSelected(bool select)
{
    Q_D(Worksheet);
    d->tabSelected = select;
}

/*!
 * Return whether is ruler is shown.
 */
bool Worksheet::isRulerVisible() const
{
    Q_D(const Worksheet);
    return d->showRuler;
}

/*!
 * Show or hide the ruler based on \a visible.
 */
void Worksheet::setRulerVisible(bool visible)
{
    Q_D(Worksheet);
    d->showRuler = visible;
}

/*!
 * Return whether is outline symbols is shown.
 */
bool Worksheet::isOutlineSymbolsVisible() const
{
    Q_D(const Worksheet);
    return d->showOutlineSymbols;
}

/*!
 * Show or hide the outline symbols based ib \a visible.
 */
void Worksheet::setOutlineSymbolsVisible(bool visible)
{
    Q_D(Worksheet);
    d->showOutlineSymbols = visible;
}

/*!
 * Return whether is white space is shown.
 */
bool Worksheet::isWhiteSpaceVisible() const
{
    Q_D(const Worksheet);
    return d->showWhiteSpace;
}

/*!
 * Show or hide the white space based on \a visible.
 */
void Worksheet::setWhiteSpaceVisible(bool visible)
{
    Q_D(Worksheet);
    d->showWhiteSpace = visible;
}

/*!
 * Write \a value to cell (\a row, \a column) with the \a format.
 * Both \a row and \a column are all 1-indexed value.
 *
 * Returns true on success.
 */
bool Worksheet::write(int row, int column, const QVariant &value, const Format &format)
{
    Q_D(Worksheet);

    if (d->checkDimensions(row, column))
        return false;

    bool ret = true;
    if (value.isNull()) {
        // Blank
        ret = writeBlank(row, column, format);
    } else if (value.userType() == QMetaType::QString) {
        // String
        QString token = value.toString();
        bool ok;

        if (token.startsWith(QLatin1String("="))) {
            // convert to formula
            ret = writeFormula(row, column, CellFormula(token), format);
        } else if (d->workbook->isStringsToHyperlinksEnabled() && token.contains(d->urlPattern)) {
            // convert to url
            ret = writeHyperlink(row, column, QUrl(token));
        } else if (d->workbook->isStringsToNumbersEnabled() && (value.toDouble(&ok), ok)) {
            // Try convert string to number if the flag enabled.
            ret = writeNumeric(row, column, value.toDouble(), format);
        } else {
            // normal string now
            ret = writeString(row, column, token, format);
        }
    } else if (value.userType() == qMetaTypeId<RichString>()) {
        ret = writeString(row, column, value.value<RichString>(), format);
    } else if (value.userType() == QMetaType::Int || value.userType() == QMetaType::UInt ||
               value.userType() == QMetaType::LongLong ||
               value.userType() == QMetaType::ULongLong || value.userType() == QMetaType::Double ||
               value.userType() == QMetaType::Float) {
        // Number

        ret = writeNumeric(row, column, value.toDouble(), format);
    } else if (value.userType() == QMetaType::Bool) {
        // Bool
        ret = writeBool(row, column, value.toBool(), format);
    } else if (value.userType() == QMetaType::QDateTime) // dev67
    {
        // DateTime, Date
        //   note that, QTime can't convert to QDateTime
        ret = writeDateTime(row, column, value.toDateTime(), format);
    } else if (value.userType() == QMetaType::QDate) // dev67
    {
        ret = writeDate(row, column, value.toDate(), format);
    } else if (value.userType() == QMetaType::QTime) {
        // Time
        ret = writeTime(row, column, value.toTime(), format);
    } else if (value.userType() == QMetaType::QUrl) {
        // Url
        ret = writeHyperlink(row, column, value.toUrl(), format);
    } else {
        // Wrong type
        return false;
    }

    return ret;
}

/*!
 * \overload
 * Write \a value to cell \a row_column with the \a format.
 * Both row and column are all 1-indexed value.
 * Returns true on success.
 */
bool Worksheet::write(const CellReference &row_column, const QVariant &value, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return write(row_column.row(), row_column.column(), value, format);
}

/*!
        \overload
        Return the contents of the cell \a row_column.
 */
QVariant Worksheet::read(const CellReference &row_column) const
{
    if (!row_column.isValid())
        return QVariant();

    return read(row_column.row(), row_column.column());
}

/*!
        Return the contents of the cell (\a row, \a column).
 */
QVariant Worksheet::read(int row, int column) const
{
    Q_D(const Worksheet);

    Cell *cell = cellAt(row, column);
    if (!cell)
        return QVariant();

    if (cell->hasFormula()) {
        if (cell->formula().formulaType() == CellFormula::NormalType) {
            return QVariant(QLatin1String("=") + cell->formula().formulaText());
        } else if (cell->formula().formulaType() == CellFormula::SharedType) {
            if (!cell->formula().formulaText().isEmpty()) {
                return QVariant(QLatin1String("=") + cell->formula().formulaText());
            } else {
                int si                         = cell->formula().sharedIndex();
                const CellFormula &rootFormula = d->sharedFormulaMap[si];
                CellReference rootCellRef      = rootFormula.reference().topLeft();
                QString rootFormulaText        = rootFormula.formulaText();
                QString newFormulaText =
                    convertSharedFormula(rootFormulaText, rootCellRef, CellReference(row, column));
                return QVariant(QLatin1String("=") + newFormulaText);
            }
        }
    }

    if (cell->isDateTime()) {
        QVariant vDateTime = cell->dateTime();
        return vDateTime;
    }

    return cell->value();
}

/*!
 * Returns the cell at the given \a row_column. If there
 * is no cell at the specified position, the function returns 0.
 */
Cell *Worksheet::cellAt(const CellReference &row_column) const
{
    if (!row_column.isValid())
        return nullptr;

    return cellAt(row_column.row(), row_column.column());
}

/*!
 * Returns the cell at the given \a row and \a column. If there
 * is no cell at the specified position, the function returns 0.
 */
Cell *Worksheet::cellAt(int row, int col) const
{
    Q_D(const Worksheet);
    auto it = d->cellTable.constFind(row);
    if (it == d->cellTable.constEnd())
        return nullptr;
    if (!it->contains(col))
        return nullptr;

    return (*it)[col].get();
}

Format WorksheetPrivate::cellFormat(int row, int col) const
{
    auto it = cellTable.constFind(row);
    if (it == cellTable.constEnd())
        return Format();
    if (!it->contains(col))
        return Format();
    return (*it)[col]->format();
}

/*!
  \overload
  Write string \a value to the cell \a row_column with the \a format.

  Returns true on success.
 */
bool Worksheet::writeString(const CellReference &row_column,
                            const RichString &value,
                            const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeString(row_column.row(), row_column.column(), value, format);
}

/*!
  Write string \a value to the cell (\a row, \a column) with the \a format.
  Returns true on success.
*/
bool Worksheet::writeString(int row, int column, const RichString &value, const Format &format)
{
    Q_D(Worksheet);
    //    QString content = value.toPlainString();
    if (d->checkDimensions(row, column))
        return false;

    //    if (content.size() > d->xls_strmax) {
    //        content = content.left(d->xls_strmax);
    //        error = -2;
    //    }

    d->sharedStrings()->addSharedString(value);
    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    if (value.fragmentCount() == 1 && value.fragmentFormat(0).isValid())
        fmt.mergeFormat(value.fragmentFormat(0));
    d->workbook->styles()->addXfFormat(fmt);
    auto cell = std::make_shared<Cell>(value.toPlainString(), Cell::SharedStringType, fmt, this);
    cell->d_ptr->richString   = value;
    d->cellTable[row][column] = cell;
    return true;
}

/*!
        \overload
        Write string \a value to the cell \a row_column with the \a format.
 */
bool Worksheet::writeString(const CellReference &row_column,
                            const QString &value,
                            const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeString(row_column.row(), row_column.column(), value, format);
}

/*!
        \overload

        Write string \a value to the cell (\a row, \a column) with the \a format.
        Returns true on success.
*/
bool Worksheet::writeString(int row, int column, const QString &value, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    RichString rs;
    if (d->workbook->isHtmlToRichStringEnabled() && Qt::mightBeRichText(value))
        rs.setHtml(value);
    else
        rs.addFragment(value, Format());

    return writeString(row, column, rs, format);
}

/*!
        \overload
        Write string \a value to the cell \a row_column with the \a format
 */
bool Worksheet::writeInlineString(const CellReference &row_column,
                                  const QString &value,
                                  const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeInlineString(row_column.row(), row_column.column(), value, format);
}

/*!
        Write string \a value to the cell (\a row, \a column) with the \a format.
        Returns true on success.
*/
bool Worksheet::writeInlineString(int row, int column, const QString &value, const Format &format)
{
    Q_D(Worksheet);
    // int error = 0;
    QString content = value;
    if (d->checkDimensions(row, column))
        return false;

    if (value.size() > XLSX_STRING_MAX) {
        content = value.left(XLSX_STRING_MAX);
        // error = -2;
    }

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    d->workbook->styles()->addXfFormat(fmt);
    d->cellTable[row][column] = std::make_shared<Cell>(value, Cell::InlineStringType, fmt, this);
    return true;
}

/*!
        \overload
        Write numeric \a value to the cell \a row_column with the \a format.
        Returns true on success.
 */
bool Worksheet::writeNumeric(const CellReference &row_column, double value, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeNumeric(row_column.row(), row_column.column(), value, format);
}

/*!
        Write numeric \a value to the cell (\a row, \a column) with the \a format.
        Returns true on success.
*/
bool Worksheet::writeNumeric(int row, int column, double value, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    d->workbook->styles()->addXfFormat(fmt);
    d->cellTable[row][column] = std::make_shared<Cell>(value, Cell::NumberType, fmt, this);
    return true;
}

/*!
        \overload
        Write \a formula to the cell \a row_column with the \a format and \a result.
        Returns true on success.
 */
bool Worksheet::writeFormula(const CellReference &row_column,
                             const CellFormula &formula,
                             const Format &format,
                             double result)
{
    if (!row_column.isValid())
        return false;

    return writeFormula(row_column.row(), row_column.column(), formula, format, result);
}

/*!
        Write \a formula_ to the cell (\a row, \a column) with the \a format and \a result.
        Returns true on success.
*/
bool Worksheet::writeFormula(int row,
                             int column,
                             const CellFormula &formula_,
                             const Format &format,
                             double result)
{
    Q_D(Worksheet);

    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    d->workbook->styles()->addXfFormat(fmt);

    CellFormula formula = formula_;
    formula.d->ca       = true;
    if (formula.formulaType() == CellFormula::SharedType) {
        // Assign proper shared index for shared formula
        int si = 0;
        while (d->sharedFormulaMap.contains(si)) {
            ++si;
        }
        formula.d->si           = si;
        d->sharedFormulaMap[si] = formula;
    }

    auto data                 = std::make_shared<Cell>(result, Cell::NumberType, fmt, this);
    data->d_ptr->formula      = formula;
    d->cellTable[row][column] = data;

    CellRange range = formula.reference();
    if (formula.formulaType() == CellFormula::SharedType) {
        CellFormula sf(QString(), CellFormula::SharedType);
        sf.d->si = formula.sharedIndex();
        for (int r = range.firstRow(); r <= range.lastRow(); ++r) {
            for (int c = range.firstColumn(); c <= range.lastColumn(); ++c) {
                if (!(r == row && c == column)) {
                    if (Cell *cell = cellAt(r, c)) {
                        cell->d_ptr->formula = sf;
                    } else {
                        auto newCell = std::make_shared<Cell>(result, Cell::NumberType, fmt, this);
                        newCell->d_ptr->formula = sf;
                        d->cellTable[r][c]      = newCell;
                    }
                }
            }
        }
    }

    return true;
}

/*!
        \overload
        Write a empty cell \a row_column with the \a format.
        Returns true on success.
 */
bool Worksheet::writeBlank(const CellReference &row_column, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeBlank(row_column.row(), row_column.column(), format);
}

/*!
        Write a empty cell (\a row, \a column) with the \a format.
        Returns true on success.
 */
bool Worksheet::writeBlank(int row, int column, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    d->workbook->styles()->addXfFormat(fmt);

    // Note: NumberType with an invalid QVariant value means blank.
    d->cellTable[row][column] = std::make_shared<Cell>(QVariant{}, Cell::NumberType, fmt, this);

    return true;
}
/*!
        \overload
        Write a bool \a value to the cell \a row_column with the \a format.
        Returns true on success.
 */
bool Worksheet::writeBool(const CellReference &row_column, bool value, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeBool(row_column.row(), row_column.column(), value, format);
}

/*!
        Write a bool \a value to the cell (\a row, \a column) with the \a format.
        Returns true on success.
 */
bool Worksheet::writeBool(int row, int column, bool value, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    d->workbook->styles()->addXfFormat(fmt);
    d->cellTable[row][column] = std::make_shared<Cell>(value, Cell::BooleanType, fmt, this);

    return true;
}
/*!
        \overload
        Write a QDateTime \a dt to the cell \a row_column with the \a format.
        Returns true on success.
 */
bool Worksheet::writeDateTime(const CellReference &row_column,
                              const QDateTime &dt,
                              const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeDateTime(row_column.row(), row_column.column(), dt, format);
}

/*!
        Write a QDateTime \a dt to the cell (\a row, \a column) with the \a format.
        Returns true on success.
 */
bool Worksheet::writeDateTime(int row, int column, const QDateTime &dt, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    if (!fmt.isValid() || !fmt.isDateTimeFormat())
        fmt.setNumberFormat(d->workbook->defaultDateFormat());
    d->workbook->styles()->addXfFormat(fmt);

    double value = datetimeToNumber(dt, d->workbook->isDate1904());

    d->cellTable[row][column] = std::make_shared<Cell>(value, Cell::NumberType, fmt, this);

    return true;
}

// dev67
bool Worksheet::writeDate(const CellReference &row_column, const QDate &dt, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeDate(row_column.row(), row_column.column(), dt, format);
}

// dev67
bool Worksheet::writeDate(int row, int column, const QDate &dt, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);

    if (!fmt.isValid() || !fmt.isDateTimeFormat())
        fmt.setNumberFormat(d->workbook->defaultDateFormat());

    d->workbook->styles()->addXfFormat(fmt);

    double value = datetimeToNumber(QDateTime(dt, QTime(0, 0, 0)), d->workbook->isDate1904());

    d->cellTable[row][column] = std::make_shared<Cell>(value, Cell::NumberType, fmt, this);

    return true;
}

/*!
        \overload
        Write a QTime \a t to the cell \a row_column with the \a format.
        Returns true on success.
 */
bool Worksheet::writeTime(const CellReference &row_column, const QTime &t, const Format &format)
{
    if (!row_column.isValid())
        return false;

    return writeTime(row_column.row(), row_column.column(), t, format);
}

/*!
        Write a QTime \a t to the cell (\a row, \a column) with the \a format.
        Returns true on success.
 */
bool Worksheet::writeTime(int row, int column, const QTime &t, const Format &format)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    if (!fmt.isValid() || !fmt.isDateTimeFormat())
        fmt.setNumberFormat(QStringLiteral("hh:mm:ss"));
    d->workbook->styles()->addXfFormat(fmt);

    d->cellTable[row][column] =
        std::make_shared<Cell>(timeToNumber(t), Cell::NumberType, fmt, this);

    return true;
}

/*!
        \overload
        Write a QUrl \a url to the cell \a row_column with the given \a format \a display and \a
   tip. Returns true on success.
 */
bool Worksheet::writeHyperlink(const CellReference &row_column,
                               const QUrl &url,
                               const Format &format,
                               const QString &display,
                               const QString &tip)
{
    if (!row_column.isValid())
        return false;

    return writeHyperlink(row_column.row(), row_column.column(), url, format, display, tip);
}

/*!
        Write a QUrl \a url to the cell (\a row, \a column) with the given \a format \a display and
   \a tip. Returns true on success.
 */
bool Worksheet::writeHyperlink(int row,
                               int column,
                               const QUrl &url,
                               const Format &format,
                               const QString &display,
                               const QString &tip)
{
    Q_D(Worksheet);
    if (d->checkDimensions(row, column))
        return false;

    // int error = 0;

    QString urlString = url.toString();

    // Generate proper display string
    QString displayString = display.isEmpty() ? urlString : display;
    if (displayString.startsWith(QLatin1String("mailto:")))
        displayString.replace(QLatin1String("mailto:"), QString());
    if (displayString.size() > XLSX_STRING_MAX) {
        displayString = displayString.left(XLSX_STRING_MAX);
        // error = -2;
    }

    /*
      Location within target. If target is a workbook (or this workbook)
      this shall refer to a sheet and cell or a defined name. Can also
      be an HTML anchor if target is HTML file.

      c:\temp\file.xlsx#Sheet!A1
      http://a.com/aaa.html#aaaaa
    */
    QString locationString;
    if (url.hasFragment()) {
        locationString = url.fragment();
        urlString      = url.toString(QUrl::RemoveFragment);
    }

    Format fmt = format.isValid() ? format : d->cellFormat(row, column);
    // Given a default style for hyperlink
    if (!fmt.isValid()) {
        fmt.setVerticalAlignment(Format::AlignVCenter);
        fmt.setFontColor(Qt::blue);
        fmt.setFontUnderline(Format::FontUnderlineSingle);
    }
    d->workbook->styles()->addXfFormat(fmt);

    // Write the hyperlink string as normal string.
    d->sharedStrings()->addSharedString(displayString);
    d->cellTable[row][column] =
        std::make_shared<Cell>(displayString, Cell::SharedStringType, fmt, this);

    // Store the hyperlink data in a separate table
    d->urlTable[row][column] = QSharedPointer<XlsxHyperlinkData>(new XlsxHyperlinkData(
        XlsxHyperlinkData::External, urlString, locationString, QString(), tip));

    return true;
}

/*!
 * Add one DataValidation \a validation to the sheet.
 * Returns true on success.
 */
bool Worksheet::addDataValidation(const DataValidation &validation)
{
    Q_D(Worksheet);
    if (validation.ranges().isEmpty() || validation.validationType() == DataValidation::None)
        return false;

    d->dataValidationsList.append(validation);
    return true;
}

/*!
 * Add one ConditionalFormatting \a cf to the sheet.
 * Returns true on success.
 */
bool Worksheet::addConditionalFormatting(const ConditionalFormatting &cf)
{
    Q_D(Worksheet);
    if (cf.ranges().isEmpty())
        return false;

    for (int i = 0; i < cf.d->cfRules.size(); ++i) {
        const std::shared_ptr<XlsxCfRuleData> &rule = cf.d->cfRules[i];
        if (!rule->dxfFormat.isEmpty())
            d->workbook->styles()->addDxfFormat(rule->dxfFormat);
        rule->priority = 1;
    }
    d->conditionalFormattingList.append(cf);
    return true;
}

/*!
 * Insert an \a image  at the position \a row, \a column
 * Returns true on success.
 */
int Worksheet::insertImage(int row, int column, const QImage &image)
{
    Q_D(Worksheet);

    int imageIndex = 0;

    if (image.isNull())
        return imageIndex;

    if (!d->drawing) {
        d->drawing = std::make_shared<Drawing>(this, F_NewFromScratch);
    }

    DrawingOneCellAnchor *anchor =
        new DrawingOneCellAnchor(d->drawing.get(), DrawingAnchor::Picture);

    /*
            The size are expressed as English Metric Units (EMUs).
            EMU is 1/360 000 of centimiter.
    */
    anchor->from = XlsxMarker(row, column, 0, 0);
    float scaleX = 36e6f / std::max(1, image.dotsPerMeterX());
    float scaleY = 36e6f / std::max(1, image.dotsPerMeterY());
    anchor->ext  = QSize(int(image.width() * scaleX), int(image.height() * scaleY));

    anchor->setObjectPicture(image);

    imageIndex = anchor->getm_id();

    return imageIndex;
}

bool Worksheet::getImage(int imageIndex, QImage &img)
{
    Q_D(Worksheet);

    if (imageIndex <= (-1)) {
        return false;
    }

    if (d->drawing == nullptr) {
        return false;
    }

    int realImageIndex = imageIndex - 1; // minus one

    DrawingAnchor *danchor = d->drawing->anchors.at(realImageIndex);
    // QSharedPointer<Drawing> // for multithread
    if (danchor == nullptr) {
        return false;
    }

    bool ret = danchor->getObjectPicture(img);
    return ret;
}

bool Worksheet::getImage(int row, int column, QImage &img)
{
    Q_D(Worksheet);

    if (d->drawing == nullptr) {
        return false;
    }

    for (int i = 0; i < d->drawing->anchors.size(); i++) {
        if (d->drawing->anchors[i]->row() == row && d->drawing->anchors[i]->col() == column) {
            DrawingAnchor *danchor = d->drawing->anchors.at(i);

            if (danchor == nullptr) {
                return false;
            }

            bool ret = danchor->getObjectPicture(img);
            return ret;
        }
    }
    return false;
}

uint Worksheet::getImageCount()
{
    Q_D(Worksheet);

    if (d->drawing == nullptr) {
        return false;
    }

    int size = d->drawing->anchors.size();
    return uint(size);
}

/*!
 * Creates an chart with the given \a size and insert
 * at the position \a row, \a column.
 * The chart will be returned.
 */
Chart *Worksheet::insertChart(int row, int column, const QSize &size)
{
    Q_D(Worksheet);

    if (!d->drawing)
        d->drawing = std::make_shared<Drawing>(this, F_NewFromScratch);

    DrawingOneCellAnchor *anchor =
        new DrawingOneCellAnchor(d->drawing.get(), DrawingAnchor::Picture);

    /*
            The size are expressed as English Metric Units (EMUs). There are
            12,700 EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per
            pixel
    */
    anchor->from = XlsxMarker(row, column, 0, 0);
    anchor->ext  = size * 9525;

    QSharedPointer<Chart> chart = QSharedPointer<Chart>(new Chart(this, F_NewFromScratch));
    anchor->setObjectGraphicFrame(chart);

    return chart.data();
}

/*!
        Merge a \a range of cells. The first cell should contain the data and the others should
        be blank. All cells will be applied the same style if a valid \a format is given.
        Returns true on success.

        \note All cells except the top-left one will be cleared.
 */
bool Worksheet::mergeCells(const CellRange &range, const Format &format)
{
    Q_D(Worksheet);
    if (range.rowCount() < 2 && range.columnCount() < 2)
        return false;

    if (d->checkDimensions(range.firstRow(), range.firstColumn()))
        return false;

    if (format.isValid()) {
        d->workbook->styles()->addXfFormat(format);
    }

    for (int row = range.firstRow(); row <= range.lastRow(); ++row) {
        for (int col = range.firstColumn(); col <= range.lastColumn(); ++col) {
            if (row == range.firstRow() && col == range.firstColumn()) {
                Cell *cell = cellAt(row, col);
                if (cell) {
                    if (format.isValid())
                        cell->d_ptr->format = format;
                } else {
                    writeBlank(row, col, format);
                }
            } else {
                writeBlank(row, col, format);
            }
        }
    }

    d->merges.append(range);
    return true;
}

/*!
        Unmerge the cells in the \a range. Returns true on success.

*/
bool Worksheet::unmergeCells(const CellRange &range)
{
    Q_D(Worksheet);
    return d->merges.removeOne(range);
}

/*!
  Returns all the merged cells.
*/
QList<CellRange> Worksheet::mergedCells() const
{
    Q_D(const Worksheet);

    // dev57

    QList<CellRange> emptyList;

    if (d->type == AbstractSheet::ST_WorkSheet) {
        return d->merges;
    } else if (d->type == AbstractSheet::ST_ChartSheet) {
    } else if (d->type == AbstractSheet::ST_DialogSheet) {
    } else if (d->type == AbstractSheet::ST_MacroSheet) {
    } else { // undefined
    }

    return emptyList;
}

/*!
 * \internal
 */
void Worksheet::saveToXmlFile(QIODevice *device) const
{
    Q_D(const Worksheet);
    d->relationships->clear();

    QXmlStreamWriter writer(device);

    writer.writeStartDocument(QStringLiteral("1.0"), true);
    writer.writeStartElement(QStringLiteral("worksheet"));
    writer.writeAttribute(
        QStringLiteral("xmlns"),
        QStringLiteral("http://schemas.openxmlformats.org/spreadsheetml/2006/main"));
    writer.writeAttribute(
        QStringLiteral("xmlns:r"),
        QStringLiteral("http://schemas.openxmlformats.org/officeDocument/2006/relationships"));

    // for Excel 2010
    //     writer.writeAttribute("xmlns:mc",
    //     "http://schemas.openxmlformats.org/markup-compatibility/2006");
    //     writer.writeAttribute("xmlns:x14ac",
    //     "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac");
    //     writer.writeAttribute("mc:Ignorable", "x14ac");

    writer.writeStartElement(QStringLiteral("dimension"));
    writer.writeAttribute(QStringLiteral("ref"), d->generateDimensionString());
    writer.writeEndElement(); // dimension

    writer.writeStartElement(QStringLiteral("sheetViews"));
    writer.writeStartElement(QStringLiteral("sheetView"));
    if (d->windowProtection)
        writer.writeAttribute(QStringLiteral("windowProtection"), QStringLiteral("1"));
    if (d->showFormulas)
        writer.writeAttribute(QStringLiteral("showFormulas"), QStringLiteral("1"));
    if (!d->showGridLines)
        writer.writeAttribute(QStringLiteral("showGridLines"), QStringLiteral("0"));
    if (!d->showRowColHeaders)
        writer.writeAttribute(QStringLiteral("showRowColHeaders"), QStringLiteral("0"));
    if (!d->showZeros)
        writer.writeAttribute(QStringLiteral("showZeros"), QStringLiteral("0"));
    if (d->rightToLeft)
        writer.writeAttribute(QStringLiteral("rightToLeft"), QStringLiteral("1"));
    if (d->tabSelected)
        writer.writeAttribute(QStringLiteral("tabSelected"), QStringLiteral("1"));
    if (!d->showRuler)
        writer.writeAttribute(QStringLiteral("showRuler"), QStringLiteral("0"));
    if (!d->showOutlineSymbols)
        writer.writeAttribute(QStringLiteral("showOutlineSymbols"), QStringLiteral("0"));
    if (!d->showWhiteSpace)
        writer.writeAttribute(QStringLiteral("showWhiteSpace"), QStringLiteral("0"));
    writer.writeAttribute(QStringLiteral("workbookViewId"), QStringLiteral("0"));
    writer.writeEndElement(); // sheetView
    writer.writeEndElement(); // sheetViews

    writer.writeStartElement(QStringLiteral("sheetFormatPr"));
    writer.writeAttribute(QStringLiteral("defaultRowHeight"),
                          QString::number(d->sheetFormatProps.defaultRowHeight));
    writer.writeAttribute(QStringLiteral("customHeight"),
                          xsdBoolean(d->sheetFormatProps.customHeight));
    writer.writeAttribute(QStringLiteral("zeroHeight"), xsdBoolean(d->sheetFormatProps.zeroHeight));
    writer.writeAttribute(QStringLiteral("outlineLevelRow"),
                          QString::number(d->sheetFormatProps.outlineLevelRow));
    writer.writeAttribute(QStringLiteral("outlineLevelCol"),
                          QString::number(d->sheetFormatProps.outlineLevelCol));
    // for Excel 2010
    //     writer.writeAttribute("x14ac:dyDescent", "0.25");
    writer.writeEndElement(); // sheetFormatPr

    if (!d->colsInfo.isEmpty()) {
        writer.writeStartElement(QStringLiteral("cols"));
        QMapIterator<int, QSharedPointer<XlsxColumnInfo>> it(d->colsInfo);
        while (it.hasNext()) {
            it.next();
            QSharedPointer<XlsxColumnInfo> col_info = it.value();
            writer.writeStartElement(QStringLiteral("col"));
            writer.writeAttribute(QStringLiteral("min"), QString::number(col_info->firstColumn));
            writer.writeAttribute(QStringLiteral("max"), QString::number(col_info->lastColumn));
            if (col_info->width)
                writer.writeAttribute(QStringLiteral("width"),
                                      QString::number(col_info->width, 'g', 15));
            if (!col_info->format.isEmpty())
                writer.writeAttribute(QStringLiteral("style"),
                                      QString::number(col_info->format.xfIndex()));
            if (col_info->hidden)
                writer.writeAttribute(QStringLiteral("hidden"), QStringLiteral("1"));
            if (col_info->width)
                writer.writeAttribute(QStringLiteral("customWidth"), QStringLiteral("1"));
            if (col_info->outlineLevel)
                writer.writeAttribute(QStringLiteral("outlineLevel"),
                                      QString::number(col_info->outlineLevel));
            if (col_info->collapsed)
                writer.writeAttribute(QStringLiteral("collapsed"), QStringLiteral("1"));
            writer.writeEndElement(); // col
        }
        writer.writeEndElement(); // cols
    }

    writer.writeStartElement(QStringLiteral("sheetData"));
    if (d->dimension.isValid())
        d->saveXmlSheetData(writer);
    writer.writeEndElement(); // sheetData

    d->saveXmlMergeCells(writer);
    for (const ConditionalFormatting &cf : d->conditionalFormattingList)
        cf.saveToXml(writer);
    d->saveXmlDataValidations(writer);

    //{{ liufeijin :  write  pagesettings  add by liufeijin 20181028

    // fixed by j2doll [dev18]
    // NOTE: empty element is not problem. but, empty structure of element is not parsed by Excel.

    // pageMargins
    if (false == d->PMleft.isEmpty() && false == d->PMright.isEmpty() &&
        false == d->PMtop.isEmpty() && false == d->PMbotton.isEmpty() &&
        false == d->PMheader.isEmpty() && false == d->PMfooter.isEmpty()) {
        writer.writeStartElement(QStringLiteral("pageMargins"));

        writer.writeAttribute(QStringLiteral("left"), d->PMleft);
        writer.writeAttribute(QStringLiteral("right"), d->PMright);
        writer.writeAttribute(QStringLiteral("top"), d->PMtop);
        writer.writeAttribute(QStringLiteral("bottom"), d->PMbotton);
        writer.writeAttribute(QStringLiteral("header"), d->PMheader);
        writer.writeAttribute(QStringLiteral("footer"), d->PMfooter);

        writer.writeEndElement(); // pageMargins
    }

    // dev57
    if (!d->Prid.isEmpty()) {
        writer.writeStartElement(QStringLiteral("pageSetup")); // pageSetup

        writer.writeAttribute(QStringLiteral("r:id"), d->Prid);

        if (!d->PverticalDpi.isEmpty()) {
            writer.writeAttribute(QStringLiteral("verticalDpi"), d->PverticalDpi);
        }

        if (!d->PhorizontalDpi.isEmpty()) {
            writer.writeAttribute(QStringLiteral("horizontalDpi"), d->PhorizontalDpi);
        }

        if (!d->PuseFirstPageNumber.isEmpty()) {
            writer.writeAttribute(QStringLiteral("useFirstPageNumber"), d->PuseFirstPageNumber);
        }

        if (!d->PfirstPageNumber.isEmpty()) {
            writer.writeAttribute(QStringLiteral("firstPageNumber"), d->PfirstPageNumber);
        }

        if (!d->Pscale.isEmpty()) {
            writer.writeAttribute(QStringLiteral("scale"), d->Pscale);
        }

        if (!d->PpaperSize.isEmpty()) {
            writer.writeAttribute(QStringLiteral("paperSize"), d->PpaperSize);
        }

        if (!d->Porientation.isEmpty()) {
            writer.writeAttribute(QStringLiteral("orientation"), d->Porientation);
        }

        if (!d->Pcopies.isEmpty()) {
            writer.writeAttribute(QStringLiteral("copies"), d->Pcopies);
        }

        writer.writeEndElement(); // pageSetup

    } // if ( !d->Prid.isEmpty() )

    // headerFooter
    if (!(d->ModdHeader.isNull()) || !(d->MoodFooter.isNull())) {
        writer.writeStartElement(QStringLiteral("headerFooter")); // headerFooter

        if (!d->MoodalignWithMargins.isEmpty()) {
            writer.writeAttribute(QStringLiteral("alignWithMargins"), d->MoodalignWithMargins);
        }

        if (!d->ModdHeader.isNull()) {
            writer.writeStartElement(QStringLiteral("oddHeader"));
            writer.writeCharacters(d->ModdHeader);
            writer.writeEndElement(); // oddHeader
        }

        if (!d->MoodFooter.isNull()) {
            writer.writeTextElement(QStringLiteral("oddFooter"), d->MoodFooter);
        }

        writer.writeEndElement(); // headerFooter
    }

    d->saveXmlHyperlinks(writer);
    d->saveXmlDrawings(writer);

    writer.writeEndElement(); // worksheet
    writer.writeEndDocument();
}

//{{ liufeijin
bool Worksheet::setStartPage(int spagen)
{
    Q_D(Worksheet);

    d->PfirstPageNumber = QString::number(spagen);

    return true;
}
//}}

void WorksheetPrivate::saveXmlSheetData(QXmlStreamWriter &writer) const
{
    calculateSpans();
    for (int row_num = dimension.firstRow(); row_num <= dimension.lastRow(); row_num++) {
        auto ctIt = cellTable.constFind(row_num);
        auto riIt = rowsInfo.constFind(row_num);
        if (ctIt == cellTable.constEnd() && riIt == rowsInfo.constEnd() &&
            !comments.contains(row_num)) {
            // Only process rows with cell data / comments / formatting
            continue;
        }

        int span_index = (row_num - 1) / 16;
        QString span;
        auto rsIt = row_spans.constFind(span_index);
        if (rsIt != row_spans.constEnd())
            span = rsIt.value();

        writer.writeStartElement(QStringLiteral("row"));
        writer.writeAttribute(QStringLiteral("r"), QString::number(row_num));

        if (!span.isEmpty())
            writer.writeAttribute(QStringLiteral("spans"), span);

        if (riIt != rowsInfo.constEnd()) {
            QSharedPointer<XlsxRowInfo> rowInfo = riIt.value();
            if (!rowInfo->format.isEmpty()) {
                writer.writeAttribute(QStringLiteral("s"),
                                      QString::number(rowInfo->format.xfIndex()));
                writer.writeAttribute(QStringLiteral("customFormat"), QStringLiteral("1"));
            }

            //! Todo: support customHeight from info struct
            //! Todo: where does this magic number '15' come from?
            if (rowInfo->customHeight) {
                writer.writeAttribute(QStringLiteral("ht"), QString::number(rowInfo->height));
                writer.writeAttribute(QStringLiteral("customHeight"), QStringLiteral("1"));
            } else {
                writer.writeAttribute(QStringLiteral("customHeight"), QStringLiteral("0"));
            }

            if (rowInfo->hidden)
                writer.writeAttribute(QStringLiteral("hidden"), QStringLiteral("1"));
            if (rowInfo->outlineLevel > 0)
                writer.writeAttribute(QStringLiteral("outlineLevel"),
                                      QString::number(rowInfo->outlineLevel));
            if (rowInfo->collapsed)
                writer.writeAttribute(QStringLiteral("collapsed"), QStringLiteral("1"));
        }

        // Write cell data if row contains filled cells
        if (ctIt != cellTable.constEnd()) {
            for (int col_num = dimension.firstColumn(); col_num <= dimension.lastColumn();
                 col_num++) {
                if (ctIt->contains(col_num)) {
                    saveXmlCellData(writer, row_num, col_num, (*ctIt)[col_num]);
                }
            }
        }
        writer.writeEndElement(); // row
    }
}

void WorksheetPrivate::saveXmlCellData(QXmlStreamWriter &writer,
                                       int row,
                                       int col,
                                       std::shared_ptr<Cell> cell) const
{
    Q_Q(const Worksheet);

    // This is the innermost loop so efficiency is important.
    QString cell_pos = CellReference(row, col).toString();

    writer.writeStartElement(QStringLiteral("c"));
    writer.writeAttribute(QStringLiteral("r"), cell_pos);

    QMap<int, QSharedPointer<XlsxRowInfo>>::ConstIterator rIt;
    QMap<int, QSharedPointer<XlsxColumnInfo>>::ConstIterator cIt;

    // Style used by the cell, row or col
    if (!cell->format().isEmpty())
        writer.writeAttribute(QStringLiteral("s"), QString::number(cell->format().xfIndex()));
    else if ((rIt = rowsInfo.constFind(row)) != rowsInfo.constEnd() && !(*rIt)->format.isEmpty())
        writer.writeAttribute(QStringLiteral("s"), QString::number((*rIt)->format.xfIndex()));
    else if ((cIt = colsInfoHelper.constFind(col)) != colsInfoHelper.constEnd() &&
             !(*cIt)->format.isEmpty())
        writer.writeAttribute(QStringLiteral("s"), QString::number((*cIt)->format.xfIndex()));

    if (cell->cellType() == Cell::SharedStringType) // 's'
    {
        int sst_idx;
        if (cell->isRichString())
            sst_idx = sharedStrings()->getSharedStringIndex(cell->d_ptr->richString);
        else
            sst_idx = sharedStrings()->getSharedStringIndex(cell->value().toString());

        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("s"));
        writer.writeTextElement(QStringLiteral("v"), QString::number(sst_idx));
    } else if (cell->cellType() == Cell::InlineStringType) // 'inlineStr'
    {
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("inlineStr"));
        writer.writeStartElement(QStringLiteral("is"));
        if (cell->isRichString()) {
            // Rich text string
            RichString string = cell->d_ptr->richString;
            for (int i = 0; i < string.fragmentCount(); ++i) {
                writer.writeStartElement(QStringLiteral("r"));
                if (string.fragmentFormat(i).hasFontData()) {
                    writer.writeStartElement(QStringLiteral("rPr"));
                    //: Todo
                    writer.writeEndElement(); // rPr
                }
                writer.writeStartElement(QStringLiteral("t"));
                if (isSpaceReserveNeeded(string.fragmentText(i)))
                    writer.writeAttribute(QStringLiteral("xml:space"), QStringLiteral("preserve"));
                writer.writeCharacters(string.fragmentText(i));
                writer.writeEndElement(); // t
                writer.writeEndElement(); // r
            }
        } else {
            writer.writeStartElement(QStringLiteral("t"));
            QString string = cell->value().toString();
            if (isSpaceReserveNeeded(string))
                writer.writeAttribute(QStringLiteral("xml:space"), QStringLiteral("preserve"));
            writer.writeCharacters(string);
            writer.writeEndElement(); // t
        }
        writer.writeEndElement();                    // is
    } else if (cell->cellType() == Cell::NumberType) // 'n'
    {
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("n")); // dev67

        if (cell->hasFormula()) {
            QString strFormula = cell->formula().d->formula;
            Q_UNUSED(strFormula);
            cell->formula().saveToXml(writer);
        }

        if (cell->value().isValid()) { // note that, invalid value means 'v' is blank
            double value = cell->value().toDouble();
            writer.writeTextElement(QStringLiteral("v"), QString::number(value, 'g', 15));
        }
    } else if (cell->cellType() == Cell::StringType) // 'str'
    {
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("str"));
        if (cell->hasFormula())
            cell->formula().saveToXml(writer);

        writer.writeTextElement(QStringLiteral("v"), cell->value().toString());
    } else if (cell->cellType() == Cell::BooleanType) // 'b'
    {
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("b"));

        // dev34

        if (cell->hasFormula()) {
            QString strFormula = cell->formula().d->formula;
            Q_UNUSED(strFormula);
            cell->formula().saveToXml(writer);
        }

        writer.writeTextElement(QStringLiteral("v"),
                                cell->value().toBool() ? QStringLiteral("1") : QStringLiteral("0"));
    } else if (cell->cellType() == Cell::DateType) // 'd'
    {
        // dev67

        double num  = cell->value().toDouble();
        bool is1904 = q->workbook()->isDate1904();
        if (!is1904 && num > 60) // for mac os excel
        {
            num = num - 1;
        }

        // number type. see for 18.18.11 ST_CellType (Cell Type) more information.
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("n"));
        writer.writeTextElement(QStringLiteral("v"), cell->value().toString());

    } else if (cell->cellType() == Cell::ErrorType) // 'e'
    {
        writer.writeAttribute(QStringLiteral("t"), QStringLiteral("e"));
        writer.writeTextElement(QStringLiteral("v"), cell->value().toString());
    } else // if (cell->cellType() == Cell::CustomType)
    {
        // custom type

        if (cell->hasFormula()) {
            QString strFormula = cell->formula().d->formula;
            Q_UNUSED(strFormula);
            cell->formula().saveToXml(writer);
        }

        if (cell->value().isValid()) { // note that, invalid value means 'v' is blank
            double value = cell->value().toDouble();
            writer.writeTextElement(QStringLiteral("v"), QString::number(value, 'g', 15));
        }
    }

    writer.writeEndElement(); // c
}

void WorksheetPrivate::saveXmlMergeCells(QXmlStreamWriter &writer) const
{
    if (merges.isEmpty())
        return;

    writer.writeStartElement(QStringLiteral("mergeCells"));
    writer.writeAttribute(QStringLiteral("count"), QString::number(merges.size()));

    for (const CellRange &range : merges) {
        writer.writeEmptyElement(QStringLiteral("mergeCell"));
        writer.writeAttribute(QStringLiteral("ref"), range.toString());
    }

    writer.writeEndElement(); // mergeCells
}

void WorksheetPrivate::saveXmlDataValidations(QXmlStreamWriter &writer) const
{
    if (dataValidationsList.isEmpty())
        return;

    writer.writeStartElement(QStringLiteral("dataValidations"));
    writer.writeAttribute(QStringLiteral("count"), QString::number(dataValidationsList.size()));

    for (const DataValidation &validation : dataValidationsList)
        validation.saveToXml(writer);

    writer.writeEndElement(); // dataValidations
}

void WorksheetPrivate::saveXmlHyperlinks(QXmlStreamWriter &writer) const
{
    if (urlTable.isEmpty())
        return;

    writer.writeStartElement(QStringLiteral("hyperlinks"));
    QMapIterator<int, QMap<int, QSharedPointer<XlsxHyperlinkData>>> it(urlTable);

    while (it.hasNext()) {
        it.next();
        int row = it.key();
        QMapIterator<int, QSharedPointer<XlsxHyperlinkData>> it2(it.value());

        while (it2.hasNext()) {
            it2.next();
            int col                                = it2.key();
            QSharedPointer<XlsxHyperlinkData> data = it2.value();
            QString ref                            = CellReference(row, col).toString();

            // dev57
            // writer.writeEmptyElement(QStringLiteral("hyperlink"));
            writer.writeStartElement(QStringLiteral("hyperlink"));

            writer.writeAttribute(QStringLiteral("ref"), ref); // required field

            if (data->linkType == XlsxHyperlinkData::External) {
                // Update relationships
                relationships->addWorksheetRelationship(
                    QStringLiteral("/hyperlink"), data->target, QStringLiteral("External"));

                writer.writeAttribute(QStringLiteral("r:id"),
                                      QStringLiteral("rId%1").arg(relationships->count()));
            }

            if (!data->location.isEmpty()) {
                writer.writeAttribute(QStringLiteral("location"), data->location);
            }

            if (!data->display.isEmpty()) {
                writer.writeAttribute(QStringLiteral("display"), data->display);
            }

            if (!data->tooltip.isEmpty()) {
                writer.writeAttribute(QStringLiteral("tooltip"), data->tooltip);
            }

            // dev57
            writer.writeEndElement(); // hyperlink
        }
    }

    writer.writeEndElement(); // hyperlinks
}

void WorksheetPrivate::saveXmlDrawings(QXmlStreamWriter &writer) const
{
    if (!drawing)
        return;

    int idx = workbook->drawings().indexOf(drawing.get());
    relationships->addWorksheetRelationship(
        QStringLiteral("/drawing"), QStringLiteral("../drawings/drawing%1.xml").arg(idx + 1));

    writer.writeEmptyElement(QStringLiteral("drawing"));
    writer.writeAttribute(QStringLiteral("r:id"),
                          QStringLiteral("rId%1").arg(relationships->count()));
}

void WorksheetPrivate::splitColsInfo(int colFirst, int colLast)
{
    // Split current columnInfo, for example, if "A:H" has been set,
    // we are trying to set "B:D", there should be "A", "B:D", "E:H".
    // This will be more complex if we try to set "C:F" after "B:D".
    {
        QMapIterator<int, QSharedPointer<XlsxColumnInfo>> it(colsInfo);
        while (it.hasNext()) {
            it.next();
            QSharedPointer<XlsxColumnInfo> info = it.value();
            if (colFirst > info->firstColumn && colFirst <= info->lastColumn) {
                // split the range,
                QSharedPointer<XlsxColumnInfo> info2(new XlsxColumnInfo(*info));
                info->lastColumn   = colFirst - 1;
                info2->firstColumn = colFirst;
                colsInfo.insert(colFirst, info2);
                for (int c = info2->firstColumn; c <= info2->lastColumn; ++c)
                    colsInfoHelper[c] = info2;

                break;
            }
        }
    }
    {
        QMapIterator<int, QSharedPointer<XlsxColumnInfo>> it(colsInfo);
        while (it.hasNext()) {
            it.next();
            QSharedPointer<XlsxColumnInfo> info = it.value();
            if (colLast >= info->firstColumn && colLast < info->lastColumn) {
                QSharedPointer<XlsxColumnInfo> info2(new XlsxColumnInfo(*info));
                info->lastColumn   = colLast;
                info2->firstColumn = colLast + 1;
                colsInfo.insert(colLast + 1, info2);
                for (int c = info2->firstColumn; c <= info2->lastColumn; ++c)
                    colsInfoHelper[c] = info2;

                break;
            }
        }
    }
}

bool WorksheetPrivate::isColumnRangeValid(int colFirst, int colLast)
{
    bool ignore_row = true;
    bool ignore_col = false;

    if (colFirst > colLast)
        return false;

    if (checkDimensions(1, colLast, ignore_row, ignore_col))
        return false;
    if (checkDimensions(1, colFirst, ignore_row, ignore_col))
        return false;

    return true;
}

QList<int> WorksheetPrivate ::getColumnIndexes(int colFirst, int colLast)
{
    splitColsInfo(colFirst, colLast);

    QList<int> nodes;
    nodes.append(colFirst);
    for (int col = colFirst; col <= colLast; ++col) {
        auto it = colsInfo.constFind(col);
        if (it != colsInfo.constEnd()) {
            if (nodes.last() != col)
                nodes.append(col);

            int nextCol = (*it)->lastColumn + 1;
            if (nextCol <= colLast)
                nodes.append(nextCol);
        }
    }

    return nodes;
}

/*!
  Sets width in characters of a \a range of columns to \a width.
  Returns true on success.
 */
bool Worksheet::setColumnWidth(const CellRange &range, double width)
{
    if (!range.isValid())
        return false;

    return setColumnWidth(range.firstColumn(), range.lastColumn(), width);
}

/*!
  Sets format property of a \a range of columns to \a format. Columns are 1-indexed.
  Returns true on success.
 */
bool Worksheet::setColumnFormat(const CellRange &range, const Format &format)
{
    if (!range.isValid())
        return false;

    return setColumnFormat(range.firstColumn(), range.lastColumn(), format);
}

/*!
  Sets hidden property of a \a range of columns to \a hidden. Columns are 1-indexed.
  Hidden columns are not visible.
  Returns true on success.
 */
bool Worksheet::setColumnHidden(const CellRange &range, bool hidden)
{
    if (!range.isValid())
        return false;

    return setColumnHidden(range.firstColumn(), range.lastColumn(), hidden);
}

/*!
  Sets width in characters for columns [\a colFirst, \a colLast] to \a width.
  Columns are 1-indexed.
  Returns true on success.
 */
bool Worksheet::setColumnWidth(int colFirst, int colLast, double width)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxColumnInfo>> columnInfoList =
        d->getColumnInfoList(colFirst, colLast);
    for (const QSharedPointer<XlsxColumnInfo> &columnInfo : columnInfoList) {
        columnInfo->width = width;
    }

    return (columnInfoList.count() > 0);
}

/*!
  Sets format property of a range of columns [\a colFirst, \a colLast] to \a format.
  Columns are 1-indexed.
  Returns true on success.
 */
bool Worksheet::setColumnFormat(int colFirst, int colLast, const Format &format)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxColumnInfo>> columnInfoList =
        d->getColumnInfoList(colFirst, colLast);
    for (const QSharedPointer<XlsxColumnInfo> &columnInfo : columnInfoList)
        columnInfo->format = format;

    if (columnInfoList.count() > 0) {
        d->workbook->styles()->addXfFormat(format);
        return true;
    }

    return false;
}

/*!
  Sets hidden property of a range of columns [\a colFirst, \a colLast] to \a hidden.
  Columns are 1-indexed. Returns true on success.
 */
bool Worksheet::setColumnHidden(int colFirst, int colLast, bool hidden)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxColumnInfo>> columnInfoList =
        d->getColumnInfoList(colFirst, colLast);
    for (const QSharedPointer<XlsxColumnInfo> &columnInfo : columnInfoList)
        columnInfo->hidden = hidden;

    return (columnInfoList.count() > 0);
}

/*!
  Returns width of the \a column in characters of the normal font. Columns are 1-indexed.
 */
double Worksheet::columnWidth(int column)
{
    Q_D(Worksheet);

    QList<QSharedPointer<XlsxColumnInfo>> columnInfoList = d->getColumnInfoList(column, column);

    // [dev54]
    if (columnInfoList.size() == 0) {
        // column information is not found
        // qDebug() << "[debug]" << __FUNCTION__ <<  "column (info) is not found. " << column;
    }

    if (columnInfoList.count() == 1) {
        // column information is found
        // qDebug() << "[debug]" << __FUNCTION__ <<  "column (info) is found. " << column <<
        // oneColWidth;
        double oneColWidth = columnInfoList.at(0)->width;
        bool isSetWidth    = columnInfoList.at(0)->isSetWidth;
        if (isSetWidth) {
            return oneColWidth;
        }
    }

    // use default width
    double defaultColWidth = d->sheetFormatProps.defaultColWidth;
    return defaultColWidth;
}

/*!
  Returns formatting of the \a column. Columns are 1-indexed.
 */
Format Worksheet::columnFormat(int column)
{
    Q_D(Worksheet);

    QList<QSharedPointer<XlsxColumnInfo>> columnInfoList = d->getColumnInfoList(column, column);
    if (columnInfoList.count() == 1)
        return columnInfoList.at(0)->format;

    return Format();
}

/*!
  Returns true if \a column is hidden. Columns are 1-indexed.
 */
bool Worksheet::isColumnHidden(int column)
{
    Q_D(Worksheet);

    QList<QSharedPointer<XlsxColumnInfo>> columnInfoList = d->getColumnInfoList(column, column);
    if (columnInfoList.count() == 1)
        return columnInfoList.at(0)->hidden;

    return false;
}

/*!
  Sets the \a height of the rows including and between \a rowFirst and \a rowLast.
  Row height measured in point size.
  Rows are 1-indexed.

  Returns true if success.
*/
bool Worksheet::setRowHeight(int rowFirst, int rowLast, double height)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxRowInfo>> rowInfoList = d->getRowInfoList(rowFirst, rowLast);
    for (const QSharedPointer<XlsxRowInfo> &rowInfo : rowInfoList) {
        rowInfo->height       = height;
        rowInfo->customHeight = true;
    }

    return rowInfoList.count() > 0;
}

/*!
  Sets the \a format of the rows including and between \a rowFirst and \a rowLast.
  Rows are 1-indexed.

  Returns true if success.
*/
bool Worksheet::setRowFormat(int rowFirst, int rowLast, const Format &format)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxRowInfo>> rowInfoList = d->getRowInfoList(rowFirst, rowLast);
    for (const QSharedPointer<XlsxRowInfo> &rowInfo : rowInfoList)
        rowInfo->format = format;

    d->workbook->styles()->addXfFormat(format);
    return rowInfoList.count() > 0;
}

/*!
  Sets the \a hidden property of the rows including and between \a rowFirst and \a rowLast.
  Rows are 1-indexed. If hidden is true rows will not be visible.

  Returns true if success.
*/
bool Worksheet::setRowHidden(int rowFirst, int rowLast, bool hidden)
{
    Q_D(Worksheet);

    const QList<QSharedPointer<XlsxRowInfo>> rowInfoList = d->getRowInfoList(rowFirst, rowLast);
    for (const QSharedPointer<XlsxRowInfo> &rowInfo : rowInfoList)
        rowInfo->hidden = hidden;

    return rowInfoList.count() > 0;
}

/*!
 Returns height of \a row in points.
*/
double Worksheet::rowHeight(int row)
{
    Q_D(Worksheet);
    const int min_col = d->dimension.isValid() ? d->dimension.firstColumn() : 1;

    auto it = d->rowsInfo.constFind(row);
    if (d->checkDimensions(row, min_col, false, true) || it == d->rowsInfo.constEnd()) {
        return d->sheetFormatProps.defaultRowHeight; // return default on invalid row
    }

    return (*it)->height;
}

/*!
 Returns format of \a row.
*/
Format Worksheet::rowFormat(int row)
{
    Q_D(Worksheet);
    const int min_col = d->dimension.isValid() ? d->dimension.firstColumn() : 1;
    auto it           = d->rowsInfo.constFind(row);
    if (d->checkDimensions(row, min_col, false, true) || it == d->rowsInfo.constEnd())
        return Format(); // return default on invalid row

    return (*it)->format;
}

/*!
 Returns true if \a row is hidden.
*/
bool Worksheet::isRowHidden(int row)
{
    Q_D(Worksheet);
    const int min_col = d->dimension.isValid() ? d->dimension.firstColumn() : 1;
    auto it           = d->rowsInfo.constFind(row);
    if (d->checkDimensions(row, min_col, false, true) || it == d->rowsInfo.constEnd())
        return false; // return default on invalid row

    return (*it)->hidden;
}

/*!
   Groups rows from \a rowFirst to \a rowLast with the given \a collapsed.

   Returns false if error occurs.
 */
bool Worksheet::groupRows(int rowFirst, int rowLast, bool collapsed)
{
    Q_D(Worksheet);

    for (int row = rowFirst; row <= rowLast; ++row) {
        auto it = d->rowsInfo.find(row);
        if (it != d->rowsInfo.end()) {
            (*it)->outlineLevel += 1;
        } else {
            QSharedPointer<XlsxRowInfo> info(new XlsxRowInfo);
            info->outlineLevel += 1;
            it = d->rowsInfo.insert(row, info);
        }
        if (collapsed)
            (*it)->hidden = true;
    }
    if (collapsed) {
        auto it = d->rowsInfo.find(rowLast + 1);
        if (it == d->rowsInfo.end())
            it = d->rowsInfo.insert(rowLast + 1, QSharedPointer<XlsxRowInfo>(new XlsxRowInfo));
        (*it)->collapsed = true;
    }
    return true;
}

/*!
        \overload

        Groups columns with the given \a range and \a collapsed.
 */
bool Worksheet::groupColumns(const CellRange &range, bool collapsed)
{
    if (!range.isValid())
        return false;

    return groupColumns(range.firstColumn(), range.lastColumn(), collapsed);
}

/*!
   Groups columns from \a colFirst to \a colLast with the given \a collapsed.
   Returns false if error occurs.
*/
bool Worksheet::groupColumns(int colFirst, int colLast, bool collapsed)
{
    Q_D(Worksheet);

    d->splitColsInfo(colFirst, colLast);

    QList<int> nodes;
    nodes.append(colFirst);
    for (int col = colFirst; col <= colLast; ++col) {
        auto it = d->colsInfo.constFind(col);
        if (it != d->colsInfo.constEnd()) {
            if (nodes.last() != col)
                nodes.append(col);
            int nextCol = (*it)->lastColumn + 1;
            if (nextCol <= colLast)
                nodes.append(nextCol);
        }
    }

    for (int idx = 0; idx < nodes.size(); ++idx) {
        int colStart = nodes[idx];
        auto it      = d->colsInfo.constFind(colStart);
        if (it != d->colsInfo.constEnd()) {
            (*it)->outlineLevel += 1;
            if (collapsed)
                (*it)->hidden = true;
        } else {
            int colEnd = (idx == nodes.size() - 1) ? colLast : nodes[idx + 1] - 1;
            QSharedPointer<XlsxColumnInfo> info(new XlsxColumnInfo(colStart, colEnd, false));
            info->outlineLevel += 1;
            d->colsInfo.insert(colFirst, info);
            if (collapsed)
                info->hidden = true;
            for (int c = colStart; c <= colEnd; ++c)
                d->colsInfoHelper[c] = info;
        }
    }

    if (collapsed) {
        int col = colLast + 1;
        d->splitColsInfo(col, col);
        auto it = d->colsInfo.constFind(col);
        if (it != d->colsInfo.constEnd())
            (*it)->collapsed = true;
        else {
            QSharedPointer<XlsxColumnInfo> info(new XlsxColumnInfo(col, col, false));
            info->collapsed = true;
            d->colsInfo.insert(col, info);
            d->colsInfoHelper[col] = info;
        }
    }

    return false;
}

/*!
        Return the range that contains cell data.
 */
CellRange Worksheet::dimension() const
{
    Q_D(const Worksheet);
    return d->dimension;
}

/*
 Convert the height of a cell from user's units to pixels. If the
 height hasn't been set by the user we use the default value. If
 the row is hidden it has a value of zero.
*/
int WorksheetPrivate::rowPixelsSize(int row) const
{
    double height;
    auto it = row_sizes.constFind(row);
    if (it != row_sizes.constEnd())
        height = it.value();
    else
        height = sheetFormatProps.defaultRowHeight;
    return static_cast<int>(4.0 / 3.0 * height);
}

/*
 Convert the width of a cell from user's units to pixels. Excel rounds
 the column width to the nearest pixel. If the width hasn't been set
 by the user we use the default value. If the column is hidden it
 has a value of zero.
*/
int WorksheetPrivate::colPixelsSize(int col) const
{
    double max_digit_width = 7.0; // For Calabri 11
    double padding         = 5.0;
    int pixels             = 0;

    auto it = col_sizes.constFind(col);
    if (it != col_sizes.constEnd()) {
        double width = it.value();
        if (width < 1)
            pixels = static_cast<int>(width * (max_digit_width + padding) + 0.5);
        else
            pixels = static_cast<int>(width * max_digit_width + 0.5) + padding;
    } else {
        pixels = 64;
    }
    return pixels;
}

void WorksheetPrivate::loadXmlSheetData(QXmlStreamReader &reader)
{
    Q_Q(Worksheet);

    Q_ASSERT(reader.name() == QLatin1String("sheetData"));

    int row_num = 0;
    int col_num = 0;

    while (!reader.atEnd() && !(reader.name() == QLatin1String("sheetData") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        if (reader.readNextStartElement()) {
            if (reader.name() == QLatin1String("row")) {
                QXmlStreamAttributes attributes = reader.attributes();

                if (attributes.hasAttribute(QLatin1String("customFormat")) ||
                    attributes.hasAttribute(QLatin1String("customHeight")) ||
                    attributes.hasAttribute(QLatin1String("hidden")) ||
                    attributes.hasAttribute(QLatin1String("outlineLevel")) ||
                    attributes.hasAttribute(QLatin1String("collapsed"))) {

                    QSharedPointer<XlsxRowInfo> info(new XlsxRowInfo);
                    if (attributes.hasAttribute(QLatin1String("customFormat")) &&
                        attributes.hasAttribute(QLatin1String("s"))) {
                        int idx      = attributes.value(QLatin1String("s")).toInt();
                        info->format = workbook->styles()->xfFormat(idx);
                    }

                    if (attributes.hasAttribute(QLatin1String("customHeight"))) {
                        info->customHeight =
                            attributes.value(QLatin1String("customHeight")) == QLatin1String("1");
                        // Row height is only specified when customHeight is set
                        if (attributes.hasAttribute(QLatin1String("ht"))) {
                            info->height = attributes.value(QLatin1String("ht")).toDouble();
                        }
                    }

                    // both "hidden" and "collapsed" default are false
                    info->hidden = attributes.value(QLatin1String("hidden")) == QLatin1String("1");
                    info->collapsed =
                        attributes.value(QLatin1String("collapsed")) == QLatin1String("1");

                    if (attributes.hasAttribute(QLatin1String("outlineLevel")))
                        info->outlineLevel =
                            attributes.value(QLatin1String("outlineLevel")).toInt();

                    //"r" is optional too.
                    if (attributes.hasAttribute(QLatin1String("r"))) {
                        int row       = attributes.value(QLatin1String("r")).toInt();
                        rowsInfo[row] = info;
                    }
                }

                if (attributes.hasAttribute(QLatin1String("r")))
                    row_num = attributes.value(QLatin1String("r")).toInt();
                else
                    ++row_num;
                col_num = 0;

            } else if (reader.name() == QLatin1String("c")) // Cell
            {

                // Cell
                QXmlStreamAttributes attributes = reader.attributes();
                QString r                       = attributes.value(QLatin1String("r")).toString();
                CellReference pos(r);
                if (r.isEmpty())
                {
                    pos.setRow(row_num);
                    pos.setColumn(++col_num);
                }

                // get format
                Format format;
                qint32 styleIndex = -1;
                if (attributes.hasAttribute(
                        QLatin1String("s"))) // Style (defined in the styles.xml file)
                {
                    //"s" == style index
                    int idx    = attributes.value(QLatin1String("s")).toInt();
                    format     = workbook->styles()->xfFormat(idx);
                    styleIndex = idx;
                }

                // Cell::CellType cellType = Cell::NumberType;
                Cell::CellType cellType = Cell::CustomType;

                if (attributes.hasAttribute(QLatin1String("t"))) // Type
                {
                    const auto typeString = attributes.value(QLatin1String("t"));
                    if (typeString == QLatin1String("s")) // Shared string
                    {
                        cellType = Cell::SharedStringType;
                    } else if (typeString == QLatin1String("inlineStr")) //  Inline String
                    {
                        cellType = Cell::InlineStringType;
                    } else if (typeString == QLatin1String("str")) // String
                    {
                        cellType = Cell::StringType;
                    } else if (typeString == QLatin1String("b")) // Boolean
                    {
                        cellType = Cell::BooleanType;
                    } else if (typeString == QLatin1String("e")) // Error
                    {
                        cellType = Cell::ErrorType;
                    } else if (typeString == QLatin1String("d")) // Date
                    {
                        cellType = Cell::DateType;
                    } else if (typeString == QLatin1String("n")) // Number
                    {
                        cellType = Cell::NumberType;
                    } else {
                        // custom type
                        cellType = Cell::CustomType;
                    }
                }

                if (Cell::isDateType(cellType, format)) {
                    cellType = Cell::DateType;
                }

                // create a heap of new cell
                auto cell = std::make_shared<Cell>(QVariant{}, cellType, format, q, styleIndex);

                while (!reader.atEnd() && !(reader.name() == QLatin1String("c") &&
                                            reader.tokenType() == QXmlStreamReader::EndElement)) {
                    if (reader.readNextStartElement()) {
                        if (reader.name() == QLatin1String("f")) // formula
                        {
                            CellFormula &formula = cell->d_func()->formula;
                            formula.loadFromXml(reader);
                            if (formula.formulaType() == CellFormula::SharedType &&
                                !formula.formulaText().isEmpty()) {
                                int si               = formula.sharedIndex();
                                sharedFormulaMap[si] = formula;
                            }
                        } else if (reader.name() == QLatin1String("v")) // Value
                        {
                            QString value = reader.readElementText();
                            if (cellType == Cell::SharedStringType) {
                                int sst_idx = value.toInt();
                                sharedStrings()->incRefByStringIndex(sst_idx);
                                RichString rs          = sharedStrings()->getSharedString(sst_idx);
                                QString strPlainString = rs.toPlainString();
                                cell->d_func()->value  = strPlainString;
                                if (rs.isRichString())
                                    cell->d_func()->richString = rs;
                            } else if (cellType == Cell::NumberType) {
                                cell->d_func()->value = value.toDouble();
                            } else if (cellType == Cell::BooleanType) {
                                cell->d_func()->value = value.toInt() ? true : false;
                            } else if (cellType == Cell::DateType) {
                                // [dev54] DateType

                                double dValue    = value.toDouble(); // days from 1900(or 1904)
                                bool bIsDate1904 = q->workbook()->isDate1904();

                                QVariant vDatetimeValue = datetimeFromNumber(dValue, bIsDate1904);
                                Q_UNUSED(vDatetimeValue);
                                // cell->d_func()->value = vDatetimeValue;
                                cell->d_func()->value = dValue; // dev67
                            } else {
                                // ELSE type
                                cell->d_func()->value = value;
                            }

                        } else if (reader.name() == QLatin1String("is")) {
                            while (!reader.atEnd() &&
                                   !(reader.name() == QLatin1String("is") &&
                                     reader.tokenType() == QXmlStreamReader::EndElement)) {
                                if (reader.readNextStartElement()) {
                                    //: Todo, add rich text read support
                                    if (reader.name() == QLatin1String("t")) {
                                        cell->d_func()->value = reader.readElementText();
                                    }
                                }
                            }
                        } else if (reader.name() == QLatin1String("extLst")) {
                            // skip extLst element
                            while (!reader.atEnd() &&
                                   !(reader.name() == QLatin1String("extLst") &&
                                     reader.tokenType() == QXmlStreamReader::EndElement)) {
                                reader.readNextStartElement();
                            }
                        }
                    }
                }

                cellTable[pos.row()][pos.column()] = cell;
            }
        }
    }

    if (dimension.lastRow() < row_num)
        dimension.setLastRow(row_num);

    if (dimension.lastColumn() < col_num)
        dimension.setLastColumn(col_num);
}

void WorksheetPrivate::loadXmlColumnsInfo(QXmlStreamReader &reader)
{
    Q_ASSERT(reader.name() == QLatin1String("cols"));

    while (!reader.atEnd() && !(reader.name() == QLatin1String("cols") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement) {
            if (reader.name() == QLatin1String("col")) {
                QSharedPointer<XlsxColumnInfo> info(new XlsxColumnInfo(0, 1, false));

                QXmlStreamAttributes colAttrs = reader.attributes();
                int min                       = colAttrs.value(QLatin1String("min")).toInt();
                int max                       = colAttrs.value(QLatin1String("max")).toInt();
                info->firstColumn             = min;
                info->lastColumn              = max;

                // Flag indicating that the column width for the affected column(s) is different
                // from the
                //  default or has been manually set
                if (colAttrs.hasAttribute(QLatin1String("customWidth"))) {
                    info->customWidth =
                        colAttrs.value(QLatin1String("customWidth")) == QLatin1String("1");
                }

                // Note, node may have "width" without "customWidth"
                // [dev54]
                if (colAttrs.hasAttribute(QLatin1String("width"))) {
                    double width     = colAttrs.value(QLatin1String("width")).toDouble();
                    info->width      = width;
                    info->isSetWidth = true; // [dev54]
                }

                info->hidden    = colAttrs.value(QLatin1String("hidden")) == QLatin1String("1");
                info->collapsed = colAttrs.value(QLatin1String("collapsed")) == QLatin1String("1");

                if (colAttrs.hasAttribute(QLatin1String("style"))) {
                    int idx      = colAttrs.value(QLatin1String("style")).toInt();
                    info->format = workbook->styles()->xfFormat(idx);
                }

                if (colAttrs.hasAttribute(QLatin1String("outlineLevel"))) {
                    info->outlineLevel = colAttrs.value(QLatin1String("outlineLevel")).toInt();
                }

                // qDebug() << "[debug] " << __FUNCTION__ << min << max << info->width << hasWidth;

                colsInfo.insert(min, info);
                for (int col = min; col <= max; ++col) {
                    colsInfoHelper[col] = info;
                }
            }
        }
    }
}

void WorksheetPrivate::loadXmlMergeCells(QXmlStreamReader &reader)
{
    // issue #173 https://github.com/QtExcel/QXlsx/issues/173

    Q_ASSERT(reader.name() == QLatin1String("mergeCells"));

    QXmlStreamAttributes attributes = reader.attributes();

    bool isCount = attributes.hasAttribute(QLatin1String("count"));
    int count    = 0;
    if (!isCount) {
        qWarning("no count");
    } else {
        count = attributes.value(QLatin1String("count")).toInt();
    }

    while (!reader.atEnd() && !(reader.name() == QLatin1String("mergeCells") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement) {
            if (reader.name() == QLatin1String("mergeCell")) {
                QXmlStreamAttributes attrs = reader.attributes();
                QString rangeStr           = attrs.value(QLatin1String("ref")).toString();
                merges.append(CellRange(rangeStr));
            }
        }
    }

    if (isCount) {
        int mergesSize = merges.size();
        if (mergesSize != count) {
            qWarning("read merge cells error");
        }
    }
}

void WorksheetPrivate::loadXmlDataValidations(QXmlStreamReader &reader)
{
    Q_ASSERT(reader.name() == QLatin1String("dataValidations"));
    QXmlStreamAttributes attributes = reader.attributes();
    int count                       = attributes.value(QLatin1String("count")).toInt();

    while (!reader.atEnd() && !(reader.name() == QLatin1String("dataValidations") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement &&
            reader.name() == QLatin1String("dataValidation")) {
            dataValidationsList.append(DataValidation::loadFromXml(reader));
        }
    }

    if (dataValidationsList.size() != count)
        qDebug("read data validation error");
}

void WorksheetPrivate::loadXmlSheetViews(QXmlStreamReader &reader)
{
    Q_ASSERT(reader.name() == QLatin1String("sheetViews"));

    while (!reader.atEnd() && !(reader.name() == QLatin1String("sheetViews") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement &&
            reader.name() == QLatin1String("sheetView")) {
            QXmlStreamAttributes attrs = reader.attributes();
            // default false
            windowProtection = attrs.value(QLatin1String("windowProtection")) == QLatin1String("1");
            showFormulas     = attrs.value(QLatin1String("showFormulas")) == QLatin1String("1");
            rightToLeft      = attrs.value(QLatin1String("rightToLeft")) == QLatin1String("1");
            tabSelected      = attrs.value(QLatin1String("tabSelected")) == QLatin1String("1");
            // default true
            showGridLines = attrs.value(QLatin1String("showGridLines")) != QLatin1String("0");
            showRowColHeaders =
                attrs.value(QLatin1String("showRowColHeaders")) != QLatin1String("0");
            showZeros = attrs.value(QLatin1String("showZeros")) != QLatin1String("0");
            showRuler = attrs.value(QLatin1String("showRuler")) != QLatin1String("0");
            showOutlineSymbols =
                attrs.value(QLatin1String("showOutlineSymbols")) != QLatin1String("0");
            showWhiteSpace = attrs.value(QLatin1String("showWhiteSpace")) != QLatin1String("0");
        }
    }
}

void WorksheetPrivate::loadXmlSheetFormatProps(QXmlStreamReader &reader)
{
    Q_ASSERT(reader.name() == QLatin1String("sheetFormatPr"));

    const QXmlStreamAttributes attributes = reader.attributes();
    XlsxSheetFormatProps formatProps;
    bool isSetWidth = false;

    // Retain default values
    for (const QXmlStreamAttribute &attrib : attributes) {
        if (attrib.name() == QLatin1String("baseColWidth")) {
            formatProps.baseColWidth = attrib.value().toInt();
        } else if (attrib.name() == QLatin1String("customHeight")) {
            formatProps.customHeight = attrib.value() == QLatin1String("1");
        } else if (attrib.name() == QLatin1String("defaultColWidth")) {
            double dDefaultColWidth     = attrib.value().toDouble();
            formatProps.defaultColWidth = dDefaultColWidth;
            isSetWidth                  = true;
        } else if (attrib.name() == QLatin1String("defaultRowHeight")) {
            formatProps.defaultRowHeight = attrib.value().toDouble();
        } else if (attrib.name() == QLatin1String("outlineLevelCol")) {
            formatProps.outlineLevelCol = attrib.value().toInt();
        } else if (attrib.name() == QLatin1String("outlineLevelRow")) {
            formatProps.outlineLevelRow = attrib.value().toInt();
        } else if (attrib.name() == QLatin1String("thickBottom")) {
            formatProps.thickBottom = attrib.value() == QLatin1String("1");
        } else if (attrib.name() == QLatin1String("thickTop")) {
            formatProps.thickTop = attrib.value() == QLatin1String("1");
        } else if (attrib.name() == QLatin1String("zeroHeight")) {
            formatProps.zeroHeight = attrib.value() == QLatin1String("1");
        }
    }

    // if (formatProps.defaultColWidth == 0.0)
    if (!isSetWidth) {
        // not set
        double dCalcWidth           = WorksheetPrivate::calculateColWidth(formatProps.baseColWidth);
        formatProps.defaultColWidth = dCalcWidth;
    }

    // [dev54]
    // Where is code of setting 'formatProps'?
    this->sheetFormatProps = formatProps;
}
double WorksheetPrivate::calculateColWidth(int characters)
{
    // //!Todo
    // Take normal style' font maximum width and add padding and margin pixels
    // return characters + 0.5;
    return characters;
}

void WorksheetPrivate::loadXmlHyperlinks(QXmlStreamReader &reader)
{
    Q_ASSERT(reader.name() == QLatin1String("hyperlinks"));

    while (!reader.atEnd() && !(reader.name() == QLatin1String("hyperlinks") &&
                                reader.tokenType() == QXmlStreamReader::EndElement)) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement &&
            reader.name() == QLatin1String("hyperlink")) {
            QXmlStreamAttributes attrs = reader.attributes();
            CellReference pos(attrs.value(QLatin1String("ref")).toString());
            if (pos.isValid()) { // Valid
                QSharedPointer<XlsxHyperlinkData> link(new XlsxHyperlinkData);
                link->display  = attrs.value(QLatin1String("display")).toString();
                link->tooltip  = attrs.value(QLatin1String("tooltip")).toString();
                link->location = attrs.value(QLatin1String("location")).toString();

                if (attrs.hasAttribute(QLatin1String("r:id"))) {
                    link->linkType        = XlsxHyperlinkData::External;
                    XlsxRelationship ship = relationships->getRelationshipById(
                        attrs.value(QLatin1String("r:id")).toString());
                    link->target = ship.target;
                } else {
                    link->linkType = XlsxHyperlinkData::Internal;
                }

                urlTable[pos.row()][pos.column()] = link;
            }
        }
    }
}

QList<QSharedPointer<XlsxColumnInfo>> WorksheetPrivate::getColumnInfoList(int colFirst, int colLast)
{
    QList<QSharedPointer<XlsxColumnInfo>> columnsInfoList;
    if (isColumnRangeValid(colFirst, colLast)) {
        QList<int> nodes = getColumnIndexes(colFirst, colLast);

        for (int idx = 0; idx < nodes.size(); ++idx) {
            int colStart = nodes[idx];
            auto it      = colsInfo.constFind(colStart);
            if (it != colsInfo.constEnd()) {
                columnsInfoList.append(*it);
            } else {
                int colEnd = (idx == nodes.size() - 1) ? colLast : nodes[idx + 1] - 1;
                QSharedPointer<XlsxColumnInfo> info(new XlsxColumnInfo(colStart, colEnd, false));
                colsInfo.insert(colFirst, info);
                columnsInfoList.append(info);
                for (int c = colStart; c <= colEnd; ++c) {
                    colsInfoHelper[c] = info;
                }
            }
        }
    }

    return columnsInfoList;
}

QList<QSharedPointer<XlsxRowInfo>> WorksheetPrivate::getRowInfoList(int rowFirst, int rowLast)
{
    QList<QSharedPointer<XlsxRowInfo>> rowInfoList;

    int min_col = dimension.firstColumn() < 1 ? 1 : dimension.firstColumn();

    for (int row = rowFirst; row <= rowLast; ++row) {
        if (checkDimensions(row, min_col, false, true))
            continue;

        QSharedPointer<XlsxRowInfo> rowInfo;
        if ((rowsInfo[row]).isNull()) {
            rowsInfo[row] = QSharedPointer<XlsxRowInfo>(new XlsxRowInfo());
        }
        rowInfoList.append(rowsInfo[row]);
    }

    return rowInfoList;
}

bool Worksheet::loadFromXmlFile(QIODevice *device)
{
    Q_D(Worksheet);

    QXmlStreamReader reader(device);
    while (!reader.atEnd()) {
        reader.readNextStartElement();
        if (reader.tokenType() == QXmlStreamReader::StartElement) {
            if (reader.name() == QLatin1String("dimension")) {
                QXmlStreamAttributes attributes = reader.attributes();
                QString range                   = attributes.value(QLatin1String("ref")).toString();
                d->dimension                    = CellRange(range);
            } else if (reader.name() == QLatin1String("sheetViews")) {
                d->loadXmlSheetViews(reader);
            } else if (reader.name() == QLatin1String("sheetFormatPr")) {
                d->loadXmlSheetFormatProps(reader);
            } else if (reader.name() == QLatin1String("cols")) {
                d->loadXmlColumnsInfo(reader);
            } else if (reader.name() == QLatin1String("sheetData")) {
                d->loadXmlSheetData(reader);
            } else if (reader.name() == QLatin1String("mergeCells")) {
                d->loadXmlMergeCells(reader);
            } else if (reader.name() == QLatin1String("dataValidations")) {
                d->loadXmlDataValidations(reader);
            } else if (reader.name() == QLatin1String("conditionalFormatting")) {
                ConditionalFormatting cf;
                cf.loadFromXml(reader, workbook()->styles());
                d->conditionalFormattingList.append(cf);
            } else if (reader.name() == QLatin1String("hyperlinks")) {
                d->loadXmlHyperlinks(reader);
            } else if (reader.name() == QLatin1String("pageSetup")) {
                QXmlStreamAttributes attributes = reader.attributes();

                d->PpaperSize = attributes.value(QLatin1String("paperSize")).toString().trimmed();
                d->Pscale     = attributes.value(QLatin1String("scale")).toString().trimmed();
                d->PfirstPageNumber =
                    attributes.value(QLatin1String("firstPageNumber")).toString().trimmed();
                d->Porientation =
                    attributes.value(QLatin1String("orientation")).toString().trimmed();
                d->PuseFirstPageNumber =
                    attributes.value(QLatin1String("useFirstPageNumber")).toString().trimmed();
                d->PhorizontalDpi =
                    attributes.value(QLatin1String("horizontalDpi")).toString().trimmed();
                d->PverticalDpi =
                    attributes.value(QLatin1String("verticalDpi")).toString().trimmed();
                d->Prid    = attributes.value(QLatin1String("r:id")).toString().trimmed();
                d->Pcopies = attributes.value(QLatin1String("copies")).toString().trimmed();
            } else if (reader.name() == QLatin1String("pageMargins")) {
                QXmlStreamAttributes attributes = reader.attributes();

                d->PMfooter = attributes.value(QLatin1String("footer")).toString().trimmed();
                d->PMheader = attributes.value(QLatin1String("header")).toString().trimmed();
                d->PMbotton = attributes.value(QLatin1String("bottom")).toString().trimmed();
                d->PMtop    = attributes.value(QLatin1String("top")).toString().trimmed();
                d->PMright  = attributes.value(QLatin1String("right")).toString().trimmed();
                d->PMleft   = attributes.value(QLatin1String("left")).toString().trimmed();
            } else if (reader.name() == QLatin1String("headerFooter")) {
                // dev40
                while (reader.readNextStartElement()) {
                    if (reader.name() == QLatin1String("oddHeader"))
                        d->ModdHeader = reader.readElementText();

                    if (reader.name() == QLatin1String("oddFooter"))
                        d->MoodFooter = reader.readElementText();
                }
            } else if (reader.name() == QLatin1String("drawing")) {
                QString rId  = reader.attributes().value(QStringLiteral("r:id")).toString();
                QString name = d->relationships->getRelationshipById(rId).target;

                const auto parts = splitPath(filePath());
                QString path     = QDir::cleanPath(parts.first() + QLatin1String("/") + name);

                d->drawing = std::make_shared<Drawing>(this, F_LoadFromExists);
                d->drawing->setFilePath(path);
            } else if (reader.name() == QLatin1String("extLst")) {
                // Todo: add extLst support
                while (!reader.atEnd() && !(reader.name() == QLatin1String("extLst") &&
                                            reader.tokenType() == QXmlStreamReader::EndElement)) {
                    reader.readNextStartElement();
                }
            }
        }
    }

    d->validateDimension();
    return true;
}

/*
 *  Documents imported from Google Docs does not contain dimension data.
 */
void WorksheetPrivate::validateDimension()
{
    if (dimension.isValid() || cellTable.isEmpty())
        return;

    const auto firstRow = cellTable.constBegin().key();

    const auto lastRow = (--cellTable.constEnd()).key();

    int firstColumn = -1;
    int lastColumn  = -1;

    for (auto &&it = cellTable.constBegin(); it != cellTable.constEnd(); ++it) {
        Q_ASSERT(!it.value().isEmpty());

        if (firstColumn == -1 || it.value().constBegin().key() < firstColumn)
            firstColumn = it.value().constBegin().key();

        if (lastColumn == -1 || (--it.value().constEnd()).key() > lastColumn) {
            lastColumn = (--it.value().constEnd()).key();
        }
    }

    CellRange cr(firstRow, firstColumn, lastRow, lastColumn);

    if (cr.isValid())
        dimension = cr;
}

/*!
 * \internal
 *  Unit test can use this member to get sharedString object.
 */
SharedStrings *WorksheetPrivate::sharedStrings() const
{
    return workbook->sharedStrings();
}

QVector<CellLocation> Worksheet::getFullCells(int *maxRow, int *maxCol)
{
    Q_D(const Worksheet);

    // return values
    (*maxRow) = -1;
    (*maxCol) = -1;
    QVector<CellLocation> ret;

    // QString privateName = d->name; // name of sheet (not object type)
    // qDebug() << privateName ;

    if (d->type == AbstractSheet::ST_WorkSheet) {
        // use current sheet
    } else if (d->type == AbstractSheet::ST_ChartSheet) {
        return ret;
    } else {
        qWarning("unsupported sheet type.");
        Q_ASSERT(false);
        return ret;
    }

    QMapIterator<int, QMap<int, std::shared_ptr<Cell>>> _it(d->cellTable);

    while (_it.hasNext()) {
        _it.next();

        int keyI = _it.key();                                       // key (cell row)
        QMapIterator<int, std::shared_ptr<Cell>> _iit(_it.value()); // value

        while (_iit.hasNext()) {
            _iit.next();

            int keyII                     = _iit.key();   // key (cell column)
            std::shared_ptr<Cell> ptrCell = _iit.value(); // value

            CellLocation cl;

            cl.row = keyI;
            if (keyI > (*maxRow)) {
                (*maxRow) = keyI;
            }

            cl.col = keyII;
            if (keyII > (*maxCol)) {
                (*maxCol) = keyII;
            }

            cl.cell = ptrCell;

            ret.push_back(cl);
        }
    }

    return ret;
}

QT_END_NAMESPACE_XLSX
