(*
 * Informations Taken from :
 *
 * Windows :
 * http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v=vs.85%29.aspx // ReadDirectoryChangesW
 * http://msdn.microsoft.com/en-us/library/windows/desktop/aa364391%28v=vs.85%29.aspx // FILE_NOTIFY_INFORMATION
 * http://msdn.microsoft.com/en-us/library/aa365261%28v=vs.85%29.aspx // Obtaining Directory Change Notifications
 *
 * Linux :
 * http://www.friends-of-fpc.com/research/LinuxProgramming/inotify/
 * http://www.tutorialspoint.com/unix_system_calls/inotify_init.htm
 * http://www.tutorialspoint.com/unix_system_calls/inotify_rm_watch.htm
 * http://www.tutorialspoint.com/unix_system_calls/inotify_add_watch.htm
 *)

(******************************************************************************)
(*                                                                            *)
(* Known Bugs : recursive watch on linux is not implemented yet               *)
(*              - workaround, scan the directory by yourself                  *)
(******************************************************************************)
Unit cn_monitor;

{$MODE objfpc}{$H+}

Interface

Uses
  Classes, SysUtils
{$IFDEF WINDOWS}
  , windows
{$ENDIF}
{$IFDEF LINUX}
  , BaseUnix
  , Linux
{$ENDIF}
  , FileUtil
  , math;

Const
  (*
   * Error Codes
   *)
  CM_NO_ERROR = 0; // Everything worked like expected, there is no error
  CM_MONITOR_OVERFLOW = 1; // Reached maximum number of File Monitors
  CM_INVALID_HANDLE = 2; // The Monitor Class could not Register the Monitor Handle
  CM_FILE_NOT_EXISTS = 3; // Tried to monitor a file that does not exist
  CM_DIR_NOT_EXISTS = 4; // Tried to monitor a Directory that does not exist
  CM_FILE_ALREADY_MONITORED = 5; // File is already monitored

  CM_ERROR_UNKNOWN = 255; // An Error occured, that should not be possible

  (*
   * Timing Consts
   *)
  ReportBlockedblockerTime = 100; // Debouncing Value in ms for ChangeReportings.
  WaitForTime = 10; // Time in Milli Seconds wich will used for WaitForMultipleObjects

Type

  (*
   * The Callbacktypes wich need to be set if monitoring is used
   *)
  TDirectoryChangeEvent = Procedure(Sender: tobject; Directory: String) Of Object;
  TFileChangeEvent = Procedure(Sender: tObject; Filename: String) Of Object;

  PQueue = ^TQueue;

{$IFDEF LINUX}
  TFileTrigger = Record
    Filename: String;
    ReportBlockedblocker: integer; // Blocks multiple Reports on that spezifik Monitor (this is because OS reports changes really often vor only one ChangeEvent)
  End;
{$ENDIF}

  TQueue = Record
    // All Settings for directories
    Directory: String;
    Recursive: Boolean;
    // All Settings for Files
    Filename: String;
    // Switch between File or Directory Watch
    IsFile: Boolean;
    ReportBlockedblocker: integer; // Blocks multiple Reports on that spezifik Monitor (this is because OS reports changes really often vor only one ChangeEvent)
{$IFDEF LINUX}
    WD: Cint; // watch descriptor
    FileTrigger: Array Of TFileTrigger;
{$ENDIF}
  End;

  //    TNotifyRequest = Record
  //      Directory: String;
  //      IsFile: Boolean;
  //      Filename: String;
  //
  //      ReportBlockedblocker: integer;
  //    End;

  TReport = Record
    Value: String; // Name of the Directory or Filename
    IsFile: Boolean; // Selector for File or Directory
  End;

  { TCnMonitor }

  TCnMonitor = Class(tThread) // Die Klasse, welche das Monitoring Bereit Stellt
  private
    (*
     * Synchronize Variables
     *)
    FQueueRes: integer; // Variable is set by Add Queue from the Thread
    fResultReport: TReport; // Holds the Monitor Information for the Synchronized Report Routine
    (*
     * Thread Variables
     *)
    FqueueClear: Boolean; // If true, than the Thread delete all monitors
    FQueue_Add: PQueue; // Job to Add a new Monitor
    FQueue_Del: PQueue; // Job to del a Monitor
{$IFDEF WINDOWS}
    fTWOHandleArray: TWOHandleArray; // All Monitor Handles
    fqueue_infos: Array[0..MAXIMUM_WAIT_OBJECTS - 1] Of TQueue; // Additional Informations about the Monitor Handles
{$ENDIF}
{$IFDEF LINUX}
    instance_handle: Cint; // Handle für die Suche Instanz
    fqueue_infos: Array Of TQueue; // Additional Informations about the Monitor Handles
{$ENDIF}
    fTWOHandleArray_index: integer; // Count of Aktual Used Monitors
    fdeactivated: Boolean;
    fneeddeactivated: integer;
    fTWOHandleArray_index_bkup: integer;
    (*
     * Synchronized Methods
     *)
    Procedure Report(); // Calls the Callbacks within the Main Thread
    (*
     * Thread Methods
     *)
    Procedure DoDeAktivate;
    Procedure DoAktivate;
    Function AddQueue(): integer;
    Function DelQueue(): integer;
    Procedure ClearMonitors();
  protected
    Procedure Execute; override;
  public
    (*
     * Routines that are Allowed to call from the Main Thread
     *)
    OnDirectoryChangeEvent: TDirectoryChangeEvent;
    OnFileChangeEvent: TFileChangeEvent;
    Function MonitorFile(Filename: String): integer; // Add a File monitor
    Function MonitorDirectory(Directory: String; WithSubDirectorys: Boolean): integer; // Add a Directory Monitor
    Function RemoveMonitorDirectory(Directory: String): Boolean;
    Function RemoveMonitorFile(Filename: String): Boolean;
    Procedure Clear; // Deletes all Monitors
    Constructor Create();
    Destructor Destroy; override;
    Procedure DeActivate;
    Procedure Activate;
  End;

Implementation

{$IFDEF WINDOWS}
Type
  TFILE_NOTIFY_INFORMATION = Record
    NextEntryOffset: Longword;
    Action: Longword;
    FileNameLength: Longword;
    FileName: Array[0..MAX_PATH] Of WCHAR;
  End;

  PFILE_NOTIFY_INFORMATION = ^TFILE_NOTIFY_INFORMATION;

Function ReadDirectoryChangesW(hDirectory: HANDLE; lpBuffer: LPVOID;
  nBufferLength: DWORD; bWatchSubtree: BOOL; dwNotifyFilter: DWORD;
  lpBytesReturned: LPDWORD; lpOverlapped: LPOVERLAPPED;
  lpCompletionRoutine: LPOVERLAPPED_COMPLETION_ROUTINE): BOOL; stdcall; external kernel32 name 'ReadDirectoryChangesW';
{$ENDIF}

{ TCnMonitor }

Procedure TCnMonitor.Execute;
Var
  remover: PQueue;
  i: Integer;
  s: String;
{$IFDEF WINDOWS}
  b: Boolean;
  ss: Widestring;
  obj: DWORD;
  buffer: Array[0..65535] Of byte;
  bufferbytes: LongWord;
  work: PFILE_NOTIFY_INFORMATION;
{$ENDIF}
{$IFDEF LINUX}
  rfds: TFDSet;
  Ret: Integer;
  Buffer: PChar;
  buflen: Integer = (SizeOf(inotify_event) + 16) * 512;
  TotalRead: Longint;
  p: Longint;
  evnt: Pinotify_event;
  j: Integer;
{$ENDIF}
Begin
  remover := Nil;
  While Not Terminated Do Begin
    // Entgegennehmen eines Auftrages
    If assigned(FQueue_Add) Then Begin // Neuer Monitor Auftrag
      FQueueRes := AddQueue();
      dispose(FQueue_Add);
      FQueue_Add := Nil;
    End;
    If Assigned(FQueue_Del) Then Begin // Entfernen eines Bestehenden Auftrages
{$IFDEF LINUX}
      // On Linux deletion is realized by the wd Index, so we have to locate this one first.
      For i := 0 To high(fqueue_infos) Do Begin
        If FQueue_Del^.Directory = fqueue_infos[i].Directory Then Begin
          FQueue_Del^.WD := fqueue_infos[i].WD;
          break;
        End;
      End;
{$ENDIF}
      FQueueRes := delQueue();
      dispose(FQueue_del);
      FQueue_del := Nil;
    End;
    If assigned(Remover) Then Begin
      FQueue_Del := remover;
      delQueue();
      dispose(FQueue_del);
      FQueue_del := Nil;
      remover := Nil;
    End;
    If FqueueClear Then Begin
      ClearMonitors();
      FqueueClear := false;
    End;
    If fneeddeactivated <> 0 Then Begin
      If fneeddeactivated = 1 Then Begin // Need Deaktivate
        DoDeAktivate;
      End;
      If fneeddeactivated = 2 Then Begin // Need Aktivate
        DoAktivate;
      End;
      fneeddeactivated := 0;
    End;
    // Wenn Monitor Aufträge bestehen, dann Werden diese nun geprüft
    If fTWOHandleArray_index <> 0 Then Begin
{$IFDEF WINDOWS}
      obj := WaitForMultipleObjects(fTWOHandleArray_index, @fTWOHandleArray, false, WaitForTime);
      If obj <> WAIT_TIMEOUT Then Begin
        obj := obj - WAIT_OBJECT_0;
        // Additional Range Check, should not be neccessary
        If {(obj >= 0) And}(obj < fTWOHandleArray_index) Then Begin
          If (fqueue_infos[obj].IsFile) Then Begin
            // Monitored Handle was a File, so check whether the File that changed was the right one
            If FileExistsUTF8(fqueue_infos[obj].Filename) Then Begin
              // Prüfen ob sich die Datei geändert hat
              ReadDirectoryChangesW(fTWOHandleArray[obj], @buffer[0], 65535, false, 31, @bufferbytes, Nil, Nil);
              b := true;
              work := @buffer[0];
              While b Do Begin
                // Wie verdammt noch mal extrahiert man das ohne Warnungen ??
                ss := StringOfChar(#00, work^.FileNameLength);
                StrMove(@ss[1], @work^.FileName[0], work^.FileNameLength);
                s := WideCharToString(PWideChar(ss));
                s := copy(s, 1, length(s) Shl 1);
                s := fqueue_infos[obj].Directory + s;
                // If our file was really changed
                If lowercase(s) = lowercase(fqueue_infos[obj].Filename) Then Begin
                  If (fqueue_infos[obj].ReportBlockedblocker = 0) Then Begin
                    fResultReport.Value := fqueue_infos[obj].Filename;
                    fResultReport.IsFile := true;
                    Synchronize(@Report);
                    fqueue_infos[obj].ReportBlockedblocker := ReportBlockedblockerTime;
                  End;
                End;
                If work^.NextEntryOffset = 0 Then Begin
                  b := false;
                End
                Else Begin
                  work := @buffer[work^.NextEntryOffset];
                  If bufferbytes < work^.NextEntryOffset Then Begin
                    b := false;
                  End;
                End;
              End;
            End
            Else Begin
              // Die Datei wurde gelöscht
              fResultReport.Value := fqueue_infos[obj].Filename;
              fResultReport.IsFile := true;
              Synchronize(@Report);
              new(remover);
              remover^ := fqueue_infos[obj];
            End;
          End
          Else Begin
            // Monitored Directory, report the change
            If (fqueue_infos[obj].ReportBlockedblocker = 0) Then Begin
              fResultReport.Value := fqueue_infos[obj].Directory;
              fResultReport.IsFile := false;
              Synchronize(@Report);
              fqueue_infos[obj].ReportBlockedblocker := ReportBlockedblockerTime;
            End;
            // Directory was deleted
            If Not DirectoryExistsUTF8(fqueue_infos[obj].Directory) Then Begin
              new(remover);
              remover^ := fqueue_infos[obj];
            End;
          End;
          If Not FindNextChangeNotification(fTWOHandleArray[obj]) Then Begin
            // Wenn die Datei/ der Ordner gelöscht wurde, kann das Handle nicht mehr weiter Geschaltet werden.
            If Not assigned(remover) Then Begin
              new(remover);
              remover^ := fqueue_infos[obj];
            End;
          End;
        End
        Else Begin
          Raise exception.create('Error invalid handle from WaitForMultipleObjects.');
        End;
      End
      Else Begin
        // Timeout, Reduce the Debounce Values
        For i := 0 To fTWOHandleArray_index - 1 Do Begin
          fqueue_infos[i].ReportBlockedblocker := max(fqueue_infos[i].ReportBlockedblocker - WaitForTime, 0);
        End;
      End;
{$ENDIF}
{$IFDEF LINUX}
      If instance_handle <> -1 Then Begin
        fpFD_Zero(rfds);
        fpFD_Set(instance_handle, rfds);
        Ret := fpSelect(instance_handle + 1, @rfds, Nil, Nil, WaitForTime);
        If ret = 0 Then Begin
          // Timeout, Reduce the Debounce Values
          For i := 0 To fTWOHandleArray_index - 1 Do Begin
            fqueue_infos[i].ReportBlockedblocker := max(fqueue_infos[i].ReportBlockedblocker - WaitForTime, 0);
            For j := 0 To high(fqueue_infos[i].FileTrigger) Do Begin
              fqueue_infos[i].FileTrigger[j].ReportBlockedblocker := max(fqueue_infos[i].FileTrigger[j].ReportBlockedblocker - WaitForTime, 0);
            End;
          End;
        End;
        If ret > 0 Then Begin
          Buffer := Nil;
          ReAllocMem(Buffer, buflen);
          TotalRead := fpRead(instance_handle, Buffer, buflen);
          P := 0;
          While P < TotalRead Do Begin
            evnt := pinotify_event(buffer + P);
            If {(((evnt^.mask And IN_MODIFY) = IN_MODIFY)
            //or (((evnt^.mask and IN_ATTRIB)=IN_ATTRIB))
            Or (((evnt^.mask And IN_DELETE) = IN_DELETE))
            Or (((evnt^.mask And IN_DELETE_SELF) = IN_DELETE_SELF))
            Or (((evnt^.mask And IN_MOVE_SELF) = IN_MOVE_SELF))
            Or (((evnt^.mask And IN_CREATE) = IN_CREATE)))
            And }
            (String(@evnt^.Name) <> '.') And (String(@evnt^.Name) <> '..') Then Begin
              // Suchen des Auslösenden Events // Evtl müsste das auch mit einem direkten Array zugriff gehen ???
              For i := 0 To high(fqueue_infos) Do Begin
                If fqueue_infos[i].WD = evnt^.wd Then Begin
                  // Report the Changed Directory if it is watched
                  If Not fqueue_infos[i].IsFile Then Begin
                    If fqueue_infos[i].ReportBlockedblocker = 0 Then Begin
                      fqueue_infos[i].ReportBlockedblocker := ReportBlockedblockerTime;
                      fResultReport.Value := fqueue_infos[i].Directory;
                      fResultReport.IsFile := false;
                      Synchronize(@Report);
                    End;
                  End;
                  // Are there any File Trigger, that need to be fired to ?
                  s := fqueue_infos[i].Directory + pchar(@evnt^.Name);
                  For j := 0 To high(fqueue_infos[i].FileTrigger) Do Begin
                    If fqueue_infos[i].FileTrigger[j].Filename = s Then Begin
                      If fqueue_infos[i].FileTrigger[j].ReportBlockedblocker = 0 Then Begin
                        fqueue_infos[i].FileTrigger[j].ReportBlockedblocker := ReportBlockedblockerTime;
                        fResultReport.Value := fqueue_infos[i].FileTrigger[j].Filename;
                        fResultReport.IsFile := true;
                        Synchronize(@Report);
                      End;
                      // The File was Removed, so remove its Trigger
                      If Not FileExists(fqueue_infos[i].FileTrigger[j].Filename) Then Begin
                        new(remover);
                        remover^.WD := fqueue_infos[i].WD;
                        remover^.Filename := fqueue_infos[i].FileTrigger[j].Filename;
                        remover^.IsFile := true;
                      End;
                      break;
                    End;
                  End;
                  // Directory was deleted
                  If Not DirectoryExistsUTF8(fqueue_infos[i].Directory) Then Begin
                    If Not assigned(remover) Then Begin
                      new(remover);
                      remover^.WD := fqueue_infos[i].WD;
                      remover^.Filename := '';
                      remover^.IsFile := false;
                    End
                    Else Begin
                      If remover^.WD <> fqueue_infos[i].WD Then Begin
                        Raise exception.Create('Internal logic error, trying to remove two different notifier. Monitoring enging is now in invalid state.');
                      End;
                      remover^.IsFile := true;
                    End;
                  End;
                  break;
                End;
              End;
            End;
            Inc(P, evnt^.len + 16);
          End;
          ReAllocMem(Buffer, 0);
        End;
      End
      Else Begin
        Raise exception.create('Error invalid handle.');
      End;
{$ENDIF}
    End
    Else Begin
      sleep(1);
    End;
  End;
  // If the remover is set within the period the Application goes down.
  If assigned(remover) Then
    Dispose(remover);
  remover := Nil;
End;

Procedure TCnMonitor.Report;
Begin
  If Not fdeactivated Then Begin // Wenn wir deaktiviert sind, gibts keine Reports !!
    If fResultReport.IsFile Then Begin
      If Assigned(OnFileChangeEvent) Then Begin
        OnFileChangeEvent(self, fResultReport.Value);
      End;
    End
    Else Begin
      If Assigned(OnDirectoryChangeEvent) Then Begin
        OnDirectoryChangeEvent(self, fResultReport.Value);
      End;
    End;
  End;
End;

Procedure TCnMonitor.DoDeAktivate;
Var
  i: Integer;
Begin
  If fdeactivated Then exit;
{$IFDEF Windows}
{$WARNING not implemented yet.}
{$ENDIF}
  fTWOHandleArray_index_bkup := fTWOHandleArray_index;
  fTWOHandleArray_index := 0;
{$IFDEF LINUX}
  If instance_handle <> -1 Then Begin
    For i := 0 To High(fqueue_infos) Do Begin
      inotify_rm_watch(instance_handle, fqueue_infos[i].WD);
    End;
  End;
{$ENDIF}
  fdeactivated := true;
End;

Procedure TCnMonitor.DoAktivate;
Var
  i: Integer;
Begin
  If Not fdeactivated Then exit;
{$IFDEF Windows}
{$WARNING not implemented yet.}
{$ENDIF}

{$IFDEF LINUX}
  If instance_handle <> -1 Then Begin
    For i := 0 To High(fqueue_infos) Do Begin
      //      inotify_rm_watch(instance_handle, );
      fqueue_infos[i].WD := inotify_add_watch(instance_handle, Pchar(fqueue_infos[i].Directory),
        IN_MODIFY Or IN_ATTRIB Or IN_MOVE Or IN_CREATE Or
        IN_DELETE Or IN_DELETE_SELF Or IN_MOVE_SELF Or IN_DONT_FOLLOW);
    End;
  End;
{$ENDIF}
  fdeactivated := false;
  fTWOHandleArray_index := fTWOHandleArray_index_bkup;
End;

Function TCnMonitor.AddQueue: integer;
Var
{$IFDEF WINDOWS}
  wideChars: Array[0..MAX_PATH] Of WideChar;
{$ENDIF}
{$IFDEF LINUX}
  nt: Cint;
  b: Boolean;
{$ENDIF}
  i: integer;
  j: Integer;
Begin
  If assigned(FQueue_Add) Then Begin // Ist Redundant
{$IFDEF WINDOWS}
    // Wenn Keine Weiteren Objekte mehr Gemonitored werden können
    If fTWOHandleArray_index + 1 > high(fTWOHandleArray) Then Begin
      result := CM_MONITOR_OVERFLOW;
      exit;
    End;
    // Hinzufügen des Monitor Containers
    StringToWideChar(FQueue_Add^.Directory, @wideChars[0], MAX_PATH);
    fTWOHandleArray[fTWOHandleArray_index] := FindFirstChangeNotificationW(@wideChars[0], FQueue_Add^.Recursive, 31);
    If fTWOHandleArray[fTWOHandleArray_index] <> INVALID_HANDLE_VALUE Then Begin
      fqueue_infos[fTWOHandleArray_index] := FQueue_Add^;
      fTWOHandleArray_index := fTWOHandleArray_index + 1;
      result := CM_NO_ERROR;
    End
    Else Begin
      result := CM_INVALID_HANDLE;
    End;
{$ENDIF}
{$IFDEF LINUX}
    If instance_handle = -1 Then Begin
      result := CM_MONITOR_OVERFLOW;
    End
    Else Begin
      nt := inotify_add_watch(instance_handle, Pchar(FQueue_Add^.Directory),
        IN_MODIFY Or IN_ATTRIB Or IN_MOVE Or IN_CREATE Or
        IN_DELETE Or IN_DELETE_SELF Or IN_MOVE_SELF Or IN_DONT_FOLLOW);
      If nt < 0 Then Begin
        result := CM_MONITOR_OVERFLOW;
        exit;
      End
      Else Begin
        result := CM_NO_ERROR;
      End;
      b := true;
      For i := 0 To high(fqueue_infos) Do Begin
        // Directory is already beeing Watched
        If fqueue_infos[i].WD = nt Then Begin
          If Not FQueue_Add^.IsFile Then Begin // If we have to monitor a directory
            fqueue_infos[i].IsFile := false;
          End
          Else Begin
            // We have to monitor a File, so add it to the Filelist
            For j := 0 To high(fqueue_infos[i].FileTrigger) Do Begin
              If fqueue_infos[i].FileTrigger[j].Filename = FQueue_Add^.Filename Then Begin // The File is already monitored, skip all
                b := false;
                result := CM_FILE_ALREADY_MONITORED;
                break;
              End;
            End;
            If b Then Begin
              setlength(fqueue_infos[i].FileTrigger, high(fqueue_infos[i].FileTrigger) + 2);
              fqueue_infos[i].FileTrigger[high(fqueue_infos[i].FileTrigger)].Filename := FQueue_Add^.Filename;
              fqueue_infos[i].FileTrigger[high(fqueue_infos[i].FileTrigger)].ReportBlockedblocker := 0;
            End;
          End;
          b := false;
        End;
      End;
      If b Then Begin // The File or Directory is not watched jet
        setlength(fqueue_infos, high(fqueue_infos) + 2);
        fqueue_infos[high(fqueue_infos)].WD := nt;
        fqueue_infos[high(fqueue_infos)].Directory := FQueue_Add^.Directory;
        fqueue_infos[high(fqueue_infos)].Filename := '';
        fqueue_infos[high(fqueue_infos)].IsFile := FQueue_Add^.IsFile;
        fqueue_infos[high(fqueue_infos)].ReportBlockedblocker := 0;
        If FQueue_Add^.IsFile Then Begin
          setlength(fqueue_infos[high(fqueue_infos)].FileTrigger, 1);
          fqueue_infos[high(fqueue_infos)].FileTrigger[0].Filename := FQueue_Add^.Filename;
          fqueue_infos[high(fqueue_infos)].FileTrigger[0].ReportBlockedblocker := 0;
        End
        Else Begin
          fqueue_infos[high(fqueue_infos)].FileTrigger := Nil;
        End;
        fTWOHandleArray_index := fTWOHandleArray_index + 1; // Count all Watched Directories
      End;
      If FQueue_Add^.Recursive Then Begin
        // Todo : Add Recursive Directories
        Raise exception.create('Error feature is not implemented yet.');
      End;
      // On Adding Recursives Structure a Error Occured Undo all our work
      If result <> CM_NO_ERROR Then Begin
        inotify_rm_watch(instance_handle, nt);
        // Todo : Remove the Directory Watch by WD if not needed by other watched files, otherwise only set isFile true
      End;
    End;
{$ENDIF}
  End
  Else
    result := CM_ERROR_UNKNOWN;
End;

Function TCnMonitor.DelQueue: integer;
Var
  i: Integer;
  j: Integer;
  k: Integer;
Begin
  result := CM_ERROR_UNKNOWN;
  // suchen des Jobs
{$IFDEF WINDOWS}
  If FQueue_Del^.IsFile Then Begin
    For i := 0 To fTWOHandleArray_index - 1 Do Begin
      If (LowerCase(FQueue_Del^.Directory) = lowercase(fqueue_infos[i].Directory)) And
        (fqueue_infos[i].IsFile) And
        (LowerCase(FQueue_Del^.Filename) = lowercase(fqueue_infos[i].Filename)) Then Begin
        FindCloseChangeNotification(fTWOHandleArray[i]);
        For j := i To fTWOHandleArray_index - 2 Do Begin
          fTWOHandleArray[j] := fTWOHandleArray[j + 1];
          fqueue_infos[j] := fqueue_infos[j + 1];
        End;
        fTWOHandleArray_index := fTWOHandleArray_index - 1;
        result := CM_NO_ERROR;
        exit;
      End;
    End;
  End
  Else Begin
    For i := 0 To fTWOHandleArray_index - 1 Do Begin
      If (LowerCase(FQueue_Del^.Directory) = lowercase(fqueue_infos[i].Directory)) And
        (Not (fqueue_infos[i].IsFile)) Then Begin
        FindCloseChangeNotification(fTWOHandleArray[i]);
        For j := i To fTWOHandleArray_index - 2 Do Begin
          fTWOHandleArray[j] := fTWOHandleArray[j + 1];
          fqueue_infos[j] := fqueue_infos[j + 1];
        End;
        fTWOHandleArray_index := fTWOHandleArray_index - 1;
        result := CM_NO_ERROR;
        exit;
      End;
    End;
  End;
{$ENDIF}
{$IFDEF LINUX}
  For i := 0 To high(fqueue_infos) Do Begin
    If fqueue_infos[i].WD = FQueue_Del^.WD Then Begin
      If FQueue_Del^.Filename <> '' Then Begin // Delete a File
        For j := 0 To high(fqueue_infos[i].FileTrigger) Do Begin
          If fqueue_infos[i].FileTrigger[j].Filename = FQueue_Del^.Filename Then Begin
            For k := j To high(fqueue_infos[i].FileTrigger) - 1 Do Begin
              fqueue_infos[i].FileTrigger[k] := fqueue_infos[i].FileTrigger[k + 1];
            End;
            setlength(fqueue_infos[i].FileTrigger, high(fqueue_infos[i].FileTrigger));
          End;
        End;
      End;
      // Remove the wd if it is not needed any more
      If (high(fqueue_infos[i].FileTrigger) = -1) And ((FQueue_Del^.IsFile And fqueue_infos[i].IsFile) Or // Case 1 The Last FileWatch was deleted and the Dir is not watched
        (Not FQueue_Del^.IsFile)) Then Begin // No FileWatched and die Dir is deleted
        inotify_rm_watch(instance_handle, fqueue_infos[i].WD);
        For j := i To high(fqueue_infos) - 1 Do Begin
          fqueue_infos[j] := fqueue_infos[j + 1];
        End;
        setlength(fqueue_infos, high(fqueue_infos));
        fTWOHandleArray_index := fTWOHandleArray_index - 1;
      End;
      break;
    End;
  End;
{$ENDIF}
End;

Procedure TCnMonitor.ClearMonitors;
Var
  i: Integer;
Begin
{$IFDEF WINDOWS}
  For i := 0 To fTWOHandleArray_index - 1 Do Begin
    FindCloseChangeNotification(fTWOHandleArray[i]);
  End;
{$ENDIF}
{$IFDEF LINUX}
  If instance_handle <> -1 Then Begin
    For i := 0 To High(fqueue_infos) Do Begin
      inotify_rm_watch(instance_handle, fqueue_infos[i].WD);
      setlength(fqueue_infos[i].FileTrigger, 0);
    End;
  End;
{$ENDIF}
  fTWOHandleArray_index := 0;
End;

Function TCnMonitor.MonitorFile(Filename: String): integer;
Var
  job: PQueue;
Begin
  If FileExistsUTF8(Filename) Then Begin
    FQueueRes := CM_ERROR_UNKNOWN;
    new(job);
    job^.Directory := IncludeTrailingPathDelimiter(ExtractFilePath(Filename));
    job^.IsFile := true;
    job^.Recursive := false;
    job^.Filename := Filename;
    job^.ReportBlockedblocker := 0;
{$IFDEF LINUX}
    job^.FileTrigger := Nil;
    job^.WD := -1;
{$ENDIF}
    // From here the Thread looks for FQueue_Add and set it to nil when progressed
    FQueue_Add := job;
    While assigned(FQueue_Add) Do Begin
      sleep(1);
    End;
    result := FQueueRes;
  End
  Else Begin
    result := CM_FILE_NOT_EXISTS;
  End;
End;

Function TCnMonitor.MonitorDirectory(Directory: String;
  WithSubDirectorys: Boolean): integer;
Var
  job: PQueue;
Begin
  If DirectoryExistsUTF8(Directory) Then Begin
    FQueueRes := CM_ERROR_UNKNOWN;
    new(job);
    job^.Directory := IncludeTrailingPathDelimiter(Directory);
    job^.IsFile := false;
    job^.Recursive := WithSubDirectorys;
    job^.Filename := '';
    job^.ReportBlockedblocker := 0;
{$IFDEF LINUX}
    job^.FileTrigger := Nil;
    job^.WD := -1;
{$ENDIF}
    // From here the Thread looks for FQueue_Add and set it to nil when progressed
    FQueue_Add := job;
    While assigned(FQueue_Add) Do Begin
      sleep(1);
    End;
    result := FQueueRes;
  End
  Else Begin
    result := CM_DIR_NOT_EXISTS;
  End;
End;

Function TCnMonitor.RemoveMonitorDirectory(Directory: String): Boolean;
Var
  job: PQueue;
Begin
  FQueueRes := CM_ERROR_UNKNOWN;
  new(job);
  job^.Directory := IncludeTrailingPathDelimiter(Directory);
  job^.IsFile := false;
  job^.Recursive := false;
  job^.Filename := '';
  job^.ReportBlockedblocker := 0;
{$IFDEF LINUX}
  job^.FileTrigger := Nil;
  job^.WD := -1;
{$ENDIF}
  // From here the Thread looks for FQueue_Add and set it to nil when progressed
  FQueue_del := job;
  While assigned(FQueue_del) Do Begin
    sleep(1);
  End;
  result := FQueueRes = CM_NO_ERROR;
End;

Function TCnMonitor.RemoveMonitorFile(Filename: String): Boolean;
Var
  job: PQueue;
Begin
  FQueueRes := CM_ERROR_UNKNOWN;
  new(job);
  job^.Directory := ExtractFilePath(Filename);
  job^.IsFile := true;
  job^.Recursive := false;
  job^.Filename := Filename;
  job^.ReportBlockedblocker := 0;
{$IFDEF LINUX}
  job^.FileTrigger := Nil;
  job^.WD := -1;
{$ENDIF}
  // From here the Thread looks for FQueue_Add and set it to nil when progressed
  FQueue_del := job;
  While assigned(FQueue_del) Do Begin
    sleep(1);
  End;
  result := FQueueRes = CM_NO_ERROR;
End;

Procedure TCnMonitor.Clear;
Begin
  FqueueClear := true;
  While FqueueClear Do // Will be cleared by the thread
    sleep(1);
End;

Constructor TCnMonitor.Create;
Begin
  Inherited create(false);
  fdeactivated := false;
  fneeddeactivated := 0;
  fTWOHandleArray_index := 0;
  FQueue_Add := Nil;
  FQueue_Del := Nil;
  OnDirectoryChangeEvent := Nil;
  OnFileChangeEvent := Nil;
{$IFDEF LINUX}
  instance_handle := inotify_init();
{$ENDIF}
End;

Destructor TCnMonitor.Destroy;
Var
  i: Integer;
Begin
  Terminate;
  While Not Terminated Do Begin
    Sleep(1);
  End;
  // Should never happen
  If assigned(FQueue_Add) Then
    Dispose(FQueue_Add);
  If assigned(FQueue_del) Then
    Dispose(FQueue_del);
{$IFDEF WINDOWS}
  For i := 0 To fTWOHandleArray_index - 1 Do Begin
    FindCloseChangeNotification(fTWOHandleArray[i]);
  End;
{$ENDIF}
{$IFDEF LINUX}
  If instance_handle <> -1 Then Begin
    For i := 0 To High(fqueue_infos) Do Begin
      inotify_rm_watch(instance_handle, fqueue_infos[i].WD);
      setlength(fqueue_infos[i].FileTrigger, 0);
    End;
    FpClose(instance_handle);
  End;
{$ENDIF}
  Inherited destroy;
End;

Procedure TCnMonitor.DeActivate;
Begin
  While fneeddeactivated <> 0 Do Begin
    sleep(1);
  End;
  fneeddeactivated := 1;
  While fneeddeactivated <> 0 Do Begin
    sleep(1);
  End;
End;

Procedure TCnMonitor.Activate;
Begin
  While fneeddeactivated <> 0 Do Begin
    sleep(1);
  End;
  fneeddeactivated := 2;
  While fneeddeactivated <> 0 Do Begin
    sleep(1);
  End;
End;

End.
