#include <efsw/WatcherKqueue.hpp>

#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS

#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <efsw/Debug.hpp>
#include <efsw/String.hpp>
#include <efsw/System.hpp>
#include <efsw/FileSystem.hpp>
#include <efsw/WatcherGeneric.hpp>
#include <efsw/FileWatcherKqueue.hpp>

#define KEVENT_RESERVE_VALUE (10)

#ifndef O_EVTONLY
#define O_EVTONLY (O_RDONLY | O_NONBLOCK)
#endif

namespace efsw {

int comparator(const void* ke1, const void* ke2)
{
	const KEvent * kev1	= reinterpret_cast<const KEvent*>( ke1 );
	const KEvent * kev2	= reinterpret_cast<const KEvent*>( ke2 );

	if ( NULL != kev2->udata )
	{
		FileInfo * fi1		= reinterpret_cast<FileInfo*>( kev1->udata );
		FileInfo * fi2		= reinterpret_cast<FileInfo*>( kev2->udata );

		return strcmp( fi1->Filepath.c_str(), fi2->Filepath.c_str() );
	}

	return 1;
}

WatcherKqueue::WatcherKqueue(WatchID watchid, const std::string& dirname, FileWatchListener* listener, bool recursive, FileWatcherKqueue * watcher, WatcherKqueue * parent ) :
	Watcher( watchid, dirname, listener, recursive ),
	mLastWatchID(0),
	mChangeListCount( 0 ),
	mKqueue( kqueue() ),
	mWatcher( watcher ),
	mParent( parent ),
	mInitOK( true ),
	mErrno(0)
{
	if ( -1 == mKqueue )
	{
		efDEBUG( "kqueue() returned invalid descriptor for directory %s. File descriptors count: %ld\n", Directory.c_str(), mWatcher->mFileDescriptorCount );
		
		mInitOK = false;
		mErrno = errno;
	}
	else
	{
		mWatcher->addFD();
	}
}

WatcherKqueue::~WatcherKqueue()
{
	// Remove the childs watchers ( sub-folders watches )
	removeAll();

	for ( size_t i = 0; i < mChangeListCount; i++ )
	{
		if ( NULL != mChangeList[i].udata )
		{
			FileInfo * fi		= reinterpret_cast<FileInfo*>( mChangeList[i].udata );

			efSAFE_DELETE( fi );
		}
	}

	close( mKqueue );
	
	mWatcher->removeFD();
}

void WatcherKqueue::addAll()
{
	if ( -1 == mKqueue )
	{
		return;
	}

	// scan directory and call addFile(name, false) on each file
	FileSystem::dirAddSlashAtEnd( Directory );

	efDEBUG( "addAll(): Added folder: %s\n", Directory.c_str());

	// add base dir
	int fd = open( Directory.c_str(), O_EVTONLY );
	
	if ( -1 == fd )
	{
		efDEBUG( "addAll(): Couldn't open folder: %s\n", Directory.c_str() );
		
		if ( EACCES != errno )
		{
			mInitOK = false;
		}

		mErrno = errno;
		
		return;
	}

	mDirSnap.setDirectoryInfo( Directory );
	mDirSnap.scan();

	mChangeList.resize( KEVENT_RESERVE_VALUE );

	// Creates the kevent for the folder
	EV_SET(
		&mChangeList[0],
		fd,
		EVFILT_VNODE,
		EV_ADD | EV_ENABLE | EV_ONESHOT,
		NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME,
		0,
		0
	);

	mWatcher->addFD();

	// Get the files and directories from the directory
	FileInfoMap files = FileSystem::filesInfoFromPath( Directory );

	for ( FileInfoMap::iterator it = files.begin(); it != files.end(); it++ )
	{
		FileInfo& fi = it->second;

		if ( fi.isRegularFile() )
		{
			// Add the regular files kevent
			addFile( fi.Filepath , false );
		}
		else if ( Recursive && fi.isDirectory() && fi.isReadable() )
		{
			// Create another watcher for the subfolders ( if recursive )
			WatchID id = addWatch( fi.Filepath, Listener, Recursive, this );

			// If the watcher is not adding the watcher means that the directory was created
			if ( id > 0 && !mWatcher->isAddingWatcher() )
			{
				handleFolderAction( fi.Filepath, Actions::Add );
			}
		}
	}
}

void WatcherKqueue::removeAll()
{
	efDEBUG( "removeAll(): Removing all child watchers\n" );

	std::list<WatchID> erase;

	for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); it++ )
	{
		efDEBUG( "removeAll(): Removed child watcher %s\n", it->second->Directory.c_str() );

		erase.push_back( it->second->ID );
	}

	for ( std::list<WatchID>::iterator eit = erase.begin(); eit != erase.end(); eit++ )
	{
		removeWatch( *eit );
	}
}

void WatcherKqueue::addFile(const std::string& name, bool emitEvents)
{
	efDEBUG( "addFile(): Added: %s\n", name.c_str() );

	// Open the file to get the file descriptor
	int fd = open( name.c_str(), O_EVTONLY );

	if( fd == -1 )
	{
		efDEBUG( "addFile(): Could open file descriptor for %s. File descriptor count: %ld\n", name.c_str(), mWatcher->mFileDescriptorCount );
		
		Errors::Log::createLastError( Errors::FileNotReadable, name );
		
		if ( EACCES != errno )
		{
			mInitOK = false;
		}

		mErrno = errno;
		
		return;
	}

	mWatcher->addFD();

	// increase the file kevent file count
	mChangeListCount++;

	if ( mChangeListCount + KEVENT_RESERVE_VALUE > mChangeList.size() &&
		 mChangeListCount % KEVENT_RESERVE_VALUE == 0 )
	{
		size_t reserve_size = mChangeList.size() + KEVENT_RESERVE_VALUE;
		mChangeList.resize( reserve_size );
		efDEBUG( "addFile(): Reserverd more KEvents space for %s, space reserved %ld, list actual size %ld.\n", Directory.c_str(), reserve_size, mChangeListCount );
	}

	// create entry
	FileInfo * entry = new FileInfo( name );

	// set the event data at the end of the list
	EV_SET(
		&mChangeList[mChangeListCount],
		fd,
		EVFILT_VNODE,
		EV_ADD | EV_ENABLE | EV_ONESHOT,
		NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME,
		0,
		(void*)entry
	);

	// qsort sort the list by name
	qsort(&mChangeList[1], mChangeListCount, sizeof(KEvent), comparator);

	// handle action
	if( emitEvents )
	{
		handleAction(name, Actions::Add);
	}
}

void WatcherKqueue::removeFile( const std::string& name, bool emitEvents )
{
	efDEBUG( "removeFile(): Trying to remove file: %s\n", name.c_str() );

	// bsearch
	KEvent target;

	// Create a temporary file info to search the kevent ( searching the directory )
	FileInfo tempEntry( name );

	target.udata = &tempEntry;

	// Search the kevent
	KEvent * ke = (KEvent*)bsearch(&target, &mChangeList[0], mChangeListCount + 1, sizeof(KEvent), comparator);

	// Trying to remove a non-existing file?
	if( !ke )
	{
		Errors::Log::createLastError( Errors::FileNotFound, name );
		efDEBUG( "File not removed\n" );
		return;
	}

	efDEBUG( "File removed\n" );

	// handle action
	if ( emitEvents )
	{
		handleAction( name, Actions::Delete );
	}

	// Delete the user data ( FileInfo ) from the kevent closed
	FileInfo * del = reinterpret_cast<FileInfo*>( ke->udata );

	efSAFE_DELETE( del );

	// close the file descriptor from the kevent
	close( ke->ident );

	mWatcher->removeFD();

	memset(ke, 0, sizeof(KEvent));

	// move end to current
	memcpy(ke, &mChangeList[mChangeListCount], sizeof(KEvent));
	memset(&mChangeList[mChangeListCount], 0, sizeof(KEvent));
	--mChangeListCount;
}

void WatcherKqueue::rescan()
{
	efDEBUG( "rescan(): Rescanning: %s\n", Directory.c_str() );

	DirectorySnapshotDiff Diff = mDirSnap.scan();

	if ( Diff.DirChanged )
	{
		sendDirChanged();
	}

	if ( Diff.changed() )
	{
		FileInfoList::iterator it;
		MovedList::iterator mit;

		/// Files
		DiffIterator( FilesCreated )
		{
			addFile( (*it).Filepath );
		}

		DiffIterator( FilesModified )
		{
			handleAction( (*it).Filepath, Actions::Modified );
		}

		DiffIterator( FilesDeleted )
		{
			removeFile( (*it).Filepath );
		}

		DiffMovedIterator( FilesMoved )
		{
			handleAction( (*mit).second.Filepath, Actions::Moved, (*mit).first );
			removeFile( Directory + (*mit).first, false );
			addFile( (*mit).second.Filepath, false );
		}

		/// Directories
		DiffIterator( DirsCreated )
		{
			handleFolderAction( (*it).Filepath, Actions::Add );
			addWatch( (*it).Filepath, Listener, Recursive, this );
		}

		DiffIterator( DirsModified )
		{
			handleFolderAction( (*it).Filepath, Actions::Modified );
		}

		DiffIterator( DirsDeleted )
		{
			handleFolderAction( (*it).Filepath, Actions::Delete );

			Watcher * watch = findWatcher( (*it).Filepath );

			if ( NULL != watch )
			{
				removeWatch( watch->ID );

			}
		}

		DiffMovedIterator( DirsMoved )
		{
			moveDirectory( Directory + (*mit).first, (*mit).second.Filepath );
		}
	}
}

WatchID WatcherKqueue::watchingDirectory( std::string dir )
{
	Watcher * watch = findWatcher( dir );

	if ( NULL != watch )
	{
		return watch->ID;
	}

	return Errors::FileNotFound;
}

void WatcherKqueue::handleAction( const std::string& filename, efsw::Action action, const std::string& oldFilename )
{
	Listener->handleFileAction( ID, Directory, FileSystem::fileNameFromPath( filename ), action, FileSystem::fileNameFromPath( oldFilename ) );
}

void WatcherKqueue::handleFolderAction( std::string filename, efsw::Action action , const std::string &oldFilename )
{
	FileSystem::dirRemoveSlashAtEnd( filename );

	handleAction( filename, action, oldFilename );
}

void WatcherKqueue::sendDirChanged()
{
	if ( NULL != mParent )
	{
		Listener->handleFileAction( mParent->ID, mParent->Directory, FileSystem::fileNameFromPath( Directory ), Actions::Modified );
	}
}

void WatcherKqueue::watch()
{
	if ( -1 == mKqueue )
	{
		return;
	}

	int nev = 0;
	KEvent event;

	// First iterate the childs, to get the events from the deepest folder, to the watcher childs
	for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it )
	{
		it->second->watch();
	}

	bool needScan = false;

	// Then we get the the events of the current folder
	while( ( nev = kevent( mKqueue, &mChangeList[0], mChangeListCount + 1, &event, 1, &mWatcher->mTimeOut ) ) != 0 )
	{
		// An error ocurred?
		if( nev == -1 )
		{
			efDEBUG( "watch(): Error on directory %s\n", Directory.c_str() );
			perror("kevent");
			break;
		}
		else
		{
			FileInfo * entry = NULL;

			// If udate == NULL means that it is the fisrt element of the change list, the folder.
			// otherwise it is an event of some file inside the folder
			if( ( entry = reinterpret_cast<FileInfo*> ( event.udata ) ) != NULL )
			{
				efDEBUG( "watch(): File: %s ", entry->Filepath.c_str() );

				// If the event flag is delete... the file was deleted
				if ( event.fflags & NOTE_DELETE )
				{
					efDEBUG( "deleted\n" );

					mDirSnap.removeFile( entry->Filepath );

					removeFile( entry->Filepath );
				}
				else if (	event.fflags & NOTE_EXTEND	||
							event.fflags & NOTE_WRITE	||
							event.fflags & NOTE_ATTRIB
				)
				{
					// The file was modified
					efDEBUG( "modified\n" );

					FileInfo fi( entry->Filepath );

					if ( fi != *entry )
					{
						*entry = fi;

						mDirSnap.updateFile( entry->Filepath );

						handleAction( entry->Filepath, efsw::Actions::Modified );
					}
				}
				else if ( event.fflags & NOTE_RENAME )
				{
					efDEBUG( "moved\n" );

					needScan = true;
				}
			}
			else
			{
				needScan = true;
			}
		}
	}

	if ( needScan )
	{
		rescan();
	}
}

Watcher * WatcherKqueue::findWatcher( const std::string path )
{
	WatchMap::iterator it = mWatches.begin();

	for ( ; it != mWatches.end(); it++ )
	{
		if ( it->second->Directory == path )
		{
			return it->second;
		}
	}

	return NULL;
}

void WatcherKqueue::moveDirectory( std::string oldPath, std::string newPath, bool emitEvents )
{
	// Update the directory path if it's a watcher
	std::string opath2( oldPath );
	FileSystem::dirAddSlashAtEnd( opath2 );

	Watcher * watch = findWatcher( opath2 );

	if ( NULL != watch )
	{
		watch->Directory = opath2;
	}

	if ( emitEvents )
	{
		handleFolderAction( newPath, efsw::Actions::Moved, oldPath );
	}
}

WatchID WatcherKqueue::addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive , WatcherKqueue *parent)
{
	static long s_fc = 0;
	static bool s_ug = false;

	std::string dir( directory );

	FileSystem::dirAddSlashAtEnd( dir );

	// This should never happen here
	if( !FileSystem::isDirectory( dir ) )
	{
		return Errors::Log::createLastError( Errors::FileNotFound, dir );
	}
	else if ( pathInWatches( dir ) || pathInParent( dir ) )
	{
		return Errors::Log::createLastError( Errors::FileRepeated, directory );
	}
	else if ( NULL != parent && FileSystem::isRemoteFS( dir ) )
	{
		return Errors::Log::createLastError( Errors::FileRemote, dir );
	}

	std::string curPath;
	std::string link( FileSystem::getLinkRealPath( dir, curPath ) );

	if ( "" != link )
	{
		/// Avoid adding symlinks directories if it's now enabled
		if ( NULL != parent && !mWatcher->mFileWatcher->followSymlinks() )
		{
			return Errors::Log::createLastError( Errors::FileOutOfScope, dir );
		}

		if ( pathInWatches( link ) || pathInParent( link ) )
		{
			return Errors::Log::createLastError( Errors::FileRepeated, link );
		}
		else if ( !mWatcher->linkAllowed( curPath, link ) )
		{
			return Errors::Log::createLastError( Errors::FileOutOfScope, link );
		}
		else
		{
			dir = link;
		}
	}

	if ( mWatcher->availablesFD() )
	{
		WatcherKqueue* watch = new WatcherKqueue( ++mLastWatchID, dir, watcher, recursive, mWatcher, parent );

		mWatches.insert(std::make_pair(mLastWatchID, watch));

		watch->addAll();

		s_fc++;

		// if failed to open the directory... erase the watcher
		if ( !watch->initOK() )
		{
			int le = watch->lastErrno();

			mWatches.erase( watch->ID );

			efSAFE_DELETE( watch );

			mLastWatchID--;

			// Probably the folder has too many files, create a generic watcher
			if ( EACCES != le )
			{
				WatcherGeneric * watch = new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive );

				mWatches.insert(std::make_pair(mLastWatchID, watch));
			}
			else
			{
				return Errors::Log::createLastError( Errors::Unspecified, link );
			}
		}
	}
	else
	{
		if ( !s_ug )
		{
			efDEBUG( "Started using WatcherGeneric, reached file descriptors limit: %ld. Folders added: %ld\n", mWatcher->mFileDescriptorCount, s_fc );
			s_ug = true;
		}

		WatcherGeneric * watch = new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive );

		mWatches.insert(std::make_pair(mLastWatchID, watch));
	}

	return mLastWatchID;
}

bool WatcherKqueue::initOK()
{
	return mInitOK;
}

void WatcherKqueue::removeWatch( WatchID watchid )
{
	WatchMap::iterator iter = mWatches.find(watchid);

	if(iter == mWatches.end())
		return;

	Watcher * watch = iter->second;

	mWatches.erase(iter);

	efSAFE_DELETE( watch );
}

bool WatcherKqueue::pathInWatches( const std::string& path )
{
	return NULL != findWatcher( path );
}

bool WatcherKqueue::pathInParent( const std::string &path )
{
	WatcherKqueue * pNext = mParent;

	while ( NULL != pNext )
	{
		if ( pNext->pathInWatches( path ) )
		{
			return true;
		}

		pNext = pNext->mParent;
	}

	if ( mWatcher->pathInWatches( path ) )
	{
		return true;
	}

	if ( path == Directory )
	{
		return true;
	}

	return false;
}

int WatcherKqueue::lastErrno()
{
	return mErrno;
}

}

#endif
