/*
 * dirwatch_unix.cpp - detect changes of directory content
 * Copyright (C) 2003  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
 * version 2.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include"dirwatch.h"

// A great many hints taken from KDE's KDirWatch system.  Thank you KDE!

#ifdef HAVE_DNOTIFY
#include<qtimer.h>
#include<qsocketnotifier.h>
#include<qvaluelist.h>
#include<qptrlist.h>
#include<qfile.h>
#include<unistd.h>
#include<fcntl.h>
#include<signal.h>
#include<sys/utsname.h>

#define DNOTIFY_SIGNAL (SIGRTMIN + 8)

class DWEntry
{
public:
	static DWEntry * open(const QString &s)
	{
		int fd = ::open(QFile::encodeName(s), O_RDONLY);
		if(fd == -1)
			return 0;

		int mask = DN_DELETE|DN_CREATE|DN_RENAME|DN_MULTISHOT|DN_MODIFY|DN_ATTRIB;
		if(fcntl(fd, F_SETSIG, DNOTIFY_SIGNAL) == -1 || fcntl(fd, F_NOTIFY, mask) == -1) {
			close(fd);
			return 0;
		}
		DWEntry *e = new DWEntry(s, fd);
		return e;
	}

	~DWEntry()
	{
		close(fd);
	}

	QString dir;
	int fd;
	QValueList<int> idList;
	bool dirty;

private:
	DWEntry(const QString &_dir, int _fd)
	{
		dir = _dir;
		fd = _fd;
		dirty = false;
	}
};

#endif

class DirWatchPlatform::Private : public QObject
{
	Q_OBJECT
public:
#ifdef HAVE_DNOTIFY
	QTimer t;
	QSocketNotifier *sn;
	int pipe_notify[2];
	bool ok;
	QPtrList<DWEntry> list;
#endif
	DirWatchPlatform *par;
	static Private *self;

	Private(DirWatchPlatform *parent)
	{
		par = parent;
		self = this;

#ifdef HAVE_DNOTIFY
		list.setAutoDelete(true);
		ok = false;

		// from KDE
		bool supports_dnotify = true; // not guilty until proven guilty
		struct utsname uts;
		int major, minor, patch;
		if(uname(&uts) < 0)
			supports_dnotify = false; // *shrug*
		else if(sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch) != 3)
			supports_dnotify = false; // *shrug*
		else if( major * 1000000 + minor * 1000 + patch < 2004019 ) // <2.4.19
			supports_dnotify = false;

		if(!supports_dnotify)
			return;

		if(pipe(pipe_notify) == -1)
			return;

		if(fcntl(pipe_notify[0], F_SETFL, O_NONBLOCK) == -1)
			return;

		connect(&t, SIGNAL(timeout()), this, SLOT(slotNotify()));

		sn = new QSocketNotifier(pipe_notify[0], QSocketNotifier::Read);
		connect(sn, SIGNAL(activated(int)), this, SLOT(slotActivated()));

		struct sigaction act;
		act.sa_sigaction = dnotify_handler;
		sigemptyset(&act.sa_mask);
		act.sa_flags = SA_SIGINFO;
#ifdef SA_RESTART
		act.sa_flags |= SA_RESTART;
#endif
		sigaction(DNOTIFY_SIGNAL, &act, NULL);

		ok = true;
#endif
	}

	~Private()
	{
#ifdef HAVE_DNOTIFY
		if(ok) {
			delete sn;
			close(pipe_notify[0]);
			close(pipe_notify[1]);
		}
		list.clear();
#endif

		self = 0;
	}

#ifdef HAVE_DNOTIFY
	static void dnotify_handler(int, siginfo_t *si, void *)
	{
		// bad news
		if(!si)
			return;

		int fd = si->si_fd;
		//printf("dnotify_handler: fd=%d\n", fd);

		DWEntry *e = self->findEntryByFd(fd);
		if(!e)
			return;
		e->dirty = true;

		// write a byte into the pipe
		char c = 1;
		write(self->pipe_notify[1], &c, 1);
	}
#endif

private slots:
	void slotActivated()
	{
#ifdef HAVE_DNOTIFY
		// clear out the pipe
		while(1) {
			char buf[64];
			int ret = read(pipe_notify[0], buf, 64);
			if(ret < 64)
				break;
		}

		// use a timer to combine multiple dnotify events into one
		if(!t.isActive())
			t.start(200, true);
#endif
	}

	void slotNotify()
	{
#ifdef HAVE_DNOTIFY
		// see who is dirty
		QPtrListIterator<DWEntry> it(list);
		for(DWEntry *e; (e = it.current()); ++it) {
			if(e->dirty) {
				e->dirty = false;
				for(QValueList<int>::ConstIterator idi = e->idList.begin(); idi != e->idList.end(); ++idi) {
					par->triggerDirChanged(*idi);
				}
			}
		}
#endif
	}

public:
#ifdef HAVE_DNOTIFY
	int addItem(const QString &s)
	{
		//printf("setting up DNOTIFY for [%s]\n", s.latin1());

		DWEntry *e = findEntryByDir(s);
		if(!e) {
			e = DWEntry::open(s);
			if(!e)
				return -1;
			list.append(e);
		}
		int id = getUniqueId();
		e->idList.append(id);

		//printf("success (fd=%d)!\n", e->fd);
		return id;
	}

	void removeItem(int id)
	{
		DWEntry *e = findEntryById(id);
		if(!e)
			return;
		e->idList.remove(id);
		if(e->idList.isEmpty())
			list.removeRef(e);
	}

	int getUniqueId()
	{
		for(int n = 0;; ++n) {
			QPtrListIterator<DWEntry> it(list);
			bool found = false;
			for(DWEntry *e; (e = it.current()); ++it) {
				for(QValueList<int>::ConstIterator idi = e->idList.begin(); idi != e->idList.end(); ++idi) {
					if(*idi == n) {
						found = true;
						break;
					}
				}
				if(found)
					break;
			}
			if(!found)
				return n;
		}
	}

	DWEntry * findEntryByDir(const QString &s)
	{
		QPtrListIterator<DWEntry> it(list);
		for(DWEntry *e; (e = it.current()); ++it) {
			if(e->dir == s)
				return e;
		}
		return 0;
	}

	DWEntry * findEntryById(int id)
	{
		QPtrListIterator<DWEntry> it(list);
		for(DWEntry *e; (e = it.current()); ++it) {
			for(QValueList<int>::ConstIterator idi = e->idList.begin(); idi != e->idList.end(); ++idi) {
				if(*idi == id)
					return e;
			}
		}
		return 0;
	}

	DWEntry * findEntryByFd(int fd)
	{
		QPtrListIterator<DWEntry> it(list);
		for(DWEntry *e; (e = it.current()); ++it) {
			if(e->fd == fd)
				return e;
		}
		return 0;
	}

#endif
};

DirWatchPlatform::Private *DirWatchPlatform::Private::self;

#ifdef HAVE_DNOTIFY

DirWatchPlatform::DirWatchPlatform()
:QObject(0)
{
	d = 0;
}

DirWatchPlatform::~DirWatchPlatform()
{
	delete d;
}

bool DirWatchPlatform::init()
{
	Private *p = new Private(this);
	if(p->ok) {
		d = p;
		//printf("DirWatchPlatform::init() ok!\n");
		return true;
	}
	else {
		delete p;
		//printf("DirWatchPlatform::init() fail!\n");
		return false;
	}
}

int DirWatchPlatform::addDir(const QString &s)
{
	return d->addItem(s);
}

void DirWatchPlatform::removeDir(int id)
{
	d->removeItem(id);
}

#else

DirWatchPlatform::DirWatchPlatform():QObject(0) {}
DirWatchPlatform::~DirWatchPlatform() {}
bool DirWatchPlatform::init() { return false; }
int DirWatchPlatform::addDir(const QString &) { return -1; }
void DirWatchPlatform::removeDir(int) {}

#endif

#include"dirwatch_unix.moc"
