text: /**********************************************************************
   * LeechCraft - modular cross-platform feature rich internet client.
   * Copyright (C) 2006-2014  Georg Rudoy
   *
   * Boost Software License - Version 1.0 - August 17th, 2003
   *
   * Permission is hereby granted, free of charge, to any person or organization
   * obtaining a copy of the software and accompanying documentation covered by
   * this license (the "Software") to use, reproduce, display, distribute,
   * execute, and transmit the Software, and to prepare derivative works of the
   * Software, and to permit third-parties to whom the Software is furnished to
   * do so, all subject to the following:
   *
   * The copyright notices in the Software and this entire statement, including
   * the above license grant, this restriction and the following disclaimer,
   * must be included in all copies of the Software, in whole or in part, and
   * all derivative works of the Software, unless such copies or derivative
   * works are solely in the form of machine-executable object code generated by
   * a source language processor.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
   * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
   * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   **********************************************************************/
  
  #include "torrentfilesmodel.h"
  #include <iterator>
  #include <boost/functional/hash.hpp>
  #include <QUrl>
  #include <QTimer>
  #include <QtDebug>
  #include <util/util.h>
  #include <util/xpc/util.h>
  #include <util/sys/extensionsdata.h>
  #include <util/models/modelitembase.h>
  #include <interfaces/core/ientitymanager.h>
  #include "core.h"
  #include "cachedstatuskeeper.h"
  
  namespace LeechCraft
  {
  namespace BitTorrent
  {
  	TorrentFilesModel::TorrentFilesModel (int index)
  	: TorrentFilesModelBase { { tr ("Name"), tr ("Priority"), tr ("Progress") } }
  	, Index_ { index }
  	{
  		auto timer = new QTimer (this);
  		connect (timer,
  				SIGNAL (timeout ()),
  				this,
  				SLOT (update ()));
  		timer->start (2000);
  
  		QTimer::singleShot (0,
  				this,
  				SLOT (update ()));
  	}
  
  	QVariant TorrentFilesModel::data (const QModelIndex& index, int role) const
  	{
  		if (!index.isValid ())
  			return {};
  
  		const auto node = static_cast<TorrentNodeInfo*> (index.internalPointer ());
  		switch (role)
  		{
  		case Qt::CheckStateRole:
  			if (index.column () != ColumnPath)
  				return {};
  
  			if (node->Priority_ > 0)
  				return Qt::Checked;
  			else if (node->Priority_ < 0)
  				return Qt::PartiallyChecked;
  			else
  				return Qt::Unchecked;
  		case Qt::DisplayRole:
  			switch (index.column ())
  			{
  			case ColumnPath:
  				return node->Name_;
  			case ColumnPriority:
  				return node->Priority_ >= 0 ?
  						node->Priority_ :
  						QVariant {};
  			case ColumnProgress:
  				return node->Progress_;
  			}
  		case Qt::DecorationRole:
  			return index.column () == ColumnPath ?
  					node->Icon_ :
  					QIcon {};
  		case RoleFullPath:
  			return node->GetFullPathStr ();
  		case RoleFileName:
  			return node->Name_;
  		case RoleProgress:
  			return std::max ({}, node->Progress_);
  		case RoleSize:
  			return node->SubtreeSize_;
  		case RolePriority:
  			return node->Priority_;
  		case RoleSort:
  			switch (index.column ())
  			{
  			case ColumnPath:
  				return node->Name_;
  			case ColumnPriority:
  				return node->Priority_;
  			case ColumnProgress:
  				return node->Progress_;
  			}
  		}
  		return {};
  	}
  
  	Qt::ItemFlags TorrentFilesModel::flags (const QModelIndex& index) const
  	{
  		if (!index.isValid ())
  			return 0;
  
  		Qt::ItemFlags flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled };
  		switch (index.column ())
  		{
  		case ColumnPath:
  			flags |= Qt::ItemIsEditable;
  			flags |= Qt::ItemIsUserCheckable;
  			break;
  		case ColumnPriority:
  			flags |= Qt::ItemIsEditable;
  			break;
  		}
  		return flags;
  	}
  
  	bool TorrentFilesModel::setData (const QModelIndex& index, const QVariant& value, int role)
  	{
  		if (!index.isValid ())
  			return false;
  
  		const auto node = static_cast<TorrentNodeInfo*> (index.internalPointer ());
  		switch (index.column ())
  		{
  		case ColumnPriority:
  			if (const auto rc = rowCount (index))
  				for (int i = 0; i < rc; ++i)
  					setData (this->index (i, index.column (), index), value, role);
  			else
  			{
  				const auto newPriority = value.toInt ();
  				Core::Instance ()->SetFilePriority (node->FileIndex_, newPriority, Index_);
  				node->Priority_ = newPriority;
  				emit dataChanged (index.sibling (index.row (), ColumnPath), index);
  
  				UpdatePriorities (node);
  			}
  			return true;
  		case ColumnPath:
  		{
  			switch (role)
  			{
  			case Qt::EditRole:
  			{
  				auto newPath = value.toString ();
  				const auto& curPath = node->GetFullPathStr ();
  				if (curPath.contains ('/') && !newPath.contains ('/'))
  				{
  					auto newCurPath = curPath;
  					const auto lastIdx = newCurPath.lastIndexOf ('/');
  					newPath = newCurPath.left (lastIdx + 1) + newPath;
  				}
  
  				if (!node->IsEmpty ())
  				{
  					const auto curPathSize = curPath.size ();
  					std::function<void (TorrentNodeInfo*)> setter =
  							[this, &setter, &newPath, curPathSize] (TorrentNodeInfo *node)
  							{
  								if (node->IsEmpty ())
  								{
  									auto specificPath = node->GetFullPathStr ();
  									specificPath.replace (0, curPathSize, newPath);
  									Core::Instance ()->SetFilename (node->FileIndex_, specificPath, Index_);
  								}
  								else
  									for (const auto& subnode : *node)
  										setter (subnode.get ());
  							};
  					setter (node);
  				}
  				else
  					Core::Instance ()->SetFilename (node->FileIndex_, newPath, Index_);
  				return true;
  			}
  			case Qt::CheckStateRole:
  				return setData (index.sibling (index.row (), ColumnPriority),
  						value.toInt () == Qt::Checked ? 1 : 0,
  						Qt::EditRole);
  			}
  		}
  		}
  
  		return false;
  	}
  
  	void TorrentFilesModel::ResetFiles (const boost::filesystem::path& basePath,
  			const QList<FileInfo>& infos)
  	{
  		Clear ();
  
  		BasePath_ = basePath;
  
  		beginInsertRows ({}, 0, 0);
  		FilesInTorrent_ = infos.size ();
  		Path2Node_ [{}] = RootNode_;
  
  		const auto& inst = Util::ExtensionsData::Instance ();
  
  		for (int i = 0; i < infos.size (); ++i)
  		{
  			const auto& fi = infos.at (i);
  			const auto& parentItem = MkParentIfDoesntExist (fi.Path_);
  
  			const auto& filename =
  #ifdef Q_OS_WIN32
  					QString::fromUtf16 (reinterpret_cast<const ushort*> (fi.Path_.leaf ().c_str ()));
  #else
  					QString::fromUtf8 (fi.Path_.leaf ().c_str ());
  #endif
  
  			const auto item = parentItem->AppendChild (parentItem);
  			item->Name_ = filename;
  			item->ParentPath_ = fi.Path_.branch_path ();
  			item->Priority_ = fi.Priority_;
  			item->FileIndex_ = i;
  			item->SubtreeSize_ = fi.Size_;
  			item->Progress_ = fi.Progress_;
  			item->Icon_ = inst.GetExtIcon (filename.section ('.', -1));
  
  			Path2Node_ [fi.Path_] = item;
  
  			UpdatePriorities (item.get ());
  		}
  
  		UpdateSizeGraph (RootNode_);
  
  		endInsertRows ();
  	}
  
  	void TorrentFilesModel::UpdateFiles (const boost::filesystem::path& basePath,
  			const QList<FileInfo>& infos)
  	{
  		BasePath_ = basePath;
  		if (Path2Node_.size () <= 1)
  		{
  			ResetFiles (BasePath_, infos);
  			return;
  		}
  
  		for (const auto& fi : infos)
  		{
  			const auto pos = Path2Node_.find (fi.Path_);
  			if (pos == Path2Node_.end ())
  				continue;
  
  			const auto& item = pos->second;
  			item->Progress_ = fi.Progress_;
  			item->SubtreeSize_ = fi.Size_;
  		}
  
  		UpdateSizeGraph (RootNode_);
  
  		if (const auto rc = RootNode_->GetRowCount ())
  			emit dataChanged (index (0, 2), index (rc - 1, 2));
  	}
  
  	void TorrentFilesModel::HandleFileActivated (QModelIndex index) const
  	{
  		if (!index.isValid ())
  			return;
  
  		if (index.column () != ColumnPath)
  			index = index.sibling (index.row (), ColumnPath);
  
  		const auto item = static_cast<TorrentNodeInfo*> (index.internalPointer ());
  
  		const auto iem = Core::Instance ()->GetProxy ()->GetEntityManager ();
  		if (std::abs (item->Progress_ - 1) >= std::numeric_limits<decltype (item->Progress_)>::epsilon ())
  			iem->HandleEntity (Util::MakeNotification ("BitTorrent",
  					tr ("%1 hasn't finished downloading yet.")
  						.arg ("<em>" + item->Name_ + "</em>"),
  					PWarning_));
  		else
  		{
  			const auto& full = BasePath_ / item->GetFullPath ();
  			const auto& path = QString::fromUtf8 (full.string ().c_str ());
  			const auto& e = Util::MakeEntity (QUrl::fromLocalFile (path),
  					{},
  					FromUserInitiated);
  			iem->HandleEntity (e);
  		}
  	}
  
  	void TorrentFilesModel::UpdateSizeGraph (const TorrentNodeInfo_ptr& node)
  	{
  		if (!node->GetRowCount ())
  			return;
  
  		qulonglong size = 0;
  		qulonglong done = 0;
  
  		for (const auto & child : *node)
  		{
  			UpdateSizeGraph (child);
  			size += child->SubtreeSize_;
  			done += child->SubtreeSize_ * child->Progress_;
  		}
  
  		node->SubtreeSize_ = size;
  		node->Progress_ = size ? static_cast<double> (done) / size : 1;
  	}
  
  	void TorrentFilesModel::UpdatePriorities (TorrentNodeInfo *node)
  	{
  		const auto& parent = node->GetParent ();
  		if (node == RootNode_.get () || parent == RootNode_)
  			return;
  
  		const auto prio = node->Priority_;
  		const bool allSame = std::all_of (parent->begin (), parent->end (),
  				[prio] (const TorrentNodeInfo_ptr& child) { return child->Priority_ == prio; });
  
  		const auto newPrio = allSame ? prio : -1;
  		if (newPrio == parent->Priority_)
  			return;
  
  		parent->Priority_ = newPrio;
  		const auto& idx = IndexForNode (parent, ColumnPriority);
  		emit dataChanged (idx.sibling (idx.row (), ColumnPath), idx);
  
  		UpdatePriorities (parent.get ());
  	}
  
  	void TorrentFilesModel::ClearEmptyParents (boost::filesystem::path path)
  	{
  		const auto pos = Path2Node_.find (path);
  		if (pos == Path2Node_.end ())
  		{
  			qWarning () << Q_FUNC_INFO
  					<< "unknown path"
  					<< path.c_str ();
  			return;
  		}
  
  		const auto& node = pos->second;
  		if (!node->IsEmpty ())
  		{
  			UpdateSizeGraph (RootNode_);
  			return;
  		}
  
  		const auto& parentNode = node->GetParent ();
  
  		const auto nodeRow = node->GetRow ();
  		const auto& parentIndex = FindIndex (path.branch_path ());
  		beginRemoveRows (parentIndex, nodeRow, nodeRow);
  		parentNode->EraseChild (parentNode->begin () + nodeRow);
  		Path2Node_.erase (pos);
  		endRemoveRows ();
  
  		ClearEmptyParents (path.branch_path ());
  	}
  
  	void TorrentFilesModel::update ()
  	{
  		const auto& handle = Core::Instance ()->GetTorrentHandle (Index_);
  #if LIBTORRENT_VERSION_NUM >= 10000
  		const auto& base = Core::Instance ()->GetStatusKeeper ()->
  				GetStatus (handle, libtorrent::torrent_handle::query_save_path).save_path;
  #else
  		const auto& base = handle.save_path ();
  #endif
  
  		const auto& files = Core::Instance ()->GetTorrentFiles (Index_);
  		UpdateFiles (base, files);
  	}
  
  	void TorrentFilesModel::handleFileRenamed (int torrent, int file, const QString& newName)
  	{
  		if (torrent != Index_)
  			return;
  
  		const auto filePos = std::find_if (Path2Node_.begin (), Path2Node_.end (),
  				[file] (const Path2Node_t::value_type& pair)
  					{ return pair.second->FileIndex_ == file; });
  		if (filePos == Path2Node_.end ())
  		{
  			qWarning () << Q_FUNC_INFO
  					<< "unknown file index"
  					<< file
  					<< "for torrent"
  					<< torrent
  					<< "was renamed to"
  					<< newName;
  			return;
  		}
  
  		const auto node = filePos->second;
  		ClearEmptyParents (filePos->first);
  
  		const boost::filesystem::path newPath { newName.toUtf8 ().constData () };
  
  		const auto& parentNode = MkParentIfDoesntExist (newPath, true);
  
  		node->Name_ = QString::fromUtf8 (newPath.leaf ().string ().c_str ());
  		node->Reparent (parentNode);
  
  		beginInsertRows (FindIndex (newPath.branch_path ()), parentNode->GetRowCount (), parentNode->GetRowCount ());
  		Path2Node_ [newPath] = node;
  		parentNode->AppendExisting (node);
  		endInsertRows ();
  
  		UpdateSizeGraph (RootNode_);
  	}
  }
  }
  