/*
 * 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"

#include<qthread.h>
#include<qptrlist.h>
#include<qfile.h>
#include<qdir.h>
#include<qtimer.h>
#include<windows.h>

class DWEvent : public QEvent
{
public:
	DWEvent(int _state)
	:QEvent(QEvent::User)
	{
		state = _state;
	}

	int state;
};

class DWThread : public QThread
{
public:
	DWThread(QObject *_par, HANDLE _h) : QThread()
	{
		par = _par;
		h = _h;
	}

	void run()
	{
		while(WaitForSingleObject(h, INFINITE) == WAIT_OBJECT_0) {
			// send a changed event
			QThread::postEvent(par, new DWEvent(1));
			if(!FindNextChangeNotification(h)) {
				// send an error event
				QThread::postEvent(par, new DWEvent(0));
				return;
			}
		}
	}

	QObject *par;
	HANDLE h;
};

class DWEntry : public QObject
{
	Q_OBJECT
public:
	static DWEntry * create(const QString &s)
	{
		HANDLE h = FindFirstChangeNotificationA(QFile::encodeName(QDir::convertSeparators(s)), TRUE,
			FILE_NOTIFY_CHANGE_FILE_NAME |
			FILE_NOTIFY_CHANGE_DIR_NAME |
			FILE_NOTIFY_CHANGE_ATTRIBUTES |
			FILE_NOTIFY_CHANGE_SIZE |
			FILE_NOTIFY_CHANGE_LAST_WRITE |
			FILE_NOTIFY_CHANGE_SECURITY);

		if(h == INVALID_HANDLE_VALUE)
			return 0;

		DWEntry *e = new DWEntry;
		e->dir = s;
		e->h = h;
		e->thread = new DWThread(e, h);
		e->thread->start();
		return e;
	}

	~DWEntry()
	{
		if(thread) {
			FindCloseChangeNotification(h);
			thread->wait();
			delete thread;
		}
	}

signals:
	void changed();

protected:
	bool event(QEvent *e)
	{
		if(e->type() == QEvent::User) {
			DWEvent *de = (DWEvent *)e;
			// error?
			if(de->state == 0) {
				thread->wait();
				delete thread;
				FindCloseChangeNotification(h);
				thread = 0;
			}
			else {
				dirty = true;
				changed();
			}

			return true;
		}
		return false;
	}

public:
	QString dir;
	QValueList<int> idList;
	bool dirty;

private:
	DWEntry()
	{
		dirty = false;
	}

	HANDLE h;
	DWThread *thread;
};

class DirWatchPlatform::Private : public QObject
{
	Q_OBJECT
public:
	Private(DirWatchPlatform *_par)
	{
		par = _par;
		list.setAutoDelete(true);
		connect(&t, SIGNAL(timeout()), this, SLOT(slotNotify()));
	}

	~Private()
	{
		list.clear();
	}

	QTimer t;
	DirWatchPlatform *par;
	QPtrList<DWEntry> list;

	int addItem(const QString &s)
	{
		DWEntry *e = findEntryByDir(s);
		if(!e) {
			e = DWEntry::create(s);
			if(!e)
				return -1;
			connect(e, SIGNAL(changed()), SLOT(slotChanged()));
			list.append(e);
		}
		int id = getUniqueId();
		e->idList.append(id);
		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;
	}

private slots:
	void slotChanged()
	{
		// use a timer to combine multiple changed events into one
		if(!t.isActive())
			t.start(200, true);
	}

	void slotNotify()
	{
		// 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);
				}
			}
		}
	}
};

DirWatchPlatform::DirWatchPlatform()
:QObject(0)
{
	d = new Private(this);
}

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

bool DirWatchPlatform::init()
{
	return true;
}

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

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

#include"dirwatch_win.moc"
