#include <efsw/Debug.hpp>
#include <efsw/FileSystem.hpp>
#include <efsw/FileWatcherFSEvents.hpp>
#include <efsw/WatcherFSEvents.hpp>

#if EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS

namespace efsw {

WatcherFSEvents::WatcherFSEvents() :
	Watcher(), FWatcher( NULL ), FSStream( NULL ), WatcherGen( NULL ), initializedAsync( false ) {}

WatcherFSEvents::WatcherFSEvents( WatchID id, std::string directory, FileWatchListener* listener,
								  bool recursive, WatcherFSEvents* /*parent*/ ) :
	Watcher( id, directory, listener, recursive ),
	FWatcher( NULL ),
	FSStream( NULL ),
	WatcherGen( NULL ),
	initializedAsync( false ) {}

WatcherFSEvents::~WatcherFSEvents() {
	if ( NULL != FSStream ) {
		if ( initializedAsync ) {
			FSEventStreamStop( FSStream );
		}

		FSEventStreamInvalidate( FSStream );
		FSEventStreamRelease( FSStream );
	}

	efSAFE_DELETE( WatcherGen );
}

void WatcherFSEvents::init() {
	CFStringRef CFDirectory =
		CFStringCreateWithCString( NULL, Directory.c_str(), kCFStringEncodingUTF8 );
	CFArrayRef CFDirectoryArray = CFArrayCreate( NULL, (const void**)&CFDirectory, 1, NULL );

	Uint32 streamFlags = kFSEventStreamCreateFlagNone;

	if ( FileWatcherFSEvents::isGranular() ) {
		streamFlags = efswFSEventStreamCreateFlagFileEvents;
	} else {
		WatcherGen = new WatcherGeneric( ID, Directory, Listener, FWatcher.load(), Recursive );
	}

	FSEventStreamContext ctx;
	/* Initialize context */
	ctx.version = 0;
	ctx.info = this;
	ctx.retain = NULL;
	ctx.release = NULL;
	ctx.copyDescription = NULL;

	FSStream =
		FSEventStreamCreate( kCFAllocatorDefault, &FileWatcherFSEvents::FSEventCallback, &ctx,
							 CFDirectoryArray, kFSEventStreamEventIdSinceNow, 0.25, streamFlags );
	FWatcher.load()->mNeedInitMutex.lock();
	FWatcher.load()->mNeedInit.push_back( this );
	FWatcher.load()->mNeedInitMutex.unlock();

	CFRelease( CFDirectoryArray );
	CFRelease( CFDirectory );
}

void WatcherFSEvents::initAsync() {
	FSEventStreamScheduleWithRunLoop( FSStream, FWatcher.load()->mRunLoopRef.load(),
									  kCFRunLoopDefaultMode );
	FSEventStreamStart( FSStream );
	initializedAsync = true;
}

void WatcherFSEvents::sendFileAction( WatchID watchid, const std::string& dir,
									  const std::string& filename, Action action,
									  std::string oldFilename ) {
	Listener->handleFileAction( watchid, FileSystem::precomposeFileName( dir ),
								FileSystem::precomposeFileName( filename ), action, oldFilename );
}

void WatcherFSEvents::handleAddModDel( const Uint32& flags, const std::string& path,
									   std::string& dirPath, std::string& filePath ) {
	if ( flags & efswFSEventStreamEventFlagItemCreated ) {
		if ( FileInfo::exists( path ) ) {
			sendFileAction( ID, dirPath, filePath, Actions::Add );
		}
	}

	if ( flags & efswFSEventsModified ) {
		sendFileAction( ID, dirPath, filePath, Actions::Modified );
	}

	if ( flags & efswFSEventStreamEventFlagItemRemoved ) {
		// Since i don't know the order, at least i try to keep the data consistent with the real
		// state
		if ( !FileInfo::exists( path ) ) {
			sendFileAction( ID, dirPath, filePath, Actions::Delete );
		}
	}
}

void WatcherFSEvents::handleActions( std::vector<FSEvent>& events ) {
	size_t esize = events.size();

	for ( size_t i = 0; i < esize; i++ ) {
		FSEvent& event = events[i];

		if ( event.Flags &
			 ( kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped |
			   kFSEventStreamEventFlagEventIdsWrapped | kFSEventStreamEventFlagHistoryDone |
			   kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount |
			   kFSEventStreamEventFlagRootChanged ) ) {
			continue;
		}

		if ( !Recursive ) {
			/** In case that is not recursive the watcher, ignore the events from subfolders */
			if ( event.Path.find_last_of( FileSystem::getOSSlash() ) != Directory.size() - 1 ) {
				continue;
			}
		}

		if ( FileWatcherFSEvents::isGranular() ) {
			std::string dirPath( FileSystem::pathRemoveFileName( event.Path ) );
			std::string filePath( FileSystem::fileNameFromPath( event.Path ) );

			if ( event.Flags &
				 ( efswFSEventStreamEventFlagItemCreated | efswFSEventStreamEventFlagItemRemoved |
				   efswFSEventStreamEventFlagItemRenamed ) ) {
				if ( dirPath != Directory ) {
					DirsChanged.insert( dirPath );
				}
			}

			// This is a mess. But it's FSEvents faults, because shrinks events from the same file
			// in one single event ( so there's no order for them ) For example a file could have
			// been added modified and erased, but i can't know if first was erased and then added
			// and modified, or added, then modified and then erased. I don't know what they were
			// thinking by doing this...
			efDEBUG( "Event in: %s - flags: %ld\n", event.Path.c_str(), event.Flags );

			if ( event.Flags & efswFSEventStreamEventFlagItemRenamed ) {
				if ( ( i + 1 < esize ) &&
					 ( events[i + 1].Flags & efswFSEventStreamEventFlagItemRenamed ) &&
					 ( events[i + 1].Id == event.Id + 1 ) ) {
					FSEvent& nEvent = events[i + 1];
					std::string newDir( FileSystem::pathRemoveFileName( nEvent.Path ) );
					std::string newFilepath( FileSystem::fileNameFromPath( nEvent.Path ) );

					if ( event.Path != nEvent.Path ) {
						if ( dirPath == newDir ) {
							if ( !FileInfo::exists( event.Path ) ) {
								sendFileAction( ID, dirPath, newFilepath, Actions::Moved,
												filePath );
							} else {
								sendFileAction( ID, dirPath, filePath, Actions::Moved,
												newFilepath );
							}
						} else {
							sendFileAction( ID, dirPath, filePath, Actions::Delete );
							sendFileAction( ID, newDir, newFilepath, Actions::Add );

							if ( nEvent.Flags & efswFSEventsModified ) {
								sendFileAction( ID, newDir, newFilepath, Actions::Modified );
							}
						}
					} else {
						handleAddModDel( nEvent.Flags, nEvent.Path, dirPath, filePath );
					}

					if ( nEvent.Flags & ( efswFSEventStreamEventFlagItemCreated |
										  efswFSEventStreamEventFlagItemRemoved |
										  efswFSEventStreamEventFlagItemRenamed ) ) {
						if ( newDir != Directory ) {
							DirsChanged.insert( newDir );
						}
					}

					// Skip the renamed file
					i++;
				} else if ( FileInfo::exists( event.Path ) ) {
					sendFileAction( ID, dirPath, filePath, Actions::Add );

					if ( event.Flags & efswFSEventsModified ) {
						sendFileAction( ID, dirPath, filePath, Actions::Modified );
					}
				} else {
					sendFileAction( ID, dirPath, filePath, Actions::Delete );
				}
			} else {
				handleAddModDel( event.Flags, event.Path, dirPath, filePath );
			}
		} else {
			efDEBUG( "Directory: %s changed\n", event.Path.c_str() );
			DirsChanged.insert( event.Path );
		}
	}
}

void WatcherFSEvents::process() {
	std::set<std::string>::iterator it = DirsChanged.begin();

	for ( ; it != DirsChanged.end(); it++ ) {
		if ( !FileWatcherFSEvents::isGranular() ) {
			WatcherGen->watchDir( ( *it ) );
		} else {
			sendFileAction( ID, FileSystem::pathRemoveFileName( ( *it ) ),
							FileSystem::fileNameFromPath( ( *it ) ), Actions::Modified );
		}
	}

	DirsChanged.clear();
}

} // namespace efsw

#endif
