/*
 * Copyright 2003-2018 The Music Player Daemon Project
 * http://www.musicpd.org
 *
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "config.h"
#include "UdisksNeighborPlugin.hxx"
#include "lib/dbus/Connection.hxx"
#include "lib/dbus/Error.hxx"
#include "lib/dbus/Glue.hxx"
#include "lib/dbus/Message.hxx"
#include "lib/dbus/PendingCall.hxx"
#include "lib/dbus/ReadIter.hxx"
#include "lib/dbus/ObjectManager.hxx"
#include "lib/dbus/UDisks2.hxx"
#include "neighbor/NeighborPlugin.hxx"
#include "neighbor/Explorer.hxx"
#include "neighbor/Listener.hxx"
#include "neighbor/Info.hxx"
#include "event/Call.hxx"
#include "thread/Mutex.hxx"
#include "thread/SafeSingleton.hxx"
#include "util/Domain.hxx"
#include "util/StringAPI.hxx"
#include "util/Manual.hxx"
#include "Log.hxx"

#include <stdexcept>
#include <string>
#include <list>
#include <map>

static constexpr Domain udisks_domain("udisks");

struct UdisksObject {
	std::string path;

	std::string drive_id, block_id;

	bool is_filesystem = false;

	bool IsValid() const noexcept {
		return !drive_id.empty() || !block_id.empty();
	}

	std::string GetUri() const noexcept {
		if (!drive_id.empty())
			return "udisks://" + drive_id;
		else if (!block_id.empty())
			return "udisks://" + block_id;
		else
			return {};
	}

	NeighborInfo ToNeighborInfo() const noexcept {
		return {GetUri(), path};
	}
};

class UdisksNeighborExplorer final
	: public NeighborExplorer {

	EventLoop &event_loop;

	Manual<SafeSingleton<ODBus::Glue>> dbus_glue;

	ODBus::PendingCall pending_list_call;

	/**
	 * Protects #by_uri, #by_path.
	 */
	mutable Mutex mutex;

	using ByUri = std::map<std::string, NeighborInfo>;
	ByUri by_uri;
	std::map<std::string, ByUri::iterator> by_path;

public:
	UdisksNeighborExplorer(EventLoop &_event_loop,
			       NeighborListener &_listener) noexcept
		:NeighborExplorer(_listener), event_loop(_event_loop) {}

	auto &GetEventLoop() noexcept {
		return event_loop;
	}

	auto &&GetConnection() noexcept {
		return dbus_glue.Get()->GetConnection();
	}

	/* virtual methods from class NeighborExplorer */
	void Open() override;
	void Close() noexcept override;
	List GetList() const noexcept override;

private:
	void DoOpen();
	void DoClose() noexcept;

	void Insert(UdisksObject &&o) noexcept;
	void Remove(const std::string &path) noexcept;

	void OnListNotify(DBusPendingCall *pending) noexcept;

	static void OnListNotify(DBusPendingCall *pending,
				 void *user_data) noexcept {
		auto &e = *(UdisksNeighborExplorer *)user_data;
		e.OnListNotify(pending);
	}

	DBusHandlerResult HandleMessage(DBusConnection *dbus_connection,
					DBusMessage *message) noexcept;
	static DBusHandlerResult HandleMessage(DBusConnection *dbus_connection,
					       DBusMessage *message,
					       void *user_data) noexcept;
};

inline void
UdisksNeighborExplorer::DoOpen()
{
	using namespace ODBus;

	dbus_glue.Construct(event_loop);

	auto &connection = GetConnection();

	try {
		Error error;
		dbus_bus_add_match(connection,
				   "type='signal',sender='" UDISKS2_INTERFACE "',"
				   "interface='" DBUS_OM_INTERFACE "',"
				   "path='" UDISKS2_PATH "'",
				   error);
		error.CheckThrow("DBus AddMatch error");

		dbus_connection_add_filter(connection,
					   HandleMessage, this,
					   nullptr);

		auto msg = Message::NewMethodCall(UDISKS2_INTERFACE,
						  UDISKS2_PATH,
						  DBUS_OM_INTERFACE,
						  "GetManagedObjects");
		pending_list_call = PendingCall::SendWithReply(connection, msg.Get());
		pending_list_call.SetNotify(OnListNotify, this);
	} catch (...) {
		dbus_glue.Destruct();
		throw;
	}
}

void
UdisksNeighborExplorer::Open()
{
	BlockingCall(GetEventLoop(), [this](){ DoOpen(); });
}

inline void
UdisksNeighborExplorer::DoClose() noexcept
{
	if (pending_list_call) {
		pending_list_call.Cancel();
	}

	// TODO: remove_match
	// TODO: remove_filter

	dbus_glue.Destruct();
}

void
UdisksNeighborExplorer::Close() noexcept
{
	BlockingCall(GetEventLoop(), [this](){ DoClose(); });
}

template<typename I>
gcc_pure
static const char *
CheckString(I &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_STRING)
		return nullptr;

	return i.GetString();
}

template<typename I>
gcc_pure
static const char *
CheckVariantString(I &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_VARIANT)
		return nullptr;

	return CheckString(i.Recurse());
}

static void
ParseDriveDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_STRING)
		return;

	const char *name = i.GetString();
	i.Next();

	if (StringIsEqual(name, "Id")) {
		const char *value = CheckVariantString(i);
		if (value != nullptr && o.drive_id.empty())
			o.drive_id = value;
	}
}

static void
ParseBlockDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_STRING)
		return;

	const char *name = i.GetString();
	i.Next();

	if (StringIsEqual(name, "Id")) {
		const char *value = CheckVariantString(i);
		if (value != nullptr && o.block_id.empty())
			o.block_id = value;
	}
}

static void
ParseInterface(UdisksObject &o, const char *interface,
	       ODBus::ReadMessageIter &&i) noexcept
{
	if (StringIsEqual(interface, "org.freedesktop.UDisks2.Drive")) {
		for (; i.GetArgType() == DBUS_TYPE_DICT_ENTRY; i.Next())
			ParseDriveDictEntry(o, i.Recurse());
	} else if (StringIsEqual(interface, "org.freedesktop.UDisks2.Block")) {
		for (; i.GetArgType() == DBUS_TYPE_DICT_ENTRY; i.Next())
			ParseBlockDictEntry(o, i.Recurse());
	} else if (StringIsEqual(interface, "org.freedesktop.UDisks2.Filesystem")) {
		o.is_filesystem = true;
	}
}

static void
ParseInterfaceDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_STRING)
		return;

	const char *interface = i.GetString();
	i.Next();

	if (i.GetArgType() != DBUS_TYPE_ARRAY)
		return;

	ParseInterface(o, interface, i.Recurse());
}

static bool
ParseObject(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept
{
	if (i.GetArgType() != DBUS_TYPE_OBJECT_PATH)
		return false;

	o.path = i.GetString();

	i.Next();

	if (i.GetArgType() != DBUS_TYPE_ARRAY)
		return false;

	i.Recurse().ForEach(DBUS_TYPE_DICT_ENTRY, [&o](auto &&j){
			ParseInterfaceDictEntry(o, j.Recurse());
		});

	return true;
}

NeighborExplorer::List
UdisksNeighborExplorer::GetList() const noexcept
{
	const std::lock_guard<Mutex> lock(mutex);

	NeighborExplorer::List result;

	for (const auto &i : by_uri)
		result.emplace_front(i.second);
	return result;
}

void
UdisksNeighborExplorer::Insert(UdisksObject &&o) noexcept
{
	assert(o.IsValid());

	const NeighborInfo info = o.ToNeighborInfo();

	{
		const std::lock_guard<Mutex> protect(mutex);
		auto i = by_uri.emplace(std::make_pair(o.GetUri(), info));
		if (!i.second)
			i.first->second = info;

		by_path.emplace(std::make_pair(o.path, i.first));
		// TODO: do we need to remove a conflicting path?
	}

	listener.FoundNeighbor(info);
}

void
UdisksNeighborExplorer::Remove(const std::string &path) noexcept
{
	std::unique_lock<Mutex> lock(mutex);

	auto i = by_path.find(path);
	if (i == by_path.end())
		return;

	const auto info = std::move(i->second->second);

	by_uri.erase(i->second);
	by_path.erase(i);

	lock.unlock();
	listener.LostNeighbor(info);
}

inline void
UdisksNeighborExplorer::OnListNotify(DBusPendingCall *pending) noexcept
{
	assert(pending == pending_list_call.Get());

	pending_list_call = {};

	using namespace ODBus;
	Message reply = Message::StealReply(*pending);

	try {
		reply.CheckThrowError();
	} catch (...) {
		LogError(std::current_exception());
		return;
	}

	ReadMessageIter i(*reply.Get());
	if (i.GetArgType() != DBUS_TYPE_ARRAY) {
		LogError(udisks_domain, "Malformed response");
		return;
	}

	i.Recurse().ForEach(DBUS_TYPE_DICT_ENTRY, [this](auto &&j){
			UdisksObject o;
			if (ParseObject(o, j.Recurse()) && o.IsValid())
				Insert(std::move(o));
		});
}

inline DBusHandlerResult
UdisksNeighborExplorer::HandleMessage(DBusConnection *, DBusMessage *message) noexcept
{
	using namespace ODBus;

	if (dbus_message_is_signal(message, DBUS_OM_INTERFACE,
				   "InterfacesAdded") &&
	    dbus_message_has_signature(message, DBUS_OM_INTERFACES_ADDED_SIGNATURE)) {
		UdisksObject o;
		if (ParseObject(o, ReadMessageIter(*message)) && o.IsValid())
			Insert(std::move(o));

		return DBUS_HANDLER_RESULT_HANDLED;
	} else if (dbus_message_is_signal(message, DBUS_OM_INTERFACE,
					  "InterfacesRemoved") &&
		   dbus_message_has_signature(message, DBUS_OM_INTERFACES_REMOVED_SIGNATURE)) {
		Remove(ReadMessageIter(*message).GetString());
		return DBUS_HANDLER_RESULT_HANDLED;
	} else
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

DBusHandlerResult
UdisksNeighborExplorer::HandleMessage(DBusConnection *connection,
				      DBusMessage *message,
				      void *user_data) noexcept
{
	auto &agent = *(UdisksNeighborExplorer *)user_data;

	return agent.HandleMessage(connection, message);
}

static std::unique_ptr<NeighborExplorer>
udisks_neighbor_create(EventLoop &event_loop,
		     NeighborListener &listener,
		     gcc_unused const ConfigBlock &block)
{
	return std::make_unique<UdisksNeighborExplorer>(event_loop, listener);
}

const NeighborPlugin udisks_neighbor_plugin = {
	"udisks",
	udisks_neighbor_create,
};