{
   Seksi Commander
   ----------------------------
   Licence  : GNU GPL v 2.0
   Author   : Pavel Letko (letcuv@centrum.cz)

   Advanced multi rename tool

   contributors:

   Copyright (C) 2007-2014  Koblov Alexander (Alexx2000@mail.ru)
}

unit fMultiRename;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, Graphics, Forms, StdCtrls, Menus, SynRegExpr, Controls, LCLType,
  DCClassesUtf8, uClassesEx, uFile, uFileSource, StringHashList, Grids, ExtCtrls,
  DCXmlConfig, uOSForms;

type

  PMultiRenamePreset = ^TMultiRenamePreset;
  TMultiRenamePreset = record
    FileName: String;
    Extension: String;
    FileNameStyle: Integer;
    ExtensionStyle: Integer;
    Find: String;
    Replace: String;
    RegExp: Boolean;
    UseSubs: Boolean;
    Counter: String;
    Interval: String;
    Width: Integer;
    Log: Boolean;
    LogFile: String;
  end;

  { TfrmMultiRename }

  TfrmMultiRename = class(TAloneForm)
    btnLoadPreset: TButton;
    btnSavePreset: TButton;
    btnDeletePreset: TButton;
    cbRegExp: TCheckBox;
    cbUseSubs: TCheckBox;
    cmbExtensionStyle: TComboBox;
    cbPresets: TComboBox;
    miDay2: TMenuItem;
    miHour1: TMenuItem;
    miMinute1: TMenuItem;
    miSecond1: TMenuItem;
    pnlOptions: TPanel;
    StringGrid: TStringGrid;
    gbPresets: TGroupBox;
    gbMaska: TGroupBox;
    lbName: TLabel;
    lbExt: TLabel;
    edName: TEdit;
    edExt: TEdit;
    btnNameMenu: TButton;
    btnExtMenu: TButton;
    gbFindReplace: TGroupBox;
    lbFind: TLabel;
    lbReplace: TLabel;
    edFind: TEdit;
    edReplace: TEdit;
    cmbNameStyle: TComboBox;
    gbCounter: TGroupBox;
    lbStNb: TLabel;
    lbInterval: TLabel;
    lbWidth: TLabel;
    edPoc: TEdit;
    edInterval: TEdit;
    cmbxWidth: TComboBox;
    btnRename: TButton;
    btnClose: TButton;
    gbLog: TGroupBox;
    edFile: TEdit;
    cbLog: TCheckBox;
    btnRestore: TButton;
    miPlugin: TMenuItem;
    N5: TMenuItem;
    miDay3: TMenuItem;
    miDay1: TMenuItem;
    miMonth3: TMenuItem;
    miMonth2: TMenuItem;
    miMonth1: TMenuItem;
    miYear1: TMenuItem;
    ppNameMenu: TPopupMenu;
    miNextName: TMenuItem;
    miName: TMenuItem;
    miNameX: TMenuItem;
    miNameXX: TMenuItem;
    N1: TMenuItem;
    miNextExtension: TMenuItem;
    Extension: TMenuItem;
    miExtensionX: TMenuItem;
    miExtensionXX: TMenuItem;
    N2: TMenuItem;
    miCounter: TMenuItem;
    N3: TMenuItem;
    miNext: TMenuItem;
    miYear: TMenuItem;
    miMonth: TMenuItem;
    miDay: TMenuItem;
    N4: TMenuItem;
    miHour: TMenuItem;
    miMinute: TMenuItem;
    miSecond: TMenuItem;
    procedure btnLoadPresetClick(Sender: TObject);
    procedure btnSavePresetClick(Sender: TObject);
    procedure btnDeletePresetClick(Sender: TObject);
    procedure cbRegExpChange(Sender: TObject);
    procedure cmbNameStyleChange(Sender: TObject);
    procedure StringGridKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure StringGridMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure StringGridMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure StringGridSelection(Sender: TObject; aCol, aRow: Integer);
    procedure StringGridTopLeftChanged(Sender: TObject);
    procedure edPocChange(Sender: TObject);
    procedure edIntervalChange(Sender: TObject);
    procedure btnRenameClick(Sender: TObject);
    procedure btnCloseClick(Sender: TObject);
    procedure btnRestoreClick(Sender: TObject);
    procedure btnNameMenuClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure miDay1Click(Sender: TObject);
    procedure miDay2Click(Sender: TObject);
    procedure miDay3Click(Sender: TObject);
    procedure miDayClick(Sender: TObject);
    procedure miHour1Click(Sender: TObject);
    procedure miHourClick(Sender: TObject);
    procedure miMinute1Click(Sender: TObject);
    procedure miMinuteClick(Sender: TObject);
    procedure miMonth1Click(Sender: TObject);
    procedure miMonth2Click(Sender: TObject);
    procedure miMonth3Click(Sender: TObject);
    procedure miMonthClick(Sender: TObject);
    procedure miSecond1Click(Sender: TObject);
    procedure miSecondClick(Sender: TObject);
    procedure miYear1Click(Sender: TObject);
    procedure miYearClick(Sender: TObject);
    procedure miPluginClick(Sender: TObject);
    procedure NameClick(Sender: TObject);
    procedure NameXClick(Sender: TObject);
    procedure NameXXClick(Sender: TObject);
    procedure ExtensionClick(Sender: TObject);
    procedure CounterClick(Sender: TObject);
    procedure btnExtMenuClick(Sender: TObject);
    procedure cbLogClick(Sender: TObject);
    procedure ExtensionXClick(Sender: TObject);
    procedure ExtensionXXClick(Sender: TObject);
 private
    IniPropStorage: TIniPropStorageEx;
    FLastPreset: String;
    FFileSource: IFileSource;
    FFiles: TFiles;
    FPresets: TStringHashList; // of PMultiRenamePreset
    FSourceRow: Integer;
    FMoveRow : Boolean;

    {Handles a single formatting string}
    function sHandleFormatString(const sFormatStr: string; ItemNr: Integer): string;
    {Function sReplace call sReplaceXX with parametres}
    function sReplace(sMask: string; ItemNr: Integer): string;
    {sReplaceXX doing N, Nx, Nx:y and E, Ex, Ex:y}
    function sReplaceXX(const sFormatStr, sOrig: string): string;
    {InsertMask is for write key symbols from buttons}
    procedure InsertMask(const Mask:string;edChoose:Tedit);
    procedure InsertMask(const Mask:string;editNr:PtrInt);
    {Get new file name for file with ItemIndex}
    function FreshText(ItemIndex: Integer): String;
    {Executes the main operation of renaming files}
    procedure RenameFiles;
    {Changes first char to uppercase and the rest to lowercase}
    function FirstCharToUppercaseUTF8(InputString: String): String;
    {Changes first char of first word to uppercase and the rest to lowercase}
    function FirstCharOfFirstWordToUppercaseUTF8(InputString: String): String;
    {Changes first char of every word to uppercase and the rest to lowercase}
    function FirstCharOfEveryWordToUppercaseUTF8(InputString: String): String;
    {Returns true if a byte represents a letter.}
    function IsLetter(AChar: AnsiChar): Boolean;
    {Applies style (uppercase, lowercase, etc.) to a string}
    function ApplyStyle(InputString: String; Style: Integer): String;
    {Load preset configuration}
    procedure LoadPresets;
    procedure LoadPresetsXml(AConfig: TXmlConfig);
    {Save preset configuration}
    procedure SavePresets;
    procedure SavePresetsXml(AConfig: TXmlConfig);
    {Loads specified preset from the configuration}
    procedure LoadPreset(PresetName: String);
    {Saves specified preset to the configuration}
    procedure SavePreset(PresetName: String);
    {Delete specified preset from configuration}
    procedure DeletePreset(PresetName: String);
    {Fills presets list with preset from configuration}
    procedure FillPresetsList;
    {Removes all presets from the presets list}
    procedure ClearPresetsList;
  public
    { Public declarations }
    constructor Create(TheOwner: TComponent; aFileSource: IFileSource; var aFiles: TFiles); reintroduce;
    destructor Destroy; override;

    // Temporary for switching configuration from INI to XML
    procedure PublicSavePresets;
    procedure LoadPresetsIni(IniFile: TIniFileEx);
    procedure SavePresetsIni(IniFile: TIniFileEx);
  end;

{initialization function}
  function ShowMultiRenameForm(aFileSource: IFileSource; var aFiles: TFiles):Boolean;

implementation

{$R *.lfm}

uses
  LCLProc, FileUtil, uDebug, uLng, uGlobs, uFileProcs, DCOSUtils, DCStrUtils,
  fSelectTextRange, uShowMsg, uFileSourceUtil, uFileProperty, uFileFunctions;

const
  sPresetsSection = 'MultiRenamePresets';

function ShowMultiRenameForm(aFileSource: IFileSource; var aFiles: TFiles):Boolean;
begin
  Result:= True;
  try
    with TfrmMultiRename.Create(Application, aFileSource, aFiles) do
    begin
      Show;
    end;
  except
    Result:= False;
  end;
end;

constructor TfrmMultiRename.Create(TheOwner: TComponent; aFileSource: IFileSource; var aFiles: TFiles);
begin
  FPresets := TStringHashList.Create(False);
  FFileSource := aFileSource;
  FFiles := aFiles;
  aFiles := nil;
  FSourceRow := -1;
  FMoveRow := False;
  inherited Create(TheOwner);
end;

destructor TfrmMultiRename.Destroy;
begin
  inherited Destroy;
  ClearPresetsList;
  FreeAndNil(FPresets);
  if Assigned(FFiles) then
    FreeAndNil(FFiles);
end;

procedure TfrmMultiRename.FormCreate(Sender: TObject);
begin
  // Localize File name style ComboBox
  ParseLineToList(rsMulRenFileNameStyleList, cmbNameStyle.Items);
  ParseLineToList(rsMulRenFileNameStyleList, cmbExtensionStyle.Items);

  // Set row count
  StringGrid.RowCount:= FFiles.Count + 1;
  StringGrid.FocusRectVisible := False;

  // Initialize property storage
  IniPropStorage:= InitPropStorage(Self);
  IniPropStorage.StoredValues.Add.DisplayName:= 'lsvwFile_Columns.Item0_Width';
  IniPropStorage.StoredValues.Add.DisplayName:= 'lsvwFile_Columns.Item1_Width';
  IniPropStorage.StoredValues.Add.DisplayName:= 'lsvwFile_Columns.Item2_Width';

  // Set default values for controls.
  btnRestoreClick(nil);

  // Initialize presets.
  LoadPresets;
  FillPresetsList;
  cbPresets.Text := FLastPreset;
  LoadPreset(FLastPreset);
end;

procedure TfrmMultiRename.FormShow(Sender: TObject);
begin
  with StringGrid.Columns do
  begin
    Items[0].Width:= StrToIntDef(IniPropStorage.StoredValue['lsvwFile_Columns.Item0_Width'], Items[0].Width);
    Items[1].Width:= StrToIntDef(IniPropStorage.StoredValue['lsvwFile_Columns.Item1_Width'], Items[1].Width);
    Items[2].Width:= StrToIntDef(IniPropStorage.StoredValue['lsvwFile_Columns.Item2_Width'], Items[2].Width);
  end;
end;

procedure TfrmMultiRename.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  SavePresets;

  CloseAction:= caFree;
  with StringGrid.Columns do
  begin
    IniPropStorage.StoredValue['lsvwFile_Columns.Item0_Width']:= IntToStr(Items[0].Width);
    IniPropStorage.StoredValue['lsvwFile_Columns.Item1_Width']:= IntToStr(Items[1].Width);
    IniPropStorage.StoredValue['lsvwFile_Columns.Item2_Width']:= IntToStr(Items[2].Width);
  end;
end;

procedure TfrmMultiRename.miDay1Click(Sender: TObject);
begin
  InsertMask('[DD]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miDay2Click(Sender: TObject);
begin
  InsertMask('[DDD]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miDay3Click(Sender: TObject);
begin
  InsertMask('[DDDD]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miDayClick(Sender: TObject);
begin
  InsertMask('[D]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miHour1Click(Sender: TObject);
begin
  InsertMask('[hh]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miHourClick(Sender: TObject);
begin
  InsertMask('[h]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMinute1Click(Sender: TObject);
begin
  InsertMask('[nn]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMinuteClick(Sender: TObject);
begin
  InsertMask('[n]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMonth1Click(Sender: TObject);
begin
  InsertMask('[MM]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMonth2Click(Sender: TObject);
begin
  InsertMask('[MMM]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMonth3Click(Sender: TObject);
begin
  InsertMask('[MMMM]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miMonthClick(Sender: TObject);
begin
  InsertMask('[M]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miSecond1Click(Sender: TObject);
begin
  InsertMask('[ss]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miSecondClick(Sender: TObject);
begin
  InsertMask('[s]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miYear1Click(Sender: TObject);
begin
  InsertMask('[YYYY]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.miYearClick(Sender: TObject);
begin
  InsertMask('[Y]',ppNameMenu.Tag);
end;

function TfrmMultiRename.FreshText(ItemIndex: Integer): String;
var
  bError: Boolean;
  sTmpName, sTmpExt: String;
begin
  bError:= False;

  // Use mask
  sTmpName:=sReplace(edName.Text, ItemIndex);
  sTmpExt:=sReplace(edExt.Text, ItemIndex);

  // Join
  Result := sTmpName;
  if sTmpExt <> '' then
    Result := Result + '.' + sTmpExt;

  // Find and replace
  if cbRegExp.Checked and (edFind.Text <> '') then
    try
      Result:= ReplaceRegExpr(edFind.Text, Result, edReplace.Text, cbUseSubs.Checked);
    except
      Result:= rsMsgErrRegExpSyntax;
      bError:= True;
    end
  else
    Result:=StringReplace(Result,edFind.Text,edReplace.Text,[rfReplaceAll,rfIgnoreCase]);

  // File name style
  sTmpName := ExtractOnlyFileName(Result);
  sTmpExt  := ExtractFileExt(Result);

  sTmpName := ApplyStyle(sTmpName, cmbNameStyle.ItemIndex);
  sTmpExt  := ApplyStyle(sTmpExt, cmbExtensionStyle.ItemIndex);

  Result := sTmpName + sTmpExt;

  btnRename.Enabled:= not bError;
  if bError then
    begin
      edFind.Color := clRed;
      edFind.Font.Color := clWhite;
    end
  else
    begin
      edFind.Color := clWindow;
      edFind.Font.Color := clWindowText;
    end;
end;

procedure TfrmMultiRename.cmbNameStyleChange(Sender: TObject);
begin
  StringGridTopLeftChanged(StringGrid);
end;

procedure TfrmMultiRename.StringGridKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  tmpFile: TFile;
  DestRow: Integer;
begin

  DestRow := StringGrid.Row;

  if (Shift = [ssShift]) then
  begin
    case Key of
      VK_UP: begin DestRow := StringGrid.Row - 1; end;
      VK_DOWN: begin DestRow := StringGrid.Row + 1 end;
    end;

    if (DestRow <> StringGrid.Row) and (0 < DestRow) and (DestRow < StringGrid.RowCount) then
      begin
        tmpFile := FFiles.Items[DestRow - 1];
        FFiles.Items[DestRow - 1] := FFiles.Items[StringGrid.Row - 1];
        FFiles.Items[StringGrid.Row - 1] := tmpFile;

        StringGridTopLeftChanged(StringGrid);
      end;
  end;
end;

procedure TfrmMultiRename.StringGridMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  SourceCol: Integer;
begin
  if(Button = mbLeft) then
  begin
    StringGrid.MouseToCell(X, Y, SourceCol, FSourceRow);
    if (FSourceRow > 0) then
    begin
      FMoveRow := True;
    end;
  end;
end;

procedure TfrmMultiRename.StringGridMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbLeft then
  begin
    FMoveRow := False;
  end;
end;

procedure TfrmMultiRename.StringGridSelection(Sender: TObject; aCol,
  aRow: Integer);
var
  tmpFile: TFile;
begin
  if FMoveRow and (aRow <> FSourceRow)then
    begin
      tmpFile := FFiles.Items[aRow-1];
      FFiles.Items[aRow-1] := FFiles.Items[FSourceRow-1];
      FFiles.Items[FSourceRow-1] := tmpFile;

      FSourceRow := aRow;
      StringGridTopLeftChanged(StringGrid);
    end;
end;

procedure TfrmMultiRename.StringGridTopLeftChanged(Sender: TObject);
var
  I, iRowCount: Integer;
begin
  iRowCount:= StringGrid.TopRow + StringGrid.VisibleRowCount;
  if iRowCount > FFiles.Count then iRowCount:= FFiles.Count;
  for I:= StringGrid.TopRow to iRowCount do
  begin
    StringGrid.Cells[0, I]:= FFiles[I - 1].Name;
    StringGrid.Cells[1, I]:= FreshText(I - 1);
    StringGrid.Cells[2, I]:= FFiles[I - 1].Path;
  end;
end;

procedure TfrmMultiRename.cbRegExpChange(Sender: TObject);
begin
  if cbRegExp.Checked then
    cbUseSubs.Checked:= Boolean(cbUseSubs.Tag)
  else
    begin
      cbUseSubs.Tag:= Integer(cbUseSubs.Checked);
      cbUseSubs.Checked:= False;
    end;
  cbUseSubs.Enabled:= cbRegExp.Checked;
  StringGridTopLeftChanged(StringGrid);
end;

procedure TfrmMultiRename.btnLoadPresetClick(Sender: TObject);
begin
  LoadPreset(cbPresets.Text);
end;

procedure TfrmMultiRename.btnSavePresetClick(Sender: TObject);
begin
  if cbPresets.Text <> '' then
  begin
    if FPresets.Find(cbPresets.Text) <> -1 then
    begin
      if msgYesNo(Format(rsMsgPresetAlreadyExists, [cbPresets.Text])) = False then
        Exit;
    end;

    SavePreset(cbPresets.Text);

    if cbPresets.Items.IndexOf(cbPresets.Text) = -1 then
      cbPresets.Items.Add(cbPresets.Text);
  end;
end;

procedure TfrmMultiRename.btnDeletePresetClick(Sender: TObject);
var
  Index: Integer;
begin
  if cbPresets.Text <> '' then
  begin
    DeletePreset(cbPresets.Text);

    Index := cbPresets.Items.IndexOf(cbPresets.Text);
    if Index <> -1 then
      cbPresets.Items.Delete(Index);

    cbPresets.Text := '';
  end;
end;

procedure TfrmMultiRename.edPocChange(Sender: TObject);
var
  c:integer;
begin
  c:=StrToIntDef(edPoc.Text,maxint);
  if c=MaxInt then
    with edPoc do              //editbox only for numbers
    begin
       Text:='1';
       SelectAll;
    end;
  StringGridTopLeftChanged(StringGrid);
end;

procedure TfrmMultiRename.edIntervalChange(Sender: TObject);
var
  c:integer;
begin
  c:=StrToIntDef(edInterval.Text,maxint);
  if c=MaxInt then
    with edInterval do         //editbox only for numbers
    begin
       Text:='1';
       SelectAll;
    end;
  StringGridTopLeftChanged(StringGrid);
end;

procedure TfrmMultiRename.InsertMask(const Mask:string;edChoose:Tedit);
var
  sTmp: String;
  I: Integer;
begin
  if edChoose.SelLength > 0 then
    edChoose.SelText:= Mask // Replace selected text
  else
    begin
      sTmp:= edChoose.Text;
      I:= edChoose.SelStart + 1;  // Insert on current position
      UTF8Insert(Mask, sTmp, I);
      Inc(I, UTF8Length(Mask));
      edChoose.Text:= sTmp;
      edChoose.SelStart:= I - 1;
    end;
end;

procedure TfrmMultiRename.InsertMask(const Mask:string;editNr:PtrInt);
begin
  if editNr = 0 then
    InsertMask(Mask, edName)
  else
    InsertMask(Mask, edExt);
end;

procedure TfrmMultiRename.btnRestoreClick(Sender: TObject);
begin
  edName.Text:='[N]';
  edName.SelStart:= UTF8Length(edName.Text);
  edExt.Text:='[E]';
  edExt.SelStart:= UTF8Length(edExt.Text);
  edFind.Text:='';
  edReplace.Text:='';
  cbRegExp.Checked:=False;
  cbUseSubs.Checked:=False;
  cmbNameStyle.ItemIndex:=0;
  cmbExtensionStyle.ItemIndex:=0;
  edPoc.Text:='1';
  edInterval.Text:='1';
  cmbxWidth.ItemIndex:=0;
  cbLog.Checked:=False;
  edFile.Enabled:=cbLog.Checked;
  if (FFiles.Count > 0) then
    edFile.Text:= FFiles[0].Path + 'default.log'
  else
    edFile.Text:= 'default.log';
  edFile.SelStart:= UTF8Length(edFile.Text);
  cbPresets.Text:='';
  FLastPreset:='';
end;

function TfrmMultiRename.sHandleFormatString(const sFormatStr: string; ItemNr: Integer): string;
var
  aFile: TFile;
  Index: Integer;
  Counter: Int64;
begin
  Result := '';
  if Length(sFormatStr) > 0 then
  begin
    aFile := FFiles[ItemNr];
    case sFormatStr[1] of
      '[',']':
        begin
          Result := sFormatStr;
        end;
      'N':
        begin
          Result := sReplaceXX(sFormatStr, aFile.NameNoExt);
        end;
      'E':
        begin
          Result := sReplaceXX(sFormatStr, aFile.Extension);
        end;
      'C':
        begin
          Counter := StrToInt64Def(edPoc.Text, 1) +
                     StrToInt64Def(edInterval.Text, 1) * ItemNr;
          Result := Format('%.' + cmbxWidth.Items[cmbxWidth.ItemIndex] + 'd', [Counter]);
        end;
      '=':
        begin
          Result := FormatFileFunction(UTF8Copy(sFormatStr, 2, UTF8Length(sFormatStr) - 1), FFiles.Items[ItemNr], FFileSource, True);
          for Index := 1 to Length(Result) - 1 do
          begin
            if Result[Index] in ['\', '/', ':', '*', '?', '"', '<', '>', '|'] then
              Result[Index] := '.';
          end;
        end;
      else
      begin
        // Assume it is date/time formatting string ([h][n][s][Y][M][D]).
        with FFiles.Items[ItemNr] do
          if fpModificationTime in SupportedProperties then
          try
            Result := SysToUTF8(FormatDateTime(sFormatStr, ModificationTime));
          except
            Result := sFormatStr;
          end;
      end;
    end;
  end;
end;

function TfrmMultiRename.sReplace(sMask: string; ItemNr: Integer): string;
var
  iStart, iEnd: Integer;
begin
  Result := '';
  while Length(sMask) > 0 do
  begin
    iStart := Pos('[', sMask);
    if iStart > 0 then
    begin
      iEnd := Pos(']', sMask);
      if iEnd > 0 then
      begin
        Result := Result + Copy(sMask, 1, iStart - 1) +
                  sHandleFormatString(Copy(sMask, iStart + 1, iEnd - iStart - 1), ItemNr);
        Delete(sMask, 1, iEnd);
      end
      else
        Break;
    end
    else
      Break;
  end;
  Result := Result + sMask;
end;

function TfrmMultiRename.sReplaceXX(const sFormatStr, sOrig: string): string;
var
  iFrom, iTo, iSemiColon: Integer;
begin
  if Length(sFormatStr) = 1 then
    Result := sOrig
  else
  begin
    iSemiColon := Pos(':', sFormatStr);
    if iSemiColon = 0 then
    begin
      iFrom := StrToIntDef(Copy(sFormatStr, 2, MaxInt), 1);
      iTo   := iFrom;
    end
    else
    begin
      iFrom := StrToIntDef(Copy(sFormatStr, 2, iSemiColon - 2), 1);
      iTo   := StrToIntDef(Copy(sFormatStr, iSemiColon + 1, MaxInt), MaxInt);
    end;
    Result := UTF8Copy(sOrig, iFrom, iTo - iFrom + 1);
  end;
end;

procedure TfrmMultiRename.btnNameMenuClick(Sender: TObject);
begin
  ppNameMenu.AutoPopup:= False;
  FillContentFieldMenu(miPlugin, @miPluginClick);
  btnNameMenu.PopupMenu.PopUp;
  ppNameMenu.Tag:= 0;
end;

procedure TfrmMultiRename.btnExtMenuClick(Sender: TObject);
begin
  ppNameMenu.AutoPopup:= False;
  FillContentFieldMenu(miPlugin, @miPluginClick);
  btnExtMenu.PopupMenu.PopUp;
  ppNameMenu.Tag:= 1;
end;

procedure TfrmMultiRename.NameClick(Sender: TObject);
begin
  InsertMask('[N]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.NameXClick(Sender: TObject);
var
  ASelection: TPoint;
begin
  if ShowSelectTextRangeDlg(Self, Caption, FFiles[0].NameNoExt, ASelection) then
  begin
    InsertMask('[N' + IntToStr(ASelection.X) + ']', ppNameMenu.Tag);
  end;
end;

procedure TfrmMultiRename.NameXXClick(Sender: TObject);
var
  ASelection: TPoint;
begin
  if ShowSelectTextRangeDlg(Self, Caption, FFiles[0].NameNoExt, ASelection) then
  begin
    InsertMask('[N' + IntToStr(ASelection.X) + ':' + IntToStr(ASelection.Y) + ']', ppNameMenu.Tag);
  end;
end;

procedure TfrmMultiRename.ExtensionClick(Sender: TObject);
begin
  InsertMask('[E]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.ExtensionXClick(Sender: TObject);
var
  ASelection: TPoint;
begin
  if ShowSelectTextRangeDlg(Self, Caption, FFiles[0].Extension, ASelection) then
  begin
    InsertMask('[E' + IntToStr(ASelection.X) + ']', ppNameMenu.Tag);
  end;
end;

procedure TfrmMultiRename.ExtensionXXClick(Sender: TObject);
var
  ASelection: TPoint;
begin
  if ShowSelectTextRangeDlg(Self, Caption, FFiles[0].Extension, ASelection) then
  begin
    InsertMask('[E' + IntToStr(ASelection.X) + ':' + IntToStr(ASelection.Y) + ']', ppNameMenu.Tag);
  end;
end;

procedure TfrmMultiRename.miPluginClick(Sender: TObject);
var
  sMask: String;
  MenuItem: TMenuItem absolute Sender;
begin
  case MenuItem.Tag of
    0:  begin
          sMask := '[=DC().' + MenuItem.Hint + '{}]';
        end;
    1: begin
          sMask := '[=Plugin(' + MenuItem.Parent.Caption + ').' + MenuItem.Caption + '{}]';
       end;
    2: begin
          sMask := '[=Plugin(' + MenuItem.Parent.Parent.Caption + ').' + MenuItem.Parent.Caption + '{' + MenuItem.Caption + '}]';
       end;
  end;
  InsertMask(sMask, ppNameMenu.Tag);
end;

procedure TfrmMultiRename.CounterClick(Sender: TObject);
begin
  InsertMask('[C]',ppNameMenu.Tag);
end;

procedure TfrmMultiRename.cbLogClick(Sender: TObject);
begin
  edFile.Enabled:=not edFile.Enabled;
end;

procedure TfrmMultiRename.btnRenameClick(Sender: TObject);
begin
  RenameFiles;
end;

procedure TfrmMultiRename.btnCloseClick(Sender: TObject);
begin
  Close;
end;

procedure TfrmMultiRename.RenameFiles;
var
  hFile: THandle;
  c: Integer;
  sNewName,
  sResult: String;
begin
  try
    if cbLog.Checked then
    begin
      if edFile.Text = EmptyStr then
        edFile.Text:= FFiles[0].Path + 'default.log';
      mbForceDirectory(ExtractFileDir(edFile.Text));

      if mbFileExists(edFile.Text) then
        begin
          hFile:= mbFileOpen(edFile.Text, fmOpenReadWrite);
          FileTruncate(hFile, 0);
        end
      else
        begin
          hFile:= mbFileCreate(edFile.Text);
        end;
    end;
    for c:= 0 to FFiles.Count - 1 do
    begin
      sResult:= FFiles[c].Name;
      sNewName:= FreshText(c);
      if RenameFile(FFileSource, FFiles[c], sNewName, True) = True then
        begin
          FFiles[c].Name := sNewName; // Write new name to the file object
          sResult := 'OK    ' + sResult + ' -> ' + sNewName;
        end
      else
        begin
          sResult := 'FAILED' + sResult + ' -> ' + sNewName;
        end;

      if cbLog.Checked then
        FileWriteLn(hFile, sResult);
    end;
  finally
    if cbLog.Checked then
      FileClose(hFile);
  end;

  StringGridTopLeftChanged(StringGrid);
end;

function TfrmMultiRename.FirstCharToUppercaseUTF8(InputString: String): String;
var
  FirstChar: String;
begin
  if UTF8Length(InputString) > 0 then
    begin
      Result := UTF8LowerCase(InputString);
      FirstChar := UTF8Copy(Result, 1, 1);
      UTF8Delete(Result, 1, 1);
      Result := UTF8UpperCase(FirstChar) + Result;
    end
  else
    Result := '';
end;

function TfrmMultiRename.FirstCharOfFirstWordToUppercaseUTF8(InputString: String): String;
var
  SeparatorPos: Integer;
begin
  InputString := UTF8LowerCase(InputString);
  Result := '';

  // Search for first letter.
  for SeparatorPos := 1 to Length(InputString) do
    if IsLetter(InputString[SeparatorPos]) then
      break;

  Result := Copy(InputString, 1, SeparatorPos - 1)
          + FirstCharToUppercaseUTF8(Copy(InputString, SeparatorPos, Length(InputString) - SeparatorPos + 1));
end;

function TfrmMultiRename.FirstCharOfEveryWordToUppercaseUTF8(InputString: String): String;
var
  SeparatorPos: Integer;
begin
  InputString := UTF8LowerCase(InputString);
  Result := '';

  while InputString <> '' do
  begin
    // Search for first non-letter (word separator).
    for SeparatorPos := 1 to Length(InputString) do
      if not IsLetter(InputString[SeparatorPos]) then
        break;

    Result := Result
            + FirstCharToUppercaseUTF8(Copy(InputString, 1, SeparatorPos));

    Delete(InputString, 1, SeparatorPos);
  end;
end;

function TfrmMultiRename.IsLetter(AChar: AnsiChar): Boolean;
begin
  Result :=  // Ascii letters
             ( (AChar < #128)
               and
               (((AChar >= 'a') and (AChar <= 'z')) or
                ((AChar >= 'A') and (AChar <= 'Z'))) )
         or
             // maybe Ansi or UTF8
             (AChar >= #128);
end;

function TfrmMultiRename.ApplyStyle(InputString: String; Style: Integer): String;
begin
  case Style of
    1: Result := UTF8UpperCase(InputString);
    2: Result := UTF8LowerCase(InputString);
    3: Result := FirstCharOfFirstWordToUppercaseUTF8(InputString);
    4: Result := FirstCharOfEveryWordToUppercaseUTF8(InputString);
    else
       Result := InputString;
  end;
end;

procedure TfrmMultiRename.LoadPresets;
begin
  if Assigned(gIni) then
    LoadPresetsIni(gIni)
  else
    LoadPresetsXml(gConfig);
end;

procedure TfrmMultiRename.LoadPresetsIni(IniFile: TIniFileEx);
var
  i: Integer;
  PresetIndex: Integer;
  PresetName: String;
  sPresetNr: String;
  PresetsCount: Integer;
begin
  ClearPresetsList;

  FLastPreset := IniFile.ReadString(sPresetsSection, 'LastPreset', '');
  PresetsCount := IniFile.ReadInteger(sPresetsSection, 'Presets', -1);

  for i := 0 to PresetsCount - 1 do
  begin
    sPresetNr := 'Preset' + IntToStr(I + 1);

    PresetName := IniFile.ReadString(sPresetsSection, sPresetNr + 'PresetName', '');
    if PresetName <> '' then
    begin
      PresetIndex := FPresets.Add(PresetName, New(PMultiRenamePreset));

      with PMultiRenamePreset(FPresets.List[PresetIndex]^.Data)^ do
      begin
        FileName := IniFile.ReadString(sPresetsSection, sPresetNr + 'Filename', '[N]');
        Extension := IniFile.ReadString(sPresetsSection, sPresetNr + 'Extension', '[E]');
        FileNameStyle := IniFile.ReadInteger(sPresetsSection, sPresetNr + 'FilenameStyle', 0);
        ExtensionStyle := IniFile.ReadInteger(sPresetsSection, sPresetNr + 'ExtensionStyle', 0);
        Find := IniFile.ReadString(sPresetsSection, sPresetNr + 'Find', '');
        Replace := IniFile.ReadString(sPresetsSection, sPresetNr + 'Replace', '');
        RegExp := IniFile.ReadBool(sPresetsSection, sPresetNr + 'RegExp', False);
        UseSubs := IniFile.ReadBool(sPresetsSection, sPresetNr + 'UseSubs', False);
        Counter := IniFile.ReadString(sPresetsSection, sPresetNr + 'Counter', '1');
        Interval := IniFile.ReadString(sPresetsSection, sPresetNr + 'Interval', '1');
        Width := IniFile.ReadInteger(sPresetsSection, sPresetNr + 'Width', 0);
        Log := IniFile.ReadBool(sPresetsSection, sPresetNr + 'Log', False);
        LogFile := IniFile.ReadString(sPresetsSection, sPresetNr + 'LogFile', '');
      end;
    end;
  end;
end;

procedure TfrmMultiRename.LoadPresetsXml(AConfig: TXmlConfig);
var
  PresetName: String;
  APreset: PMultiRenamePreset;
  ANode: TXmlNode;
begin
  ClearPresetsList;

  ANode := AConfig.FindNode(AConfig.RootNode, sPresetsSection);
  FLastPreset := AConfig.GetValue(ANode, 'LastPreset', '');

  ANode := AConfig.FindNode(ANode, 'Presets');
  if Assigned(ANode) then
  begin
    ANode := ANode.FirstChild;
    while Assigned(ANode) do
    begin
      if ANode.CompareName('Preset') = 0 then
      begin
        if AConfig.TryGetValue(ANode, 'Name', PresetName) then
        begin
          APreset := New(PMultiRenamePreset);
          FPresets.Add(PresetName, APreset);
          with APreset^ do
          begin
            FileName := AConfig.GetValue(ANode, 'Filename', '[N]');
            Extension := AConfig.GetValue(ANode, 'Extension', '[E]');
            FileNameStyle := AConfig.GetValue(ANode, 'FilenameStyle', 0);
            ExtensionStyle := AConfig.GetValue(ANode, 'ExtensionStyle', 0);
            Find := AConfig.GetValue(ANode, 'Find', '');
            Replace := AConfig.GetValue(ANode, 'Replace', '');
            RegExp := AConfig.GetValue(ANode, 'RegExp', False);
            UseSubs := AConfig.GetValue(ANode, 'UseSubs', False);
            Counter := AConfig.GetValue(ANode, 'Counter', '1');
            Interval := AConfig.GetValue(ANode, 'Interval', '1');
            Width := AConfig.GetValue(ANode, 'Width', 0);
            Log := AConfig.GetValue(ANode, 'Log/Enabled', False);
            LogFile := AConfig.GetValue(ANode, 'Log/File', '');
          end;
        end
        else
          DCDebug('Invalid entry in configuration: ' + AConfig.GetPathFromNode(ANode) + '.');
      end;
      ANode := ANode.NextSibling;
    end;
  end;
end;

procedure TfrmMultiRename.SavePresets;
begin
  if Assigned(gIni) then
    SavePresetsIni(gIni);
  SavePresetsXml(gConfig);
  gConfig.Save;
end;

procedure TfrmMultiRename.SavePresetsIni(IniFile: TIniFileEx);
var
  i: Integer;
  sPresetNr: String;
begin
  IniFile.EraseSection(sPresetsSection);
  IniFile.WriteString(sPresetsSection, 'LastPreset', FLastPreset);
  IniFile.WriteInteger(sPresetsSection, 'Presets', FPresets.Count);

  for i := 0 to FPresets.Count - 1 do
    with PMultiRenamePreset(FPresets.List[i]^.Data)^ do
    begin
      sPresetNr := 'Preset' + IntToStr(I + 1);

      IniFile.WriteString(sPresetsSection, sPresetNr + 'PresetName', FPresets.List[i]^.Key);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Filename', FileName);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Extension', Extension);
      IniFile.WriteInteger(sPresetsSection, sPresetNr + 'FilenameStyle', FileNameStyle);
      IniFile.WriteInteger(sPresetsSection, sPresetNr + 'ExtensionStyle', ExtensionStyle);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Find', Find);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Replace', Replace);
      IniFile.WriteBool(sPresetsSection, sPresetNr + 'RegExp', RegExp);
      IniFile.WriteBool(sPresetsSection, sPresetNr + 'UseSubs', UseSubs);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Counter', Counter);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'Interval', Interval);
      IniFile.WriteInteger(sPresetsSection, sPresetNr + 'Width', Width);
      IniFile.WriteBool(sPresetsSection, sPresetNr + 'Log', Log);
      IniFile.WriteString(sPresetsSection, sPresetNr + 'LogFile', LogFile);
    end;
end;

procedure TfrmMultiRename.SavePresetsXml(AConfig: TXmlConfig);
var
  i: Integer;
  ANode, SubNode: TXmlNode;
begin
  ANode := AConfig.FindNode(AConfig.RootNode, sPresetsSection, True);
  AConfig.ClearNode(ANode);
  AConfig.SetValue(ANode, 'LastPreset', FLastPreset);

  ANode := AConfig.FindNode(ANode, 'Presets', True);
  for i := 0 to FPresets.Count - 1 do
  begin
    SubNode := AConfig.AddNode(ANode, 'Preset');
    with PMultiRenamePreset(FPresets.List[i]^.Data)^ do
    begin
      AConfig.AddValue(SubNode, 'Name', FPresets.List[i]^.Key);
      AConfig.AddValue(SubNode, 'Filename', FileName);
      AConfig.AddValue(SubNode, 'Extension', Extension);
      AConfig.AddValue(SubNode, 'FilenameStyle', FileNameStyle);
      AConfig.AddValue(SubNode, 'ExtensionStyle', ExtensionStyle);
      AConfig.AddValue(SubNode, 'Find', Find);
      AConfig.AddValue(SubNode, 'Replace', Replace);
      AConfig.AddValue(SubNode, 'RegExp', RegExp);
      AConfig.AddValue(SubNode, 'UseSubs', UseSubs);
      AConfig.AddValue(SubNode, 'Counter', Counter);
      AConfig.AddValue(SubNode, 'Interval', Interval);
      AConfig.AddValue(SubNode, 'Width', Width);
      AConfig.SetValue(SubNode, 'Log/Enabled', Log);
      AConfig.SetValue(SubNode, 'Log/File', LogFile);
    end;
  end;
end;

procedure TfrmMultiRename.LoadPreset(PresetName: String);
var
  PresetIndex: Integer;
begin
  if PresetName <> '' then
  begin
    PresetIndex := FPresets.Find(PresetName);
    if PresetIndex = -1 then
      PresetIndex := FPresets.Add(PresetName, New(PMultiRenamePreset));

    with PMultiRenamePreset(FPresets.List[PresetIndex]^.Data)^ do
    begin
      edName.Text := FileName;
      edExt.Text := Extension;
      cmbNameStyle.ItemIndex := FileNameStyle;
      cmbExtensionStyle.ItemIndex := ExtensionStyle;
      edFind.Text := Find;
      edReplace.Text := Replace;
      cbRegExp.Checked := RegExp;
      cbUseSubs.Checked := UseSubs;
      edPoc.Text := Counter;
      edInterval.Text := Interval;
      cmbxWidth.ItemIndex := Width;
      cbLog.Checked := Log;
      edFile.Text := LogFile;
    end;

    FLastPreset := PresetName;

    StringGridTopLeftChanged(StringGrid);
  end;
end;

procedure TfrmMultiRename.SavePreset(PresetName: String);
var
  PresetIndex: Integer;
begin
  if PresetName <> '' then
  begin
    PresetIndex := FPresets.Find(PresetName);
    if PresetIndex = -1 then
      PresetIndex := FPresets.Add(PresetName, New(PMultiRenamePreset));

    with PMultiRenamePreset(FPresets.List[PresetIndex]^.Data)^ do
    begin
      FileName := edName.Text;
      Extension := edExt.Text;
      FileNameStyle := cmbNameStyle.ItemIndex;
      ExtensionStyle := cmbExtensionStyle.ItemIndex;
      Find := edFind.Text;
      Replace := edReplace.Text;
      RegExp := cbRegExp.Checked;
      UseSubs := cbUseSubs.Checked;
      Counter := edPoc.Text;
      Interval := edInterval.Text;
      Width := cmbxWidth.ItemIndex;
      Log := cbLog.Checked;
      LogFile := edFile.Text;
    end;

    FLastPreset := PresetName;
    SavePresets;
  end;
end;

procedure TfrmMultiRename.DeletePreset(PresetName: String);
var
  PresetIndex: Integer;
begin
  if PresetName <> '' then
  begin
    PresetIndex := FPresets.Find(PresetName);
    if PresetIndex <> -1 then
    begin
      Dispose(PMultiRenamePreset(FPresets.List[PresetIndex]^.Data));
      FPresets.Remove(PresetName);
      FLastPreset := '';
      SavePresets;
    end;
  end;
end;

procedure TfrmMultiRename.FillPresetsList;
var
  i: Integer;
  PresetName: String;
begin
  cbPresets.Clear;

  for i := 0 to FPresets.Count - 1 do
  begin
    PresetName := FPresets.List[i]^.Key;
    if cbPresets.Items.IndexOf(PresetName) = -1 then
      cbPresets.Items.Add(PresetName);
  end;
end;

procedure TfrmMultiRename.ClearPresetsList;
var
  i: Integer;
begin
  for i := 0 to FPresets.Count - 1 do
    Dispose(PMultiRenamePreset(FPresets.List[i]^.Data));
  FPresets.Clear;
end;

// Temporary for switching configuration from INI to XML
procedure TfrmMultiRename.PublicSavePresets;
begin
  SavePresetsXml(gConfig);
end;

end.

