/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2014  Georg Rudoy
 *
 * Distributed under the Boost Software License, Version 1.0.
 * (See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
 **********************************************************************/

#include "dumbsync.h"
#include <memory>
#include <QConcatenateTablesProxyModel>
#include <QFileInfo>
#include <QDir>
#include <QIcon>
#include <QLineEdit>
#include <QSortFilterProxyModel>
#include <QtConcurrentRun>
#include <interfaces/core/icoreproxy.h>
#include <interfaces/core/iiconthememanager.h>
#include <interfaces/core/ipluginsmanager.h>
#include <interfaces/devices/deviceroles.h>
#include <interfaces/devices/iremovabledevmanager.h>
#include <util/sll/qtutil.h>
#include <util/threads/coro.h>
#include <util/util.h>
#include <xmlsettingsdialog/basesettingsmanager.h>
#include <xmlsettingsdialog/xmlsettingsdialog.h>
#include <interfaces/lmp/ilmpproxy.h>
#include <interfaces/lmp/ilmputilproxy.h>
#include <util/lmp/util.h>

using QFile_ptr = std::shared_ptr<QFile>;

namespace LC::LMP::DumbSync
{
	using XmlSettingsManager = Util::SingletonSettingsManager<"LMP_DumbSync">;

	namespace
	{
		class MountableFilterModel : public QSortFilterProxyModel
		{
		public:
			using QSortFilterProxyModel::QSortFilterProxyModel;

			QVariant data (const QModelIndex& index, int role) const override
			{
				if (role == Qt::DecorationRole)
					return GetProxyHolder ()->GetIconThemeManager ()->GetIcon ("drive-removable-media-usb"_qs);

				if (role != Qt::DisplayRole)
					return QSortFilterProxyModel::data (index, role);

				const auto& srcIdx = mapToSource (index);

				const auto& mounts = srcIdx.data (MassStorageRole::MountPoints).toStringList ();
				const auto& mountText = mounts.isEmpty () ?
						Plugin::tr ("not mounted") :
						Plugin::tr ("mounted at %1").arg (mounts.join ("; "));

				const auto& size = srcIdx.data (MassStorageRole::TotalSize).toLongLong ();
				return "%1 (%2, %3), %4"_qs
						.arg (srcIdx.data (MassStorageRole::VisibleName).toString (),
								Util::MakePrettySize (size),
								srcIdx.data (MassStorageRole::DevFile).toString (),
								mountText);
			}
		protected:
			bool filterAcceptsRow (int row, const QModelIndex&) const override
			{
				const auto& srcIdx = sourceModel ()->index (row, 0);
				return srcIdx.data (MassStorageRole::IsMountable).toBool ();
			}
		};
	}

	void Plugin::Init (ICoreProxy_ptr)
	{
		XSD_ = std::make_shared<Util::XmlSettingsDialog> ();
		XSD_->RegisterObject (&XmlSettingsManager::Instance (), "lmpdumbsyncsettings.xml"_qs);

		DevModelsMerger_ = new QConcatenateTablesProxyModel { this };
		SyncTargets_ = new MountableFilterModel { this };
	}

	void Plugin::SecondInit ()
	{
		for (const auto& mgr : GetProxyHolder ()->GetPluginsManager ()->GetAllCastableTo<IRemovableDevManager*> ())
			if (mgr->SupportsDevType (DeviceType::MassStorage))
			{
				const auto model = mgr->GetDevicesModel ();
				DevModelsMerger_->addSourceModel (model);
				Model2DevManager_ [model] = mgr;
			}

		SyncTargets_->setSourceModel (DevModelsMerger_);
	}

	void Plugin::Release ()
	{
	}

	QByteArray Plugin::GetUniqueID () const
	{
		return "org.LeechCraft.LMP.DumbSync"_qba;
	}

	QString Plugin::GetName () const
	{
		return "LMP DumbSync"_qs;
	}

	QString Plugin::GetInfo () const
	{
		return tr ("Music collection synchronization with Mass Storage-like devices, like USB Flash drives and Rockbox players.");
	}

	QIcon Plugin::GetIcon () const
	{
		return {};
	}

	Util::XmlSettingsDialog_ptr Plugin::GetSettingsDialog () const
	{
		return XSD_;
	}

	QSet<QByteArray> Plugin::GetPluginClasses () const
	{
		return { "org.LeechCraft.LMP.CollectionSync"_qba };
	}

	void Plugin::SetLMPProxy (ILMPProxy_ptr proxy)
	{
		LMPProxy_ = proxy;
	}

	QObject* Plugin::GetQObject ()
	{
		return this;
	}

	QString Plugin::GetSyncSystemName () const
	{
		return tr ("dumb copying");
	}

	QAbstractItemModel& Plugin::GetSyncTargetsModel ()
	{
		return *SyncTargets_;
	}

	void Plugin::RefreshSyncTargets ()
	{
	}

	namespace
	{
		struct UploadConfig : ISyncPluginConfig
		{
			QString Mask_;

			explicit UploadConfig (QString mask)
			: Mask_ { std::move (mask) }
			{
			}
		};
	}

	ISyncPluginConfigWidget_ptr Plugin::MakeConfigWidget (const QModelIndex&)
	{
		class ConfigWidget : public QLineEdit
						   , public ISyncPluginConfigWidget
		{
		public:
			explicit ConfigWidget ()
			{
				const auto& defaultMask = "music/$artist/$year $album/$trackNumber - $title"_qs;
				setText (XmlSettingsManager::Instance ().Property ("FileMask", defaultMask).toString ());
			}

			QWidget* GetQWidget () override
			{
				return this;
			}

			ISyncPluginConfig_cptr GetConfig () const override
			{
				return std::make_shared<const UploadConfig> (text ());
			}
		};

		return std::make_unique<ConfigWidget> ();
	}

	namespace
	{
		QString FixMask (const MediaInfo& info, const QString& mask)
		{
			auto result = PerformSubstitutions (mask, info, SubstitutionFlag::SFSafeFilesystem);
			const auto& ext = QFileInfo { info.LocalPath_ }.suffix ();
			if (!result.endsWith (ext))
				result += '.' + ext;
			return result;
		}

		QImage GetScaledPixmap (const QString& pxFile)
		{
			if (pxFile.isEmpty ())
				return {};

			QImage img { pxFile };
			const int maxDim = XmlSettingsManager::Instance ().property ("CoverDim").toInt ();
			if (img.size ().width () <= maxDim && img.size ().height () <= maxDim)
				return img;

			return img.scaled (maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
		}

		void WriteScaledPixmap (const QString& pxFile, const QString& target)
		{
			const auto& coverName = XmlSettingsManager::Instance ().property ("CoverName").toString ();

			const auto& targetDir = QFileInfo { target }.absoluteDir ();
			if (targetDir.exists (coverName))
				return;

			const auto& px = GetScaledPixmap (pxFile);
			if (px.isNull ())
				return;

			constexpr auto aaQuality = 80;
			if (const auto& fullCoverPath = targetDir.absoluteFilePath (coverName);
				!px.save (fullCoverPath, "JPG", aaQuality))
				qWarning () << "unable to save album art from" << pxFile << "to" << fullCoverPath;
		}
	}

	Util::ContextTask<Plugin::UploadResult> Plugin::Upload (UploadJob job)
	{
		const auto& config = dynamic_cast<const UploadConfig&> (*job.Config_);
		XmlSettingsManager::Instance ().setProperty ("FileMask", config.Mask_);

		const auto& targetRelPath = FixMask (job.MediaInfo_, config.Mask_);
		const auto& targetMountPoint = co_await co_await EnsureMounted (SyncTargets_->mapToSource (job.Target_));

		auto target = targetMountPoint;
		if (!target.endsWith ('/') && !targetRelPath.startsWith ('/'))
			target += '/';
		target += targetRelPath;
		qDebug () << "uploading" << job.LocalPath_ << "(from" << job.OriginalLocalPath_ << ") to " << target;

		const QString& dirPath = targetRelPath.section ('/', 0, -2);
		if (!QDir (targetMountPoint).mkpath (dirPath))
			co_return UploadFailure
			{
				QFile::PermissionsError,
				tr ("Unable to create the directory path %1 on target device %2.").arg (dirPath, targetMountPoint)
			};

		const auto& artPath = LMPProxy_->GetUtilProxy ()->FindAlbumArt (job.OriginalLocalPath_);

		co_return co_await QtConcurrent::run ([&] -> UploadResult
				{
					if (QFile file { job.LocalPath_ };
						!file.copy (target))
					{
						qWarning () << "failed to copy" << job.LocalPath_ << "to" << target << ":" << file.error () << file.errorString ();
						return { Util::AsLeft, { file.error (), file.errorString () } };
					}
					if (XmlSettingsManager::Instance ().property ("UploadCovers").toBool ())
						WriteScaledPixmap (artPath, target);
					return UploadSuccess {};
				});
	}

	Util::ContextTask<Util::Either<ISyncPlugin::UploadFailure, QString>> Plugin::EnsureMounted (QPersistentModelIndex idx)
	{
		if (const auto isMounted = idx.data (MassStorageRole::IsMounted).toBool ();
			!isMounted)
		{
			const auto srcModel = DevModelsMerger_->mapToSource (idx).model ();
			const auto mgr = Model2DevManager_.value (srcModel);
			if (!mgr)
			{
				qWarning () << "unable to find source manager for" << idx << srcModel << Model2DevManager_;
				co_return UploadFailure { QFile::OpenError, tr ("Unable to mount target.") };
			}

			co_await mgr->MountDevice (idx.data (CommonDevRole::DevID).toString ());
		}

		const auto& targetMountPoint = idx.data (MassStorageRole::MountPoints).toStringList ().value (0);
		if (targetMountPoint.isEmpty ())
		{
			qWarning () << "bad target" << idx;
			co_return UploadFailure { QFile::OpenError, tr ("Target mount point does not exist.") };
		}

		co_return targetMountPoint;
	}
}

LC_EXPORT_PLUGIN (leechcraft_lmp_dumbsync, LC::LMP::DumbSync::Plugin);
