unit uPlugin;
{$INCLUDE 'Plugin.inc'}

interface

uses
  Windows, SysUtils, Classes,
  uOptions,
  {$INCLUDE 'DelphiFarUnits.inc'} ;

type
  EFilterPluginError = class(EFarPluginError);

  TPlugin = class(TFarPlugin)
  private
    fOptions: TOptions;
    function GetOptions: TOptions;
  protected
    function PluginID: TGUID; override;
    function GetPluginTitle(out Description, Author: string): string; override;
    procedure GetPluginInfo(var Flags: TPluginFlags; var MainMenu, ConfigMenu, DiskMenu: TFarPlugin.TFarPluginMenu; var CommandPrefix: string); override;
    function GetCommonString(ID: TFarPlugin.TCommonString): string; override;
    function PluginConfig(GUID: PGUID; MenuItem: TIntPtr): boolean; override;
    function Execute(OpenFrom: DWORD; GUID: PGUID; Data: TIntPtr): THandle; override;
    procedure DoLog(const Msg: string); override;
    function ApplyFileNameTemplate(const FileName: string; Counter, NumberOfFiles: integer): string;
    function LeadingZeroes(const Str: string): string;
    procedure Replace(var Str: string; const Replacement: string; var Index: integer; RemoveChars: integer);
    function CountTrulySelectedFiles(Panel: TFarPanel): integer;
    function DoRenameFiles(Panel: TFarPanel; SaveNextStartingNumber: boolean): boolean;
    procedure RefreshOptions;
    property Options: TOptions read GetOptions;
  public
    constructor Create;
    destructor Destroy; override;
  end;

implementation

uses
  uOptionsUI,
  {$IFDEF DEBUG_LOGS} uLog, {$ENDIF}
  uSystem, uPluginConsts, uNewNameDialog, uMessages, uFiles;

const
  PLUGIN_TITLE = 'NewName3';
  PLUGIN_DESCRIPTION = 'New Name 3';
  PLUGIN_AUTHOR = 'Pepak, https://www.pepak.net';

{ TPlugin }

function TPlugin.ApplyFileNameTemplate(const FileName: string; Counter, NumberOfFiles: integer): string;
var
  i, n: Integer;
  C: Char;
begin
  Result := Options.FileNamePattern;
  i := 1;
  n := Length(Result);
  while i < n do
  begin
    if Result[i] <> '%' then
    begin
      Inc(i);
    end
    else
    begin
      C := Result[Succ(i)];
      case C of
        '#':      // Ordinal number of file
          Replace(Result, LeadingZeroes(IntToStr(Counter)), i, 2);
        'h':      // Ordinal number of file, hex, lower-case
          Replace(Result, LeadingZeroes(LowerCase(Format('%x', [Counter]))), i, 2);
        'H':      // Ordinal number of file, hex, upper-case
          Replace(Result, LeadingZeroes(UpperCase(Format('%x', [Counter]))), i, 2);
        't', 'T': // Total number of files
          Replace(Result, LeadingZeroes(IntToStr(NumberOfFiles)), i, 2);
        'f', 'F': // Filename and extension
          Replace(Result, FileName, i, 2);
        'n', 'N': // Filename without extension
          Replace(Result, ChangeFileExt(FileName, ''), i, 2);
        'e', 'E': // Extension (without the dot)
          Replace(Result, Copy(ExtractFileExt(FileName), 2, MaxInt), i, 2);
        else      // As-is
          Replace(Result, C, i, 2);
      end;
      n := Length(Result);
    end;
  end;
end;

function TPlugin.CountTrulySelectedFiles(Panel: TFarPanel): integer;
var
  i: integer;
begin
  Result := 0;
  for i := 0 to Pred(Panel.ItemCount) do
  begin
    if Panel.Items[i].Selected then
      Inc(Result);
  end;
end;

constructor TPlugin.Create;
begin
  inherited;
  fOptions := nil;
end;

destructor TPlugin.Destroy;
begin
  FreeAndNil(fOptions);
  inherited;
end;

procedure TPlugin.DoLog(const Msg: string);
begin
  {$IFDEF DEBUG_LOGS}
  uLog.Log(Msg);
  {$ENDIF}
end;

function TPlugin.DoRenameFiles(Panel: TFarPanel; SaveNextStartingNumber: boolean): boolean;
var
  Dir, SourceFN, DestFN, FailuresFN: string;
  i, NumberOfFiles, Counter: integer;
  FailuresStream: TTextStream;
  EditorID: TIntPtr;
begin
  Result := False;
  NumberOfFiles := CountTrulySelectedFiles(Panel);
  if (NumberOfFiles = 0) and Options.AllowRenameFileUnderCursor then
  begin
    if Panel.CurrentItemIndex >= 0 then
    begin
      Panel.Items[Panel.CurrentItemIndex].Selected := True;
      Inc(NumberOfFiles);
    end;
  end;
  if NumberOfFiles <= 0 then
    Exit;
  Result := True;
  Counter := Options.StartingNumber;
  FailuresFN := '';
  FailuresStream := nil;
  try
    for i := 0 to Pred(Panel.ItemCount) do
    begin
      if Panel.Items[i].Selected then
      begin
        Dir := FullFileName(IncludeTrailingPathDelimiter(Panel.Directory), True);
        SourceFN := Panel.Items[i].FileName;
        DestFN := ApplyFileNameTemplate(SourceFN, Counter, NumberOfFiles);
        Inc(Counter);
        //Log('Renaming "%s" to "%s"', [SourceFN, DestFN]);
        if (SourceFN = DestFn) or RenameFile(Dir + SourceFN, Dir + DestFN) then
        begin
          Panel.Items[i].Selected := False;
        end
        else
        begin
          Result := False;
          Dec(Counter);
          if FailuresStream = nil then
          begin
            FailuresFN := SystemTempFile(SystemTempDir, 'newname', '.txt');
            FailuresStream := TTextStream.Create(FailuresFN, fmCreate);
            FailuresStream.Encoding := {$IFDEF UNICODE} feUtf8 {$ELSE} feAnsi {$ENDIF} ;
            FailuresStream.WriteLine(GetMsg(MFailuresInfo));
          end;
          FailuresStream.WriteLine(AnsiQuotedStr(SourceFN, '"') + ' -> ' + AnsiQuotedStr(DestFN, '"'));
        end;
      end;
    end;
    if SaveNextStartingNumber and (Counter <> Options.StartingNumber) then
    begin
      Options.StartingNumberAsString := LeadingZeroes(IntToStr(Counter));
      Options.SavedPatterns.Add(Options.FileNamePattern, Options.StartingNumberAsString);
      Options.Save;
    end;
    if (FailuresStream <> nil) and FileExists(FailuresFN) then
    begin
      FreeAndNil(FailuresStream);
      TFarEditor.Show(EditorID, FailuresFN, GetMsg(MTitle), 0, 0, -1, -1,
        EF_NONMODAL or EF_IMMEDIATERETURN or EF_DELETEONLYFILEONCLOSE or EF_DISABLEHISTORY {$IFDEF FAR3_UP} or EF_DISABLESAVEPOS {$ENDIF} ,
        0, 1, {$IFDEF UNICODE} CP_UTF8 {$ELSE} CP_ANSI {$ENDIF}
        );
    end;
  finally
    FreeAndNil(FailuresStream);
  end;
end;

function TPlugin.Execute(OpenFrom: DWORD; GUID: PGUID; Data: TIntPtr): THandle;
var
  Panel: TFarPanel;
  SelectedItemsCount: integer;
  Dlg: TNewNameDialog;
begin
  Result := {$IFDEF FAR3_UP} 0 {$ELSE} INVALID_HANDLE_VALUE {$ENDIF} ;
  Options.Load;
  // Process from panels
  Panel := TFarPanel.Create(True);
  try
    SelectedItemsCount := Panel.SelectedItemsCount;
    if not Options.AllowRenameFileUnderCursor then
      SelectedItemsCount := CountTrulySelectedFiles(Panel);
    if SelectedItemsCount <= 0 then
    begin
      ShowMessage([GetMsg(MTitle), GetMsg(MNoFilesSelected), GetMsg(MOk)], FMSG_WARNING, 1);
      Exit;
    end;
    Dlg := TNewNameDialog.Create;
    try
      Dlg.Options := Options;
      Dlg.SavePatternSettings := Options.SavePatternSettings;
      if Dlg.Execute then
      begin
        Options.Save;
        DoRenameFiles(Panel, Dlg.SavePatternSettings);
        //Panel.Refresh;
        Panel.ApplySelection;
      end;
    finally
      FreeAndNil(Dlg);
    end;
  finally
    FreeAndNil(Panel);
  end;
end;

function TPlugin.GetCommonString(ID: TFarPlugin.TCommonString): string;
begin
  case ID of
    TFarPlugin.TCommonString.MTitle:
      Result := GetMsg(MTitle);
    TFarPlugin.TCommonString.MExceptionOccurredIn:
      Result := GetMsg(MExceptionOccurredIn);
    TFarPlugin.TCommonString.MOk:
      Result := GetMsg(MOk);
    TFarPlugin.TCommonString.MCancel:
      Result := GetMsg(MCancel);
    TFarPlugin.TCommonString.MYes:
      Result := GetMsg(MYes);
    TFarPlugin.TCommonString.MNo:
      Result := GetMsg(MNo);
    else
      Result := inherited GetCommonString(ID);
  end;
end;

function TPlugin.GetOptions: TOptions;
begin
  if fOptions = nil then
  begin
    fOptions := TOptions.Create;
    fOptions.Load;
  end;
  Result := fOptions;
end;

procedure TPlugin.GetPluginInfo(var Flags: TPluginFlags; var MainMenu, ConfigMenu, DiskMenu: TFarPlugin.TFarPluginMenu; var CommandPrefix: string);
begin
  SetLength(MainMenu, 1);
  MainMenu[0].Title := GetMsg(MTitle);
  MainMenu[0].ID := PLUGIN_MENU_MAIN;
  SetLength(ConfigMenu, 1);
  ConfigMenu[0].Title := GetMsg(MTitle);
  ConfigMenu[0].ID := PLUGIN_MENU_OPTIONS_GUID;
end;

function TPlugin.GetPluginTitle(out Description, Author: string): string;
begin
  Result := PLUGIN_TITLE;
  Description := PLUGIN_DESCRIPTION;
  Author := PLUGIN_AUTHOR;
end;

function TPlugin.LeadingZeroes(const Str: string): string;
var
  n: integer;
begin
  n := Options.StartingNumberLength - Length(Str);
  if n > 0 then
    Result := StringOfChar('0', n) + Str
  else
    Result := Str;
end;

function TPlugin.PluginConfig(GUID: PGUID; MenuItem: TIntPtr): boolean;
var
  Dialog: TOptionsDialog;
begin
  Result := False;
  RefreshOptions;
  Dialog := TOptionsDialog.Create;
  try
    Dialog.Options := Options;
    if Dialog.Execute then
    begin
      Options.Save;
      Result := True;
    end;
  finally
    FreeAndNil(Dialog);
  end;
end;

function TPlugin.PluginID: TGUID;
begin
  Result := PLUGIN_GUID;
end;

procedure TPlugin.RefreshOptions;
begin
  FreeAndNil(fOptions);
end;

procedure TPlugin.Replace(var Str: string; const Replacement: string; var Index: integer; RemoveChars: integer);
var
  i, n: integer;
begin
  n := Length(Replacement);
  // If I need to replace more characters than I have available, I add a dummy string
  if n > RemoveChars then
  begin
    System.Insert(StringOfChar('.', n-RemoveChars), Str, Index);
    RemoveChars := n;
  end;
  // Perform the actual replacement
  for i := 1 to n do
  begin
    Str[Index] := Replacement[i];
    Inc(Index);
  end;
  // If some extra characters are left from the original string, delete them
  if n < RemoveChars then
  begin
    System.Delete(Str, Index, RemoveChars-n);
  end;
end;

initialization
  TFarPlugin.InitInstance(TPlugin.Create);

end.
