/*
 * Copyright (C) 2006  Justin Karneges
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * either version 2
   of the License, or (at your option) any later version.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 *
 */

#include "netinterface.h"

#include "irisnetplugin.h"
#include "irisnetglobal_p.h"

namespace XMPP {

//----------------------------------------------------------------------------
// NetTracker
//----------------------------------------------------------------------------
class NetTracker : public QObject
{
	Q_OBJECT
public:
	static NetTracker *self;

	NetInterfaceProvider *c;
	QList<NetInterfaceProvider::Info> info;
	QMutex m;

	NetTracker()
	{
		self = this;
		QList<IrisNetProvider*> list = irisNetProviders();
		c = 0;
		for(int n = 0; n < list.count(); ++n)
		{
			IrisNetProvider *p = list[n];
			c = p->createNetInterfaceProvider();
			if(c)
				break;
		}
		Q_ASSERT(c); // we have built-in support, so this should never fail
		connect(c, &NetInterfaceProvider::updated, this, &NetTracker::c_updated);
	}

	~NetTracker() override
	{
		delete c;
		self = 0;
	}

	static NetTracker *instance()
	{
		return self;
	}

	void start()
	{
		c->start();
		info = filterList(c->interfaces());
	}

	QList<NetInterfaceProvider::Info> getInterfaces()
	{
		QMutexLocker locker(&m);
		return info;
	}

signals:
	void updated();

public slots:
	void c_updated()
	{
		{
			QMutexLocker locker(&m);
			info = filterList(c->interfaces());
		}
		emit updated();
	}

private:
	static QList<NetInterfaceProvider::Info> filterList(const QList<NetInterfaceProvider::Info> &in)
	{
		QList<NetInterfaceProvider::Info> out;
		for(int n = 0; n < in.count(); ++n)
		{
			if(!in[n].isLoopback)
				out += in[n];
		}
		return out;
	}
};

NetTracker *NetTracker::self = 0;

class SyncThread : public QThread
{
	Q_OBJECT
public:
	QMutex control_mutex;
	QWaitCondition control_wait;
	QEventLoop *loop;

	SyncThread(QObject *parent = 0) : QThread(parent)
	{
		loop = 0;
	}

	~SyncThread() override
	{
		stop();
	}

	void start()
	{
		control_mutex.lock();
		QThread::start();
		control_wait.wait(&control_mutex);
		control_mutex.unlock();
	}

	void stop()
	{
		{
			QMutexLocker locker(&control_mutex);
			if(loop)
				QMetaObject::invokeMethod(loop, "quit");
		}
		wait();
	}

	void run() override
	{
		control_mutex.lock();
		loop = new QEventLoop;
		begin();
		control_wait.wakeOne();
		control_mutex.unlock();
		loop->exec();
		QMutexLocker locker(&control_mutex);
		end();
		delete loop;
		loop = 0;
	}

	virtual void begin()
	{
	}

	virtual void end()
	{
	}
};

class NetThread : public SyncThread
{
	Q_OBJECT
public:
	NetTracker *tracker;

	NetThread(QObject *parent = 0) : SyncThread(parent)
	{
	}

	~NetThread() override
	{
		stop();
	}

	void begin() override
	{
		tracker = new NetTracker;
		tracker->start();
	}

	void end() override
	{
		delete tracker;
	}
};

//----------------------------------------------------------------------------
// NetInterface
//----------------------------------------------------------------------------
class NetInterfacePrivate : public QObject
{
	Q_OBJECT
public:
	friend class NetInterfaceManagerPrivate;

	NetInterface *q;

	NetInterfaceManager *man;
	bool valid;
	QString id, name;
	QList<QHostAddress> addrs;
	QHostAddress gw;

	NetInterfacePrivate(NetInterface *_q) : QObject(_q), q(_q)
	{
		valid = false;
	}

	void doUnavailable()
	{
		man->unreg(q);
		valid = false;
		emit q->unavailable();
	}
};

NetInterface::NetInterface(const QString &id, NetInterfaceManager *manager)
:QObject(manager)
{
	d = new NetInterfacePrivate(this);
	d->man = manager;

	NetInterfaceProvider::Info *info = (NetInterfaceProvider::Info *)d->man->reg(id, this);
	if(info)
	{
		d->valid = true;
		d->id = info->id;
		d->name = info->name;
		d->addrs = info->addresses;
		d->gw = info->gateway;
		delete info;
	}
}

NetInterface::~NetInterface()
{
	d->man->unreg(this);
	delete d;
}

bool NetInterface::isValid() const
{
	return d->valid;
}

QString NetInterface::id() const
{
	return d->id;
}

QString NetInterface::name() const
{
	return d->name;
}

QList<QHostAddress> NetInterface::addresses() const
{
	return d->addrs;
}

QHostAddress NetInterface::gateway() const
{
	return d->gw;
}

//----------------------------------------------------------------------------
// NetInterfaceManager
//----------------------------------------------------------------------------
class NetInterfaceManagerGlobal;

Q_GLOBAL_STATIC(QMutex, nim_mutex)
static NetInterfaceManagerGlobal *g_nim = 0;

class NetInterfaceManagerGlobal
{
public:
	NetThread *thread;
	int refs;

	NetInterfaceManagerGlobal()
	{
		thread = 0;
		refs = 0;
	}

	~NetInterfaceManagerGlobal()
	{
	}

	// global mutex must be locked while calling this
	void addRef()
	{
		if(refs == 0)
		{
			thread = new NetThread;
			thread->moveToThread(QCoreApplication::instance()->thread());
			thread->start();
		}
		++refs;
	}

	// global mutex must be locked while calling this
	void removeRef()
	{
		Q_ASSERT(refs > 0);
		--refs;
		if(refs == 0)
		{
			delete thread;
			thread = 0;
		}
	}
};

class NetInterfaceManagerPrivate : public QObject
{
	Q_OBJECT
public:
	NetInterfaceManager *q;

	QMutex m;
	QList<NetInterfaceProvider::Info> info;
	QList<NetInterface*> listeners;
	bool pending;

	NetInterfaceManagerPrivate(NetInterfaceManager *_q) : QObject(_q), q(_q)
	{
		pending = false;
	}

	static int lookup(const QList<NetInterfaceProvider::Info> &list, const QString &id)
	{
		for(int n = 0; n < list.count(); ++n)
		{
			if(list[n].id == id)
				return n;
		}
		return -1;
	}

	static bool sameContent(const NetInterfaceProvider::Info &a, const NetInterfaceProvider::Info &b)
	{
		// assume ids are the same already
		if(a.name == b.name && a.isLoopback == b.isLoopback && a.addresses == b.addresses && a.gateway == b.gateway)
			return true;
		return false;
	}

	void do_update()
	{
		// grab the latest info
		QList<NetInterfaceProvider::Info> newinfo = NetTracker::instance()->getInterfaces();

		QStringList here_ids, gone_ids;

		// removed / changed
		for(int n = 0; n < info.count(); ++n)
		{
			int i = lookup(newinfo, info[n].id);
			// id is still here
			if(i != -1)
			{
				// content changed?
				if(!sameContent(info[n], newinfo[i]))
				{
					gone_ids += info[n].id;
					here_ids += info[n].id;
				}
			}
			// id is gone
			else
				gone_ids += info[n].id;
		}

		// added
		for(int n = 0; n < newinfo.count(); ++n)
		{
			int i = lookup(info, newinfo[n].id);
			if(i == -1)
				here_ids += newinfo[n].id;
		}
		info = newinfo;

		// announce gone
		for(int n = 0; n < gone_ids.count(); ++n)
		{
			// work on a copy, just in case the list changes.
			//   it is important to make the copy here, and not
			//   outside the outer loop, in case the items
			//   get deleted
			QList<NetInterface*> list = listeners;
			for(int i = 0; i < list.count(); ++i)
			{
				if(list[i]->d->id == gone_ids[n])
					list[i]->d->doUnavailable();
			}
		}

		// announce here
		for(int n = 0; n < here_ids.count(); ++n)
			emit q->interfaceAvailable(here_ids[n]);
	}

public slots:
	void tracker_updated()
	{
		QMutexLocker locker(&m);
		if(!pending)
		{
			QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
			pending = true;
		}
	}

	void update()
	{
		m.lock();
		pending = false;
		m.unlock();

		do_update();
	}
};

NetInterfaceManager::NetInterfaceManager(QObject *parent)
:QObject(parent)
{
	QMutexLocker locker(nim_mutex());
	if(!g_nim)
		g_nim = new NetInterfaceManagerGlobal;

	d = new NetInterfaceManagerPrivate(this);
	g_nim->addRef();
	d->connect(NetTracker::instance(), &NetTracker::updated, d, &NetInterfaceManagerPrivate::tracker_updated, Qt::DirectConnection);
}

NetInterfaceManager::~NetInterfaceManager()
{
	QMutexLocker locker(nim_mutex());
	g_nim->removeRef();
	delete d;
	if(g_nim->refs == 0)
	{
		delete g_nim;
		g_nim = 0;
	}
}

QStringList NetInterfaceManager::interfaces() const
{
	d->info = NetTracker::instance()->getInterfaces();
	QStringList out;
	for(int n = 0; n < d->info.count(); ++n)
		out += d->info[n].id;
	return out;
}

QString NetInterfaceManager::interfaceForAddress(const QHostAddress &a)
{
	NetInterfaceManager netman;
	QStringList list = netman.interfaces();
	for(int n = 0; n < list.count(); ++n)
	{
		NetInterface iface(list[n], &netman);
		if(iface.addresses().contains(a))
			return list[n];
	}
	return QString();
}

void *NetInterfaceManager::reg(const QString &id, NetInterface *i)
{
	for(int n = 0; n < d->info.count(); ++n)
	{
		if(d->info[n].id == id)
		{
			d->listeners += i;
			return new NetInterfaceProvider::Info(d->info[n]);
		}
	}
	return 0;
}

void NetInterfaceManager::unreg(NetInterface *i)
{
	d->listeners.removeAll(i);
}

}

#include "netinterface.moc"
