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

interface

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

type
  TDifferenceFileItem = class
  private
    fFileName: string;
    fDifferenceType: TDifferenceType;
    function GetAsString: string;
  public
    class function CreateFromString(const Str: string): TDifferenceFileItem;
    property FileName: string read fFileName write fFileName;
    property DifferenceType: TDifferenceType read fDifferenceType write fDifferenceType;
    property AsString: string read GetAsString;
  end;

  TDifferenceFile = class(TObjectList<TDifferenceFileItem>)
  private
    fHasDifference: array[TDifferenceType] of boolean;
    function GetHasDifference(DiffType: TDifferenceType): boolean;
  public
    procedure LoadFromFile(const FileName: string);
    property HasDifference[DiffType: TDifferenceType]: boolean read GetHasDifference;
  end;

  TSynchronizeTask = class
  private
    fEditorID: integer;
    fFileName: string;
    fLeftDirectory: string;
    fRightDirectory: string;
    fProgress: TFarProgress;
    fIgnoreInvalidAttributes: boolean;
    fCopyFileSourceFileName: string;
    fCopyFileDestinationFileName: string;
    fCopyFileAborted: boolean;
    procedure SetLeftDirectory(const Value: string);
    procedure SetRightDirectory(const Value: string);
  protected
    type
      TSyncFileActionFlag = (afAsk, afYes, afNo, afAll, afNone);
      TSyncFileDirection = (sfdSkip, sfdSkipAll, sfdCopyToLeft, sfdCopyToLeftAll, sfdCopyToRight, sfdCopyToRightAll);
  protected
    function GetFileAttributes(const FileName: string; out Attributes: DWORD): boolean;
    function LoadFiles(out List: TDifferenceFile): boolean;
    procedure ShowProgress(const SourceFileName, DestinationFileName: string; Progress, Max: int64; Msg1, Msg2: TMessages);
    procedure ShowFileCopyProgress(Progress, Max: int64);
    function DeleteItemMissingOnTheOtherPanel(const SourceDir: string; FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
    function DeleteDirectoryMissingOnTheOtherPanel(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
    function DeleteFileMissingOnTheOtherPanel(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
    function IsItDirectory(var FileName: string): boolean; // Also strips the ending '\*'
    function AskOverwriteConfirmation(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; const FromRightToLeft: boolean): boolean;
    function AskDeleteConfirmation(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; const DeleteOnLeft: boolean): boolean;
    function AskWhatToDoWithDifferentFiles(const LeftDirectory, RightDirectory, FileName: string): TSyncFileDirection;
    function InternalCopyFileWithProgress(const SourceFileName, DestinationFileName: string; out ErrorCode: DWORD): boolean;
    function CopyItem(const SourceDirectory, DestinationDirectory: string; FileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
    function CopyFile(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
    function CopyDirectory(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
    function SyncItem(const SourceDirectory, DestinationDirectory: string; FileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
    function SyncFile(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
    procedure EscapePressedEvent(Sender: TObject; var Value: boolean);
  public
    constructor Create;
    destructor Destroy; override;
    function ShowEditor: boolean;
    function Execute(out ReturnToEditor: boolean): boolean;
    procedure VisualCompare(LeftFileName, RightFileName: string);
    procedure Edit(const FileName: string);
    property EditorID: integer read fEditorID write fEditorID;
    property FileName: string read fFileName write fFileName;
    property LeftDirectory: string read fLeftDirectory write SetLeftDirectory;
    property RightDirectory: string read fRightDirectory write SetRightDirectory;
  end;

  TSynchronizeTaskList = class(TObjectList<TSynchronizeTask>)
  private
  protected
  public
    function Add(const EditorID: integer; const FileName, LeftDirectory, RightDirectory: string): TSynchronizeTask;
    function FindByEditorID(const EditorID: integer; out Item: TSynchronizeTask; out ItemIndex: integer): boolean;
    function FindByFileName(const FileName: string; out Item: TSynchronizeTask; out ItemIndex: integer): boolean;
    procedure DeleteByEditorID(const EditorID: integer);
    procedure DeleteByFileName(const FileName: string);
  end;

implementation

{ TSyncFileInfoList }

procedure TSynchronizeTaskList.DeleteByEditorID(const EditorID: integer);
var
  Item: TSynchronizeTask;
  ItemIndex: integer;
begin
  if FindByEditorID(EditorID, Item, ItemIndex) then
    Delete(ItemIndex);
end;

procedure TSynchronizeTaskList.DeleteByFileName(const FileName: string);
var
  Item: TSynchronizeTask;
  ItemIndex: integer;
begin
  if FindByFileName(FileName, Item, ItemIndex) then
    Delete(ItemIndex);
end;

function TSynchronizeTaskList.FindByEditorID(const EditorID: integer; out Item: TSynchronizeTask; out ItemIndex: integer): boolean;
var
  i: Integer;
begin
  Result := False;
  for i := 0 to Pred(Count) do
    if Items[i].EditorID = EditorID then begin
      Item := Items[i];
      ItemIndex := i;
      Result := True;
      Break;
    end;
end;

function TSynchronizeTaskList.FindByFileName(const FileName: string; out Item: TSynchronizeTask; out ItemIndex: integer): boolean;
var
  i: Integer;
  FN: string;
begin
  Result := False;
  FN := ExpandFileName(FileName);
  for i := 0 to Pred(Count) do
    if AnsiCompareText(Items[i].FileName, FN) = 0 then begin
      Item := Items[i];
      ItemIndex := i;
      Result := True;
      Break;
    end;
end;

function TSynchronizeTaskList.Add(const EditorID: integer; const FileName, LeftDirectory, RightDirectory: string): TSynchronizeTask;
begin
  Result := TSynchronizeTask.Create;
  try
    Result.EditorID := EditorID;
    Result.FileName := ExpandFileName(FileName);
    Result.LeftDirectory := LeftDirectory;
    Result.RightDirectory := RightDirectory;
    inherited Add(Result);
  except
    FreeAndNil(Result);
    Raise;
  end;
end;

{ TSyncFileInfo }

constructor TSynchronizeTask.Create;
begin
  inherited Create;
  fProgress := nil;
end;

function TSynchronizeTask.DeleteDirectoryMissingOnTheOtherPanel(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
var
  Attr: DWORD;
  SR: TSearchRec;
  Dir: string;
  ContentDeleted: boolean;
begin
  Result := False;
  ShowProgress(FileName, '', 0, 1, MDeletingDirectory, MNoLngStringDefined);
  //Log('Deleting directory "%s"', [FileName]);
  repeat
    if not GetFileAttributes(FileName, Attr) then begin
      //Log('- File does not exist');
      Result := True;
      Break;
    end
    else begin
      if not AskDeleteConfirmation(FileName, CanDelete, CanDeleteReadOnly, DeleteOnLeft) then begin
        //Log('- User did not allow deleting the destination file');
        Break;
      end;
      // The user allowed me to delete the directory. First delete the content.
      Dir := IncludeTrailingPathDelimiter(FileName);
      //Log('Searching "%s*.*"', [Dir]);
      ContentDeleted := True;
      if FindFirst(Dir + '*.*', faAnyFile, SR) = 0 then
        try
          repeat
            //Log('Found item: "%s" (directory=%d)', [SR.Name, Integer((SR.Attr and faDirectory) <> 0)]);
            if (SR.Attr and faDirectory) <> 0 then begin
              if (SR.Name <> '.') and (SR.Name <> '..') then begin
                if not DeleteDirectoryMissingOnTheOtherPanel(Dir + SR.Name, CanDelete, CanDeleteReadOnly, DeleteOnLeft) then begin
                  ContentDeleted := False;
                  //Log('Failed to delete');
                end;
              end;
            end
            else begin
              if not DeleteFileMissingOnTheOtherPanel(Dir + SR.Name, CanDelete, CanDeleteReadOnly, DeleteOnLeft) then begin
                ContentDeleted := False;
                //Log('Failed to delete');
              end;
            end;
          until FindNext(SR) <> 0;
        finally
          SysUtils.FindClose(SR);
        end;
      // Then delete the directory itself
      if ContentDeleted then begin
        //Log('Content deleted, deleting "%s"', [FileName]);
        SetFileAttributes(PChar(FileName), Attr and (not (FILE_ATTRIBUTE_READONLY or FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM)));
        if RemoveDirectory(PChar(FileName)) then begin
          //Log('- Deleted successfully');
          Result := True;
          Break;
        end;
      end;
      // If I got here, then the directory couldn't be deleted
      //case TFarUtils.ShowMessage([GetMsg(MWarning), GetMsg(MFailedEraseDir), PChar(FileName), GetMsg(MRetry), GetMsg(MSkip), GetMsg(MCancel)], FMSG_WARNING, 3) of
      case FarMessage([MWarning, MFailedEraseDir, MExtraString1, MRetry, MSkip, MCancel], [PFarChar(FileName)], FMSG_WARNING, 3) of
        1:
          Break;
        2:
          Abort;
      end;
    end;
  until False;
  //Log('Result: %d', [Integer(Result)]);
  if not Result then
    {$MESSAGE HINT 'TODO: Report failed file'}
end;

function TSynchronizeTask.DeleteFileMissingOnTheOtherPanel(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
var
  Attr: DWORD;
begin
  Result := False;
  ShowProgress(FileName, '', 0, 1, MDeletingFile, MNoLngStringDefined);
  //Log('Deleting file "%s"', [FileName]);
  repeat
    if not GetFileAttributes(FileName, Attr) then begin
      //Log('- File does not exist');
      Result := True;
      Break;
    end
    else begin
      if not AskDeleteConfirmation(FileName, CanDelete, CanDeleteReadOnly, DeleteOnLeft) then begin
        //Log('- User did not allow deleting the destination file');
        Break;
      end;
      SetFileAttributes(PChar(FileName), Attr and (not (FILE_ATTRIBUTE_READONLY or FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM)));
      if DeleteFile(PChar(FileName)) then begin
        //Log('- Deleted successfully');
        Result := True;
        Break;
      end
      else begin
        //case TFarUtils.ShowMessage([GetMsg(MWarning), GetMsg(MFailedEraseFile), PChar(FileName), GetMsg(MRetry), GetMsg(MSkip), GetMsg(MCancel)], FMSG_WARNING, 3) of
        case FarMessage([MWarning, MFailedEraseFile, MExtraString1, MRetry, MSkip, MCancel], [PFarChar(FileName)], FMSG_WARNING, 3) of
          1:
            Break;
          2:
            Abort;
        end;
      end;
    end;
  until False;
  //Log('- Result: %d', [Integer(Result)]);
  if not Result then
    {$MESSAGE HINT 'TODO: Report failed file'}
end;

function TSynchronizeTask.DeleteItemMissingOnTheOtherPanel(const SourceDir: string; FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; DeleteOnLeft: boolean): boolean;
begin
  if IsItDirectory(FileName) then
    Result := DeleteDirectoryMissingOnTheOtherPanel(SourceDir + FileName, CanDelete, CanDeleteReadOnly, DeleteOnLeft)
  else
    Result := DeleteFileMissingOnTheOtherPanel(SourceDir + FileName, CanDelete, CanDeleteReadOnly, DeleteOnLeft);
end;

destructor TSynchronizeTask.Destroy;
begin
  FreeAndNil(fProgress);
  inherited;
end;

procedure TSynchronizeTask.Edit(const FileName: string);
begin
  FarApi.Editor(PFarChar(FileName), GetMsg(MCmpTitle), 0, 0, -1, -1,
    EF_NONMODAL or EF_IMMEDIATERETURN,
    0, 1, {$IFDEF FAR3} CP_DEFAULT {$ELSE} CP_AUTODETECT {$ENDIF}
  );
end;

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

function TSynchronizeTask.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;

function CopyProgressRoutine(TotalFileSize, TotalBytesTransferred, StreamSize, StreamBytesTransferred: int64; dwStreamNumber, dwCallbackReason: DWORD; hSourceFile, hDestinationFile: THandle; lpData: TSynchronizeTask): DWORD; stdcall;
begin
  Result := PROGRESS_CONTINUE;
  if lpData <> nil then
    try
      lpData.ShowFileCopyProgress(TotalBytesTransferred, TotalFileSize);
    except
      on EAbort do
        begin
          lpData.fCopyFileAborted := True;
          Result := PROGRESS_CANCEL;
        end;
      on Exception do
        Result := PROGRESS_CONTINUE;
    end;
end;

function TSynchronizeTask.InternalCopyFileWithProgress(const SourceFileName, DestinationFileName: string; out ErrorCode: DWORD): boolean;
var SrcAttr, DestAttr: DWORD;
begin
  if GetFileAttributes(SourceFileName, SrcAttr) then begin
    if GetFileAttributes(DestinationFileName, DestAttr) then begin
      SetFileAttributes(PChar(DestinationFileName), DestAttr and (not (FILE_ATTRIBUTE_READONLY or FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM)));
      DeleteFile(PChar(DestinationFileName));
    end;
    fCopyFileSourceFileName := SourceFileName;
    fCopyFileDestinationFileName := DestinationFileName;
    fCopyFileAborted := False;
    Result := CopyFileEx(PChar(SourceFileName), PChar(DestinationFileName), @CopyProgressRoutine, Self, nil, 0);
    ErrorCode := GetLastError;
    if Result then
      SetFileAttributes(PChar(DestinationFileName), SrcAttr)
    else if fCopyFileAborted then
      Abort;
  end
  else
    Result := False;
end;

function TSynchronizeTask.IsItDirectory(var FileName: string): boolean;
var
  n: integer;
  Attr: DWORD;
begin
  FileName := Trim(FileName);
  n := Length(FileName);
  if (n >= 2) and (FileName[n] = '*') and (FileName[Pred(n)] = '\') then begin
    Result := True;
    FileName := ExcludeTrailingPathDelimiter(Copy(FileName, 1, n-2));
  end
  else
    Result := GetFileAttributes(FileName, Attr) and ((Attr and FILE_ATTRIBUTE_DIRECTORY) <> 0);
end;

function TSynchronizeTask.LoadFiles(out List: TDifferenceFile): boolean;
begin
  Result := False;
  List := TDifferenceFile.Create(True);
  try
    List.LoadFromFile(FileName);
    Result := True;
  finally
    if not Result then
      FreeAndNil(List);
  end;
end;

procedure TSynchronizeTask.SetLeftDirectory(const Value: string);
begin
  fLeftDirectory := IncludeTrailingPathDelimiter(Value);
end;

procedure TSynchronizeTask.SetRightDirectory(const Value: string);
begin
  fRightDirectory := IncludeTrailingPathDelimiter(Value);
end;

function TSynchronizeTask.ShowEditor: 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_CTRL_F3: TFarKey = (VirtualKeyCode: VK_F3; 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;
  {$IFDEF EDITOR_SHORTCUTS_ACTIVE}
  {$IFDEF FAR3}
  SetKeyBarTitles: TFarSetKeyBarTitles;
  KeyBarTitles: TKeyBarTitles;
  KeyBarLabels: packed array[0..5] of TKeyBarLabel;
  {$ELSE}
  KeyBarTitles: TKeyBarTitles;
  {$ENDIF}
  {$ENDIF}
begin
  Result := False;
  if FarApi.Editor(PFarChar(FileName), 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
      Self.EditorID := EditorInfo.EditorID;
      {$IFDEF EDITOR_SHORTCUTS_ACTIVE}
      {$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);
      KeyBarLabels[5].Key := KEY_CTRL_F3;
      KeyBarLabels[5].Text := GetMsg(MKeyFileInfo);
      KeyBarLabels[5].LongText := GetMsg(MKeyFileInfoLong);
      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.CtrlTitles[2] := GetMsg(MKeyFileInfo);
      KeyBarTitles.AltTitles[0] := GetMsg(MKeyOverwriteLeft);
      KeyBarTitles.AltTitles[1] := GetMsg(MKeyOverwriteRight);
      KeyBarTitles.AltTitles[2] := GetMsg(MKeyCompareLong);
      FarApi.EditorControl(ECTL_SETKEYBAR, @KeyBarTitles);
      {$ENDIF}
      {$ENDIF}
    end;
    Result := True;
  end;
end;

procedure TSynchronizeTask.ShowFileCopyProgress(Progress, Max: int64);
begin
  ShowProgress(fCopyFileSourceFileName, fCopyFileDestinationFileName, Progress, Max, MCopying, MCopyingTo);
end;

procedure TSynchronizeTask.ShowProgress(const SourceFileName, DestinationFileName: string; Progress, Max: int64; Msg1, Msg2: TMessages);
begin
  if fProgress.NeedsRefresh then begin
    fProgress.Show(Integer(MCmpTitle), Integer(Msg1), Integer(Msg2), SourceFileName, DestinationFileName, Progress, Max);
  end;
end;

function TSynchronizeTask.AskDeleteConfirmation(const FileName: string; var CanDelete, CanDeleteReadOnly: TSyncFileActionFlag; const DeleteOnLeft: boolean): boolean;
var
  Attr: DWORD;
  Dlg: TDeleteConfirmationDialog;
begin
  //Log('Asking for delete confirmation.');
  //Log('- Filename = %s', [Filename]);
  if not GetFileAttributes(FileName, Attr) then
    Result := True
  else begin
    //Log('- File exists');
    Result := False;
    Dlg := TDeleteConfirmationDialog.Create;
    try
      Dlg.FileOnLeft := DeleteOnLeft;
      Dlg.ReadOnlyFile := (Attr and FILE_ATTRIBUTE_READONLY) <> 0;
      //Log('- Read only: %d', [Integer(DeleteDlg.ReadOnlyFile)]);
      Dlg.FileName := FileName;
      Dlg.IsDirectory := (Attr and FILE_ATTRIBUTE_DIRECTORY) <> 0;
      //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)]);
      if Dlg.ReadOnlyFile then begin
        if CanDeleteReadOnly = afAll then
          Result := True
        else if CanDeleteReadOnly = afAsk then
          case Dlg.Execute of
            ocbYes:
              Result := True;
            ocbYesToAll:
              begin
                Result := True;
                CanDeleteReadOnly := afAll;
                CanDelete := afAll;
              end;
            ocbNo:
              Result := False;
            ocbNoToAll:
              begin
                Result := False;
                CanDeleteReadOnly := afNone;
              end;
            ocbCancel:
              Abort;
          end;
        end
      else begin
        if CanDelete = afAll then
          Result := True
        else if CanDelete = afAsk then
          case Dlg.Execute of
            ocbYes:
              Result := True;
            ocbYesToAll:
              begin
                Result := True;
                CanDelete := afAll;
              end;
            ocbNo:
              Result := False;
            ocbNoToAll:
              begin
                Result := False;
                CanDelete := afNone;
                CanDeleteReadOnly := afNone;
              end;
            ocbCancel:
              Abort;
          end;
       end;
    finally
      FreeAndNil(Dlg);
    end;
  end;
  //Log('- Result = %d', [Integer(Result)]);
end;

function TSynchronizeTask.AskOverwriteConfirmation(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; const FromRightToLeft: boolean): boolean;
var
  DestAttr: DWORD;
  Dlg: TOverwriteConfirmationDialog;
  Again: boolean;
begin
  if not GetFileAttributes(DestinationFileName, DestAttr) then
    Result := True
  else begin
    Result := False;
    repeat
      Again := False;
      Dlg := TOverwriteConfirmationDialog.Create;
      try
        Dlg.FromRightToLeft := FromRightToLeft;
        Dlg.ReadOnlyDestination := (DestAttr and FILE_ATTRIBUTE_READONLY) <> 0;
        Dlg.SourceFileName := SourceFileName;
        Dlg.DestinationFileName := DestinationFileName;
        //Log('Asking user for overwrite confirmation. ReadOnly=%d, CanOverwrite=%d, CanOverwriteReadOnly=%d', [Integer(Dlg.ReadOnlyDestination), Integer(CanOverwrite), Integer(CanOverwriteReadOnly)]);
        if Dlg.ReadOnlyDestination then begin
          if CanOverwriteReadOnly = afAll then
            Result := True
          else if CanOverwriteReadOnly = afAsk then
            case Dlg.Execute of
              ocbYes:
                Result := True;
              ocbYesToAll:
                begin
                  Result := True;
                  CanOverwriteReadOnly := afAll;
                  CanOverwrite := afAll;
                end;
              ocbNo:
                Result := False;
              ocbNoToAll:
                begin
                  Result := False;
                  CanOverwriteReadOnly := afNone;
                end;
              ocbCompare:
                begin
                  if FromRightToLeft then
                    VisualCompare(Dlg.DestinationFileName, Dlg.SourceFileName)
                  else
                    VisualCompare(Dlg.SourceFileName, Dlg.DestinationFileName);
                  Again := True;
                end;
              ocbCancel:
                Abort;
            end;
          end
        else begin
          if CanOverwrite = afAll then
            Result := True
          else if CanOverwrite = afAsk then
            case Dlg.Execute of
              ocbYes:
                Result := True;
              ocbYesToAll:
                begin
                  Result := True;
                  CanOverwrite := afAll;
                end;
              ocbNo:
                Result := False;
              ocbNoToAll:
                begin
                  Result := False;
                  CanOverwrite := afNone;
                  CanOverwriteReadOnly := afNone;
                end;
              ocbCompare:
                begin
                  if FromRightToLeft then
                    VisualCompare(Dlg.DestinationFileName, Dlg.SourceFileName)
                  else
                    VisualCompare(Dlg.SourceFileName, Dlg.DestinationFileName);
                  Again := True;
                end;
              ocbCancel:
                Abort;
             end;
          end;
        //Log('- Result = %d', [Integer(Result)]);
      finally
        FreeAndNil(Dlg);
      end;
    until not Again;
  end;
end;

function TSynchronizeTask.AskWhatToDoWithDifferentFiles(const LeftDirectory, RightDirectory, FileName: string): TSyncFileDirection;
var
  LeftAttr, RightAttr: DWORD;
  Dlg: TWhatToDoWithDifferentFilesDialog;
  Again: boolean;
begin
  { sfdSkip, sfdSkipAll, sfdCopyToLeft, sfdCopyToLeftAll, sfdCopyToRight, sfdCopyToRightAll }
  if not GetFileAttributes(LeftDirectory + FileName, LeftAttr) then
    Result := sfdSkip
  else if not GetFileAttributes(RightDirectory + FileName, RightAttr) then
    Result := sfdSkip
  else begin
    Result := sfdSkip;
    repeat
      Again := False;
      Dlg := TWhatToDoWithDifferentFilesDialog.Create;
      try
        Dlg.LeftFileName := LeftDirectory + FileName;
        Dlg.RightFileName := RightDirectory + FileName;
        case Dlg.Execute of
          dfbOverwriteLeft:
            Result := sfdCopyToLeft;
          dfbOverwriteRight:
            Result := sfdCopyToRight;
          dfbSkip:
            Result := sfdSkip;
          dfbAlwaysOverwriteLeft:
            Result := sfdCopyToLeftAll;
          dfbAlwaysOverwriteRight:
            Result := sfdCopyToRightAll;
          dfbAlwaysSkip:
            Result := sfdSkipAll;
          dfbCompare:
            begin
              VisualCompare(Dlg.LeftFileName, Dlg.RightFileName);
              Again := True;
            end;
          dfbCancel:
            Abort;
        end;
      finally
        FreeAndNil(Dlg);
      end;
    until not Again;
  end;
end;

function TSynchronizeTask.CopyDirectory(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
var
  SourceAttr, DestAttr: DWORD;
  SourceDir, DestinationDir: string;
  SR: TSearchRec;
begin
  Result := False;
  ShowProgress(SourceFileName, DestinationFileName, 0, 1, MCopying, MCopyingTo);
  //Log('Copying directory "%s" to "%s"', [SourceFileName, DestinationFileName]);
  repeat
    if not GetFileAttributes(SourceFileName, SourceAttr) then begin
      //Log('- Source directory does not exist');
      Break;
    end
    else begin
      if not GetFileAttributes(DestinationFileName, DestAttr) then begin
        ForceDirectories(DestinationFileName);
        if not GetFileAttributes(DestinationFileName, DestAttr) then begin
          //Log('- Cannot create destination directory');
          //case TFarUtils.ShowMessage([GetMsg(MWarning), GetMsg(MCantCreateDirectory), PFarChar(DestinationFileName), GetMsg(MRetry), GetMsg(MSkip), GetMsg(MCancel)], FMSG_WARNING, 3) of
          case FarMessage([MWarning, MCantCreateDirectory, MExtraString1, MRetry, MSkip, MCancel], [PFarChar(DestinationFileName)], FMSG_WARNING, 3) of
            1:
              Break;
            2:
              Abort;
          end;
        end;
      end;
      //Log('Copying files');
      Result := True;
      SourceDir := IncludeTrailingPathDelimiter(SourceFileName);
      DestinationDir := IncludeTrailingPathDelimiter(DestinationFileName);
      if FindFirst(SourceDir + '*.*', faAnyFile, SR) = 0 then
        try
          repeat
            if (SR.Attr and faDirectory) <> 0 then
              if (SR.Name = '.') or (SR.Name = '..') then
                Continue
              else
                CopyDirectory(SourceDir + SR.Name, DestinationDir + SR.Name, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft)
            else
              CopyFile(SourceDir + SR.Name, DestinationDir + SR.Name, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft);
          until FindNext(SR) <> 0;
        finally
          SysUtils.FindClose(SR);
        end;
      Break;
    end;
  until False;
  //Log('- Result: %d', [Integer(Result)]);
  if not Result then
    {$MESSAGE HINT 'TODO: Report failed file'}
end;

function TSynchronizeTask.CopyFile(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
var
  SourceAttr, DestAttr, CopyError: DWORD;
  Handle: THandle;
  Msg: TMessages;
begin
  Result := False;
  ShowProgress(SourceFileName, DestinationFileName, 0, 1, MCopying, MCopyingTo);
  //Log('Copying file "%s" to "%s"', [SourceFileName, DestinationFileName]);
  repeat
    if not GetFileAttributes(SourceFileName, SourceAttr) then begin
      //Log('- Source file does not exist');
      Break;
    end
    else begin
      if GetFileAttributes(DestinationFileName, DestAttr) then
        if not AskOverwriteConfirmation(SourceFileName, DestinationFileName, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft) then begin
          //Log('- User did not allow overwriting the destination file');
          Break;
        end;
      if InternalCopyFileWithProgress(SourceFileName, DestinationFileName, CopyError) then begin
        //Log('- Copied successfully');
        Result := True;
        Break;
      end
      else begin
        //Log('- Error %d while copying', [CopyError]);
        Msg := MFailedCopySrcFile;
        Handle := CreateFile(PChar(SourceFileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
        if Handle = INVALID_HANDLE_VALUE then
          Msg := MFailedOpenSrcFile
        else begin
          CloseHandle(Handle);
          Handle := CreateFile(PChar(DestinationFileName), GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
          if Handle = INVALID_HANDLE_VALUE then
            if GetLastError = ERROR_FILE_NOT_FOUND then
              Msg := MFailedCreateDstFile
            else
              Msg := MFailedOpenDstFile
          else
            CloseHandle(Handle);
        end;
				SetLastError(CopyError);
        //case TFarUtils.ShowMessage([GetMsg(MWarning), GetMsg(Msg), PFarChar(SourceFileName), GetMsg(MFailedCopyDstFile), PFarChar(DestinationFileName), GetMsg(MRetry), GetMsg(MSkip), GetMsg(MCancel)], FMSG_WARNING, 3) of
        case FarMessage([MWarning, Msg, MExtraString1, MFailedCopyDstFile, MExtraString2, MRetry, MSkip, MCancel], [PFarChar(SourceFileName), PFarChar(DestinationFileName)], FMSG_WARNING, 3) of
          1:
            Break;
          2:
            Abort;
        end;
      end;
    end;
  until False;
  //Log('- Result: %d', [Integer(Result)]);
  if not Result then
    {$MESSAGE HINT 'TODO: Report failed file'}
end;

function TSynchronizeTask.CopyItem(const SourceDirectory, DestinationDirectory: string; FileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
begin
  if IsItDirectory(FileName) then
    Result := CopyDirectory(SourceDirectory + FileName, DestinationDirectory + FileName, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft)
  else
    Result := CopyFile(SourceDirectory + FileName, DestinationDirectory + FileName, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft);
end;

function TSynchronizeTask.Execute(out ReturnToEditor: boolean): boolean;
const
  ConfirmToSyncFileActionFlag: array[boolean] of TSyncFileActionFlag = (afAll, afAsk);
var
  List: TDifferenceFile;
  Dlg: TSynchronizeDirectoriesDialog;
  DiffType: TDifferenceType;
  i: Integer;
  FN: string;
  CanOverwriteLeft, CanOverwriteReadOnlyLeft, CanDeleteLeft, CanDeleteReadOnlyLeft: TSyncFileActionFlag;
  CanOverwriteRight, CanOverwriteReadOnlyRight, CanDeleteRight, CanDeleteReadOnlyRight: TSyncFileActionFlag;
begin
  Result := False;
  ReturnToEditor := True;
  try
    //FarMessage(['DirSync', '', 'Files should be synchronized here:', PChar(FileName), PChar(LeftDirectory), PChar(RightDirectory), '', 'OK'], 0, 1);
    if LoadFiles(List) then
      try
        if List.Count > 0 then begin
          Dlg := TSynchronizeDirectoriesDialog.Create;
          try
            for DiffType := Low(TDifferenceType) to High(TDifferenceType) do
              Dlg.DiffTypePresent[DiffType] := List.HasDifference[DiffType];
            case Dlg.Execute of
              sdaCancel:
                begin
                  ReturnToEditor := False;
                end;
              sdaSynchronize:
                begin
                  ReturnToEditor := False;
                  //FarMessage(['DirSync', '', 'Files should be synchronized here:', PChar(IntToStr(List.Count) + ' files total'), '', 'OK'], 0, 1);
                  fProgress := TFarProgress.Create( {$IFDEF FAR3} PLUGIN_GUID, DIALOG_READING_GUID {$ELSE} FarApi.ModuleNumber {$ENDIF} );
                  try
                    //fProgress.GranularityMSec := PROGRESS_GRANULARITY_MSEC;
                    fProgress.CheckForEscape := True;
                    fProgress.OnEscapePressed := EscapePressedEvent;
                    fIgnoreInvalidAttributes := False;
                    CanOverwriteLeft := ConfirmToSyncFileActionFlag[Dlg.ConfirmOverwriteLeft];
                    CanOverwriteReadOnlyLeft := ConfirmToSyncFileActionFlag[True];
                    CanDeleteLeft := ConfirmToSyncFileActionFlag[True];
                    CanDeleteReadOnlyLeft := ConfirmToSyncFileActionFlag[True];
                    CanOverwriteRight := ConfirmToSyncFileActionFlag[Dlg.ConfirmOverwriteRight];
                    CanOverwriteReadOnlyRight := ConfirmToSyncFileActionFlag[True];
                    CanDeleteRight := ConfirmToSyncFileActionFlag[True];
                    CanDeleteReadOnlyRight := ConfirmToSyncFileActionFlag[True];
                    for i := 0 to Pred(List.Count) do begin
                      FN := Trim(List[i].FileName);
                      case List[i].DifferenceType of
                        dtMissingLeft:
                          if Dlg.DeleteRightMissingOnLeft then
                            DeleteItemMissingOnTheOtherPanel(RightDirectory, FN, CanDeleteRight, CanDeleteReadOnlyRight, False)
                          else if Dlg.CopyToLeftPanel then
                            CopyItem(RightDirectory, LeftDirectory, FN, CanOverwriteLeft, CanOverwriteReadOnlyLeft, True);
                        dtMissingRight:
                          if Dlg.DeleteLeftMissingOnRight then
                            DeleteItemMissingOnTheOtherPanel(LeftDirectory, FN, CanDeleteLeft, CanDeleteReadOnlyLeft, True)
                          else if Dlg.CopyToRightPanel then
                            CopyItem(LeftDirectory, RightDirectory, FN, CanOverwriteRight, CanOverwriteReadOnlyRight, False);
                        dtOlderLeft:
                          if Dlg.CopyToLeftPanel then
                            CopyItem(RightDirectory, LeftDirectory, FN, CanOverwriteLeft, CanOverwriteReadOnlyLeft, True);
                        dtOlderRight:
                          if Dlg.CopyToRightPanel then
                            CopyItem(LeftDirectory, RightDirectory, FN, CanOverwriteRight, CanOverwriteReadOnlyRight, False);
                        dtDifferent:
                          case Dlg.DifferentFilesAction of
                            dfaAsk:
                              case AskWhatToDoWithDifferentFiles(LeftDirectory, RightDirectory, FN) of
                                sfdSkip:
                                  ;
                                sfdSkipAll:
                                  Dlg.DifferentFilesAction := dfaSkip;
                                sfdCopyToLeft:
                                  SyncItem(RightDirectory, LeftDirectory, FN, CanOverwriteLeft, CanOverwriteReadOnlyLeft, True);
                                sfdCopyToLeftAll:
                                  begin
                                    Dlg.DifferentFilesAction := dfaCopyToLeft;
                                    SyncItem(RightDirectory, LeftDirectory, FN, CanOverwriteLeft, CanOverwriteReadOnlyLeft, True);
                                  end;
                                sfdCopyToRight:
                                  SyncItem(LeftDirectory, RightDirectory, FN, CanOverwriteRight, CanOverwriteReadOnlyRight, False);
                                sfdCopyToRightAll:
                                  begin
                                    Dlg.DifferentFilesAction := dfaCopyToRight;
                                    SyncItem(LeftDirectory, RightDirectory, FN, CanOverwriteRight, CanOverwriteReadOnlyRight, False);
                                  end;
                              end;
                            dfaSkip:
                              ;
                            dfaCopyToLeft:
                              CopyItem(RightDirectory, LeftDirectory, FN, CanOverwriteLeft, CanOverwriteReadOnlyLeft, True);
                            dfaCopyToRight:
                              CopyItem(LeftDirectory, RightDirectory, FN, CanOverwriteRight, CanOverwriteReadOnlyRight, False);
                          end;
                      end;
                    end;
                  finally
                    FreeAndNil(fProgress);
                  end;
                  Result := True;
                end;
            end;
          finally
            FreeAndNil(Dlg);
          end;
        end;
      finally
        FreeAndNil(List);
      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), PChar(E.Message)], FMSG_ERRORTYPE, 1);
  end;
end;

function TSynchronizeTask.SyncFile(const SourceFileName, DestinationFileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
var
  OldCanOverwrite: TSyncFileActionFlag;
begin
  OldCanOverwrite := CanOverwrite;
  CanOverwrite := afYes;
  try
    Result := Self.CopyFile(SourceFileName, DestinationFileName, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft);
  finally
    if CanOverwrite = afYes then
      CanOverwrite := OldCanOverwrite;
  end;
end;

function TSynchronizeTask.SyncItem(const SourceDirectory, DestinationDirectory: string; FileName: string; var CanOverwrite, CanOverwriteReadOnly: TSyncFileActionFlag; FromRightToLeft: boolean): boolean;
begin
  Result := SyncFile(SourceDirectory + FileName, DestinationDirectory + FileName, CanOverwrite, CanOverwriteReadOnly, FromRightToLeft);
end;

procedure TSynchronizeTask.VisualCompare(LeftFileName, RightFileName: string);

  function RemoveNTSchematicsFromFileName(const FileName: string): string;
    const
      LocalResource = '\\?\';
      NetworkResource = '\\?\UNC\';
    begin
      if UpperCase(Copy(FileName, 1, Length(NetworkResource))) = NetworkResource then
        Result := '\\' + Copy(FileName, Succ(Length(NetworkResource)), MaxInt)
      else if Copy(FileName, 1, Length(LocalResource)) = LocalResource then
        Result := Copy(FileName, Succ(Length(LocalResource)), MaxInt)
      else
        Result := FileName;
      //Log('RemoveNTSchematicsFromFileName("%s") -> "%s"', [FileName, Result]);
    end;

var
  Options: TDirSyncOptions;
  Command, Arguments: string;
begin
  //Log('VisualCompare("%s", "%s")', [LeftFileName, RightFileName]);
  Options := TDirSyncOptions.Create;
  try
    Options.Load;
    Command := 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;

{ TDifferenceFileItem }

class function TDifferenceFileItem.CreateFromString(const Str: string): TDifferenceFileItem;
const
  DIFFERENCE_LENGTH = 4;
var
  Direction, FN: string;
  CurLen, CurPos: integer;
  DiffType: TDifferenceType;
begin
  Result := nil;
  CurLen := Length(Str);
  if CurLen > Succ(DIFFERENCE_LENGTH) then begin
    Direction := Copy(Str, 1, DIFFERENCE_LENGTH);
    for DiffType := Low(TDifferenceType) to High(TDifferenceType) do
      if Direction = DifferenceTypes[DiffType] then begin
        if DiffType <> dtEqual then begin
          CurPos := Succ(DIFFERENCE_LENGTH);
          while (CurPos <= CurLen) and (Str[CurPos] <> #9) do
            Inc(CurPos);
          FN := Copy(Str, Succ(CurPos), MaxInt);
          Result := TDifferenceFileItem.Create;
          try
            Result.FileName := FN;
            Result.DifferenceType := DiffType;
          except
            FreeAndNil(Result);
            Raise;
          end;
        end;
        Break;
      end;
  end;
end;

function TDifferenceFileItem.GetAsString: string;
begin
  Result := DifferenceTypes[DifferenceType] + #9 + FileName;
end;

{ TDifferenceFile }

function TDifferenceFile.GetHasDifference(DiffType: TDifferenceType): boolean;
begin
  Result := fHasDifference[DiffType];
end;

procedure TDifferenceFile.LoadFromFile(const FileName: string);
const
  DIFFERENCE_LENGTH = 4;
var
  Str: TTextStream;
  Line: string;
  DiffType: TDifferenceType;
  Item: TDifferenceFileItem;
begin
  Clear;
  for DiffType := Low(TDifferenceType) to High(TDifferenceType) do
    fHasDifference[DiffType] := False;
  Str := TTextStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    while Str.ReadLine(Line) do begin
      Item := TDifferenceFileItem.CreateFromString(Line);
      if Item <> nil then begin
        Self.Add(Item);
        fHasDifference[Item.DifferenceType] := True;
      end;
    end;
  finally
    FreeAndNil(Str);
  end;
end;

end.
