unit Runner;

interface

uses
  Classes, SysUtils, UTF8Process, StdCtrls, ExtCtrls, Options;

type
  TFinishReason = (
    frFinished,
    frStopped,
    frFailure
  );

  TFinishedEvent = procedure(Sender: TObject; Reason: TFinishReason) of object;
  TProgressEvent = procedure(Sender: TObject; Index: Integer) of object;

  { TRunner }

  TRunner = class
  private
    FProcess: TProcessUTF8;
    FTimer: TTimer;
    FOutputMemo: TCustomMemo;
    FOnFinished: TFinishedEvent;
    FOnProgress: TProgressEvent;

    FInputPos: Integer;
    FFailures: Integer;
    FOptions: TOptions;
    FRunning: Boolean;
    FStopped: Boolean;

    procedure ReadProcessOutput;
    procedure TimerTicked(Sender: TObject);
    procedure RunNextItem(IsFirstRun: Boolean = False);
    procedure Finish(Reason: TFinishReason);
  public
    constructor Create(AOutputMemo: TCustomMemo);
    destructor Destroy; override;

    procedure Run(AOptions: TOptions);
    procedure Stop;

    property IsRunning: Boolean read FRunning;
    property Failures: Integer read FFailures;
    property InputPos: Integer read FInputPos;
    property OnFinished: TFinishedEvent read FOnFinished write FOnFinished;
    property OnProgress: TProgressEvent read FOnProgress write FOnProgress;
  end;

implementation

uses
  Process, Dialogs;

{ TRunner }

constructor TRunner.Create(AOutputMemo: TCustomMemo);
begin
  // Unfortunatelly, we cannot use TAsyncProcess since it does not work reliably on all platforms
  FProcess := TProcessUTF8.Create(nil);
  FProcess.Options := [poUsePipes, {$IFDEF MSWINDOWS}poNoConsole,{$ENDIF} poStderrToOutPut];

  FTimer := TTimer.Create(nil);
  FTimer.Enabled := False;
  FTimer.Interval := 50;
  FTimer.OnTimer := TimerTicked;

  FOutputMemo := AOutputMemo;
end;

destructor TRunner.Destroy;
begin
  FProcess.Free;
  FTimer.Free;
  inherited Destroy;
end;

procedure TRunner.ReadProcessOutput;
var
  BufStr: string;
begin
  while FProcess.Output.NumBytesAvailable > 0 do
  begin
    SetLength(BufStr, FProcess.Output.NumBytesAvailable);
    FProcess.Output.Read(BufStr[1], Length(BufStr));
    FOutputMemo.Append(BufStr);
  end;
end;

procedure TRunner.TimerTicked(Sender: TObject);
begin
  ReadProcessOutput;
  if not FProcess.Running then
    RunNextItem;
end;

procedure TRunner.RunNextItem(IsFirstRun: Boolean);
begin
  if not IsFirstRun and (FProcess.ExitCode <> 0) then
    Inc(FFailures);

  Inc(FInputPos);

  if FInputPos >= FOptions.Files.Count then
  begin
    if FFailures = 0 then
      Finish(frFinished)
    else
      Finish(frFailure);

    Exit;
  end;

  if FStopped then
  begin
    Finish(frStopped);
    Exit;
  end;

  if Assigned(FOnProgress) then
    FOnProgress(Self, FInputPos);

  FOptions.ToCmdLineParameters(FProcess.Parameters, FInputPos);

  try
    FProcess.Execute;
  except
    on Ex: Exception do
    begin
      FOutputMemo.Append(Ex.ClassName + ': ' + Ex.Message);
      Dialogs.MessageDlg('Failed to execute Deskew',
        'Deskew command line executable failed to start. Check that it is in the correct location ' +
        'and has the right permissions.' + sLineBreak + sLineBreak +
        'Executable path used: ' + FProcess.Executable,
        mtError, [mbOK], '');

      Finish(frFailure);
      Exit;
    end;
  end;

  if IsFirstRun then
    FTimer.Enabled := True;
end;

procedure TRunner.Finish(Reason: TFinishReason);
begin
  FTimer.Enabled := False;
  FRunning := False;
  if Assigned(FOnFinished) then
    FOnFinished(Self, Reason);
end;

procedure TRunner.Run(AOptions: TOptions);
begin
  FInputPos := -1;
  FFailures := 0;
  FOptions := AOptions;
  FStopped := False;
  FRunning := True;

  FProcess.Executable := FOptions.EffectiveExecutablePath;
  RunNextItem(True);
end;

procedure TRunner.Stop;
begin
  FStopped := True;
end;

end.

