unit uDiscoverDifferences;
{$INCLUDE 'DirSync3.inc'}

interface

uses
  Windows, SysUtils, Classes,
  {$IFDEF FAR3} Plugin3, {$ELSE} PluginW, {$ENDIF} FarKeysW, FarColor,
  uSystem, uFiles, uFAR, uLog,
  uDirSyncConsts, uDirSync, uMessages, uOptions, uFileInfo, uDifferences,
  uSynchronizeDirectories,
  uDiscoverDifferencesDialog;

type
  TDiscoverDifferences = class
  private
    fLeftPanel: TFarPanelInfo;
    fRightPanel: TFarPanelInfo;
    fOptions: TDirSyncOptions;
    fSyncTask: TSynchronizeTask;
    fDiffFile: TTextStream;
    fCompareBufferLeft: AnsiString;
    fCompareBufferRight: AnsiString;
    fProgress: TFarProgress;
  protected
    property LeftPanel: TFarPanelInfo read fLeftPanel;
    property RightPanel: TFarPanelInfo read fRightPanel;
    property Options: TDirSyncOptions read fOptions;
  protected
    procedure PreparePanels;
    procedure CleanupDiffFile;
    function ShowMainDialog: boolean;
    procedure ShowProgress(const LeftFileName, RightFileName: string; Progress, Max: int64; Msg1, Msg2: TMessages);
    function ShowResults(DifferencesFound: boolean): boolean;
    procedure ComparePanels(ALeft, ARight: TFarPanelInfo; MaxDepth: integer; SelectedOnly: boolean; var DifferencesFound: boolean);
    procedure CompareItems(ALeft, ARight: TFileInfoList; const ALeftRoot, ARightRoot, ADir: string; MaxDepth: integer; var DifferencesFound: boolean);
    procedure CompareItem(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; MaxDepth: integer; var DifferencesFound: boolean);
    procedure CompareDirectories(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; MaxDepth: integer; var DifferencesFound: boolean);
    procedure CompareFiles(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; var DifferencesFound: boolean);
    function CompareFiles_BySize(ALeft, ARight: TBaseFileInfo): boolean;
    function CompareFiles_ByDate(ALeft, ARight: TBaseFileInfo): boolean;
    function CompareFiles_ByContent(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; out GotErrors: boolean): boolean;
    function CompareFiles_ByContent_NoIgnore(ALeft, ARight: TBaseFileInfo; const AFileNameLeft, AFileNameRight: string; AFileLeft, AFileRight: THandle; out GotErrors: boolean): boolean;
    function CompareFiles_ByContent_Ignore(ALeft, ARight: TBaseFileInfo; const AFileNameLeft, AFileNameRight: string; AFileLeft, AFileRight: THandle; out GotErrors: boolean): boolean;
    procedure IsValidPanelItem(Sender: TObject; PanelItem: TFarPanelItem; UserData: Pointer; var Accept: boolean);
    procedure WriteDifference(DifferenceType: TDifferenceType; UsedMatch: TUsedMatch; const Dir, FileName: string; IsDirectory: boolean);
    procedure EscapePressedEvent(Sender: TObject; var Value: boolean);
  public
    constructor Create;
    destructor Destroy; override;
    function Execute: THandle;
  end;

implementation

const
  COMPARE_BUFFER_SIZE = 4096; //65536;

{ TDirSync }

procedure TDiscoverDifferences.CleanupDiffFile;
begin
  FreeAndNil(fDiffFile);
  FreeAndNil(fSyncTask);
end;

procedure TDiscoverDifferences.CompareDirectories(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; MaxDepth: integer; var DifferencesFound: boolean);
var
  DirLeft, DirRight: TFileInfoList;
  Different: boolean;
begin
  if MaxDepth > 0 then begin
    DirLeft := TFileInfoList.Create(True);
    try
      DirRight := TFileInfoList.Create(True);
      try
        DirLeft.LoadFromDirectory(ALeftRoot + ADir + AFileName, nil, nil);
        DirRight.LoadFromDirectory(ARightRoot + ADir + AFileName, nil, nil);
        //Log(' - %d vs %d files', [DirLeft.Count, DirRight.Count]);
        Different := False;
        CompareItems(DirLeft, DirRight, ALeftRoot, ARightRoot, IncludeTrailingPathDelimiter(ADir + AFileName), Pred(MaxDepth), Different);
        if Different then begin
          DifferencesFound := True;
          ALeft.Selected := True;
          ARight.Selected := True;
        end;
      finally
        FreeAndNil(DirRight);
      end;
    finally
      FreeAndNil(DirLeft);
    end;
  end;
end;

procedure TDiscoverDifferences.CompareFiles(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; var DifferencesFound: boolean);

  procedure FilesAreDifferent(UsedMatch: TUsedMatch);
    begin
      //Log('- Different file "%s%s" in "%s" and "%s"', [ADir, AFileName, ALeftRoot, ARightRoot]);
      if ALeft.FileDate = ARight.FileDate then begin
        WriteDifference(dtDifferent, UsedMatch, ADir, AFileName, False);
        ALeft.Selected := True;
        ARight.Selected := True;
      end
      else if ALeft.FileDate < ARight.FileDate then begin
        WriteDifference(dtOlderLeft, UsedMatch, ADir, AFileName, False);
        ARight.Selected := True;
      end
      else begin
        WriteDifference(dtOlderRight, UsedMatch, ADir, AFileName, False);
        ALeft.Selected := True;
      end;
      if Options.SelectDifferencesOnBothPanels then begin
        ALeft.Selected := True;
        ARight.Selected := True;
      end;
      DifferencesFound := True;
    end;

  procedure ErrorReadingFiles(UsedMatch: TUsedMatch);
    begin
      //Log('- Error reading file "%s%s" in "%s" and "%s"', [ADir, AFileName, ALeftRoot, ARightRoot]);
      WriteDifference(dtDifferent, UsedMatch, ADir, AFileName, False);
      ALeft.Selected := True;
      ARight.Selected := True;
    end;

var
  GotErrors: boolean;
begin
  //Log('Comparing file "%s%s" in "%s" and "%s"', [ADir, AFileName, ALeftRoot, ARightRoot]);
  if not CompareFiles_BySize(ALeft, ARight) then
    FilesAreDifferent(umSize)
  else if not CompareFiles_ByDate(ALeft, ARight) then
    FilesAreDifferent(umWriteTime)
  else if not CompareFiles_ByContent(ALeft, ARight, ALeftRoot, ARightRoot, ADir, AFileName, GotErrors) then
    if GotErrors then
      ErrorReadingFiles(umContent)
    else
      FilesAreDifferent(umContent);
end;

function TDiscoverDifferences.CompareFiles_ByContent(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; out GotErrors: boolean): boolean;
var
  FullFileNameLeft, FullFileNameRight: string;
  FileLeft, FileRight: THandle;
begin
  //Log('Comparing "%s%s" by content in "%s" and "%s"', [ADir, AFileName, ALeftRoot, ARightRoot]);
  GotErrors := False;
  //Result := True;
  if Options.CompareContents then begin
    //Log('- Comparing by contents');
    FullFileNameLeft := ALeftRoot + ADir + AFileName;
    FullFileNameRight := ARightRoot + ADir + AFileName;
    ShowProgress(FullFileNameLeft, FullFileNameRight, 0, 0, MComparing, MComparingWith);
    FileLeft := CreateFile(PChar(FullFileNameLeft), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
    try
      FileRight := CreateFile(PChar(FullFileNameRight), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
      try
        if (FileLeft = INVALID_HANDLE_VALUE) or (FileRight = INVALID_HANDLE_VALUE) then begin
          GotErrors := True;
          Result := False;
        end
        else
          if not Options.CompareContentsIgnore then
            Result := CompareFiles_ByContent_NoIgnore(ALeft, ARight, FullFileNameLeft, FullFileNameRight, FileLeft, FileRight, GotErrors)
          else
            Result := CompareFiles_ByContent_Ignore(ALeft, ARight, FullFileNameLeft, FullFileNameRight, FileLeft, FileRight, GotErrors);
      finally
        if FileRight <> INVALID_HANDLE_VALUE then
          CloseHandle(FileRight);
      end;
    finally
      if FileLeft <> INVALID_HANDLE_VALUE then
        CloseHandle(FileLeft);
    end;
  end
  else
    Result := True;
  //Log('- Result = %d, GotErrors = %d', [Integer(Result), Integer(GotErrors)]);
end;

function TDiscoverDifferences.CompareFiles_ByContent_Ignore(ALeft, ARight: TBaseFileInfo; const AFileNameLeft, AFileNameRight: string; AFileLeft, AFileRight: THandle; out GotErrors: boolean): boolean;

  function UpdateBuffer(AFile: THandle; var Buffer: AnsiString; var BufferSize, Position: DWORD; var Progress: int64): boolean;
    begin
      if (Position > BufferSize) and (BufferSize <> 0) then
        if not ReadFile(AFile, Buffer[1], COMPARE_BUFFER_SIZE, BufferSize, nil) then
          Result := False
        else begin
          Position := 1;
          Inc(Progress, BufferSize);
          Result := True;
        end
      else
        Result := True;
    end;

  function IsWhiteSpace(C: AnsiChar): boolean;
    begin
      Result := C in [#9, #10, #11, #12, #13, #32];
    end;

  function IsNewLine(C: AnsiChar): boolean;
    begin
      Result := C in [#10, #13];
    end;

  procedure HandleWhiteSpace(var Buffer: AnsiString; var BufferSize, Position: DWORD);
    begin
      while (Position <= BufferSize) and IsWhiteSpace(Buffer[Position]) do
        Inc(Position);
    end;

  procedure HandleExpectedNewLine(var Buffer: AnsiString; var BufferSize, Position: DWORD; var ExpectNewLine: boolean);
    begin
      if ExpectNewLine then begin
        ExpectNewLine := False;
        if (Position <= BufferSize) and (Buffer[Position] = #10) then
          Inc(Position);
      end;
    end;

var
  BufferSizeLeft, BufferSizeRight, PositionLeft, PositionRight: DWORD;
  ExpectNewLineLeft, ExpectNewLineRight: boolean;
  Progress, Max: int64;
begin
  Result := False;
  BufferSizeLeft := 1;
  BufferSizeRight := 1;
  PositionLeft := Succ(BufferSizeLeft);
  PositionRight := Succ(BufferSizeRight);
  ExpectNewLineLeft := False;
  ExpectNewLineRight := False;
  SetLength(fCompareBufferLeft, COMPARE_BUFFER_SIZE);
  SetLength(fCompareBufferRight, COMPARE_BUFFER_SIZE);
  Progress := 0;
  Max := ALeft.FileSize + ARight.FileSize;
  repeat
    ShowProgress(AFileNameLeft, AFileNameRight, Progress, Max, MComparing, MComparingWith);
    // Read more data
    if (not UpdateBuffer(AFileLeft, fCompareBufferLeft, BufferSizeLeft, PositionLeft, Progress)) or (not UpdateBuffer(AFileRight, fCompareBufferRight, BufferSizeRight, PositionRight, Progress)) then begin
      GotErrors := True;
      Exit;
    end
    // If both buffers ended, comparison was successful. If only one buffer ended, it was unsuccessful
    else if (BufferSizeLeft = 0) and (BufferSizeRight = 0) then begin
      Result := True;
      Exit;
    end
    // If IgnoreWhitespace is on
    else if Options.IgnoreWhitespace then begin
      // Compare all non-whitespace characters left in buffer
      while True
            and (PositionLeft <= BufferSizeLeft)
            and (PositionRight <= BufferSizeRight)
            and (not IsWhiteSpace(fCompareBufferLeft[PositionLeft]))
            and (not IsWhiteSpace(fCompareBufferRight[PositionRight]))
      do
        if (fCompareBufferLeft[PositionLeft] <> fCompareBufferRight[PositionRight]) then
          Exit
        else begin
          Inc(PositionLeft);
          Inc(PositionRight);
        end;
      // Skip any whitespaces in eiher buffer
      HandleWhiteSpace(fCompareBufferLeft, BufferSizeLeft, PositionLeft);
      HandleWhiteSpace(fCompareBufferRight, BufferSizeRight, PositionRight);
      // And start again
    end
    // If IgnoreNewLines is on
    else begin
      // Finalize eventual CRLF
      HandleExpectedNewLine(fCompareBufferLeft, BufferSizeLeft, PositionLeft, ExpectNewLineLeft);
      HandleExpectedNewLine(fCompareBufferRight, BufferSizeRight, PositionRight, ExpectNewLineRight);
      // Compare all non-newline characters left in buffer
      while True
            and (PositionLeft <= BufferSizeLeft)
            and (PositionRight <= BufferSizeRight)
            and (not IsNewLine(fCompareBufferLeft[PositionLeft]))
            and (not IsNewLine(fCompareBufferRight[PositionRight]))
      do
        if (fCompareBufferLeft[PositionLeft] <> fCompareBufferRight[PositionRight]) then
          Exit
        else begin
          Inc(PositionLeft);
          Inc(PositionRight);
        end;
      // If both buffers have data left and one of them contains non-newline character, it's a mismatch
      if True
            and (PositionLeft <= BufferSizeLeft)
            and (PositionRight <= BufferSizeRight)
            and ((not IsNewLine(fCompareBufferLeft[PositionLeft])) or (not IsNewLine(fCompareBufferRight[PositionRight])))
      then
        Exit;
      // If I got here, I certainly have a newline character in both buffers. I will skip it,
      // and if it was a CR, mark it to skip the following LF.
      if (PositionLeft <= BufferSizeLeft) and (PositionRight <= BufferSizeRight) then begin
        if fCompareBufferLeft[PositionLeft] = #13 then
          ExpectNewLineLeft := True;
        if fCompareBufferRight[PositionRight] = #13 then
          ExpectNewLineRight := True;
        Inc(PositionLeft);
        Inc(PositionRight);
      end;
    end;
    // If one of the files ended and the other didn't, mismatch
    if ((PositionLeft <= BufferSizeLeft) and (BufferSizeRight = 0) and (not ExpectNewLineLeft)) or ((PositionRight <= BufferSizeRight) and (BufferSizeLeft = 0) and (not ExpectNewLineRight)) then
      Exit;
  until False;
end;

function TDiscoverDifferences.CompareFiles_ByContent_NoIgnore(ALeft, ARight: TBaseFileInfo; const AFileNameLeft, AFileNameRight: string; AFileLeft, AFileRight: THandle; out GotErrors: boolean): boolean;
var
  Progress, Max: int64;
  BytesReadLeft, BytesReadRight: DWORD;
begin
  Result := False;
  Progress := 0;
  Max := ALeft.FileSize + ARight.FileSize;
  repeat
    ShowProgress(AFileNameLeft, AFileNameRight, Progress, Max, MComparing, MComparingWith);
    SetLength(fCompareBufferLeft, COMPARE_BUFFER_SIZE);
    SetLength(fCompareBufferRight, COMPARE_BUFFER_SIZE);
    if False
       or (not ReadFile(AFileLeft,  fCompareBufferLeft[1],  COMPARE_BUFFER_SIZE, BytesReadLeft,  nil))
       or (not ReadFile(AFileRight, fCompareBufferRight[1], COMPARE_BUFFER_SIZE, BytesReadRight, nil))
    then begin
      GotErrors := True;
      Exit;
    end
    else if BytesReadLeft <> BytesReadRight then begin
      Exit;
    end
    else if BytesReadLeft = 0 then begin
      Result := True;
      Exit;
    end
    else begin
      SetLength(fCompareBufferLeft, BytesReadLeft);
      SetLength(fCompareBufferRight, BytesReadRight);
      if fCompareBufferLeft <> fCompareBufferRight then begin
        Exit;
      end;
    end;
    Inc(Progress, BytesReadLeft);
    Inc(Progress, BytesReadRight);
  until False;
end;

function TDiscoverDifferences.CompareFiles_ByDate(ALeft, ARight: TBaseFileInfo): boolean;
var
  Precision, TimeDiff: TDateTime;
  HoursDiff: integer;
begin
  if Options.CompareTime then
    if Options.LowPrecisionTime or Options.IgnorePossibleTimeZoneDifferences then begin
      if Options.LowPrecisionTime then
        Precision := 2/(24*60*60)
      else
        Precision := 0;
      TimeDiff := ALeft.FileDate - ARight.FileDate;
      if TimeDiff < 0 then
        TimeDiff := -TimeDiff;
      if Options.IgnorePossibleTimeZoneDifferences then
        HoursDiff := Trunc(TimeDiff * 24 + 0.5)
      else
        HoursDiff := 0;
      Result := Abs(TimeDiff - HoursDiff/24) <= Precision;
    end
    else
      Result := ALeft.FileDate = ARight.FileDate
  else
    Result := True;
end;

function TDiscoverDifferences.CompareFiles_BySize(ALeft, ARight: TBaseFileInfo): boolean;
begin
  Result := (not Options.CompareSize) or (ALeft.FileSize = ARight.FileSize);
end;

procedure TDiscoverDifferences.CompareItem(ALeft, ARight: TBaseFileInfo; const ALeftRoot, ARightRoot, ADir, AFileName: string; MaxDepth: integer; var DifferencesFound: boolean);
begin
  // Left is directory
  if (ALeft.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
    // Right is directory too
    if (ARight.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
      CompareDirectories(ALeft, ARight, ALeftRoot, ARightRoot, ADir, AFileName, MaxDepth, DifferencesFound)
    // Left is directory, Right is not - Left wins
    else begin
      WriteDifference(dtOlderRight, umFileName, ADir, AFileName, True);
      ALeft.Selected := True;
      DifferencesFound := True;
    end
  // Right is directory, Left is not - Right wins
  else if (ARight.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then begin
    WriteDifference(dtOlderLeft, umFileName, ADir, AFileName, True);
    ARight.Selected := True;
    DifferencesFound := True;
  end
  // Both are files
  else begin
    CompareFiles(ALeft, ARight, ALeftRoot, ARightRoot, ADir, AFileName, DifferencesFound);
  end;
end;

procedure TDiscoverDifferences.CompareItems(ALeft, ARight: TFileInfoList; const ALeftRoot, ARightRoot, ADir: string; MaxDepth: integer; var DifferencesFound: boolean);
var
  LeftIndex, RightIndex, Match: integer;
  LeftItem, RightItem: TBaseFileInfo;
begin
  // Will compare sorted lists
  ALeft.SortByFileNameDescending;
  ARight.SortByFileNameDescending;
  LeftIndex := Pred(ALeft.Count);
  RightIndex := Pred(ARight.Count);
  // While there is something to compare
  while (LeftIndex >= 0) and (RightIndex >= 0) do begin
    LeftItem := ALeft[LeftIndex];
    RightItem := ARight[RightIndex];
    // Remove selection
    LeftItem.Selected := False;
    RightItem.Selected := False;
    // Display progress and check for ESCAPE
    ShowProgress(ADir + LeftItem.FileName, ADir + RightItem.FileName, 0, 0, MComparing, MComparingWith);
    // Check whether the filenames match
    Match := AnsiCompareText(LeftItem.FileName, RightItem.FileName);
    // "Right filename > Left filename" means that there is no file for the right filename in the left panel
    if Match > 0 then begin
      WriteDifference(dtMissingLeft, umFileName, ADir, RightItem.FileName, (RightItem.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0);
      RightItem.Selected := True;
      Dec(RightIndex);
      DifferencesFound := True;
    end
    // "Left filename > Right filename" means that there is no file for the left filename in the right panel
    else if Match < 0 then begin
      WriteDifference(dtMissingRight, umFileName, ADir, LeftItem.FileName, (LeftItem.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0);
      LeftItem.Selected := True;
      Dec(LeftIndex);
      DifferencesFound := True;
    end
    // Otherwise the files may or may not match
    else begin
      CompareItem(LeftItem, RightItem, ALeftRoot, ARightRoot, ADir, LeftItem.FileName, MaxDepth, DifferencesFound);
      Dec(LeftIndex);
      Dec(RightIndex);
    end;
  end;
  // Files remaining on the left
  while LeftIndex >= 0 do begin
    LeftItem := ALeft[LeftIndex];
    WriteDifference(dtMissingRight, umFileName, ADir, LeftItem.FileName, (LeftItem.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0);
    LeftItem.Selected := True;
    Dec(LeftIndex);
    DifferencesFound := True;
  end;
  // Files remaining on the right
  while RightIndex >= 0 do begin
    RightItem := ARight[RightIndex];
    WriteDifference(dtMissingLeft, umFileName, ADir, RightItem.FileName, (RightItem.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0);
    RightItem.Selected := True;
    Dec(RightIndex);
    DifferencesFound := True;
  end;
end;

procedure TDiscoverDifferences.ComparePanels(ALeft, ARight: TFarPanelInfo; MaxDepth: integer; SelectedOnly: boolean; var DifferencesFound: boolean);

  procedure KeepOrDeleteItem(Items: TFileInfoList; var Index: integer; Keep: boolean);
    begin
      if Keep then
        Inc(Index)
      else
        Items.Delete(Index);
    end;

var
  LeftItems, RightItems: TFileInfoList;
  LeftRoot, RightRoot: string;
  LeftIndex, RightIndex, Cmp: integer;
  Keep: boolean;
begin
  LeftRoot := IncludeTrailingPathDelimiter(TFarUtils.FullFileName(ALeft.Directory, True));
  RightRoot := IncludeTrailingPathDelimiter(TFarUtils.FullFileName(ARight.Directory, True));
  ShowProgress(LeftRoot, RightRoot + '*', 0, 0, MComparing, MComparingWith);
  LeftItems := TFileInfoList.Create(True);
  try
    RightItems := TFileInfoList.Create(True);
    try
      LeftItems.LoadFromPanel(ALeft{, IsValidPanelItem, Pointer(SelectedOnly)});
      RightItems.LoadFromPanel(ARight{, IsValidPanelItem, Pointer(SelectedOnly)});
      LeftItems.SortByFileName;
      RightItems.SortByFileName;
      if SelectedOnly then begin
        LeftIndex := 0;
        RightIndex := 0;
        while (LeftIndex < LeftItems.Count) or (RightIndex < RightItems.Count) do begin
          // Platny levy zaznam, neplatny pravy zaznam -> Zachovat levy, pokud je selected, jinak smazat
          // Platny pravy zaznam, neplatny levy zaznam -> Zachovat pravy, pokud je selected, jinak smazat
          // Platne oba zaznamy
          //   a) Stejne jmeno -> Zachovat oba, pokud ma aspon jeden selected, jinak oba smazat
          //   b) Ruzne jmeno -> Vybrat si ten, ktery ma mensi jmeno; zachovat ho, pokud je selected, jinak smazat
          if not (LeftIndex < LeftItems.Count) then begin
            KeepOrDeleteItem(RightItems, RightIndex, RightItems[RightIndex].Selected);
          end
          else if not (RightIndex < RightItems.Count) then begin
            KeepOrDeleteItem(LeftItems, LeftIndex, LeftItems[LeftIndex].Selected);
          end
          else begin
            Cmp := AnsiCompareText(LeftItems[LeftIndex].FileName, RightItems[RightIndex].FileName);
            if Cmp = 0 then begin
              Keep := LeftItems[LeftIndex].Selected or RightItems[RightIndex].Selected;
              KeepOrDeleteItem(LeftItems, LeftIndex, Keep);
              KeepOrDeleteItem(RightItems, RightIndex, Keep);
            end
            else if Cmp < 0 then begin
              KeepOrDeleteItem(LeftItems, LeftIndex, LeftItems[LeftIndex].Selected);
            end
            else begin
              KeepOrDeleteItem(RightItems, RightIndex, RightItems[RightIndex].Selected);
            end;
          end;
        end;
      end;
      CompareItems(LeftItems, RightItems, LeftRoot, RightRoot, '', MaxDepth, DifferencesFound);
      ALeft.ApplySelection;
      ARight.ApplySelection;
    finally
      FreeAndNil(RightItems);
    end;
  finally
    FreeAndNil(LeftItems);
  end;
end;

constructor TDiscoverDifferences.Create;
begin
  inherited Create;
  fOptions := TDirSyncOptions.Create;
  fProgress := nil;
end;

destructor TDiscoverDifferences.Destroy;
begin
  FreeAndNil(fProgress);
  FreeAndNil(fDiffFile);
  FreeAndNil(fLeftPanel);
  FreeAndNil(fRightPanel);
  FreeAndNil(fOptions);
  inherited;
end;

procedure TDiscoverDifferences.EscapePressedEvent(Sender: TObject; var Value: boolean);
begin
  Value := FarMessage([MEscTitle, MEscBody, MOk, MCancel], FMSG_WARNING, 2) = 0;
end;

function TDiscoverDifferences.Execute: THandle;
var
  DifferencesFound: boolean;
  MaxDepth: integer;
begin
  Result := {$IFDEF FAR3_UP} 0 {$ELSE} INVALID_HANDLE_VALUE {$ENDIF} ;
  try
    PreparePanels;
    if (LeftPanel.PanelType <> PTYPE_FILEPANEL) or (RightPanel.PanelType <> PTYPE_FILEPANEL) then
      FarMessage([MTitle, MFilePanelsRequired, MOK], FMSG_WARNING, 1)
    else begin
      Options.Init;
      Options.Load;
      if ShowMainDialog then begin
        Options.Save;
        fProgress := TFarProgress.Create( {$IFDEF FAR3_UP} PLUGIN_GUID, DIALOG_READING_GUID {$ELSE} FarApi.ModuleNumber {$ENDIF} );
        try
          fProgress.CheckForEscape := True;
          fProgress.OnEscapePressed := EscapePressedEvent;
          try
            fSyncTask := TSynchronizeTask.Create(GetVeryLongFileName(LeftPanel.Directory), GetVeryLongFileName(RightPanel.Directory));
            fDiffFile := TTextStream.Create(fSyncTask.FileName, fmOpenReadWrite or fmShareDenyWrite);
            fDiffFile.Position := fDiffFile.Size;
            DifferencesFound := False;
            if Options.ProcessSubfolders then
              if Options.UseMaxScanDepth then
                MaxDepth := Options.MaxScanDepth
              else
                MaxDepth := MaxInt
            else
              MaxDepth := 0;
            TFarUtils.SetProgressState( {$IFDEF FAR3_UP} TBPS_INDETERMINATE {$ELSE} PS_INDETERMINATE {$ENDIF} );
            ComparePanels(LeftPanel, RightPanel, MaxDepth, Options.ProcessSelected, DifferencesFound);
            TFarUtils.SetProgressState( {$IFDEF FAR3_UP} TBPS_NOPROGRESS {$ELSE} PS_NOPROGRESS {$ENDIF} );
            FreeAndNil(fDiffFile);
            FreeAndNil(fProgress);
            //Log('Differences found: %d', [Integer(DifferencesFound)]);
            if ShowResults(DifferencesFound) then
              // If the diff file was shown in the editor, I don't want to clean it up here, but rather when the editor closes and synchronization finishes. That way I won't bother the user with the "file changed. save?" dialog.
              fSyncTask := nil;
          finally
            CleanupDiffFile;
          end;
        finally
          FreeAndNil(fProgress);
        end;
      end;
    end;
  except
    on E: EAbort do
      ; //Log('Aborted');
    on E: Exception do
      FarMessage([MTitle, MExtraString1, MExtraString2, MOk], [PFarChar(E.ClassName), PFarChar(E.Message)], FMSG_ERRORTYPE or FMSG_WARNING, 1);
    end;
end;

procedure TDiscoverDifferences.IsValidPanelItem(Sender: TObject; PanelItem: TFarPanelItem; UserData: Pointer; var Accept: boolean);
begin
  Accept := True
      and (Options.ProcessSubfolders or ((PanelItem.FileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0))
      and ((not Boolean(UserData)) or PanelItem.Selected)
      and (PanelItem.FileName <> '.')
      and (PanelItem.FileName <> '..')
      ;
end;

procedure TDiscoverDifferences.PreparePanels;
var
  ActivePanel, PassivePanel: TFarPanelInfo;
begin
  // Load the left and the right panel
  FreeAndNil(fLeftPanel);
  FreeAndNil(fRightPanel);
  ActivePanel := TFarPanelInfo.Create(True);
  PassivePanel := TFarPanelInfo.Create(False);
  if ActivePanel.Left then begin
    fLeftPanel := ActivePanel;
    fRightPanel := PassivePanel;
  end
  else begin
    fLeftPanel := PassivePanel;
    fRightPanel := ActivePanel;
  end;
  //Log('Left panel:');
  //LogPanel(LeftPanel);
  //Log('Right panel:');
  //LogPanel(RightPanel);
end;

function TDiscoverDifferences.ShowMainDialog: boolean;
var
  Dialog: TDiscoverDifferencesDialog;
begin
  Dialog := TDiscoverDifferencesDialog.Create;
  try
    Dialog.Options := Options;
    Dialog.LeftPanel := LeftPanel;
    Dialog.RightPanel := RightPanel;
    Result := Dialog.Execute;
  finally
    FreeAndNil(Dialog);
    end;
end;

procedure TDiscoverDifferences.ShowProgress(const LeftFileName, RightFileName: string; Progress, Max: int64; Msg1, Msg2: TMessages);
begin
  if fProgress.NeedsRefresh then begin
    fProgress.Show(Integer(MTitle), Integer(Msg1), Integer(Msg2), LeftFileName, RightFileName, Progress, Max);
  end;
end;

function TDiscoverDifferences.ShowResults(DifferencesFound: boolean): boolean;
var
  ReturnToEditor: boolean;
begin
  Result := False;
  if (not DifferencesFound) then begin
    FarMessage([MNoDiffTitle, MNoDiffBody, MOk], FMSG_NONE, 1);
  end
  else begin
    if fSyncTask <> nil then begin
      Result := fSyncTask.ShowEditor(True);
      if not Result then
        try
          fSyncTask.Execute(ReturnToEditor);
        finally
          FreeAndNil(fSyncTask);
        end;
    end;
  end;
end;

procedure TDiscoverDifferences.WriteDifference(DifferenceType: TDifferenceType; UsedMatch: TUsedMatch; const Dir, FileName: string; IsDirectory: boolean);
var
  DiffItem: TDifferenceItem;
begin
  //Log('Difference %s for "%s%s"', [DifferenceTypes[DifferenceType], Dir, FileName]);
  if DifferenceType <> dtEqual then begin
    DiffItem := TDifferenceItem.Create;
    try
      DiffItem.FileName := Dir + FileName;
      DiffItem.IsDirectory := IsDirectory;
      DiffItem.DifferenceType := DifferenceType;
      DiffItem.UsedMatch := UsedMatch;
      fDiffFile.WriteLine(DiffItem.AsString);
    finally
      FreeAndNil(DiffItem);
    end;
  end;
end;

end.
