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

interface

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

type
  TDiscoverDifferences = class
  private
    fLeftPanel: TFarPanelInfo;
    fRightPanel: TFarPanelInfo;
    fOptions: TDirSyncOptions;
    fDiffFileName: string;
    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;
    function PrepareDiffFile: boolean;
    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);
  if fDiffFileName <> '' then begin
    if FileExists(fDiffFileName) then
      DeleteFile(fDiffFileName);
    fDiffFileName := '';
  end;
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;
      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);
var
  LeftItems, RightItems: TFileInfoList;
  LeftRoot, RightRoot: string;
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));
      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, InEditor: boolean;
  MaxDepth: integer;
begin
  Result := {$IFDEF FAR3} 0 {$ELSE} INVALID_HANDLE_VALUE {$ENDIF} ;
  try
    PreparePanels;
    if (LeftPanel.PanelType <> PTYPE_FILEPANEL) or (RightPanel.PanelType <> PTYPE_FILEPANEL) then
      FarMessage([MCmpTitle, MFilePanelsRequired, MOK], FMSG_WARNING, 1)
    else begin
      Options.Init;
      Options.Load;
      if ShowMainDialog then begin
        Options.Save;
        fProgress := TFarProgress.Create( {$IFDEF FAR3} PLUGIN_GUID, DIALOG_READING_GUID {$ELSE} FarApi.ModuleNumber {$ENDIF} );
        try
          fProgress.CheckForEscape := True;
          fProgress.OnEscapePressed := EscapePressedEvent;
          if PrepareDiffFile then begin
            InEditor := False;
            try
              DifferencesFound := False;
              if Options.ProcessSubfolders then
                if Options.UseMaxScanDepth then
                  MaxDepth := Options.MaxScanDepth
                else
                  MaxDepth := MaxInt
              else
                MaxDepth := 0;
              ComparePanels(LeftPanel, RightPanel, MaxDepth, Options.ProcessSelected, DifferencesFound);
              FreeAndNil(fDiffFile);
              FreeAndNil(fProgress);
              //Log('Differences found: %d', [Integer(DifferencesFound)]);
              InEditor := ShowResults(DifferencesFound);
            finally
              // 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.
              if InEditor then
                FreeAndNil(fDiffFile)
              else begin
                SyncTasks.DeleteByFileName(fDiffFileName);
                CleanupDiffFile;
              end;
            end;
          end
          else
            FarMessage([MCmpTitle, MCreateTempFailed, MOK], FMSG_WARNING, 1);
        finally
          FreeAndNil(fProgress);
        end;
      end;
    end;
  except
    on E: EAbort do
      ; //Log('Aborted');
    on E: Exception do
      //TFarUtils.ShowMessage([GetMsg(MCmpTitle), PChar(E.ClassName), PChar(E.Message), GetMsg(MOk)], FMSG_ERRORTYPE, 1);
      FarMessage([MCmpTitle, MExtraString1, MExtraString2, MOk], [PFarChar(E.ClassName), PFarChar(E.Message)], FMSG_ERRORTYPE, 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;

function TDiscoverDifferences.PrepareDiffFile: boolean;
var
  n: integer;
begin
  Result := False;
  SetLength(fDiffFileName, MAX_PATH);
  n := FarApi.FSF.MkTemp(PChar(fDiffFileName), Length(fDiffFileName), nil);
  if n = 0 then
    fDiffFileName := ''
  else begin
    SetLength(fDiffFileName, n);
    fDiffFile := TTextStream.Create(fDiffFileName, fmCreate);
    try
      fDiffFile.WriteLine(Format(GetMsgPas(MReportFileTitle), [LeftPanel.Directory, RightPanel.Directory]));
      fDiffFile.WriteLine(Format('%s%s', [GetMsgPas(MReportFileNotes1), GetMsgPas(MReportFileNotes2)]));
      fDiffFile.WriteLine('');
      fDiffFile.WriteLine('');
      SyncTasks.Add(0, fDiffFileName, TFarUtils.FullFileName(LeftPanel.Directory, True), TFarUtils.FullFileName(RightPanel.Directory, True));
      Result := True;
    except
      FreeAndNil(fDiffFile);
      Raise;
    end;
  end;
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(MCmpTitle), Integer(Msg1), Integer(Msg2), LeftFileName, RightFileName, Progress, Max);
  end;
end;

function TDiscoverDifferences.ShowResults(DifferencesFound: boolean): boolean;
{$IFDEF FAR3}
const
  KEY_CTRL_F1: TFarKey = (VirtualKeyCode: VK_F1; ControlKeyState: LEFT_CTRL_PRESSED or RIGHT_CTRL_PRESSED);
  KEY_CTRL_F2: TFarKey = (VirtualKeyCode: VK_F2; ControlKeyState: LEFT_CTRL_PRESSED or RIGHT_CTRL_PRESSED);
  KEY_ALT_F1: TFarKey = (VirtualKeyCode: VK_F1; ControlKeyState: LEFT_ALT_PRESSED or RIGHT_ALT_PRESSED);
  KEY_ALT_F2: TFarKey = (VirtualKeyCode: VK_F2; ControlKeyState: LEFT_ALT_PRESSED or RIGHT_ALT_PRESSED);
  KEY_ALT_F3: TFarKey = (VirtualKeyCode: VK_F3; ControlKeyState: LEFT_ALT_PRESSED or RIGHT_ALT_PRESSED);
{$ENDIF}
var
  EditorInfo: TEditorInfo;
  SyncFile: TSynchronizeTask;
  SyncFileIndex: integer;
  {$IFDEF FAR3}
  SetKeyBarTitles: TFarSetKeyBarTitles;
  KeyBarTitles: TKeyBarTitles;
  KeyBarLabels: packed array[0..4] of TKeyBarLabel;
  {$ELSE}
  KeyBarTitles: TKeyBarTitles;
  {$ENDIF}
begin
  Result := False;
  if (not DifferencesFound) then begin
    if Options.MessageWhenNoDiff then
      FarMessage([MNoDiffTitle, MNoDiffBody, MOk], FMSG_NONE, 1);
  end
  else begin
    if SyncTasks.FindByFileName(fDiffFileName, SyncFile, SyncFileIndex) then begin
      if Options.ShowReport then begin
        if FarApi.Editor(PFarChar(fDiffFileName), GetMsg(MCmpTitle), 0, 0, -1, -1,
              EF_NONMODAL or EF_IMMEDIATERETURN or EF_DELETEONLYFILEONCLOSE or EF_DISABLEHISTORY {$IFDEF FAR3} or EF_DISABLESAVEPOS {$ENDIF} ,
              0, 1, {$IFDEF UNICODE} CP_UTF8 {$ELSE} CP_ANSI {$ENDIF} ) = EEC_MODIFIED
        then begin
          if TFarEditor.GetInfo(-1, EditorInfo) then begin
            SyncFile.EditorID := EditorInfo.EditorID;
            {$IFDEF FAR3}
            KeyBarLabels[0].Key := KEY_CTRL_F1;
            KeyBarLabels[0].Text := GetMsg(MKeyEditLeft);
            KeyBarLabels[0].LongText := GetMsg(MKeyEditLeftLong);
            KeyBarLabels[1].Key := KEY_CTRL_F2;
            KeyBarLabels[1].Text := GetMsg(MKeyEditRight);
            KeyBarLabels[1].LongText := GetMsg(MKeyEditRightLong);
            KeyBarLabels[2].Key := KEY_ALT_F1;
            KeyBarLabels[2].Text := GetMsg(MKeyOverwriteLeft);
            KeyBarLabels[2].LongText := GetMsg(MKeyOverwriteLeftLong);
            KeyBarLabels[3].Key := KEY_ALT_F2;
            KeyBarLabels[3].Text := GetMsg(MKeyOverwriteRight);
            KeyBarLabels[3].LongText := GetMsg(MKeyOverwriteRightLong);
            KeyBarLabels[4].Key := KEY_ALT_F3;
            KeyBarLabels[4].Text := GetMsg(MKeyCompare);
            KeyBarLabels[4].LongText := GetMsg(MKeyCompareLong);
            KeyBarTitles.CountLabels := Length(KeyBarLabels);
            KeyBarTitles.Labels := @KeyBarLabels[0];
            SetKeyBarTitles.StructSize := Sizeof(SetKeyBarTitles);
            SetKeyBarTitles.Titles := @KeyBarTitles;
            FarApi.EditorControl(EditorInfo.EditorID, ECTL_SETKEYBAR, 0, @SetKeyBarTitles);
            {$ELSE}
            FillChar(KeyBarTitles, Sizeof(KeyBarTitles), 0);
            KeyBarTitles.CtrlTitles[0] := GetMsg(MKeyEditLeft);
            KeyBarTitles.CtrlTitles[1] := GetMsg(MKeyEditRight);
            KeyBarTitles.AltTitles[0] := GetMsg(MKeyOverwriteLeft);
            KeyBarTitles.AltTitles[1] := GetMsg(MKeyOverwriteRight);
            KeyBarTitles.AltTitles[2] := GetMsg(MKeyCompareLong);
            FarApi.EditorControl(ECTL_SETKEYBAR, @KeyBarTitles);
            {$ENDIF}
          end;
          Result := True;
        end;
      end;
      if not Result then
        try
          SyncFile.Execute;
        finally
          if SyncFileIndex >= 0 then
            SyncTasks.Delete(SyncFileIndex);
        end;
    end;
  end;
end;

procedure TDiscoverDifferences.WriteDifference(DifferenceType: TDifferenceType; UsedMatch: TUsedMatch; const Dir, FileName: string; IsDirectory: boolean);
const
  DIR_SUFFIX: array[boolean] of string = ('', '\*');
begin
  //Log('Difference %s for "%s%s"', [DifferenceTypes[DifferenceType], Dir, FileName]);
  if DifferenceType <> dtEqual then
    fDiffFile.WriteLine(Format('%s%s'#9'%s%s%s', [DifferenceTypes[DifferenceType], UsedMatches[UsedMatch], Dir, FileName, DIR_SUFFIX[IsDirectory]]));
end;

end.
