/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <algorithm> /* ::std::sort */
#include <QColor>
#include <QDir>
#include <QMimeData>
#include <QMimeDatabase>
#include <QSet>
#include <QTemporaryDir>
#include <QSqlRecord>
#include <QUrl>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/isds/message_attachment.h"
#include "src/datovka_shared/log/log.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_tables.h"
#include "src/models/attachments_model.h"

#define LOCAL_DATABASE_STR QStringLiteral("local database")

AttachmentTblModel::AttachmentTblModel(bool highlightUnlistedSuff,
    QObject *parent)
    : TblModel(parent),
    m_highlightUnlistedSuff(highlightUnlistedSuff)
{
	/* Fixed column count. */
	m_columnCount = MAX_COL;
}

/*!
 * @brief Check whether file exists.
 *
 * @param[in] filePath Path to file.
 * @return True if file exists.
 */
static inline
bool fileExistent(const QString &filePath)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		return false;
	}
	return QFileInfo(filePath).exists();
}

/*!
 * @brief Check whether file exists and is readable.
 *
 * @param[in] filePath Path to file.
 * @return True if file is readable.
 */
static
bool fileReadable(const QString &filePath)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		return false;
	}
	QFileInfo fileInfo(filePath);
	return fileInfo.exists() && fileInfo.isReadable();
}

/*!
 * @brief Read file size.
 *
 * @param[in] filePath Path to file.
 * @return File size or negative number if cannot determine.
 */
static
qint64 getFileSize(const QString &filePath)
 {
	QFileInfo fileInfo(filePath);
	if (fileInfo.exists() && fileInfo.isReadable()) {
		return fileInfo.size();
	}
	return -1;
}

/*!
 * @brief Read file content.
 *
 * @param[in] filePath Path to file.
 * @return Raw (non-base64-emcoded) file content.
 */
static
QByteArray getFileContent(const QString &filePath)
 {
	QFile file(filePath);
	if (file.exists()) {
		if (!file.open(QIODevice::ReadOnly)) {
			logErrorNL("Could not open file '%s'.",
			    filePath.toUtf8().constData());
			goto fail;
		}
		return file.readAll();
	}
fail:
	return QByteArray();
}

/*!
 * @brief Return lower-case file suffix.
 */
#define fileSuffix(fileName) \
	QFileInfo(fileName).suffix().toLower()

/*!
 * @brief Check whether file name has allowed suffix.
 *
 * @param[in] fileName File name string.
 * @return True if file has suffix mentioned in Operation Rules of ISDS.
 */
static
bool fileHasAllowedSuffix(const QString &fileName)
{
	if (Q_UNLIKELY(fileName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	const QString suff(fileSuffix(fileName));
	if (suff.isEmpty()) {
		return false;
	}
	return Isds::Attachment::allowedFileSuffixes().contains(suff);
}

/*!
 * @brief Generate a description why the file does not meet the suffix
 *     restrictions.
 *
 * @param[in] fileName File name string.
 * @return Null string if suffix is allowed.
 */
static
QString suffixNotification(const QString &fileName)
{
	if (!fileHasAllowedSuffix(fileName)) {
		QString message(fileSuffix(fileName).isEmpty() ?
		    AttachmentTblModel::tr(
		        "The file name '%1' contains no suffix.").arg(fileName) :
		    AttachmentTblModel::tr(
		        "The suffix of file '%1' does not match the list of suffixes listed in the Operating Rules of ISDS.").arg(fileName));
		message += QStringLiteral("\n") +
		    AttachmentTblModel::tr(
		        "It may happen that the server will reject sending this message.");
		return message;
	}
	return QString();
}

/*!
 * @brief Return model file size value.
 *
 * @param[in] cSize File size.
 * @return Empty vaule or file size in bytes.
 */
static inline
QVariant modelFileSize(qint64 cSize)
{
	return (cSize > 0) ? cSize : QVariant();
}

QVariant AttachmentTblModel::data(const QModelIndex &index, int role) const
{
	switch (role) {
	case Qt::DisplayRole:
		switch (index.column()) {
		case BINARY_CONTENT_COL:
			{
				const QString fPath(_data(index.row(), FPATH_COL,
				    role).toString());

				if (fPath == LOCAL_DATABASE_STR) {
					/* Data should be in the model. */
					return _data(index, role);
				} else if (fileReadable(fPath)) {
					/* Read file. */
					return getFileContent(fPath);
				} else {
					/*
					 * This fallback is used when filling model
					 * with zfo content.
					 */
					return _data(index, role);
				}
			}
			break;
		case BINARY_SIZE_COL:
			{
				/* Read size from model if it is set. */
				qint64 binarySize = _data(index.row(),
				    BINARY_SIZE_COL, role).toLongLong();
				if (binarySize > 0) {
					return binarySize;
				}
			}
			{
				const QString fPath(_data(index.row(),
				    FPATH_COL, role).toString());
				const QByteArray rawData(_data(index.row(),
				    BINARY_CONTENT_COL, role).toByteArray());
				if (fPath == LOCAL_DATABASE_STR) {
					/* Get attachment size. */
					return modelFileSize(rawData.size());
				} else if (fileReadable(fPath)) {
					/* File size. */
					return modelFileSize(getFileSize(fPath));
				} else {
					/*
					 * This fallback is used when filling model
					 * with zfo content.
					 */
					return modelFileSize(rawData.size());
				}
			}
			break;
		case FPATH_COL:
			{
				const QString fPath(_data(index).toString());
				return (fPath == LOCAL_DATABASE_STR) ?
				    tr("local database") : QDir::toNativeSeparators(fPath);
			}
			break;
		default:
			return _data(index, role);
			break;
		}
		break;
	case Qt::ToolTipRole:
		if (m_highlightUnlistedSuff) {
			const QString notif(suffixNotification(
			    _data(index.row(), FNAME_COL, Qt::DisplayRole).toString()));
			if (!notif.isEmpty()) {
				return notif;
			}
		}
		return QVariant();
		break;
	case Qt::ForegroundRole:
		if (m_highlightUnlistedSuff &&
		    !fileHasAllowedSuffix(_data(index.row(), FNAME_COL, Qt::DisplayRole).toString())) {
			return QColor(Qt::darkRed);
		} else {
			return QVariant();
		}
		break;
	case Qt::AccessibleTextRole:
		switch (index.column()) {
		case FNAME_COL:
			{
				const QString fileName(data(index).toString());
				QString message(
				    headerData(index.column(), Qt::Horizontal).toString() +
				    QStringLiteral(" ") + fileName);
				if (m_highlightUnlistedSuff) {
					if (m_highlightUnlistedSuff) {
						const QString notif(suffixNotification(
						    _data(index.row(), FNAME_COL, Qt::DisplayRole).toString()));
						if (!notif.isEmpty()) {
							message += QStringLiteral(" ") + notif;
						}
					}
				}
				return message;
			}
			break;
		case MIME_COL:
		case FPATH_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QStringLiteral(" ") + data(index).toString();
			break;
		case BINARY_SIZE_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QStringLiteral(" ") + data(index).toString() +
			    QStringLiteral(" ") + tr("bytes");
			break;
		default:
			return QVariant();
			break;
		}
		break;
	case ROLE_PLAIN_DISPLAY:
		/* Explicitly asking associated file path. */
		if (index.column() == FPATH_COL) {
			const QString fPath(_data(index.row(), FPATH_COL,
			    Qt::DisplayRole).toString());
			const QByteArray binaryData(_data(index.row(),
			    BINARY_CONTENT_COL, Qt::DisplayRole).toByteArray());

			if ((fPath == LOCAL_DATABASE_STR) && !binaryData.isEmpty()) {
				return QVariant(); /* No file present. */
			} else if (fileReadable(fPath)) {
				return fPath; /* File present. */
			} else {
				return QVariant();
			}
		}
		return QVariant();
		break;
	default:
		return _data(index, role);
		break;
	}
}

Qt::DropActions AttachmentTblModel::supportedDropActions(void) const
{
	/* The model must provide removeRows() to be able to use move action. */
	return Qt::CopyAction | Qt::MoveAction;
}

Qt::ItemFlags AttachmentTblModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = TblModel::flags(index);

	if (index.isValid()) {
		/* Don't allow drops on items. */
		defaultFlags |= Qt::ItemIsDragEnabled; // | Qt::ItemIsDropEnabled;
	} else {
		defaultFlags |= Qt::ItemIsDropEnabled;
	}

	return defaultFlags;
}

QStringList AttachmentTblModel::mimeTypes(void) const
{
	return QStringList(QStringLiteral("text/uri-list"));
}

/*!
 * @brief Converts list of absolute file names to list of URLs.
 *
 * @param[in] fileNames List of absolute file names.
 * @return List of URLs or empty list on error.
 */
static
QList<QUrl> fileUrls(const QStringList &fileNames)
{
	QList<QUrl> uriList;

	for (const QString &fileName : fileNames) {
		uriList.append(QUrl::fromLocalFile(fileName));
	}

	return uriList;
}

QMimeData *AttachmentTblModel::mimeData(const QModelIndexList &indexes) const
{
	QModelIndexList lineIndexes = sortedUniqueLineIndexes(indexes,
	    FNAME_COL);
	if (Q_UNLIKELY(lineIndexes.isEmpty())) {
		return Q_NULLPTR;
	}

	QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);
	if (Q_UNLIKELY(tmpDirPath.isEmpty())) {
		return Q_NULLPTR;
	}

	QStringList fileNames(accessibleFiles(tmpDirPath, lineIndexes));
	if (Q_UNLIKELY(fileNames.isEmpty())) {
		logErrorNL("%s", "Could not write temporary files.");
		return Q_NULLPTR;
	}

	QMimeData *mimeData = new (::std::nothrow) QMimeData;
	if (Q_UNLIKELY(Q_NULLPTR == mimeData)) {
		return Q_NULLPTR;
	}
	QList<QUrl> urlList = fileUrls(fileNames);
	mimeData->setUrls(urlList);

	return mimeData;
}

bool AttachmentTblModel::canDropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column,
    const QModelIndex &parent) const
{
	Q_UNUSED(action);
	Q_UNUSED(row);
	Q_UNUSED(column);
	Q_UNUSED(parent);

	if (Q_UNLIKELY(Q_NULLPTR == data)) {
		return false;
	}

	return data->hasUrls();
}

/*!
 * @brief Convert a list of URLs to a list of absolute file paths.
 *
 * @param[in] uriList List of file URLs.
 * @return List of absolute file paths or empty list on error.
 */
static
QStringList filePaths(const QList<QUrl> &uriList)
{
	QStringList filePaths;

	for (const QUrl &uri : uriList) {
		if (!uri.isValid()) {
			logErrorNL("Dropped invalid URL '%s'.",
			    uri.toString().toUtf8().constData());
			return QList<QString>();
		}

		if (!uri.isLocalFile()) {
			logErrorNL("Dropped URL '%s' is not a local file.",
			    uri.toString().toUtf8().constData());
			return QList<QString>();
		}

		filePaths.append(uri.toLocalFile());
	}

	return filePaths;
}

bool AttachmentTblModel::dropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
	if (!canDropMimeData(data, action, row, column, parent)) {
		return false;
	}

	/* data->hasUrls() tested in canDropMimeData() */
	QStringList paths(filePaths(data->urls()));

	if (parent.isValid()) {
		/* Non-root node; append data. */
		row = rowCount();
	}

	for (const QString &filePath : paths) {
		insertAttachmentFile(filePath, row);
		++row; /* Move to next row. */
	}

	return true;
}

void AttachmentTblModel::setHeader(void)
{
	int i;

	for (i = 0; i < MessageDb::fileItemIds.size(); ++i) {
		/* Description. */
		setHeaderData(i, Qt::Horizontal,
		    flsTbl.attrProps.value(MessageDb::fileItemIds[i]).desc,
		    Qt::DisplayRole);
		/* Data type. */
		setHeaderData(i, Qt::Horizontal,
		    flsTbl.attrProps.value(MessageDb::fileItemIds[i]).type,
		    ROLE_DB_ENTRY_TYPE);
	}

	/* Columns with missing names. */
	setHeaderData(BINARY_SIZE_COL, Qt::Horizontal, tr("File size"),
	    Qt::DisplayRole);
	setHeaderData(FPATH_COL, Qt::Horizontal, tr("File path"),
	    Qt::DisplayRole);
}

void AttachmentTblModel::appendData(
    const QList<MessageDb::AttachmentEntry> &entryList)
{
	if (Q_UNLIKELY(MessageDb::fileItemIds.size() != 6)) {
		Q_ASSERT(0);
		return;
	}

	m_columnCount = 6;

	if (Q_UNLIKELY(entryList.isEmpty())) {
		/* Don't do anything. */
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	for (const MessageDb::AttachmentEntry &entry : entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[ATTACHID_COL] = entry.id;
		row[MSGID_COL] = entry.messageId;
		row[BINARY_CONTENT_COL] = entry.binaryContent;
		row[FNAME_COL] = entry.dmFileDescr;
		row[MIME_COL] = entry.dmMimeType;
		row[BINARY_SIZE_COL] = entry.binaryContent.size();

		m_data[m_rowCount++] = macroStdMove(row);
	}

	endInsertRows();
}

bool AttachmentTblModel::setMessage(const Isds::Message &message)
{
	if (Q_UNLIKELY(message.isNull())) {
		Q_ASSERT(0);
		return false;
	}

	beginResetModel();
	m_data.clear();
	m_rowsAllocated = 0;
	m_rowCount = 0;
	endResetModel();

	/* m_columnCount = MAX_COL; */

	return appendMessageData(message);
}

/* File content will be held within model. */
//#define HOLD_FILE_CONTENT

/*!
 * @brief Adjust MIME type name if such is insufficiently detected.
 *
 * @param[in,out] mimeName Mime type name to be altered.
 * @param[in]     suffix Lower-case suffix used to determine the adequate
 *                       mime type.
 */
static
void adjustMimeName(QString &mimeName, const QString &suffix)
{
	if (Q_UNLIKELY(!Isds::Attachment::allowedFileSuffixes().contains(suffix))) {
		/* We have no such suffix listed. Use detected MIME name. */
		logWarningNL("Unknown file suffix '%s'.",
		    suffix.toUtf8().constData());
		return;
	}

	if (Q_UNLIKELY(!Isds::Attachment::expectedMimeTypeNamesForSuffix(suffix)
	        .contains(mimeName))) {
		/* Replace detected MIME name with an alternative one. */
		QString suggestedMimeName =
		    Isds::Attachment::suggestedMimeTypeName(suffix);
		if (!suggestedMimeName.isEmpty()) {
			/* Alternative MIME name found. */
			logWarningNL(
			    "Unexpected MIME name '%s' for suffix '%s'. Changing MIME name to '%s'.",
			    mimeName.toUtf8().constData(), suffix.toUtf8().constData(),
			    suggestedMimeName.toUtf8().constData());
			mimeName = macroStdMove(suggestedMimeName);
		}
	}
}

/*!
 * @brief Determine MIME string.
 *
 * @param[in] file File.
 * @return MIME name or description that it is unknown.
 */
static
QString mimeStr(const QFileInfo &fileInfo)
{
	const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(fileInfo);
	if (mimeType.isValid()) {
		QString mimeName = mimeType.name();

		adjustMimeName(mimeName, fileInfo.suffix().toLower());

		return mimeName;
	}

	/* Get lowercase suffix. */
	const QString suffix = fileInfo.suffix().toLower();

	return AttachmentTblModel::tr("unknown");
}

/*!
 * @brief Determine MIME string.
 *
 * @param[in] fileName File name.
 * @param[in] data Binary content.
 * @return MIME name or description that it is unknown.
 */
static
QString mimeStr(const QString &fileName, const QByteArray &data)
{
	if (!fileName.isEmpty() && !data.isEmpty()) {
		const QMimeType mimeType =
		    QMimeDatabase().mimeTypeForFileNameAndData(fileName, data);
		if (mimeType.isValid()) {
			QString mimeName = mimeType.name();

			adjustMimeName(mimeName,
			    fileName.split('.').last().toLower());

			return mimeName;
		}
	}

	return AttachmentTblModel::tr("unknown");
}

int AttachmentTblModel::insertAttachmentFile(const QString &filePath, int row)
{
	if (Q_UNLIKELY(!fileExistent(filePath))) {
		logWarningNL("Ignoring non-existent file '%s'.",
		    filePath.toUtf8().constData());
		return FILE_NOT_EXISTENT;
	} else if (Q_UNLIKELY(!fileReadable(filePath))) {
		logWarningNL("Ignoring file '%s' which cannot be read.",
		    filePath.toUtf8().constData());
		return FILE_NOT_READABLE;
	}

	const QFileInfo attFileInfo(filePath);

	int fileSize = attFileInfo.size();
	if (Q_UNLIKELY(0 == fileSize)) {
		logWarningNL("Ignoring file '%s' with zero size.",
		    filePath.toUtf8().constData());
		return FILE_ZERO_SIZE;
	}

	for (int i = 0; i < rowCount(); ++i) {
		if (Q_UNLIKELY(
		        QDir::fromNativeSeparators(_data(i, FPATH_COL).toString()) ==
		            QDir::fromNativeSeparators(filePath))) {
			/* Already in table. */
			logWarning("File '%s' already in table.\n",
			    filePath.toUtf8().constData());
			return FILE_ALREADY_PRESENT;
		}
	}

	QVector<QVariant> rowVect(m_columnCount);

	//rowVect[ATTACHID_COL] = QVariant();
	//rowVect[MSGID_COL] = QVariant();
#if defined HOLD_FILE_CONTENT
	rowVect[BINARY_CONTENT_COL] = getFileContent(filePath);
#else /* !defined HOLD_FILE_CONTENT */
	rowVect[BINARY_CONTENT_COL] = QByteArray();
#endif /* defined HOLD_FILE_CONTENT */
	rowVect[FNAME_COL] = attFileInfo.fileName();
	rowVect[MIME_COL] = mimeStr(attFileInfo);
#if defined HOLD_FILE_CONTENT
	rowVect[BINARY_SIZE_COL] = fileSize;
#else /* !defined HOLD_FILE_CONTENT */
	rowVect[BINARY_SIZE_COL] = 0;
#endif /* defined HOLD_FILE_CONTENT */
	rowVect[FPATH_COL] = QDir::fromNativeSeparators(filePath);

	/* Check data duplicity. */
	if (insertVector(rowVect, row, true)) {
		return fileSize;
	} else {
		return OTHER_ERROR;
	}
}

bool AttachmentTblModel::holdsAttachmentFile(const QString &filePath) const
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		return false;
	}

	for (int row = 0; row < rowCount(); ++row) {
		const QString modelPath = QDir::fromNativeSeparators(
		    _data(row, FPATH_COL, Qt::DisplayRole).toString());
		if (modelPath == QDir::fromNativeSeparators(filePath)) {
			return true;
		}
	}

	return false;
}

bool AttachmentTblModel::appendBinaryAttachment(const QByteArray &binaryContent,
    const QString &fName)
{
	if (Q_UNLIKELY(binaryContent.isEmpty() || fName.isEmpty())) {
		return false;
	}

	QVector<QVariant> rowVect(m_columnCount);

	//rowVect[ATTACHID_COL] = QVariant();
	//rowVect[MSGID_COL] = QVariant();
	rowVect[BINARY_CONTENT_COL] = binaryContent;
	rowVect[FNAME_COL] = fName;
	rowVect[MIME_COL] = mimeStr(fName, binaryContent);
	rowVect[BINARY_SIZE_COL] = binaryContent.size();
	rowVect[FPATH_COL] = LOCAL_DATABASE_STR;

	/* Check data duplicity. */
	return insertVector(rowVect, rowCount(), true);
}

void AttachmentTblModel::overrideAttachmentContent(qint64 attachId,
    const QByteArray &binaryContent)
{
	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, ATTACHID_COL, Qt::DisplayRole).toLongLong() == attachId) {
			m_data[row][BINARY_CONTENT_COL] = binaryContent;
			m_data[row][BINARY_SIZE_COL] = binaryContent.size();
			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));
			break;
		}
	}
}

bool AttachmentTblModel::holdsAttachmentContent(int row) const
{
	return !(_data(row, BINARY_CONTENT_COL, Qt::DisplayRole)
	    .toByteArray().isEmpty());
}

qint64 AttachmentTblModel::attachId(int row) const
{
	bool iOk = false;
	qint64 ret = _data(row, ATTACHID_COL, Qt::DisplayRole).toLongLong(&iOk);
	if (Q_UNLIKELY(!iOk)) {
		Q_ASSERT(0);
		return -1;
	}

	return ret;
}

qint64 AttachmentTblModel::totalAttachmentSize(void) const
{
	qint64 sum = 0;

	for (int row = 0; row < rowCount(); ++row) {
		sum += data(index(row, BINARY_SIZE_COL),
		    Qt::DisplayRole).toLongLong();
	}

	return sum;
}

/*!
 * @brief Used for sorting index lists.
 */
class IndexRowLess {
public:
	bool operator()(const QModelIndex &a, const QModelIndex &b) const
	{
		return a < b;
	}
};

QModelIndexList AttachmentTblModel::sortedUniqueLineIndexes(
    const QModelIndexList &indexes, int dfltCoumn)
{
	QSet<int> lines;
	QModelIndexList uniqueLines;

	for (const QModelIndex &index : indexes) {
		if (lines.contains(index.row())) {
			continue;
		}
		uniqueLines.append(index.sibling(index.row(), dfltCoumn));
		lines.insert(index.row());
	}

	::std::sort(uniqueLines.begin(), uniqueLines.end(), IndexRowLess());

	return uniqueLines;
}

bool AttachmentTblModel::appendMessageData(const Isds::Message &message)
{
	if (Q_UNLIKELY(message.isNull())) {
		Q_ASSERT(0);
		return false;
	}

	const QList<Isds::Document> &docList(message.documents());
	if (docList.isEmpty()) {
		logWarning("%s\n", "Message has no documents.");
	}
	for (const Isds::Document &doc : docList) {
		if (Q_UNLIKELY(doc.isNull())) {
			Q_ASSERT(0);
			return false;
		}

		QVector<QVariant> row(m_columnCount);

		row[BINARY_CONTENT_COL] = doc.binaryContent();
		row[FNAME_COL] = doc.fileDescr();
		row[MIME_COL] = mimeStr(doc.fileDescr(), doc.binaryContent());
		row[BINARY_SIZE_COL] = doc.binaryContent().size();

		/* Don't check data duplicity! */
		insertVector(row, rowCount(), false);
	}

	return true;
}

bool AttachmentTblModel::insertVector(const QVector<QVariant> &rowVect,
    int row, bool insertUnique)
{
	if (Q_UNLIKELY(rowVect.size() != m_columnCount)) {
		return false;
	}

	if ((row < 0) || (row > rowCount())) {
		/*
		 * -1 is passed when dropping directly on index which may be
		 *  invalid for root node.
		 */
		row = rowCount();
	}

	if (insertUnique && nameAndContentPresent(rowVect.at(BINARY_CONTENT_COL),
	        rowVect.at(FNAME_COL), rowVect.at(FPATH_COL))) {
		/* Fail if data present. */
		logWarningNL("Same data '%s' already attached.",
		    rowVect.at(FNAME_COL).toString().toUtf8().constData());
		return false;
	}

	Q_ASSERT((0 <= row) && (row <= rowCount()));

	beginInsertRows(QModelIndex(), row, row);

	reserveSpace();

	if (row == rowCount()) {
		m_data[m_rowCount++] = rowVect;
	} else {
		m_data.insert(row, rowVect);
		++m_rowCount;
	}

	endInsertRows();

	return true;
}

bool AttachmentTblModel::nameAndContentPresent(const QVariant &binaryContent,
    const QVariant &fName, const QVariant &fPath) const
{
	/* Cannot use foreach (), because contains pre-allocated empty lines. */
	for (int row = 0; row < rowCount(); ++row) {
		const QVector<QVariant> &rowEntry = m_data.at(row);
		if ((rowEntry.at(FNAME_COL) == fName) &&
		    (QDir::fromNativeSeparators(rowEntry.at(FPATH_COL).toString()) ==
		         QDir::fromNativeSeparators(fPath.toString())) &&
		    (rowEntry.at(BINARY_CONTENT_COL) == binaryContent)) {
			return true;
		}
	}

	return false;
}

/*!
 * @brief Creates temporary files related to selected view items.
 *
 * @param[in] tmpDirPath Temporary directory path.
 * @param[in] index Index identifying line.
 * @param[in] fileNumber Number identifying the file.
 * @return Absolute file name or empty string on error.
 */
static
QString temporaryFile(const QString &tmpDirPath, const QModelIndex &index,
    int fileNumber)
{
	if (Q_UNLIKELY(tmpDirPath.isEmpty())) {
		Q_ASSERT(0);
		return QString();
	}

	QString subirPath(tmpDirPath + QDir::separator() +
	    QString::number(fileNumber++));
	{
		/*
		 * Create a separate subdirectory because the files
		 * may have equal names.
		 */
		QDir dir(tmpDirPath);
		if (Q_UNLIKELY(!dir.mkpath(subirPath))) {
			logError("Could not create directory '%s'.",
			    subirPath.toUtf8().constData());
			return QString();
		}
	}
	QString attachAbsPath(subirPath + QDir::separator());
	{
		/* Determine full file path. */
		QModelIndex fileNameIndex = index.sibling(index.row(),
		    AttachmentTblModel::FNAME_COL);
		if(Q_UNLIKELY(!fileNameIndex.isValid())) {
			Q_ASSERT(0);
			return QString();
		}
		QString attachFileName(fileNameIndex.data().toString());
		if (Q_UNLIKELY(attachFileName.isEmpty())) {
			Q_ASSERT(0);
			return QString();
		}
		attachAbsPath += attachFileName;
	}
	QByteArray attachData;
	{
		/* Obtain data. */
		QModelIndex dataIndex = index.sibling(index.row(),
		    AttachmentTblModel::BINARY_CONTENT_COL);
		if (Q_UNLIKELY(!dataIndex.isValid())) {
			Q_ASSERT(0);
			return QString();
		}
		attachData = dataIndex.data().toByteArray();
		if (Q_UNLIKELY(attachData.isEmpty())) {
			Q_ASSERT(0);
			return QString();
		}
	}
	if (Q_UNLIKELY(WF_SUCCESS != writeFile(attachAbsPath, attachData, false))) {
		return QString();
	}

	return attachAbsPath;
}

QStringList AttachmentTblModel::accessibleFiles(const QString &tmpDirPath,
    const QModelIndexList &indexes) const
{
	QStringList accessibleFileList;
	int fileNumber = 0;

	for (const QModelIndex &idx : indexes) {
		++fileNumber;
		const QString fPath(_data(idx.row(), FPATH_COL,
		    Qt::DisplayRole).toString());

		QString fileName;

		if (fPath == LOCAL_DATABASE_STR) {
			fileName = temporaryFile(tmpDirPath, idx, fileNumber);
		} else if (fileReadable(fPath)) {
			fileName = fPath;
		} else {
			/* This fallback should not be used. */
			fileName = temporaryFile(tmpDirPath, idx, fileNumber);
		}

		if (Q_UNLIKELY(fileName.isEmpty())) {
			return QStringList();
		}

		accessibleFileList.append(fileName);
	}

	return accessibleFileList;
}
