/**********************************************************************
 * 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 "dbupdatethreadworker.h"
#include <stdexcept>
#include <QUrl>
#include <QtDebug>
#include <util/xpc/util.h>
#include <util/xpc/defaulthookproxy.h>
#include "xmlsettingsmanager.h"
#include "core.h"
#include "storagebackend.h"
#include "regexpmatchermanager.h"
#include "tovarmaps.h"

namespace LeechCraft
{
namespace Aggregator
{
	DBUpdateThreadWorker::DBUpdateThreadWorker (QObject *parent)
	: QObject (parent)
	{
		try
		{
			const QString& strType = XmlSettingsManager::Instance ()->
					property ("StorageType").toString ();
			SB_ = StorageBackend::Create (strType, "_UpdateThread");
		}
		catch (const std::runtime_error& s)
		{
			qWarning () << Q_FUNC_INFO
					<< s.what ();
			return;
		}
		catch (...)
		{
			qWarning () << Q_FUNC_INFO
					<< "unknown exception";
			return;
		}

		SB_->Prepare ();
	}

	Feed::FeedSettings DBUpdateThreadWorker::GetFeedSettings (IDType_t feedId)
	{
		const auto itemAge = XmlSettingsManager::Instance ()->property ("ItemsMaxAge").toInt ();
		const auto items = XmlSettingsManager::Instance ()->property ("ItemsPerChannel").toUInt ();

		try
		{
			auto settings = SB_->GetFeedSettings (feedId);
			if (!settings.ItemAge_)
				settings.ItemAge_ = itemAge;
			if (!settings.NumItems_)
				settings.NumItems_ = items;
			return settings;
		}
		catch (const StorageBackend::FeedSettingsNotFoundError&)
		{
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to get feed settings for"
					<< feedId
					<< e.what ();
		}

		Feed::FeedSettings s { feedId };
		s.ItemAge_ = itemAge;
		s.NumItems_ = items;
		s.AutoDownloadEnclosures_ = false;
		return s;
	}

	void DBUpdateThreadWorker::AddChannel (const Channel_ptr& channel, const Feed::FeedSettings& settings)
	{
		const auto ipc = static_cast<size_t> (settings.NumItems_);
		const auto days = settings.ItemAge_;
		size_t truncateAt = (channel->Items_.size () <= ipc) ?
			channel->Items_.size () :
			ipc;
		for (size_t j = 0; j < channel->Items_.size (); j++)
			if (channel->Items_ [j]->PubDate_.daysTo (QDateTime::currentDateTime ()) > days)
			{
				truncateAt = std::min (j, truncateAt);
				break;
			}
		channel->Items_.resize (truncateAt);

		SB_->AddChannel (channel);
		emit gotNewChannel (channel->ToShort ());

		QString str = tr ("Added channel \"%1\" (%n item(s))",
				"", channel->Items_.size ())
			.arg (channel->Title_);

		emit gotEntity (Util::MakeNotification ("Aggregator", str, PInfo_));
	}

	bool DBUpdateThreadWorker::AddItem (const Item_ptr& item, const Channel_ptr& channel,
			const QVariantMap& channelDataMap, const Feed::FeedSettings& settings)
	{
		if (item->PubDate_.isValid ())
		{
			if (item->PubDate_.daysTo (QDateTime::currentDateTime ()) >= settings.ItemAge_)
				return false;
		}
		else
			item->FixDate ();

		item->ChannelID_ = channel->ChannelID_;
		SB_->AddItem (item);

		RegexpMatcherManager::Instance ().HandleItem (item);

		QVariantList itemData;
		itemData << GetItemMapItemPart (item).unite (channelDataMap);
		emit hookGotNewItems (Util::DefaultHookProxy_ptr (new Util::DefaultHookProxy),
				itemData);

		if (settings.AutoDownloadEnclosures_)
			for (const auto& e : item->Enclosures_)
			{
				auto de = Util::MakeEntity (QUrl (e.URL_),
						XmlSettingsManager::Instance ()->
							property ("EnclosuresDownloadPath").toString (),
						0,
						e.Type_);
				de.Additional_ [" Tags"] = channel->Tags_;
				emit gotEntity (de);
			}

		return true;
	}

	bool DBUpdateThreadWorker::UpdateItem (const Item_ptr& item, const Item_ptr& ourItem)
	{
		if (!IsModified (ourItem, item))
			return false;

		ourItem->Description_ = item->Description_;
		ourItem->Categories_ = item->Categories_;
		ourItem->NumComments_ = item->NumComments_;
		ourItem->CommentsLink_ = item->CommentsLink_;
		ourItem->CommentsPageLink_ = item->CommentsPageLink_;
		ourItem->Latitude_ = item->Latitude_;
		ourItem->Longitude_ = item->Longitude_;

		for (auto& enc : item->Enclosures_)
		{
			if (!ourItem->Enclosures_.contains (enc))
			{
				enc.ItemID_ = ourItem->ItemID_;
				ourItem->Enclosures_ << enc;
			}
		}

		for (auto& entry : item->MRSSEntries_)
			if (!ourItem->MRSSEntries_.contains (entry))
			{
				entry.ItemID_ = ourItem->ItemID_;
				ourItem->MRSSEntries_ << entry;
			}

		SB_->UpdateItem (ourItem);

		return true;
	}

	void DBUpdateThreadWorker::NotifyUpdates (int newItems, int updatedItems, const Channel_ptr& channel)
	{
		const auto& method = XmlSettingsManager::Instance ()->
				property ("NotificationsFeedUpdateBehavior").toString ();
		bool shouldShow = true;
		if (method == "ShowNo")
			shouldShow = false;
		else if (method == "ShowNew")
			shouldShow = newItems;
		else if (method == "ShowAll")
			shouldShow = newItems + updatedItems;

		if (!shouldShow)
			return;

		QStringList substrs;
		if (newItems)
			substrs << tr ("%n new item(s)", "Channel update", newItems);
		if (updatedItems)
			substrs << tr ("%n updated item(s)", "Channel update", updatedItems);
		const auto& str = tr ("Updated channel \"%1\" (%2).")
				.arg (channel->Title_)
				.arg (substrs.join (", "));
		emit gotEntity (Util::MakeNotification ("Aggregator", str, PInfo_));
	}

	void DBUpdateThreadWorker::toggleChannelUnread (IDType_t channel, bool state)
	{
		SB_->ToggleChannelUnread (channel, state);
	}

	void DBUpdateThreadWorker::updateFeed (channels_container_t channels, QString url)
	{
		auto feedId = SB_->FindFeed (url);
		if (feedId == static_cast<decltype (feedId)> (-1))
		{
			qWarning () << Q_FUNC_INFO
				<< "skipping"
				<< url
				<< "cause seems like it's not in storage yet";
			return;
		}

		const auto& feedSettings = GetFeedSettings (feedId);
		const auto ipc = feedSettings.NumItems_;
		const auto days = feedSettings.ItemAge_;

		for (const auto& channel : channels)
		{
			Channel_ptr ourChannel;
			try
			{
				const auto ourChannelID = SB_->FindChannel (channel->Title_,
						channel->Link_, feedId);
				ourChannel = SB_->GetChannel (ourChannelID, feedId);
			}
			catch (const StorageBackend::ChannelNotFoundError&)
			{
				AddChannel (channel, feedSettings);
				continue;
			}

			const auto& channelPart = GetItemMapChannelPart (ourChannel);

			int newItems = 0;
			int updatedItems = 0;

			for (const auto& item : channel->Items_)
			{
				Item_ptr ourItem;
				try
				{
					const auto ourItemID = SB_->FindItem (item->Title_, item->Link_,
							ourChannel->ChannelID_);
					ourItem = SB_->GetItem (ourItemID);
				}
				catch (const StorageBackend::ItemNotFoundError&)
				{
					if (AddItem (item, ourChannel, channelPart, feedSettings))
						++newItems;
					continue;
				}

				if (UpdateItem (item, ourItem))
					++updatedItems;
			}

			SB_->TrimChannel (ourChannel->ChannelID_, days, ipc);

			NotifyUpdates (newItems, updatedItems, channel);
		}
	}
}
}
