//
//  Reference behaviour:
//
//  rsync dir1 dir2
//    skipping directory dir1
//
//  rsync -r dir1 dir2
//    copies dir1 into dir2, that is it will create dir2/dir1
//
//  rsync -r dir1/ dir2
//    copies dir1 onto dir2, that is it will copy content of dir1 to dir2
//
//  rsync dir1 dir2 -r
//    works the same way as previous, -r position does not make difference
//
//  rsync -r dir1 -r dir2 -r
//    works the same way as previous, multiple -r's do not make difference
//
//
//  1 -- single source
//  m -- multiple sources
//  - -- dst does not exist
//  f -- dst already exists and it is a file
//  d -- dst already exists and it is a directory
//
//  1-  | copies src to dst
//  1f  | copies src to dst and rewrites old file
//  1d  | copy source recursively
//  m-  | create dst directory, copy sources recursively
//  mf  | error
//  md  | copy sources recursively
//

uses
  dfiles,
  sysutils;

type
TOpts = record
  Help: Boolean;
  Recursive: Boolean;
  SourcePaths: array of AnsiString;
  DestPath: AnsiString;
end;

var
  Opts: TOpts;

procedure DefaultOpts;
begin
  Opts.Help := False;
  Opts.Recursive := False;
  SetLength(Opts.SourcePaths, 0);
  Opts.DestPath := '';
end;

function ParseOpts: Boolean;
label
  LContinue;
var
  I: LongInt;
begin
  DefaultOpts;

  I := 1;
  while I <= ParamCount do begin
    if Length(ParamStr(I)) <= 0 then
      goto LContinue;
    if ParamStr(I) = '--' then begin
      Inc(I);
      break;
    end;
    if ParamStr(I) = '-r' then begin
      Opts.Recursive := True;
      goto LContinue;
    end;
    if (ParamStr(I) = '-?') or
       (ParamStr(I) = '-h') or
       (ParamStr(I) = '--help') then begin
      Opts.Help := True;
      goto LContinue;
    end;
    if ParamStr(I)[1] = '-' then begin
      Writeln(stderr, 'dsync error: unknown option ', ParamStr(I));
      Exit(False);
    end;
    SetLength(Opts.SourcePaths, Length(Opts.SourcePaths) + 1);
    Opts.SourcePaths[High(Opts.SourcePaths)] := ParamStr(I);
  LContinue:
    Inc(I);
  end;

  while I <= ParamCount do begin
    SetLength(Opts.SourcePaths, Length(Opts.SourcePaths) + 1);
    Opts.SourcePaths[High(Opts.SourcePaths)] := ParamStr(I);
    Inc(I);
  end;

  if Length(Opts.SourcePaths) >= 1 then begin
    Opts.DestPath := Opts.SourcePaths[High(Opts.SourcePaths)];
    SetLength(Opts.SourcePaths, Length(Opts.SourcePaths) - 1);
  end else
    Opts.Help := True;

  Exit(True);
end;

procedure PrintUsage;
begin
  Writeln('dsync [-r] source1 source2 ... sourceN dest');
end;

var
  IsDestExists: Boolean = True;
  IsDestDirectory: Boolean = False;

function PrepareSync: Boolean;
begin
  IsDestExists := FileExists(Opts.DestPath);
  IsDestDirectory := DirExists(Opts.DestPath);

  if (Length(Opts.SourcePaths) > 1) and IsDestExists and not IsDestDirectory then begin
    Writeln(stderr, 'dsync: error: destination is a file for multiple sources');
    Exit(False);
  end;
  Exit(True);
end;

function IsTemporary(const Path: AnsiString): Boolean;
var
  I: LongInt;
begin
  I := Length(Path);
  while (I >= 1) and not (Path[I] in ['/', '\']) do
    Dec(I);
  if (I + 1 <= Length(Path)) and (Path[I + 1] = '.') then
    Exit(True);
  Exit(False);
end;

function CopyFile(const SrcPath, DstPath: AnsiString): Boolean;
var
  SrcF, DstF: File;
  Buf: array[0 .. 1 shl 20] of Byte;
  Size: Int64;
begin
  Assign(SrcF, SrcPath);
{$PUSH}{$I-}
  Reset(SrcF, 1);
{$POP}
  if IOResult <> 0 then begin
    if IsTemporary(SrcPath) then begin // FIXME
      Writeln('dsync: warning: could not open ', SrcPath, ', skipped');
      Exit(True);
    end else begin
      Writeln('dsync: error: could not open ', SrcPath);
      Exit(False);
    end;
  end;
  Assign(DstF, DstPath);
{$PUSH}{$I-}
  Rewrite(DstF, 1);
{$POP}
  if IOResult <> 0 then begin
    Writeln('dsync: error: could not open ', DstPath);
    Close(SrcF);
    Exit(False);
  end;

  while not Eof(SrcF) do begin
{$PUSH}{$I-}
    BlockRead(SrcF, Buf[0], Length(Buf), Size);
{$POP}
    if IOResult <> 0 then begin
      Writeln('dsync: error: could not read ', SrcPath);
      Close(SrcF);
      Close(DstF);
      Exit(False);
    end;

{$PUSH}{$I-}
    BlockWrite(DstF, Buf[0], Size);
{$POP}
    if IOResult <> 0 then begin
      Writeln('dsync: error: could not write to ', DstPath);
      Close(SrcF);
      Close(DstF);
      Exit(False);
    end;
  end;

  Close(SrcF);
  Close(DstF);
  Exit(True);
end;

function SyncSource(const SrcPath, DstPath: AnsiString): Boolean;
var
  SrcAge, DstAge: LongInt;
begin
  SrcAge := SysUtils.FileAge(SrcPath);
  DstAge := SysUtils.FileAge(DstPath);
  if DstAge >= SrcAge then
    Exit(True);
  Exit(CopyFile(SrcPath, DstPath));
end;

function EnsureDirectory(const DstPath: AnsiString): Boolean;
begin
  if DirExists(DstPath) then
    Exit(True);
  if not MakeDirectory(DstPath) then begin
    Writeln(stderr, 'dsync: could not create a directory ', DstPath);
    Exit(False);
  end;
  Exit(True);
end;

function TrailingSync(const SrcPath, DstPath: AnsiString): Boolean;
var
  Info: TSearchRec;
begin
  if not EnsureDirectory(DstPath) then
    Exit(False);
  if FindFirst(SrcPath + '*', faAnyFile, Info) = 0 then begin
    repeat
      if (Info.Attr and faDirectory) = faDirectory then begin
        if (Info.Name <> '.') and (Info.Name <> '..') then begin
          if not TrailingSync(SrcPath + Info.Name + '/', DstPath + '/' + Info.Name) then begin
            FindClose(Info);
            Exit(False);
          end;
        end;
      end else begin
        if not SyncSource(SrcPath + Info.Name, DstPath + '/' + Info.Name) then begin
          FindClose(Info);
          Exit(False);
        end;
      end;
    until FindNext(Info) <> 0;
    FindClose(Info);
  end;
  Exit(True);
end;

var
  I: LongInt;

begin
  Opts.Help := False;
  if not ParseOpts then
    Halt(1);
  if Opts.Help then begin
    PrintUsage;
    Halt(0);
  end;
  if Length(Opts.SourcePaths) = 0 then begin
    Writeln('dsync: no input files');
    Halt(0);
  end;

  if not PrepareSync then
    Halt(1);

  if Length(Opts.SourcePaths) > 1 then begin
    if not IsDestDirectory then begin
      if not MakeDirectory(Opts.DestPath) then begin
        Writeln(stderr, 'dsync: error: could not create destination ', Opts.DestPath);
        Halt(1);
      end;
    end;
    for I := 0 to High(Opts.SourcePaths) do begin
      Writeln(Opts.SourcePaths[I], ' -> ', Opts.DestPath);
      if not SyncSource(Opts.SourcePaths[I], Opts.DestPath + '/' + Opts.SourcePaths[I]) then
        Halt(1);
    end;
  end else begin
    if Opts.SourcePaths[0][Length(Opts.SourcePaths[0])] = '/' then begin
      if not TrailingSync(Opts.SourcePaths[0], Opts.DestPath) then begin
        Halt(1);
      end;
    end else begin
      Writeln(stderr, 'dsync: error: unsupported mode');
      Halt(1);
    end;
  end;
end.
