/*
	File                 : ExcelFilter.cpp
	Project              : LabPlot
	Description          : Excel I/O-filter
	--------------------------------------------------------------------
	SPDX-FileCopyrightText: 2021 Fabian Kristof (fkristofszabolcs@gmail.com)
	SPDX-FileCopyrightText: 2022-2023 Stefan Gerlach <stefan.gerlach@uni.kn>
	SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "backend/datasources/filters/ExcelFilter.h"
#include "backend/core/column/Column.h"
#include "backend/datasources/AbstractDataSource.h"
#include "backend/datasources/filters/ExcelFilterPrivate.h"
#include "backend/matrix/Matrix.h"
#include "backend/spreadsheet/Spreadsheet.h"

#include <utility>

#include <KI18n/KLocalizedString>
#include <QStringList>
#include <QTreeWidgetItem>
#include <QVector>

ExcelFilter::ExcelFilter()
	: AbstractFileFilter(FileType::Excel)
	, d(new ExcelFilterPrivate(this)) {
}

ExcelFilter::~ExcelFilter() {
}

QString ExcelFilter::fileInfoString(const QString& fileName) {
#ifdef HAVE_EXCEL
	QXlsx::Document doc{fileName};

	ExcelFilter filter;

	QVector<int> rangesPerSheet;
	for (const auto& sheet : doc.sheetNames())
		rangesPerSheet.push_back(filter.dataRegions(fileName, sheet).size());

	const QStringList& sheetNames = doc.sheetNames();
	QString info(i18n("Sheet count: %1", QString::number(sheetNames.size())));
	info += QLatin1String("<br>");
	info += i18n("Sheets: ");
	info += QLatin1String("<br>");

	for (int i = 0; i < sheetNames.size(); ++i) {
		info += sheetNames.at(i);
		info += QLatin1String(", ranges count:  ");
		info += QString::number(rangesPerSheet.at(i));
		info += QLatin1String("<br>");
	}
	info += QLatin1String("<br>");

	return info;
#else
	Q_UNUSED(fileName)
	return {};
#endif
}

QStringList ExcelFilter::sheets() const {
	return d->sheets();
}

QStringList ExcelFilter::sheets(const QString& fileName, bool* ok) {
#ifdef HAVE_EXCEL
	QXlsx::Document doc{fileName};
	if (ok)
		*ok = doc.isLoadPackage();
	return doc.sheetNames();
#else
	Q_UNUSED(fileName)
	Q_UNUSED(ok)
	return {};
#endif
}

bool ExcelFilter::isValidCellReference(const QString& cellRefString) {
#ifdef HAVE_EXCEL
	QXlsx::CellReference ref{cellRefString};

	return ref.isValid();
#else
	Q_UNUSED(cellRefString)
#endif
	return false;
}

void ExcelFilter::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) {
	d->readDataFromFile(fileName, dataSource, importMode);
}
void ExcelFilter::write(const QString& fileName, AbstractDataSource* dataSource) {
	d->write(fileName, dataSource);
}

#ifdef HAVE_EXCEL
QVector<QStringList> ExcelFilter::previewForDataRegion(const QString& sheet, const QXlsx::CellRange& region, bool* okToMatrix, int lines) {
	return d->previewForDataRegion(sheet, region, okToMatrix, lines);
}
#endif

QVector<QStringList> ExcelFilter::previewForCurrentDataRegion(int lines, bool* okToMatrix) {
#ifdef HAVE_EXCEL
	return d->previewForDataRegion(d->currentSheet, d->currentRange, okToMatrix, lines);
#else
	Q_UNUSED(lines)
	Q_UNUSED(okToMatrix)
	return {};
#endif
}

void ExcelFilter::setSheetToAppendTo(const QString& sheetName) {
	d->sheetToAppendSpreadsheetTo = sheetName;
}

void ExcelFilter::setExportAsNewSheet(const bool b) {
	d->exportDataSourceAsNewSheet = b;
}

void ExcelFilter::setOverwriteData(const bool b) {
	d->overwriteExportData = b;
}

void ExcelFilter::setFirstRowAsColumnNames(const bool b) {
	d->firstRowAsColumnNames = b;
}
void ExcelFilter::setColumnNamesAsFirstRow(const bool b) {
	d->columnNamesAsFirstRow = b;
}

void ExcelFilter::setDataExportStartPos(const QString& dataStartPos) {
#ifdef HAVE_EXCEL
	const auto cell = QXlsx::CellReference(dataStartPos);
	if (cell.isValid()) {
		d->dataExportStartCell.setColumn(cell.column());
		d->dataExportStartCell.setRow(cell.row());
	}
#else
	Q_UNUSED(dataStartPos)
#endif
}

#ifdef HAVE_EXCEL
QVector<QXlsx::CellRange> ExcelFilter::dataRegions(const QString& fileName, const QString& sheetName) {
	return d->dataRegions(fileName, sheetName);
}
#endif

void ExcelFilter::parse(const QString& fileName, QTreeWidgetItem* root) {
	d->parse(fileName, root);
}

#ifdef HAVE_EXCEL
QXlsx::CellRange ExcelFilter::dimension() const {
	return d->dimension();
}
#endif

/*!
 * \brief Sets the startColumn to \a column
 * \param column the column to be set
 */
void ExcelFilter::setStartColumn(const int column) {
	d->startColumn = column;
}

/*!
 * \brief Returns startColumn
 * \return The startColumn
 */
int ExcelFilter::startColumn() const {
	return d->startColumn;
}

/*!
 * \brief Sets the endColumn to \a column
 * \param column the column to be set
 */
void ExcelFilter::setEndColumn(const int column) {
	d->endColumn = column;
}

/*!
 * \brief Returns endColumn
 * \return The endColumn
 */
int ExcelFilter::endColumn() const {
	return d->endColumn;
}

/*!
 * \brief Sets the startRow to \a row
 * \param row the row to be set
 */
void ExcelFilter::setStartRow(const int row) {
	d->startRow = row;
}

/*!
 * \brief Returns startRow
 * \return The startRow
 */
int ExcelFilter::startRow() const {
	return d->startRow;
}

/*!
 * \brief Sets the endRow to \a row
 * \param row the row to be set
 */
void ExcelFilter::setEndRow(const int row) {
	d->endRow = row;
}

/*!
 * \brief Returns endRow
 * \return The endRow
 */
int ExcelFilter::endRow() const {
	return d->endRow;
}

void ExcelFilter::setCurrentRange(const QString& range) {
#ifdef HAVE_EXCEL
	d->currentRange = {range};
#else
	Q_UNUSED(range)
#endif
}

void ExcelFilter::setCurrentSheet(const QString& sheet) {
	d->currentSheet = sheet;
}

void ExcelFilter::loadFilterSettings(const QString& filterName) {
	Q_UNUSED(filterName)
}

void ExcelFilter::saveFilterSettings(const QString& filterName) const {
	Q_UNUSED(filterName)
}

//##############################################################################
//##################  Serialization/Deserialization  ###########################
//##############################################################################

/*!
  Saves as XML.
*/

void ExcelFilter::save(QXmlStreamWriter*) const {
}

bool ExcelFilter::load(XmlStreamReader*) {
	return true;
}

//#####################################################################
//################### Private implementation ##########################
//#####################################################################

ExcelFilterPrivate::ExcelFilterPrivate(ExcelFilter* owner)
	: q(owner) {
}

ExcelFilterPrivate::~ExcelFilterPrivate() {
#ifdef HAVE_EXCEL
	if (m_document)
		delete m_document;
#endif
}

// TODO
//  alternating row colors?
//  bold "header" - colum names
void ExcelFilterPrivate::write(const QString& fileName, AbstractDataSource* dataSource) {
#ifdef HAVE_EXCEL
	if (!m_document || fileName.compare(m_fileName)) {
		// delete m_document;
		m_document = new QXlsx::Document(fileName);
		m_fileName = fileName;
	}

	auto dataSourceName = dataSource->name();
	if (exportDataSourceAsNewSheet) {
		const auto& sheets = m_document->sheetNames();
		if (sheets.contains(dataSourceName))
			dataSourceName += QLatin1String("_1");

		m_document->addSheet(dataSourceName);
	} else {
		// there is (should be) a selected sheet in the widget
		if (!sheetToAppendSpreadsheetTo.isEmpty()) {
			// select the sheet
			if (!m_document->selectSheet(sheetToAppendSpreadsheetTo)) {
				// couldn't select sheet
			}
		}
	}

	int startRow = 1;
	int startCol = 1;

	// we're exporting in an existing sheet
	if (!exportDataSourceAsNewSheet) {
		// don't overwrite data
		if (!overwriteExportData) {
			// write data in the "end" of the currently occupied data range
			const auto& dimension = m_document->dimension();
			// +2 to leave an empty row
			startRow = dimension.lastRow() + 2;
		} else {
			// if there was a valid starting cell
			// otherwise we will just overwrite from row 1 col 1
			if (dataExportStartCell.isValid()) {
				startRow = dataExportStartCell.row();
				startCol = dataExportStartCell.column();
			}
		}
	}

	if (auto* const spreadsheet = dynamic_cast<Spreadsheet*>(dataSource)) {
		const int columns = spreadsheet->columnCount();
		const int rows = spreadsheet->rowCount();

		for (int col = 0; col < columns; ++col) {
			const auto* const column = spreadsheet->column(col);
			const int actualCol = startCol + col;

			if (columnNamesAsFirstRow) {
				if (!m_document->write(startRow, startCol + col, column->name())) {
					// failed to write column name
				}
			}
			for (int row = 0; row < rows; ++row) {
				const int actualRow = columnNamesAsFirstRow ? startRow + row + 1 : startRow + row;
				const QString text = column->asStringColumn()->textAt(row);

				if (!m_document->write(actualRow, actualCol, text)) {
					// failed to write
				}
			}
		}

	} else if (auto* const matrix = dynamic_cast<Matrix*>(dataSource)) {
		const int columns = matrix->columnCount();
		const int rows = matrix->rowCount();
		const QVector<QVector<double>>* const data = static_cast<QVector<QVector<double>>*>(matrix->data());

		for (int col = 0; col < columns; ++col) {
			const int actualCol = startCol + col;
			const auto& column = data->at(col);
			for (int row = 0; row < rows; ++row) {
				const int actualRow = startRow + row;
				const auto& val = column.at(row);

				if (!m_document->write(actualRow, actualCol, val)) {
					// failed to write
				}
			}
		}
	}

	if (!m_document->save()) {
		// failed to save file
	}
#else
	Q_UNUSED(fileName)
	Q_UNUSED(dataSource)
#endif
}

void ExcelFilterPrivate::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) {
	DEBUG(Q_FUNC_INFO)
#ifdef HAVE_EXCEL
	if (!m_document || fileName.compare(m_fileName)) {
		delete m_document;
		m_document = new QXlsx::Document(fileName);
		m_fileName = fileName;
	}

	if (m_document->selectSheet(currentSheet)) {
		if (endRow != -1) {
			int row = currentRange.firstRow() + endRow - 1;
			if (row <= currentRange.lastRow())
				currentRange.setLastRow(row);
		}

		if (startRow > 1) {
			int rrow = currentRange.firstRow() + startRow - 1;
			if (rrow <= currentRange.lastRow())
				currentRange.setFirstRow(rrow);
			else
				currentRange.setFirstRow(currentRange.lastRow());
		}

		if (endColumn != -1) {
			int col = currentRange.firstColumn() + endColumn - 1;
			if (col <= currentRange.lastColumn())
				currentRange.setLastColumn(col);
		}

		if (startColumn > 1) {
			int col = currentRange.firstColumn() + startColumn - 1;
			if (col <= currentRange.lastColumn())
				currentRange.setFirstColumn(col);
			else
				currentRange.setFirstColumn(currentRange.lastColumn());
		}

		readDataRegion(currentRange, dataSource, importMode);
	} else {
		DEBUG(Q_FUNC_INFO << ", INVALID sheet")
		// invalid sheet
	}

#else
	Q_UNUSED(fileName)
	Q_UNUSED(dataSource)
	Q_UNUSED(importMode)
#endif
}

#ifdef HAVE_EXCEL
void ExcelFilterPrivate::readDataRegion(const QXlsx::CellRange& region, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) {
	DEBUG(Q_FUNC_INFO << ", col/row range = " << region.firstColumn() << " .. " << region.lastColumn() << ", " << region.firstRow() << " .. "
					  << region.lastRow() << ". first row as column names = " << firstRowAsColumnNames)

	int columnOffset = 0;
	const auto rowCount = currentRange.rowCount();
	const auto colCount = currentRange.columnCount();
	auto regionToRead = region;
	bool isDateOnly = true;

	if (auto* spreadsheet = dynamic_cast<Spreadsheet*>(dataSource)) {
		std::vector<void*> numericDataPointers;
		QVector<QVector<QDateTime>*> datetimeDataPointers;
		QVector<QVector<QString>*> stringDataPointers;
		QList<QXlsx::Cell::CellType> columnNumericTypes;
		QStringList columnNames;
		if (firstRowAsColumnNames)
			regionToRead.setFirstRow(region.firstRow() + 1);

		// determine column type (numeric or not)
		for (int col = regionToRead.firstColumn(); col <= regionToRead.lastColumn(); ++col) {
			columnNumericTypes.push_back(columnTypeInRange(col, regionToRead));
			if (firstRowAsColumnNames)
				columnNames.push_back(m_document->read(regionToRead.firstRow() - 1, col).toString());
			else
				columnNames.push_back(ExcelFilter::convertFromNumberToExcelColumn(col));
		}

		spreadsheet->setUndoAware(false);
		columnOffset = spreadsheet->resize(importMode, columnNames, colCount);

		if (importMode == AbstractFileFilter::ImportMode::Replace) {
			spreadsheet->clear();
			spreadsheet->setRowCount(rowCount);
		} else {
			if (spreadsheet->rowCount() < (rowCount))
				spreadsheet->setRowCount(rowCount);
		}

		for (int n = 0; n < colCount; ++n) {
			auto* col = spreadsheet->column(columnOffset + n);
			if (columnNumericTypes.at(n) == QXlsx::Cell::CellType::NumberType) {
				col->setColumnMode(AbstractColumn::ColumnMode::Double);
				auto* data = static_cast<QVector<double>*>(col->data());
				numericDataPointers.push_back(data);
				if (importMode == AbstractFileFilter::ImportMode::Replace)
					data->clear();
			} else if (columnNumericTypes.at(n) == QXlsx::Cell::CellType::DateType) {
				col->setColumnMode(AbstractColumn::ColumnMode::DateTime);
				auto* data = static_cast<QVector<QDateTime>*>(col->data());
				datetimeDataPointers.push_back(data);
				if (importMode == AbstractFileFilter::ImportMode::Replace)
					data->clear();
			} else {
				col->setColumnMode(AbstractColumn::ColumnMode::Text);
				auto* data = static_cast<QVector<QString>*>(col->data());
				stringDataPointers.push_back(data);
				if (importMode == AbstractFileFilter::ImportMode::Replace)
					data->clear();
			}
		}

		for (int row = regionToRead.firstRow(); row <= regionToRead.lastRow(); ++row) {
			int j = 0;
			unsigned int numericidx = 0;
			int datetimeidx = 0;
			int stringidx = 0;
			for (int col = regionToRead.firstColumn(); col <= regionToRead.lastColumn(); ++col) {
				const auto val = m_document->read(row, col);
				if (columnNumericTypes.at(j) == QXlsx::Cell::CellType::NumberType) {
					if (numericidx < numericDataPointers.size())
						static_cast<QVector<double>*>(numericDataPointers[numericidx++])->push_back(val.toDouble());
				} else if (columnNumericTypes.at(j) == QXlsx::Cell::CellType::DateType) {
					// QDEBUG("DATETIME:" << m_document->read(row, col).toDateTime())
					if (datetimeidx < datetimeDataPointers.size()) {
						if (val.toDateTime().time() != QTime(0, 0))
							isDateOnly = false;
						static_cast<QVector<QDateTime>*>(datetimeDataPointers[datetimeidx++])->push_back(val.toDateTime());
					}
				} else {
					if (!stringDataPointers.isEmpty() && stringidx < stringDataPointers.size()) {
						const auto s = val.toString();
						stringDataPointers[stringidx++]->operator<<(s);
					}
				}
				++j;
			}
		}
	} else if (dynamic_cast<Matrix*>(dataSource)) {
		QVector<AbstractColumn::ColumnMode> columnModes;
		QStringList vectorNames;

		columnModes.resize(colCount);
		std::vector<void*> dataContainer;
		dataContainer.reserve(colCount);
		columnOffset = dataSource->prepareImport(dataContainer, importMode, rowCount, colCount, vectorNames, columnModes);

		int i = 0;
		for (int row = region.firstRow(); row <= region.lastRow(); ++row) {
			int j = 0;
			for (int col = region.firstColumn(); col <= region.lastColumn(); ++col)
				static_cast<QVector<double>*>(dataContainer[j++])->operator[](i) = m_document->read(row, col).toDouble();
			++i;
		}
	}

	QLatin1String datetimeFormat;
	if (isDateOnly)
		datetimeFormat = QLatin1String("ddd MMM d yyyy");
	else
		datetimeFormat = QLatin1String("ddd MMM dd hh:mm:ss yyyy");

	if (dataSource)
		dataSource->finalizeImport(columnOffset, 1, colCount, datetimeFormat, importMode);
}
#endif

#ifdef HAVE_EXCEL
QVector<QXlsx::CellRange> ExcelFilterPrivate::dataRegions(const QString& fileName, const QString& sheetName) {
	DEBUG(Q_FUNC_INFO << ", sheet = " << STDSTRING(sheetName))
	QVector<QXlsx::CellRange> regions;

	if (!m_document || fileName.compare(m_fileName)) {
		delete m_document;
		m_document = new QXlsx::Document(fileName);
		m_fileName = fileName;
	}

	if (!m_document->selectSheet(sheetName))
		return regions;

	const auto& sheetDimension = m_document->dimension();

	for (int row = sheetDimension.firstRow(); row <= sheetDimension.lastRow(); ++row) {
		for (int col = sheetDimension.firstColumn(); col <= sheetDimension.lastColumn(); ++col) {
			const auto& region = cellContainedInRegions({row, col}, regions);
			// if the cell is contained in a range already
			if (region.isValid()) {
				// skip the columns of the already found range
				col += region.columnCount();
				continue;
			}

			auto cellData = m_document->read(row, col);

			// if a cell with data was found
			if (!cellData.isNull()) {
				int _row = row;
				int _col = col;

				// find the last column of this data region
				do {
					++_col;
					cellData = m_document->read(row, _col);

				} while (!cellData.isNull());

				// find the last row of this data region
				do {
					++_row;
					cellData = m_document->read(_row, col);
				} while (!cellData.isNull());

				// _col and _row will be incremented even at the last cell of the region (which was empty)
				// now decrement that
				--_col;
				--_row;

				QXlsx::CellRange range;
				// it's not just a single cell
				if (_row > row || _col > col) {
					range.setFirstColumn(col);
					range.setLastColumn(_col);
					range.setFirstRow(row);
					range.setLastRow(_row);
				} else {
					range.setFirstColumn(col);
					range.setLastColumn(col);
					range.setFirstRow(row);
					range.setLastRow(row);
				}

				if (range.isValid()) {
					regions.push_back(range);
					// jump to the end of range
					const int jump = _col - col + 1;
					col += jump;

					// special case - range goes until the last column
					if (col == m_document->dimension().lastColumn()) {
						row = _row;
					}
				}
			}
		}
	}
	return regions;
}
#endif

#ifdef HAVE_EXCEL
QVector<QStringList> ExcelFilterPrivate::previewForDataRegion(const QString& sheetName, const QXlsx::CellRange& region, bool* okToMatrix, int lines) {
	DEBUG(Q_FUNC_INFO << ", sheet name = " << STDSTRING(sheetName))
	QVector<QStringList> infoString;

	if (!m_document) {
		delete m_document;
		m_document = new QXlsx::Document(m_fileName);
	}

	if (m_document->selectSheet(sheetName) && region.isValid()) { // valid sheet name and region
		if (okToMatrix && dataRangeCanBeExportedToMatrix(region))
			*okToMatrix = true;

		const auto& documentRegion = m_document->dimension();
		if (region.lastRow() <= documentRegion.lastRow() && region.lastColumn() <= documentRegion.lastColumn()) {
			const int rows = std::min(lines, region.lastRow());
			for (int row = region.firstRow(); row <= rows; ++row) {
				QStringList line;
				for (int col = region.firstColumn(); col <= region.lastColumn(); ++col) {
					// see https://github.com/QtExcel/QXlsx/wiki for read() vs. cellAt()->value()
					const auto val = m_document->read(row, col);
					if (val.isValid())
						QDEBUG("value =" << val << ", type =" << val.type() << ", user type =" << QVariant::typeToName(val.userType()))
					// correctly read values and show with locale
					if (val.canConvert(QMetaType::Double))
						line << QLocale().toString(val.toDouble());
					else if (val.canConvert(QMetaType::QDateTime)) {
						QDateTime dt = val.toDateTime();
						// TODO: use certain date/datetime format?
						if (dt.time() == QTime(0, 0)) // just a date
							line << val.toDate().toString();
						else
							line << dt.toString();
					} else if (val.canConvert(QMetaType::QTime)) {
						QTime t = val.toTime();
						// TODO: use certain time format?
						line << t.toString();
					} else
						line << val.toString();
				}
				infoString << line;
			}
		}
	}

	return infoString;
}
#endif

#ifdef HAVE_EXCEL
QXlsx::CellRange ExcelFilterPrivate::cellContainedInRegions(const QXlsx::CellReference& cell, const QVector<QXlsx::CellRange>& regions) const {
	for (const auto& region : regions) {
		if (cell.column() >= region.firstColumn() && cell.column() <= region.lastColumn() && cell.row() >= region.firstRow() && cell.row() <= region.lastRow())
			return region;
	}
	return {};
}
#endif

void ExcelFilterPrivate::parse(const QString& fileName, QTreeWidgetItem* parentItem) {
	DEBUG(Q_FUNC_INFO)
#ifdef HAVE_EXCEL
	m_document = new QXlsx::Document(fileName);
	m_fileName = fileName;

	const auto sheets = m_document->sheetNames();
	auto* fileNameItem = new QTreeWidgetItem(QStringList() << fileName);
	parentItem->addChild(fileNameItem);

	for (const auto& sheet : sheets) {
		const auto regionsForSheet = dataRegions(fileName, sheet);

		auto* sheetItem = new QTreeWidgetItem(QStringList() << sheet);
		sheetItem->setIcon(0, QIcon::fromTheme(QStringLiteral("folder")));
		sheetItem->setFlags(sheetItem->flags() & ~Qt::ItemIsSelectable);

		fileNameItem->addChild(sheetItem);

		for (const auto& region : regionsForSheet) {
			auto* regionItem = new QTreeWidgetItem(QStringList() << QString(region.toString()));
			regionItem->setIcon(0, QIcon::fromTheme(QStringLiteral("x-office-spreadsheet")));
			regionItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);

			sheetItem->addChild(regionItem);
		}
	}
#else
	Q_UNUSED(fileName)
	Q_UNUSED(parentItem)
#endif
}

#ifdef HAVE_EXCEL
bool ExcelFilterPrivate::dataRangeCanBeExportedToMatrix(const QXlsx::CellRange& range) const {
	for (int i = range.firstRow(); i <= range.lastRow(); ++i) {
		for (int j = range.firstColumn(); j <= range.lastColumn(); ++j) {
			const auto* cell = m_document->cellAt(i, j);
			if (cell && cell->cellType() != QXlsx::Cell::CellType::NumberType) {
				if (cell->cellType() == QXlsx::Cell::CellType::CustomType) {
					bool ok = false;
					cell->value().toDouble(&ok);
					if (ok)
						continue;
				}
				return false;
			}
		}
	}
	return true;
}
#endif

QStringList ExcelFilterPrivate::sheets() const {
#ifdef HAVE_EXCEL
	return m_document ? m_document->sheetNames() : QStringList();
#else
	return {};
#endif
}

#ifdef HAVE_EXCEL
QXlsx::CellRange ExcelFilterPrivate::dimension() const {
	return m_document ? m_document->dimension() : QXlsx::CellRange();
}
#endif

#ifdef HAVE_EXCEL
QXlsx::Cell::CellType ExcelFilterPrivate::columnTypeInRange(const int column, const QXlsx::CellRange& range) const {
	bool numeric = false, datetime = false;
	if (column >= range.firstColumn() && column <= range.lastColumn()) {
		for (int row = range.firstRow(); row <= range.lastRow(); ++row) {
			const auto* cell = m_document->cellAt(row, column);
			if (!cell)
				continue;

			// QDEBUG(" cell type =" << cell->cellType())
			if (cell->cellType() == QXlsx::Cell::CellType::StringType)
				return QXlsx::Cell::CellType::StringType;
			if (cell->cellType() == QXlsx::Cell::CellType::NumberType)
				numeric = true;
			else if (cell->cellType() == QXlsx::Cell::CellType::DateType) // switch to date if date cell
				datetime = true;
			else if (cell->cellType() == QXlsx::Cell::CellType::CustomType) {
				bool ok = false;
				cell->value().toDouble(&ok);
				if (ok)
					numeric = true;
				else
					return QXlsx::Cell::CellType::StringType;
			}
		}
	}
	if (numeric && !datetime)
		return QXlsx::Cell::CellType::NumberType;
	if (datetime && !numeric)
		return QXlsx::Cell::CellType::DateType;

	// numeric and datetime
	return QXlsx::Cell::CellType::StringType;
}
#endif

QString ExcelFilter::convertFromNumberToExcelColumn(int n) {
	// main code from https://www.geeksforgeeks.org/find-excel-column-name-given-number/
	// Function to print Excel column name for a given column number

	char str[1000]; // To store result (Excel column name)
	int i = 0; // To store current index in str which is result

	while (n > 0) {
		// Find remainder
		int rem = n % 26;

		// If remainder is 0, then a 'Z' must be there in output
		if (rem == 0) {
			str[i++] = 'Z';
			n = (n / 26) - 1;
		} else // If remainder is non-zero
		{
			str[i++] = (rem - 1) + 'A';
			n = n / 26;
		}
	}
	str[i] = '\0';

	// Reverse the string and print result
	std::reverse(str, str + strlen(str));

	return QLatin1String(str);
}
