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

interface

uses
  SysUtils, Windows, Classes, Generics.Collections, Generics.Defaults,
  {$IFDEF FAR3} Plugin3, {$ELSE} PluginW, {$ENDIF} FarKeysW, FarColor,
  uSystem, uFAR, uLog,
  uMessages, uConfirmations, uOptions, uSupportFunctions;

type
  TRememberedAction = class(TObject)
  public
    type TTargetType = (ttFiles, ttFilesReadOnly, ttDirectories, ttDirectoriesReadOnly);
    type TOverwriteAction = (oaAsk, oaYes, oaNo);
    type TDeleteAction = (daAsk, daYes, daNo);
    type TDifferentFileAction = (dfaAsk, dfaSkip, dfaLeft, dfaRight);
    type TFailureReason = (frCopy, frDelete, frMakeDirectory, frNotFound, frNotDirectory);
    type TFailureAction = (faAsk, faRetry, faSkip);
  private
    fDirectory: string;
    fDeleteAction: array[boolean, TTargetType] of TDeleteAction;
    fOverwriteAction: array[boolean, boolean] of TOverwriteAction;
    fDifferentFileAction: TDifferentFileAction;
    fFailures: array[TFailureReason] of TFailureAction;
    function GetDeleteAction(Left: boolean; Target: TTargetType): TDeleteAction;
    function GetDifferentFileAction: TDifferentFileAction;
    function GetOverwriteAction(Left, ReadOnly: boolean): TOverwriteAction;
    procedure SetDeleteAction(Left: boolean; Target: TTargetType; const Value: TDeleteAction);
    procedure SetDifferentFileAction(const Value: TDifferentFileAction);
    procedure SetOverwriteAction(Left, ReadOnly: boolean; const Value: TOverwriteAction);
    function GetFailureAction(Reason: TFailureReason): TFailureAction;
    procedure SetFailureAction(Reason: TFailureReason;
      const Value: TFailureAction);
  protected
  public
    constructor Create(const ADirectory: string);
    procedure Assign(Source: TObject);
    property Directory: string read fDirectory;
    property OverwriteAction[Left: boolean; ReadOnly: boolean]: TOverwriteAction read GetOverwriteAction write SetOverwriteAction;
    property DeleteAction[Left: boolean; Target: TTargetType]: TDeleteAction read GetDeleteAction write SetDeleteAction;
    property DifferentFileAction: TDifferentFileAction read GetDifferentFileAction write SetDifferentFileAction;
    property FailureAction[Reason: TFailureReason]: TFailureAction read GetFailureAction write SetFailureAction;
  end;

  TSynchronizeUI = class(TObject)
  public
    type TSyncFileDirection = (sfdSkip, sfdCopyToLeft, sfdCopyToRight);
  protected
    type TRememberedActionList = TObjectList<TRememberedAction>;
  private
    fRememberedActions: TRememberedActionList;
    fSearchItem: TRememberedAction;
    fSilentMode: boolean;
    fGlobalRemember: boolean;
    function GetAction(const Directory: string): TRememberedAction;
  protected
    function Add(const Directory: string): TRememberedAction;
    function Find(const Directory: string; out Index: integer): boolean; overload;
    function Find(const Directory: string; out Action: TRememberedAction): boolean; overload;
    function GetFileAttributes(const FileName: string; out Attributes: DWORD): boolean;
    function IsRemembered(Action: TRememberedAction.TOverwriteAction; out AnswerIsYes: boolean): boolean; overload;
    function IsRemembered(Action: TRememberedAction.TDeleteAction; out AnswerIsYes: boolean): boolean; overload;
    function IsRemembered(Action: TRememberedAction.TDifferentFileAction; out Answer: TSyncFileDirection): boolean; overload;
    function IsRemembered(Action: TRememberedAction.TFailureAction; out AnswerIsYes: boolean): boolean; overload;
    function IsOverwriteRemembered(const Directory: string; DestinationIsReadOnly, DestinationIsOnTheLeft: boolean; out AnswerIsYes: boolean): boolean;
    function IsDeleteRemembered(const Directory: string; DestinationIsFile, DestinationIsReadOnly, DestinationIsOnTheLeft: boolean; out AnswerIsYes: boolean): boolean;
    function IsSyncRemembered(const Directory: string; out Answer: TSyncFileDirection): boolean;
    function IsRetryRemembered(const Directory: string; Reason: TRememberedAction.TFailureReason; out AnswerIsYes: boolean): boolean;
    function AskRetry(Msg1, Msg2: TMessages; const FileName1, FileName2: string; Reason: TRememberedAction.TFailureReason): boolean;
    procedure HandleCompareEvent(Sender: TObject);
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    function AskOverwriteConfirmation(const SourceFileName, DestinationFileName: string; DestinationOnTheLeft: boolean): boolean;
    function AskDeleteConfirmation(const FileName: string; const DestinationOnTheLeft: boolean): boolean;
    function AskWhatToDoWithDifferentFiles(const LeftDirectory, RightDirectory, FileName: string): TSyncFileDirection;
    function AskRetryDelete(Msg: TMessages; const FileName: string): boolean;
    function AskRetryCopy(Msg1, Msg2: TMessages; const FileName1, FileName2: string): boolean;
    function AskRetryMakeDir(Msg: TMessages; const FileName: string): boolean;
    function AskRetryNotFound(Msg: TMessages; const FileName: string): boolean;
    function AskRetryNotDirectory(Msg: TMessages; const FileName: string): boolean;
    procedure VisualCompare(LeftFileName, RightFileName: string);
    property RememberedActions[const Directory: string]: TRememberedAction read GetAction; default;
    property SilentMode: boolean read fSilentMode write fSilentMode;
    property GlobalRemember: boolean read fGlobalRemember write fGlobalRemember;
  end;

implementation

type
  TDirectoryActionComparer = class(TInterfacedObject, IComparer<TRememberedAction>)
  public
    function Compare(const Left, Right: TRememberedAction): Integer;
  end;

{ TDirectoryAction }

procedure TRememberedAction.Assign(Source: TObject);
var
  Src: TRememberedAction;
  b1, b2: boolean;
  t: TTargetType;
  f: TFailureReason;
begin
  if Source is TRememberedAction then begin
    Src := TRememberedAction(Source);
    fDirectory := Src.Directory;
    for b1 := Low(boolean) to High(boolean) do
      for t := Low(TTargetType) to High(TTargetType) do
        fDeleteAction[b1, t] := Src.DeleteAction[b1, t];
    for b1 := Low(boolean) to High(boolean) do
      for b2 := Low(boolean) to High(boolean) do
        fOverwriteAction[b1, b2] := Src.OverwriteAction[b1, b2];
    fDifferentFileAction := Src.DifferentFileAction;
    for f := Low(TFailureReason) to High(TFailureReason) do
      fFailures[f] := Src.FailureAction[f];
  end;
end;

constructor TRememberedAction.Create(const ADirectory: string);
var
  b1, b2: boolean;
  t: TTargetType;
  f: TFailureReason;
begin
  inherited Create;
  fDirectory := ADirectory;
  for b1 := Low(boolean) to High(boolean) do
    for t := Low(TTargetType) to High(TTargetType) do
      fDeleteAction[b1, t] := daAsk;
  for b1 := Low(boolean) to High(boolean) do
    for b2 := Low(boolean) to High(boolean) do
      fOverwriteAction[b1, b2] := oaAsk;
  fDifferentFileAction := dfaAsk;
  for f := Low(TFailureReason) to High(TFailureReason) do
    fFailures[f] := faAsk;
end;

function TRememberedAction.GetDeleteAction(Left: boolean; Target: TTargetType): TDeleteAction;
begin
  Result := fDeleteAction[Left, Target];
end;

function TRememberedAction.GetDifferentFileAction: TDifferentFileAction;
begin
  Result := fDifferentFileAction;
end;

function TRememberedAction.GetFailureAction(Reason: TFailureReason): TFailureAction;
begin
  Result := fFailures[Reason];
end;

function TRememberedAction.GetOverwriteAction(Left, ReadOnly: boolean): TOverwriteAction;
begin
  Result := fOverwriteAction[Left, ReadOnly];
end;

procedure TRememberedAction.SetDeleteAction(Left: boolean; Target: TTargetType; const Value: TDeleteAction);
begin
  fDeleteAction[Left, Target] := Value;
end;

procedure TRememberedAction.SetDifferentFileAction(const Value: TDifferentFileAction);
begin
  fDifferentFileAction := Value;
end;

procedure TRememberedAction.SetFailureAction(Reason: TFailureReason; const Value: TFailureAction);
begin
  fFailures[Reason] := Value;
end;

procedure TRememberedAction.SetOverwriteAction(Left, ReadOnly: boolean; const Value: TOverwriteAction);
begin
  fOverwriteAction[Left, ReadOnly] := Value;
end;

{ TDirectoryActionComparer }

function TDirectoryActionComparer.Compare(const Left, Right: TRememberedAction): Integer;
begin
  Result := AnsiCompareText(Left.Directory, Right.Directory);
end;

{ TDirectoryActions }

function TSynchronizeUI.Add(const Directory: string): TRememberedAction;
var
  Index: integer;
begin
  if Find(Directory, Index) then
    Result := fRememberedActions[Index]
  else begin
    Result := TRememberedAction.Create(Directory);
    fRememberedActions.Insert(Index, Result);
  end;
end;

function TSynchronizeUI.AskDeleteConfirmation(const FileName: string; const DestinationOnTheLeft: boolean): boolean;
const
  ToTargetType: array[boolean, boolean] of TRememberedAction.TTargetType = (
    ( {IsDirectory: False} ( {IsReadOnly: False} ttFiles),      ( {IsReadOnly: True} ttFilesReadOnly)),
    ( {IsDirectory: True } ( {IsReadOnly: False} ttDirectories),( {IsReadOnly: True} ttDirectoriesReadOnly))
  );
var
  Directory, CurDir: string;
  DestAttr: DWORD;
  IsDirectory, IsReadOnly: boolean;
  Dlg: TDeleteConfirmationDialog;
begin
  // If the file doesn't exist, it can be "deleted" without bothering the user
  if not GetFileAttributes(FileName, DestAttr) then begin
    Result := True;
    Exit;
  end;
  IsReadOnly := (DestAttr and FILE_ATTRIBUTE_READONLY) <> 0;
  IsDirectory := (DestAttr and FILE_ATTRIBUTE_DIRECTORY) <> 0;
  // First I need to check if the file is in a directory which has already been
  // remembered.
  Directory := RemoveLastPathComponent(FileName);
  CurDir := Directory;
  while True do begin
    if IsDeleteRemembered(CurDir, not IsDirectory, IsReadOnly, DestinationOnTheLeft, Result) then
      Exit;
    if CurDir = '' then
      Break;
    CurDir := RemoveLastPathComponent(CurDir);
  end;
  // In the silent mode, the confirmation is never given
  if SilentMode then begin
    Result := False;
    Exit;
  end;
  // If we got here, then the user should be asked
  Result := False;
  Dlg := TDeleteConfirmationDialog.Create;
  try
    Dlg.FileOnLeft := DestinationOnTheLeft;
    Dlg.ReadOnlyFile := IsReadOnly;
    //Log('- Read only: %d', [Integer(DeleteDlg.ReadOnlyFile)]);
    Dlg.FileName := FileName;
    Dlg.IsDirectory := IsDirectory;
    //Log('- Directory: %d', [Integer(DeleteDlg.IsDirectory)]);
    //Log('Asking user for overwrite confirmation. ReadOnly=%d, CanOverwrite=%d, CanOverwriteReadOnly=%d', [Integer(OverwriteDlg.ReadOnlyDestination), Integer(CanOverwrite), Integer(CanOverwriteReadOnly)]);
    Dlg.RememberDirectory := Directory;
    Dlg.GlobalRemember := GlobalRemember;
    case Dlg.Execute of
      ocbYes:
        begin
          Result := True;
          if IsDirectory then
            RememberedActions[FileName].DeleteAction[DestinationOnTheLeft, ToTargetType[IsDirectory, IsReadOnly]] := daYes;
        end;
      ocbYesToAll:
        begin
          Result := True;
          RememberedActions[Dlg.RememberDirectory].DeleteAction[DestinationOnTheLeft, ToTargetType[IsDirectory, IsReadOnly]] := daYes;
        end;
      ocbNo:
        begin
          Result := False;
          if IsDirectory then
            RememberedActions[FileName].DeleteAction[DestinationOnTheLeft, ToTargetType[IsDirectory, IsReadOnly]] := daNo;
        end;
      ocbNoToAll:
        begin
          Result := False;
          RememberedActions[Dlg.RememberDirectory].DeleteAction[DestinationOnTheLeft, ToTargetType[IsDirectory, IsReadOnly]] := daNo;
        end;
      ocbCancel:
        Abort;
    end;
  finally
    FreeAndNil(Dlg);
  end;
end;

function TSynchronizeUI.AskOverwriteConfirmation(const SourceFileName, DestinationFileName: string; DestinationOnTheLeft: boolean): boolean;
var
  Directory, CurDir: string;
  DestAttr: DWORD;
  IsReadOnly: boolean;
  Dlg: TOverwriteConfirmationDialog;
begin
  // If the file doesn't exist, it can be "overwritten" without bothering the user
  if not GetFileAttributes(DestinationFileName, DestAttr) then begin
    Result := True;
    Exit;
  end;
  IsReadOnly := (DestAttr and FILE_ATTRIBUTE_READONLY) <> 0;
  // First I need to check if the file is in a directory which has already been
  // remembered.
  Directory := RemoveLastPathComponent(DestinationFileName);
  CurDir := Directory;
  while True do begin
    if IsOverwriteRemembered(CurDir, IsReadOnly, DestinationOnTheLeft, Result) then
      Exit;
    if CurDir = '' then
      Break;
    CurDir := RemoveLastPathComponent(CurDir);
  end;
  // In the silent mode, the confirmation is never given
  if SilentMode then begin
    Result := False;
    Exit;
  end;
  // If we got here, then the user should be asked
  Result := False;
  Dlg := TOverwriteConfirmationDialog.Create;
  try
    Dlg.FromRightToLeft := DestinationOnTheLeft;
    Dlg.ReadOnlyDestination := IsReadOnly;
    Dlg.SourceFileName := SourceFileName;
    Dlg.DestinationFileName := DestinationFileName;
    Dlg.RememberDirectory := Directory;
    Dlg.GlobalRemember := GlobalRemember;
    Dlg.OnCompare := HandleCompareEvent;
    case Dlg.Execute of
      ocbYes:
        Result := True;
      ocbYesToAll:
        begin
          Result := True;
          //Log('YesToAll, RememberDir = "%s"', [Dlg.RememberDirectory]);
          RememberedActions[Dlg.RememberDirectory].OverwriteAction[DestinationOnTheLeft, IsReadOnly] := oaYes;
        end;
      ocbNo:
        Result := False;
      ocbNoToAll:
        begin
          Result := False;
          //Log('NoToAll, RememberDir = "%s"', [Dlg.RememberDirectory]);
          RememberedActions[Dlg.RememberDirectory].OverwriteAction[DestinationOnTheLeft, IsReadOnly] := oaNo;
        end;
      ocbCancel:
        Abort;
    end;
  finally
    FreeAndNil(Dlg);
  end;
end;

function TSynchronizeUI.AskRetry(Msg1, Msg2: TMessages; const FileName1, FileName2: string; Reason: TRememberedAction.TFailureReason): boolean;
var
  Directory, CurDir: string;
  Dlg: TRetrySkipCancelDialog;
begin
  // First I need to check if the file is in a directory which has already been
  // remembered.
  case Reason of
    frCopy:
      Directory := RemoveLastPathComponent(FileName2);
    else   
      Directory := RemoveLastPathComponent(FileName1);
  end;
  CurDir := Directory;
  while True do begin
    if IsRetryRemembered(CurDir, Reason, Result) then
      Exit;
    if CurDir = '' then
      Break;
    CurDir := RemoveLastPathComponent(CurDir);
  end;
  // In the silent mode, the confirmation is never given
  if SilentMode then begin
    Result := False;
    Exit;
  end;
  Result := False;
  Dlg := TRetrySkipCancelDialog.Create;
  try
    Dlg.Message1 := Msg1;
    Dlg.FileName1 := FileName1;
    Dlg.Message2 := Msg2;
    Dlg.FileName2 := FileName2;
    Dlg.RememberDirectory := Directory;
    Dlg.GlobalRemember := GlobalRemember;
    case Dlg.Execute of
      ocbYes, ocbYesToAll:
        Result := True;
      ocbNo:
        Result := False;
      ocbNoToAll:
        begin
          Result := False;
          RememberedActions[Dlg.RememberDirectory].FailureAction[Reason] := faSkip;
        end;
      ocbCancel:
        Abort;
    end;
  finally
    FreeAndNil(Dlg);
  end;
end;

function TSynchronizeUI.AskRetryCopy(Msg1, Msg2: TMessages; const FileName1, FileName2: string): boolean;
begin
  Result := AskRetry(Msg1, Msg2, FileName1, FileName2, frCopy);
end;

function TSynchronizeUI.AskRetryDelete(Msg: TMessages; const FileName: string): boolean;
begin
  Result := AskRetry(Msg, MNoLngStringDefined, FileName, '', frDelete);
end;

function TSynchronizeUI.AskRetryMakeDir(Msg: TMessages; const FileName: string): boolean;
begin
  Result := AskRetry(Msg, MNoLngStringDefined, FileName, '', frMakeDirectory);
end;

function TSynchronizeUI.AskRetryNotFound(Msg: TMessages; const FileName: string): boolean;
begin
  Result := AskRetry(Msg, MNoLngStringDefined, FileName, '', frNotFound);
end;

function TSynchronizeUI.AskRetryNotDirectory(Msg: TMessages; const FileName: string): boolean;
begin
  Result := AskRetry(Msg, MNoLngStringDefined, FileName, '', frNotDirectory);
end;

function TSynchronizeUI.AskWhatToDoWithDifferentFiles(const LeftDirectory, RightDirectory, FileName: string): TSyncFileDirection;
var
  Directory, CurDir: string;
  LeftAttr, RightAttr: DWORD;
  Dlg: TWhatToDoWithDifferentFilesDialog;
begin
  //Log('AskWhatToDoWithDifferentFiles("%s", "%s", "%s")', [LeftDirectory, RightDirectory, FileName]);
  // If the files don't exist, they can be ignored
  if not GetFileAttributes(LeftDirectory + FileName, LeftAttr) then begin
    Result := sfdSkip;
    Exit;
  end;
  if not GetFileAttributes(RightDirectory + FileName, RightAttr) then begin
    Result := sfdSkip;
    Exit;
  end;
  // First I need to check if the file is in a directory which has already been
  // remembered.
  Directory := RemoveLastPathComponent(FileName);
  CurDir := Directory;
  while True do begin
    if IsSyncRemembered(CurDir, Result) then
      Exit;
    if CurDir = '' then
      Break;
    CurDir := RemoveLastPathComponent(CurDir);
  end;
  // In the silent mode, the confirmation is never given
  if SilentMode then begin
    Result := sfdSkip;
    Exit;
  end;
  // If we got here, then the user should be asked
  Result := sfdSkip;
  Dlg := TWhatToDoWithDifferentFilesDialog.Create;
  try
    Dlg.LeftFileName := LeftDirectory + FileName;
    Dlg.RightFileName := RightDirectory + FileName;
    Dlg.RememberDirectory := Directory;
    Dlg.GlobalRemember := GlobalRemember;
    Dlg.OnCompare := HandleCompareEvent;
    case Dlg.Execute of
      dfbOverwriteLeft:
        Result := sfdCopyToLeft;
      dfbOverwriteRight:
        Result := sfdCopyToRight;
      dfbSkip:
        Result := sfdSkip;
      dfbAlwaysOverwriteLeft:
        begin
          Result := sfdCopyToLeft;
          RememberedActions[Dlg.RememberDirectory].DifferentFileAction := dfaLeft;
        end;
      dfbAlwaysOverwriteRight:
        begin
          Result := sfdCopyToRight;
          RememberedActions[Dlg.RememberDirectory].DifferentFileAction := dfaRight;
        end;
      dfbAlwaysSkip:
        begin
          Result := sfdSkip;
          RememberedActions[Dlg.RememberDirectory].DifferentFileAction := dfaSkip;
        end;
      dfbCancel:
        Abort;
    end;
  finally
    FreeAndNil(Dlg);
  end;
  //Log('AskWhatToDoWithDifferentFiles -> %d', [Integer(Result)]);
end;

procedure TSynchronizeUI.Clear;
begin
  fRememberedActions.Clear;
end;

constructor TSynchronizeUI.Create;
begin
  inherited Create;
  fSearchItem := TRememberedAction.Create('');
  fRememberedActions := TRememberedActionList.Create(TDirectoryActionComparer.Create, True);
end;

destructor TSynchronizeUI.Destroy;
begin
  FreeAndNil(fRememberedActions);
  FreeAndNil(fSearchItem);
  inherited;
end;

function TSynchronizeUI.Find(const Directory: string; out Action: TRememberedAction): boolean;
var
  Index: integer;
begin
  Result := Find(Directory, Index);
  if Result then
    Action := fRememberedActions[Index]
  else
    Action := nil;
end;

function TSynchronizeUI.Find(const Directory: string; out Index: integer): boolean;
begin
  fSearchItem.fDirectory := Directory;
  Result := fRememberedActions.BinarySearch(fSearchItem, Index);
end;

function TSynchronizeUI.GetAction(const Directory: string): TRememberedAction;
begin
  Result := Add(Directory);
end;

function TSynchronizeUI.GetFileAttributes(const FileName: string; out Attributes: DWORD): boolean;
var
  LastError: DWORD;
  SR: TSearchRec;
begin
  Attributes := Windows.GetFileAttributes(PChar(FileName));
  if Attributes <> INVALID_FILE_ATTRIBUTES then
    Result := True
  else begin
    LastError := GetLastError;
    if (LastError = ERROR_FILE_NOT_FOUND) or (LastError = ERROR_PATH_NOT_FOUND) or (LastError = ERROR_INVALID_NAME) then
      Result := False
    else
      if FindFirst(FileName, faAnyFile, SR) = 0 then
        try
          Attributes := SR.Attr;
          Result := True;
        finally
          SysUtils.FindClose(SR);
        end
      else
        Result := False;
  end;
end;

procedure TSynchronizeUI.HandleCompareEvent(Sender: TObject);
var
  OverwriteDlg: TOverwriteConfirmationDialog;
  WhatToDoDlg: TWhatToDoWithDifferentFilesDialog;
begin
  if Sender <> nil then
    if Sender is TOverwriteConfirmationDialog then begin
      OverwriteDlg := TOverwriteConfirmationDialog(Sender);
      if OverwriteDlg.FromRightToLeft then
        VisualCompare(OverwriteDlg.DestinationFileName, OverwriteDlg.SourceFileName)
      else
        VisualCompare(OverwriteDlg.SourceFileName, OverwriteDlg.DestinationFileName);
    end
    else if Sender is TWhatToDoWithDifferentFilesDialog then begin
      WhatToDoDlg := TWhatToDoWithDifferentFilesDialog(Sender);
      VisualCompare(WhatToDoDlg.LeftFileName, WhatToDoDlg.RightFileName);
    end;
end;

function TSynchronizeUI.IsDeleteRemembered(const Directory: string; DestinationIsFile, DestinationIsReadOnly, DestinationIsOnTheLeft: boolean; out AnswerIsYes: boolean): boolean;
var
  Action: TRememberedAction;
begin
  Result := False;
  AnswerIsYes := False;
  if Find(Directory, Action) then
    if IsRemembered(Action.DeleteAction[DestinationIsOnTheLeft, ttDirectoriesReadOnly], AnswerIsYes) then
      Result := True
    else if (not DestinationIsReadOnly) and IsRemembered(Action.DeleteAction[DestinationIsOnTheLeft, ttDirectories], AnswerIsYes) then
      Result := True
    else if DestinationIsFile then
      if IsRemembered(Action.DeleteAction[DestinationIsOnTheLeft, ttFilesReadOnly], AnswerIsYes) then
        Result := True
      else if (not DestinationIsReadOnly) and IsRemembered(Action.DeleteAction[DestinationIsOnTheLeft, ttFiles], AnswerIsYes) then
        Result := True;
  //Log('TSynchronizeUI.IsOverwriteRemembered("%s", %d, %d, %d) -> %d, %d', [Directory, Integer(DestinationIsFile), Integer(DestinationIsReadOnly), Integer(DestinationIsOnTheLeft), Integer(Result), Integer(AnswerIsYes)]);
end;

function TSynchronizeUI.IsOverwriteRemembered(const Directory: string; DestinationIsReadOnly, DestinationIsOnTheLeft: boolean; out AnswerIsYes: boolean): boolean;
var
  Action: TRememberedAction;
begin
  Result := False;
  AnswerIsYes := False;
  if Find(Directory, Action) then
    if IsRemembered(Action.OverwriteAction[DestinationIsOnTheLeft, True], AnswerIsYes) then
      Result := True
    else if (not DestinationIsReadOnly) and IsRemembered(Action.OverwriteAction[DestinationIsOnTheLeft, False], AnswerIsYes) then
      Result := True;
  //Log('TSynchronizeUI.IsOverwriteRemembered("%s", %d, %d) -> %d, %d', [Directory, Integer(DestinationIsReadOnly), Integer(DestinationIsOnTheLeft), Integer(Result), Integer(AnswerIsYes)]);
end;

function TSynchronizeUI.IsRemembered(Action: TRememberedAction.TOverwriteAction; out AnswerIsYes: boolean): boolean;
begin
  Result := Action <> oaAsk;
  AnswerIsYes := Action = oaYes;
end;

function TSynchronizeUI.IsRemembered(Action: TRememberedAction.TDeleteAction; out AnswerIsYes: boolean): boolean;
begin
  Result := Action <> daAsk;
  AnswerIsYes := Action = daYes;
end;

function TSynchronizeUI.IsRemembered(Action: TRememberedAction.TDifferentFileAction; out Answer: TSyncFileDirection): boolean;
begin
  Result := Action <> dfaAsk;
  case Action of
    dfaSkip:  Answer := sfdSkip;
    dfaLeft:  Answer := sfdCopyToLeft;
    dfaRight: Answer := sfdCopyToRight;
  end;
end;

function TSynchronizeUI.IsRemembered(Action: TRememberedAction.TFailureAction; out AnswerIsYes: boolean): boolean;
begin
  Result := Action <> faAsk;
  AnswerIsYes := Action = faRetry;
end;

function TSynchronizeUI.IsRetryRemembered(const Directory: string; Reason: TRememberedAction.TFailureReason; out AnswerIsYes: boolean): boolean;
var
  Action: TRememberedAction;
begin
  Result := False;
  AnswerIsYes := False;
  if Find(Directory, Action) then
    Result := IsRemembered(Action.FailureAction[Reason], AnswerIsYes);
  //Log('TSynchronizeUI.IsRetryRemembered("%s", %d) -> %d, %d', [Directory, Integer(Reason), Integer(Result), Integer(AnswerIsYes)]);
end;

function TSynchronizeUI.IsSyncRemembered(const Directory: string; out Answer: TSyncFileDirection): boolean;
var
  Action: TRememberedAction;
begin
  Result := False;
  Answer := sfdSkip;
  if Find(Directory, Action) then
    if IsRemembered(Action.DifferentFileAction, Answer) then
      Result := True;
  //Log('TSynchronizeUI.IsSyncRemembered("%s") -> %d, %d', [Directory, Integer(Result), Integer(Answer)]);
end;

procedure TSynchronizeUI.VisualCompare(LeftFileName, RightFileName: string);
var
  Options: TDirSyncOptions;
  Command, Arguments: string;
begin
  //Log('VisualCompare("%s", "%s")', [LeftFileName, RightFileName]);
  Options := TDirSyncOptions.Create;
  try
    Options.Load;
    Command := ExpandEnvironmentVars(Options.CompareCommand);
    Arguments := Options.CompareCommandArguments;
    if (Command = '') or (Arguments = '') then
      //TFarUtils.ShowMessage([GetMsg(MWarning), GetMsg(MVisualCompareNotSetUp), GetMsg(MOK)], FMSG_WARNING, 1)
      FarMessage([MWarning, MVisualCompareNotSetUp, MOK], FMSG_WARNING, 1)
    else begin
      //Log('- CompareCommandAcceptsNTFileNames = %d', [Integer(Options.CompareCommandAcceptsNTFileNames)]);
      if not Options.CompareCommandAcceptsNTFileNames then begin
        LeftFileName := RemoveNTSchematicsFromFileName(LeftFileName);
        RightFileName := RemoveNTSchematicsFromFileName(RightFileName);
        //Log('- Translated filenames: "%s", "%s"', [LeftFileName, RightFileName]);
      end;
      Arguments := StringReplace(Arguments, '${LEFTFILE}', AnsiQuotedStr(LeftFileName, '"'), [rfIgnoreCase, rfReplaceAll]);
      Arguments := StringReplace(Arguments, '${RIGHTFILE}', AnsiQuotedStr(RightFileName, '"'), [rfIgnoreCase, rfReplaceAll]);
      RunFile(Command, Arguments);
    end;
  finally
    FreeAndNil(Options);
  end;
end;

end.
