unit CnBaseUtils;

interface

{$I CnWizards.inc}

{$IFDEF DELPHI7_UP}
  {$WARN SYMBOL_PLATFORM OFF}
  {$WARN UNIT_PLATFORM OFF}
{$ENDIF}

uses
  SysUtils, Windows, Classes, CnBuffStr;

type
  TStringProcessProc = function(const s: string): string;

  TStringObjectList = class(TStringList)
  private
    function GetStringObjects(Index: Integer): string;
    procedure SetStringObjects(Index: Integer; const Value: string);

    procedure ClearStringObjects;
  public
    constructor Create;
    destructor Destroy; override;

    function AddStringObject(const S: string; str: string): Integer;
    procedure Clear; override;
    procedure Delete(Index: Integer); override;

    property StringObjects[Index: Integer]: string read GetStringObjects write SetStringObjects;
  end;

  TSectionList = class(TStringList)
  private
    function GetPostions(Index: Integer): TList;
    procedure SetPostions(Index: Integer; const Value: TList);
  public
    constructor Create;
    destructor Destroy; override;

    function Add(const S: string): Integer; override;
    procedure Clear; override;
    procedure Delete(Index: Integer); override;

    property Postions[Index: Integer]: TList read GetPostions write SetPostions;
  end;
  
  TFindFileCallBack = procedure(obj: TObject;
    const FileName: string; const Info: TSearchRec; var Abort: Boolean);
  TFindDirCallBack = procedure(obj: TObject; const SubDir: string);
  TProcMsgCallBack = procedure(obj: TObject);
  
procedure BulidSectionList(Stream: TStringReader; var sl: TSectionList);
procedure FreeSectionList(var sl: TSectionList);

procedure StringsLoadFromFileWithSection(ss: TStrings;
  const FileName: string; const Section: string = '';
  const SectionList: TSectionList = nil;
  IncludeBlankLine: Boolean = False);
procedure StringsLoadFromTextWithSection(ss: TStrings;
  Stream: TStringReader; const Section: string = '';
  const SectionList: TSectionList = nil;
  IncludeBlankLine: Boolean = False);
procedure StringsSaveToFileWithSection(ss: TStrings;
  FileName: string; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);
procedure StringsAppendToFileWithSection(ss: TStrings;
  FileName: string; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);
procedure StringsSaveToTextWithSection(ss: TStrings;
  var Stream: Text; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);

function MatchFileName(const Mask, FileName: string): Boolean;
procedure FileMasksToStrings(const FileMasks: string; MaskList: TStrings;
  CaseSensitive: Boolean);
function FileMatchesMasks(const FileName, FileMasks: string;
  CaseSensitive: Boolean = False): Boolean; overload;
function FileMatchesMasks(const FileName: string; MaskList: TStrings;
  CaseSensitive: Boolean = False): Boolean; overload;

function RefString(const S: string): Pointer;
procedure ReleaseString(P: Pointer);
function PointerToString(P: Pointer): string; 

procedure SetCommaText(const s: string; ss: TStrings);

function ExtractFileNames(ssFiles, ssNames, ssDuplicated: TStringList): Boolean;

function DirListExtractFileNames(ssFiles, ssNames, ssDuplicated: TStrings): Boolean;

function FindFile(obj: TObject;
  const Path: string; const FileName: string = '*.*';
  FileProc: TFindFileCallBack = nil; DirProc: TFindDirCallBack = nil;
  bSub: Boolean = True; DoMsgProc: TProcMsgCallBack = nil): Boolean;

function IsDelimiter(const S: string; Delimiter: Char; Index: Integer): Boolean;
function LastCharPos(const s: string; chr: Char): Integer;

function RelativePath_API(const aFrom, aTo: string;
  FromIsDir, ToIsDir: Boolean): string;

function FileNameMatch(Pattern, FileName: PChar): Integer;
function MatchExt(const ext, FileName: string): Boolean;
procedure FileExtsToStrings(const FileExts: string; ExtList: TStrings;
  CaseSensitive: Boolean);

procedure FileMasksToStringsStrict(const FileMasks: string; MaskList: TStrings;
  CaseSensitive: Boolean);

  function FileMatchesExts(const FileName, FileExts: string;
  CaseSensitive: Boolean = False): Boolean; overload;
function FileMatchesExts(const FileName: string; ExtList: TStrings;
  CaseSensitive: Boolean = False): Boolean; overload;

implementation

const
  cSectionStart = '[';
  cSectionEnd = ']';

// 目录尾加'\'修正
function MakePath(const Dir: string): string;
begin
  Result := Trim(Dir);
  if Result = '' then Exit;
  if not IsPathDelimiter(Result, Length(Result)) then
    Result := Result + {$IFDEF MSWINDOWS} '\'; {$ELSE} '/'; {$ENDIF};
end;

function StringIsSection(const s: string): Boolean; overload;
begin
  Result := (s <> '') and (s[1] = cSectionStart) and (s[Length(s)] = cSectionEnd) and (s <> '[]');
end;

function StringIsSection(const s: string; var Section: string): Boolean; overload;
begin
  Result := StringIsSection(s);
  if Result then
  begin
    Section := Copy(s, 2, Length(s) - 2);
  end;
end;

function SectionToString(const s: string): string;
begin
  Result := cSectionStart + s + cSectionEnd;
end;

{ TSectionList }

function TSectionList.Add(const S: string): Integer;
var
  lst: TList;
begin
  Result := inherited Add(S); // 不能调用 AddObject(s, nil)，否则会递归出错;
  if Objects[Result] = nil then
  begin
    lst := TList.Create;
    Objects[Result] := lst;
  end;
end;

procedure TSectionList.Clear;
var
  i: Integer;
begin
  for i := 0 to Count - 1 do
  begin
    Objects[i].Free;
  end;
  inherited;
end;

constructor TSectionList.Create;
begin
  inherited;
  Sorted := True;
end;

procedure TSectionList.Delete(Index: Integer);
begin
  Objects[Index].Free;
  inherited;
end;

destructor TSectionList.Destroy;
begin
  Clear;
  inherited;
end;

function TSectionList.GetPostions(Index: Integer): TList;
begin
  Result := TList(Objects[Index]);
end;

procedure TSectionList.SetPostions(Index: Integer; const Value: TList);
begin
  Objects[Index].Free;
  PutObject(Index, Value);
end;

function FindSection(sl: TSectionList; const Section: string; const iSection: Integer = -1): Integer; 
var
  i, idx: Integer;
begin
  Result := -1;
  idx := sl.IndexOf(Section);
  if idx >= 0 then
  begin
    with sl.Postions[idx] do
    begin
      if iSection >= 0 then
      begin
        i := IndexOf(Pointer(iSection));
        if (i >= 0) and (i + 1 < Count) then
        begin
          Result := Integer(Items[i + 1]);
        end;
      end
      else
      begin
        Result := Integer(Items[0]);
      end;
    end;
  end;
end;

procedure BulidSectionList(Stream: TStringReader; var sl: TSectionList);
var
  s, Section: string;
  i: Integer;
begin
  sl := TSectionList.Create;

  Stream.Seek(0, soFromBeginning);
  while not Stream.EoS do
  begin
    s := Stream.ReadLn;
    if StringIsSection(s, Section) then
    begin
      i := sl.Add(Section);
      sl.Postions[i].Add(Pointer(Stream.Position));
    end;
  end;
end;

procedure FreeSectionList(var sl: TSectionList);
begin
  sl.Free;
end;

procedure StringsLoadFromFileWithSection(ss: TStrings;
  const FileName: string; const Section: string = '';
  const SectionList: TSectionList = nil;
  IncludeBlankLine: Boolean = False);
var
  i: Integer;
  Stream: TStringReader;
begin
  if not Assigned(ss) then
  begin
    Exit;
  end;

  if Section = '' then
  begin
    ss.LoadFromFile(FileName);

    if not IncludeBlankLine then
    begin
      for i := ss.Count - 1 downto 0 do
      begin
        if ss[i] = '' then
        begin
          ss.Delete(i);
        end;
      end;
    end;

    Exit;
  end;

  Stream := TStringReader.Create;
  try
    Stream.LoadFromFile(FileName);
    StringsLoadFromTextWithSection(ss, Stream, Section, SectionList, IncludeBlankLine);
  finally
    Stream.Free;
  end;
end;

procedure StringsLoadFromTextWithSection(ss: TStrings;
  Stream: TStringReader; const Section: string = '';
  const SectionList: TSectionList = nil;
  IncludeBlankLine: Boolean = False);
var
  IsSection, IsCurrentSection: Boolean;
  s, sSection: string;
  iSection: Integer;
begin
  if not Assigned(ss) then
  begin
    Exit;
  end;

  Stream.Seek(0, soFromBeginning);

  IsCurrentSection := False;

  ss.BeginUpdate;
  try
    if Section = '' then
    begin
      while not Stream.EoS do
      begin
        s := Stream.ReadLn;
        if (not IncludeBlankLine) and (s = '') then
        begin
          Continue;
        end;
        ss.Add(s);
      end;
    end
    else if Assigned(SectionList) then
    begin
      iSection := FindSection(SectionList, Section);
      while iSection >= 0 do
      begin
        Stream.Seek(iSection, soFromBeginning);
        while not Stream.EoS do
        begin
          s := Stream.ReadLn;
          if (not IncludeBlankLine) and (s = '') then
          begin
            Continue;
          end;
          if StringIsSection(s) then
          begin
            Break;
          end;
          ss.Add(s);
        end;
        iSection := FindSection(SectionList, Section, iSection);
      end;
    end
    else
    begin
      sSection := SectionToString(Section);
      while not Stream.EoS do
      begin
        s := Stream.ReadLn;
        if (s = '') and (not IncludeBlankLine) then
        begin
          Continue;
        end;

        IsSection := StringIsSection(s);
        if IsSection then
        begin
          IsCurrentSection := s = sSection;
        end;
        if (not IsCurrentSection) or IsSection then
        begin
          Continue;
        end;
        ss.Add(s);
      end;
    end;
  finally
    ss.EndUpdate;
  end;
end;

procedure StringsSaveToFileWithSection(ss: TStrings;
  FileName: string; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);
var
  Stream: TextFile;
begin
  if not Assigned(ss) then
  begin
    Exit;
  end;

  AssignFile(Stream, FileName);
  try
    Reset(Stream);
    Rewrite(Stream);
    StringsSaveToTextWithSection(ss, Stream, Section, IncludeBlankLine, ProcessProc);
  finally
    CloseFile(Stream);
  end;
end;

procedure StringsAppendToFileWithSection(ss: TStrings;
  FileName: string; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);
var
  Stream: TextFile;
begin
  if not Assigned(ss) then
  begin
    Exit;
  end;

  AssignFile(Stream, FileName);
  try
    Append(Stream);
    StringsSaveToTextWithSection(ss, Stream, Section, IncludeBlankLine, ProcessProc);
  finally
    CloseFile(Stream);
  end;
end;

procedure StringsSaveToTextWithSection(ss: TStrings;
  var Stream: Text; const Section: string;
  IncludeBlankLine: Boolean = False;
  ProcessProc: TStringProcessProc = nil);
var
  i: Integer;
begin
  if not Assigned(ss) then
  begin
    Exit;
  end;

  if FilePos(Stream) > 0 then
  begin
    Writeln(Stream);
  end;

  Writeln(Stream, SectionToString(Section));
  for i := 0 to ss.Count - 1 do
  begin
    if (not IncludeBlankLine) and (ss[i] = '') then
    begin
      Continue;
    end;

    if Assigned(ProcessProc) then
    begin
      Writeln(Stream, ProcessProc(ss[i]));
    end
    else
    begin
      Writeln(Stream, ss[i]);
    end;
  end;
end;

function PointerXX(var X: PChar): PChar;
{$IFDEF PUREPASCAL}
begin
  Result := X;
  Inc(X);
end;
{$ELSE}
asm
  {
  EAX = X
  }
  MOV EDX, [EAX]
  INC dword ptr [EAX]
  MOV EAX, EDX
end;
{$ENDIF}

// Evaluate operation

function Evaluate(var X: Char; const Value: Char): Char;
{$IFDEF PUREPASCAL}
begin
  X := Value;
  Result := X;
end;
{$ELSE}
asm
  {
  EAX = X
  EDX = Value (DL)
  }
  MOV [EAX], DL
  MOV AL, [EAX]
end;
{$ENDIF}

function FileNameMatch(Pattern, FileName: PChar): Integer;
var
  p, n: PChar;
  c: Char;
begin
  p := Pattern;
  n := FileName;

  while Evaluate(c, PointerXX(p)^) <> #0 do
  begin
    case c of
      '?':
        begin
          if n^ = '.' then
          begin
            while (p^ <> '.') and (p^ <> #0) do
            begin
              if (p^ <> '?') and (p^ <> '*') then
              begin
                Result := -1;
                Exit;
              end;
              Inc(p);
            end;
          end
          else
          begin
            if n^ <> #0 then
            begin
              Inc(n);
            end;
          end;
        end;

      '>':
        begin
          if n^ = '.' then
          begin
            if ((n + 1)^ = #0) and (FileNameMatch(p, n + 1) = 0) then
            begin
              Result := 0;
              Exit;
            end;
            if FileNameMatch(p, n) = 0 then
            begin
              Result := 0;
              Exit;
            end;
            Result := -1;
            Exit;
          end;
          if n^ = #0 then
          begin
            Result := FileNameMatch(p, n);
            Exit;
          end;
          Inc(n);
        end;

      '*':
        begin
          while n^ <> #0 do
          begin
            if FileNameMatch(p, n) = 0 then
            begin
              Result := 0;
              Exit;
            end;
            Inc(n);
          end;
        end;

      '<':
        begin
          while n^ <> #0 do
          begin
            if FileNameMatch(p, n) = 0 then
            begin
              Result := 0;
              Exit;
            end;
            if (n^ = '.') and (StrScan(n + 1, '.') = nil) then
            begin
              Inc(n);
              Break;
            end;
            Inc(n);
          end;
        end;

      '"':
        begin
          if (n^ = #0) and (FileNameMatch(p, n) = 0) then
          begin
            Result := 0;
            Exit;
          end;
          if n^ <> '.' then
          begin
            Result := -1;
            Exit;
          end;
          Inc(n);
        end;
    else
      if (c = '.') and (n^ = #0) then
      begin
        while p^ <> #0 do
        begin
          if (p^ = '*') and ((p + 1)^ = #0) then
          begin
            Result := 0;
            Exit;
          end;
          if p^ <> '?' then
          begin
            Result := -1;
            Exit;
          end;
          Inc(p);
        end;
        Result := 0;
        Exit;
      end;
      if c <> n^ then
      begin
        Result := -1;
        Exit;
      end;
      Inc(n);
    end;
  end;

  if n^ = #0 then
  begin
    Result := 0;
    Exit;
  end;

  Result := -1;
end;

function MatchFileName(const Mask, FileName: string): Boolean;
begin
  if Mask = '*.*' then
  begin
    Result := True;
    Exit;
  end;

  Result := FileNameMatch(PChar(Mask), PChar(FileName)) = 0;
end;

function _CaseSensitive(const CaseSensitive: Boolean; const S: string): string;
begin
  if CaseSensitive then
    Result := S
  else
    Result := AnsiUpperCase(S);
end;

procedure FileMasksToStrings(const FileMasks: string; MaskList: TStrings;
  CaseSensitive: Boolean);
var
  Masks: string;
  i: Integer;
begin
  Masks := StringReplace(FileMasks, ';', ',', [rfReplaceAll]);
  SetCommaText(Masks, MaskList);

  for i := MaskList.Count - 1 downto 0 do
  begin
    if MaskList[i] = '' then
    begin
      MaskList.Delete(i);
      Continue;
    end;

    if StrScan(PChar(MaskList[i]), '.') <> nil then
    begin
      if (MaskList[i] <> '') and (MaskList[i][1] = '.') then
        MaskList[i] := '*' + _CaseSensitive(CaseSensitive, MaskList[i])
      else
        MaskList[i] := _CaseSensitive(CaseSensitive, MaskList[i]);
    end
    else
    begin
      MaskList[i] := '*.' + _CaseSensitive(CaseSensitive, MaskList[i]);
    end;
    if MaskList[i] = '*.*' then
    begin
      if i > 0 then
        MaskList.Exchange(0, i);
      Exit;
    end;
  end;
end;

function FileMatchesMasks(const FileName, FileMasks: string;
  CaseSensitive: Boolean = False): Boolean;
var
  MaskList: TStrings;
  FFileName: string;
  i: Integer;
begin
  MaskList := TStringList.Create;
  try
    FileMasksToStrings(FileMasks, MaskList, CaseSensitive);

    FFileName := _CaseSensitive(CaseSensitive, ExtractFileName(FileName));
    Result := False;
    for i := 0 to MaskList.Count - 1 do
    begin
      if MatchFileName(MaskList[i], FFileName) then
      begin
        Result := True;
        Exit;
      end;
    end;
  finally
    MaskList.Free;
  end;
end;

function FileMatchesMasks(const FileName: string; MaskList: TStrings;
  CaseSensitive: Boolean = False): Boolean;
var
  FFileName: string;
  i: Integer;
begin
  FFileName := _CaseSensitive(CaseSensitive, ExtractFileName(FileName));

  Result := False;
  for i := 0 to MaskList.Count - 1 do
  begin
    if MatchFileName(MaskList[i], FFileName) then
    begin
      Result := True;
      Exit;
    end;
  end;
end;

function RefString(const S: string): Pointer;
var
  tmpS: string;
begin
  tmpS := S;
  Result := Pointer(tmpS);
  Pointer(tmpS) := nil;
end;

procedure ReleaseString(P: Pointer);
var
  tmpS: string;
begin
  Pointer(tmpS) := P;
end;

function PointerToString(P: Pointer): string;
begin
  Result := StrPas(PChar(P));
end;

procedure SetCommaText(const s: string; ss: TStrings);
begin
  if Assigned(ss) then
  begin
    if s <> '' then
    begin
      ss.CommaText := s;
    end
    else
    begin
      ss.Clear;
    end;
  end;
end;

function ExtractFileNames(ssFiles, ssNames, ssDuplicated: TStringList): Boolean;
var
  i, idx: Integer;
  s: string;
begin
  Result := False;

  if (not Assigned(ssFiles)) or (not Assigned(ssNames)) then
  begin
    Exit;
  end;

  ssNames.Clear;
  ssNames.Sorted := True;
  Result := True;
  for i := 0 to ssFiles.Count - 1 do
  begin
    s := ExtractFileName(ssFiles[i]);
    idx := ssNames.IndexOf(s);
    if idx >= 0 then
    begin
      Result := False;
      if Assigned(ssDuplicated) then
      begin
        ssDuplicated.Add(ssFiles[Integer(ssNames.Objects[idx])]);
        ssDuplicated.Add(ssFiles[i]);
        ssDuplicated.Add('');
      end;
      Continue;
    end;
    ssNames.AddObject(s, Pointer(i));
  end;
end;

function DirListExtractFileNames(ssFiles, ssNames, ssDuplicated: TStrings): Boolean;
var
  i, idx: Integer;
  s: string;
begin
  Result := False;

  if (not Assigned(ssFiles)) or (not Assigned(ssNames)) then
  begin
    Exit;
  end;

  ssNames.Clear;
  Result := True;
  for i := 0 to ssFiles.Count - 1 do
  begin
    s := ExtractFileName(ssFiles[i]);
    idx := ssNames.IndexOf(s);
    if idx >= 0 then
    begin
      Result := False;
      if Assigned(ssDuplicated) then
      begin
        ssDuplicated.Add(s);
      end;
      Continue;
    end;
    ssNames.AddObject(s, Pointer(i));
  end;
end;

{ TStringObjectList }

function TStringObjectList.AddStringObject(const S: string;
  str: string): Integer;
begin
  Result := AddObject(S, RefString(str));
end;

procedure TStringObjectList.Clear;
begin
  ClearStringObjects;
  inherited;
end;

procedure TStringObjectList.ClearStringObjects;
var
  i: Integer;
begin
  for i := 0 to Count - 1 do
  begin
    ReleaseString(Objects[i]);
  end;
end;

constructor TStringObjectList.Create;
begin
  inherited;
end;

procedure TStringObjectList.Delete(Index: Integer);
begin
  if (Index < 0) or (Index >= Count) then
  begin
    Exit;
  end;
  ReleaseString(Objects[Index]);
  inherited;
end;

destructor TStringObjectList.Destroy;
begin
  Clear;
  inherited;
end;

function TStringObjectList.GetStringObjects(Index: Integer): string;
begin
  Result := PointerToString(Objects[Index]);
end;

procedure TStringObjectList.SetStringObjects(Index: Integer;
  const Value: string);
begin
  if (Index < 0) or (Index >= Count) then
  begin
    Exit;
  end;
  ReleaseString(Objects[Index]);
  PutObject(Index, RefString(Value));
end;

function DirectoryExists(const Directory: string): Boolean;
var
  Code: Integer;
begin
  Code := GetFileAttributes(PChar(Directory));
  Result := (Code <> -1) and (FILE_ATTRIBUTE_DIRECTORY and Code <> 0);
end;

function FindFile(obj: TObject;
  const Path: string; const FileName: string = '*.*';
  FileProc: TFindFileCallBack = nil; DirProc: TFindDirCallBack = nil;
  bSub: Boolean = True; DoMsgProc: TProcMsgCallBack = nil): Boolean;
var
  FindAbort: Boolean;

  procedure DoFindFile(obj: TObject;
    const Path, SubPath: string; const FileName: string;
    FileProc: TFindFileCallBack; DirProc: TFindDirCallBack;
    bSub: Boolean; DoMsgProc: TProcMsgCallBack);
  var
    APath: string;
    Info: TSearchRec;
    Succ: Integer;
  begin
    FindAbort := False;
    APath := MakePath(
      MakePath(Path) + SubPath);
    Succ := FindFirst(APath + FileName, faAnyFile {- faVolumeID}, Info);
    try
      while Succ = 0 do
      begin
        if (Info.Name <> '.') and (Info.Name <> '..') then
        begin
          if (Info.Attr and faDirectory) <> faDirectory then
          begin
            if Assigned(FileProc) then
            begin
              FileProc(obj, APath + Info.FindData.cFileName, Info, FindAbort);
            end;
          end;
        end;

        if Assigned(DoMsgProc) then
        begin
          DoMsgProc(obj);
        end;

        if FindAbort then
        begin
          Exit;
        end;
        Succ := FindNext(Info);
      end;
    finally
      SysUtils.FindClose(Info);
    end;

    if bSub then
    begin
      Succ := FindFirst(APath + '*.*', faAnyFile {- faVolumeID}, Info);
      try
        while Succ = 0 do
        begin
          if (Info.Name <> '.') and (Info.Name <> '..') and
            (Info.Attr and faDirectory = faDirectory) then
          begin
            if Assigned(DirProc) then
            begin
              DirProc(obj, MakePath(SubPath + Info.Name));
            end;

            DoFindFile(obj,
              Path,
              MakePath(SubPath + Info.Name),
              FileName,
              FileProc,
              DirProc,
              bSub,
              DoMsgProc);

            if FindAbort then
            begin
              Exit;
            end;
          end;
          Succ := FindNext(Info);
        end;
      finally
        SysUtils.FindClose(Info);
      end;
    end;
  end;

begin
  Result := False;
  if not DirectoryExists(Path) then
  begin
    Exit;
  end;

  DoFindFile(obj,
    MakePath(Path),
    '',
    FileName,
    FileProc,
    DirProc,
    bSub,
    DoMsgProc);
  Result := not FindAbort;
end;

function IsDelimiter(const S: string; Delimiter: Char; Index: Integer): Boolean;
begin
  Result := (Index > 0) and (Index <= Length(S)) and
    (S[Index] = Delimiter) and (ByteType(S, Index) = mbSingleByte);
end;

function LastCharPos(const s: string; chr: Char): Integer;
var
  i: Integer;
begin
  i := Length(s);
  while (i > 0) and (not IsDelimiter(s, chr, i)) do
  begin
    Dec(i);
  end;
  Result := i;
end;

const
  shlwapi32 = 'shlwapi.dll';
  
function PathRelativePathToA(pszPath: PAnsiChar; pszFrom: PAnsiChar; dwAttrFrom: DWORD;
  pszTo: PAnsiChar; dwAttrTo: DWORD): BOOL; stdcall; external shlwapi32 name
  'PathRelativePathToA';

function PathRelativePathToW(pszPath: PWideChar; pszFrom: PWideChar; dwAttrFrom: DWORD;
  pszTo: PWideChar; dwAttrTo: DWORD): BOOL; stdcall; external shlwapi32 name
  'PathRelativePathToW';

function PathRelativePathTo(pszPath: PChar; pszFrom: PChar; dwAttrFrom: DWORD;
  pszTo: PChar; dwAttrTo: DWORD): BOOL; stdcall; external shlwapi32 name
  'PathRelativePathToA';

function RelativePath_API(const aFrom, aTo: string; FromIsDir, ToIsDir: Boolean): string;
  function GetAttr(IsDir: Boolean): DWORD;
  begin
    if IsDir then
      Result := FILE_ATTRIBUTE_DIRECTORY
    else
      Result := FILE_ATTRIBUTE_NORMAL;
  end;
var
  p: array[0..MAX_PATH] of Char;
begin
  PathRelativePathTo(p, PChar(aFrom), GetAttr(FromIsDir), PChar(aTo), GetAttr(ToIsDir));
  Result := StrPas(p);
end;


function MatchExt(const ext, FileName: string): Boolean;
begin
  if ext = '.*' then
  begin
    Result := True;
    Exit;
  end;

  Result := FileNameMatch(PChar(ext), PChar(FileName)) = 0;
end;


procedure FileExtsToStrings(const FileExts: string; ExtList: TStrings;
  CaseSensitive: Boolean);
var
  Exts: string;
  i: Integer;
begin
  Exts := StringReplace(FileExts, ';', ',', [rfReplaceAll]);
  ExtList.CommaText := Exts;

  for i := ExtList.Count - 1 downto 0 do
  begin
    if ExtList[i] = '' then
    begin
      ExtList.Delete(i);
      Continue;
    end;

    if StrScan(PChar(ExtList[i]), '.') <> nil then
    begin
      ExtList[i] := _CaseSensitive(CaseSensitive, ExtractFileExt(ExtList[i]));
    end
    else
    begin
      ExtList[i] := '.' + _CaseSensitive(CaseSensitive, ExtList[i]);
    end;
    if ExtList[i] = '.*' then
    begin
      if i > 0 then
        ExtList.Exchange(0, i);
      Exit;
    end;
  end;
end;

procedure FileMasksToStringsStrict(const FileMasks: string; MaskList: TStrings;
  CaseSensitive: Boolean);
var
  Exts: string;
  i: Integer;
begin
  Exts := StringReplace(FileMasks, ';', ',', [rfReplaceAll]);
  MaskList.CommaText := Exts;

  for i := MaskList.Count - 1 downto 0 do
  begin
    if MaskList[i] = '' then
    begin
      MaskList.Delete(i);
      Continue;
    end;

    MaskList[i] := _CaseSensitive(CaseSensitive, MaskList[i]);

    if MaskList[i] = '*.*' then
    begin
      if i > 0 then
        MaskList.Exchange(0, i);
      Exit;
    end;
  end;
end;

function FileMatchesExts(const FileName, FileExts: string;
  CaseSensitive: Boolean = False): Boolean;
var
  ExtList: TStrings;
  FExt: string;
  i: Integer;
begin
  ExtList := TStringList.Create;
  try
    FileExtsToStrings(FileExts, ExtList, CaseSensitive);

    FExt := _CaseSensitive(CaseSensitive, ExtractFileExt(FileName));
    Result := False;
    for i := 0 to ExtList.Count - 1 do
    begin
      if MatchExt(ExtList[i], FExt) then
      begin
        Result := True;
        Exit;
      end;
    end;
  finally
    ExtList.Free;
  end;
end;

function FileMatchesExts(const FileName: string; ExtList: TStrings;
  CaseSensitive: Boolean = False): Boolean;
var
  FExt: string;
  i: Integer;
begin
  FExt := _CaseSensitive(CaseSensitive, ExtractFileExt(FileName));

  Result := False;
  for i := 0 to ExtList.Count - 1 do
  begin
    if MatchExt(ExtList[i], FExt) then
    begin
      Result := True;
      Exit;
    end;
  end;
end;

end.
