{*******************************************************}
{                                                       }
{       Tocsg.Strings                                   }
{                                                       }
{       Copyright (C) 2022 kkuzil                       }
{                                                       }
{*******************************************************}

unit Tocsg.Strings;

interface

uses
  System.Classes, System.SysUtils, Winapi.Windows;

function ReverseString(const sVal: string): String; inline;
procedure SumString(var sDest: String; sTail: String; sDm: String = ''; bIgrSpace: Boolean = false); inline;
procedure SumStringA(var sDest: AnsiString; sTail: AnsiString; sDm: AnsiString = ''; bIgrSpace: Boolean = false); inline;
function DeleteChars(sSrc: String; sDelChars: String): String;
function AppendChars(sSrc: String; sIfChars, sAppendStr: String): String;
function GetCapsuleStr(sCapB, sCapE: String; sText: String; bIgrCase: Boolean = true): String;
function GetRandomStr(nMin, nMax: Byte): String;
function GetRandomStrEx(nLen: Integer; sSetStr: String = ''): String;
function DeleteNullTail(sVal: String): String;
function GetGUID: String;

function SplitString(sText: String; const sDm: String;
                     aList: TStrings;
                     bNullStrInc: Boolean = false;
                     bIgrOverl: Boolean = false;
                     bClear: Boolean = true): Integer;
function SplitString2(sText: String; const sDelimiter: String;
                      aList: TStrings;
                      bNullStrInc: Boolean = false;
                      bIgrOverl: Boolean = false;
                      bClear: Boolean = true): Integer;

function InsertPointString(const sInsert: String; sText: String; nPoint: Integer): String;
function InsertPointString2(const sInsert: String; sText: String; nPoint: Integer): String;
function InsertPointComma(sText: String; nPoint: Integer): String; overload;
function InsertPointComma(llData: LONGLONG; nPoint: Integer): String; overload;
function MakeCharStr(c: Char; nCnt: Integer): String; inline;
function ExtrNumStr(const sStr: String): String;
function ExtrLastDelimiterStr(sStr: String; sDm: String): String;

function SaveStrToFile(sPath: String; sStr: String; aEncoding: TEncoding = nil): Boolean;
function LoadStrFromFile(sPath: String; aEncoding: TEncoding = nil): String;
function ExtractTextSafe(const sPath: String): String;

function StrsReplace(const S: String; const arrOld, arrNew: array of string): String; overload;
function StrsReplace(const S: String; const arrOld: array of string; sNew: String): String; overload;
function CountStr(const sSrc, sFind: string; n: Integer = 1): Integer;

function IsUTF8_AnsiChar(const str: PAnsiChar): Boolean;
function IsUTF8(const B: TBytes): Boolean;

function LastIndexOf(const sFind, sSource: string): Integer;

implementation

uses
  Tocsg.Safe, Tocsg.Exception, System.StrUtils, System.IOUtils;

function ReverseString(const sVal: string): String;
var
  i, nLen: Integer;
begin
  nLen := Length(sVal);
  SetLength(Result, nLen);
  for i := 1 to nLen do
    Result[i] := sVal[Succ(nLen - i)];
end;

procedure SumString(var sDest: String; sTail: String; sDm: String = ''; bIgrSpace: Boolean = false);
begin
  if bIgrSpace and (sTail = '') then
    exit;

  if sDest = '' then
    sDest := sTail
  else
    sDest := sDest + sDm + sTail;
end;

procedure SumStringA(var sDest: AnsiString; sTail: AnsiString; sDm: AnsiString = ''; bIgrSpace: Boolean = false);
begin
  if bIgrSpace and (sTail = '') then
    exit;

  if sDest = '' then
    sDest := sTail
  else
    sDest := sDest + sDm + sTail;
end;

function DeleteChars(sSrc: String; sDelChars: String): String;
var
  i: Integer;
begin
  Result := '';
  for i := 1 to Length(sSrc) do
    if sDelChars.IndexOf(sSrc[i]) = -1 then
      Result := Result + sSrc[i];
end;

function AppendChars(sSrc: String; sIfChars, sAppendStr: String): String;
var
  i: Integer;
begin
  Result := '';
  for i := 1 to Length(sSrc) do
    if sIfChars.IndexOf(sSrc[i]) <> -1 then
      Result := Result + sAppendStr + sSrc[i]
    else
      Result := Result + sSrc[i];
end;

function GetCapsuleStr(sCapB, sCapE: String; sText: String; bIgrCase: Boolean = true): String;
var
  nPosS, nPosE: Integer;
begin
  Result := '';

  if bIgrCase then
  begin
    sCapB := UpperCase(sCapB);
    sCapE := UpperCase(sCapE);
    sText := UpperCase(sText);
  end;

  nPosS := Pos(sCapB, sText);
  if nPosS > 0 then
  begin
    Delete(sText, 1, nPosS + Length(sCapB) - 1);
    nPosE := Pos(sCapE, sText);
    if nPosE > 0 then
      Result := Copy(sText, 1, nPosE - 1);
  end;
end;

function GetRandomStr(nMin, nMax: Byte): String;
var
  i, nLen: Integer;
begin
  Result := '';
  if nMax = 0 then
    exit;

  if nMin > nMax then
    nMin := nMax;

  Randomize;
  nLen := Random(nMax);
  if nMax >= (nMin + nLen)  then
    Inc(nLen, nMin);

  for i := 0 to nLen do
    case Random(2) of
      1 :  Result := Result + Char(Integer('a') + Round(Random(26)));
      else Result := Result + Char(Integer('A') + Round(Random(26)));
    end;
end;

function GetRandomStrEx(nLen: Integer; sSetStr: String = ''): String;
const
  DEF_SET = 'ABCDFGHIJKLMNPQRSQUVWXYZ0123456789'; // E, O 제거
var
  i, nSetLen: Integer;
begin
  if sSetStr = '' then
    sSetStr := DEF_SET;

  Randomize;  // 난수 초기화
  Result := '';
  nSetLen := Length(sSetStr);
  for i := 1 to nLen do
    Result := Result + sSetStr[Random(nSetLen) + 1];
end;

function DeleteNullTail(sVal: String): String;
var
  i: Integer;
begin
  Result := sVal;
  if Result = '' then
    exit;

  for i := 1 to Length(Result) do
    if Result[i] = #0 then
    begin
      SetLength(Result, i - 1);
      exit;
    end;
end;

function GetGUID: String;
var
  GG: TGUID;
begin
  Result := '';
  try
    if CreateGUID(GG) = S_OK then
      Result := GUIDToString(GG);
  except
  // ..
  end;
end;

function SplitString(sText: String; const sDm: String;
                     aList: TStrings;
                     bNullStrInc: Boolean = false;
                     bIgrOverl: Boolean = false;
                     bClear: Boolean = true): Integer;
var
  nPos: Integer;
  sTemp: String;
begin
  if bClear then
    aList.Clear;

  while true do
  begin
    nPos := Pos(sDm, sText);
    if nPos <> 0 then
    begin
      sTemp := Trim(Copy(sText, 0, nPos - 1));

      if sTemp <> '' then
      begin
        if not bIgrOverl or
           (aList.IndexOf(sTemp) = -1) then
          aList.Add(sTemp);
      end else
      if bNullStrInc then
        aList.Add('');

      Delete(sText, 1, nPos + Length(sDm) - 1);
    end else begin
      if sText <> '' then
      begin
        if not bIgrOverl or
           (aList.IndexOf(sText) = -1) then
          aList.Add(Trim(sText));
      end else
      if bNullStrInc then
        aList.Add('');

      break;
    end;
  end;

  Result := aList.Count;
end;

// 따옴표 묶음 인식
function SplitString2(sText: String; const sDelimiter: String;
                      aList: TStrings;
                      bNullStrInc: Boolean = false;
                      bIgrOverl: Boolean = false;
                      bClear: Boolean = true): Integer;

  procedure AddStr(s: String);
  begin
    if not bIgrOverl or
       (aList.IndexOf(s) = -1) then
      aList.Add(s);
  end;

var
  nDdPos,
  nPos: Integer;
  sTemp,
  sDdTemp: String;
begin
  if bClear then
    aList.Clear;

  while true do
  begin
    sDdTemp := '';

    if (Length(sText) > 0) and (sText[1] = '"') then
    begin
      Delete(sText, 1, 1);
      nDdPos := Pos('"', sText);
      if nDdPos > 0 then
      begin
        sDdTemp := Copy(sText, 1, nDdPos - 1);
        Delete(sText, 1, nDdPos);
      end else
        sText := '"' + sText;
    end;

    nPos := Pos(sDelimiter, sText);
    if nPos <> 0 then
    begin
      if sDdTemp <> '' then
        sTemp := sDdTemp
//        sTemp := '"' + sDdTemp + '"'
      else
        sTemp := Copy(sText, 0, nPos - 1);
      AddStr(Trim(sTemp));
      Delete(sText, 1, nPos+Length(sDelimiter)-1);
    end else begin
      if sDdTemp <> '' then
      begin
        AddStr(sDdTemp);
      end else begin
        if sText <> '' then
          AddStr(Trim(sText))
        else if bNullStrInc then
          aList.Add('');
      end;
      break;
    end;
  end;

  Result := aList.Count;
end;

function InsertPointString(const sInsert: String; sText: String; nPoint: Integer): String;
var
  i : integer;
begin
  for i := (Length(sText) - 1) div nPoint downto 1 do
    Insert(sInsert, sText, Length(sText) - (i * nPoint) + 1);

  Result := sText;
end;

function InsertPointString2(const sInsert: String; sText: String; nPoint: Integer): String;
var
  i : integer;
begin
 for i := 1 to (Length(sText) div nPoint) do
   Insert(sInsert, sText, i * nPoint);

  Result := sText;
end;

function InsertPointComma(sText: String; nPoint: Integer): String;
begin
  Result := InsertPointString(',', sText, nPoint);
end;

function InsertPointComma(llData: LONGLONG; nPoint: Integer): String;
begin
  Result := InsertPointString(',', IntToStr(llData), nPoint);
end;

procedure InsertPointCommaA(var sText: AnsiString; sAdd: AnsiString; nPoint: Integer);
begin
  if sAdd <> '' then
  begin
    if sText <> '' then
      sText := sText + ',' + sAdd
    else
      sText := sAdd;
  end;
end;

function MakeCharStr(c: Char; nCnt: Integer): String; inline;
var
  i: Integer;
begin
  Result := '';
  for i := 0 to nCnt - 1 do
    Result := Result + c;
end;

function ExtrNumStr(const sStr: String): String;
var
  i, nLen: Integer;
begin
  Result := '';
  nLen := Length(sStr);
  for i := 1 to nLen do
    case Byte(sStr[i]) of
      48 .. 57  : Result := Result + sStr[i];
    end;
end;

function ExtrLastDelimiterStr(sStr: String; sDm: String): String;
var
  nPos: Integer;
begin
  Result := sStr;
  nPos := Result.LastDelimiter(sDm);
  if nPos > 0 then
  Delete(Result, 1, nPos + 1);
end;

function SaveStrToFile(sPath: String; sStr: String; aEncoding: TEncoding = nil): Boolean;
var
  StrList: TStringList;
begin
  Result := false;
  try
    if aEncoding = nil then
      aEncoding := TEncoding.UTF8;

    Guard(StrList, TStringList.Create);
    StrList.Text := sStr;
    StrList.SaveToFile(sPath, aEncoding);
    Result := true;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. SaveStrFromFile()');
  end;
end;

function LoadStrFromFile(sPath: String; aEncoding: TEncoding = nil): String;
var
  StrList: TStringList;
begin
  Result := '';
  try
    if not FileExists(sPath) then
      exit;

    if aEncoding = nil then
      aEncoding := TEncoding.UTF8;

    Guard(StrList, TStringList.Create);
    StrList.LoadFromFile(sPath, aEncoding);
    Result := StrList.Text;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. LoadStrFromFile()');
  end;
end;

function ProcessInvalidUTF8Bytes(const Data: TBytes): TBytes;
var
  i, nLen: Integer;
begin
  try
    i := 0;
    SetLength(Result, 0);

    nLen := Length(Data);
    while i < nLen do
    begin
      if (Data[i] and $80) = 0 then
      begin
      // UTF-8 1바이트 (ASCII)
        Result := Result + [Data[i]];
        Inc(i);
      end else
      if ((Data[i] and $E0) = $C0) and (i+ 1 < nLen) and
              ((Data[i+1] and $C0) = $80) then
      begin
      // UTF-8 다중 바이트 시퀀스 시작 검사
        Result := Result + [Data[i], Data[i+1]];
        Inc(i, 2);
      end else
      if ((Data[i] and $F0) = $E0) and (i + 2 < nLen) and
         ((Data[i+1] and $C0) = $80) and ((Data[i+2] and $C0) = $80) then
      begin
        Result := Result + [Data[I], Data[I+1], Data[i+2]];
        Inc(i, 3);
      end else
      if ((Data[i] and $F8) = $F0) and (i + 3 < nLen) and
         ((Data[i+1] and $C0) = $80) and ((Data[i+2] and $C0) = $80) and
         ((Data[i+3] and $C0) = $80) then
      begin
        Result := Result + [Data[i], Data[i+1], Data[i+2], Data[i+3]];
        Inc(i, 4);
      end else
        Inc(i); // 유효하지 않은 바이트 → 무시
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. ProcessInvalidUTF8Bytes()');
  end;
end;

function ExtractTextSafe(const sPath: String): String;
var
  Raw: TBytes;
  FndEnc: TEncoding;
begin
  Result := '';
  try
  // 파일을 바이트 배열로 읽음
    Raw := TFile.ReadAllBytes(sPath);                                                                                                               
    if Length(Raw) = 0 then
      exit;

  // BOM을 기반으로 인코딩 감지
    FndEnc := nil;
    if TEncoding.GetBufferEncoding(Raw, FndEnc, TEncoding.ANSI) = 0 then
    begin
      if IsUTF8(Raw) then
        Result := TEncoding.UTF8.GetString(Raw)
      else
        Result := TEncoding.ANSI.GetString(Raw);
      exit;
    end;

    Result := FndEnc.GetString(Raw);
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. ExtractTextSafe()', 5);
  end;
end;

function StrsReplace(const S: String; const arrOld, arrNew: array of string): String;
var
  i : Integer;
begin
  Result := S;
  for i := Low(arrOld) to High(arrOld) do
    Result := StringReplace(Result, arrOld[i], arrNew[i], [rfReplaceAll, rfIgnoreCase]);
end;

function StrsReplace(const S: String; const arrOld: array of string; sNew: String): String;
var
  i : Integer;
begin
  Result := S;
  for i := Low(arrOld) to High(arrOld) do
    Result := StringReplace(Result, arrOld[i], sNew, [rfReplaceAll, rfIgnoreCase]);
end;

function CountStr(const sSrc, sFind: string; n: Integer = 1): Integer;
var
  i, nLen: Integer;
begin
  result := 0;
  nLen := length(sSrc);
  i := n;                     // 일정 인덱스 이후의 문자를 검색.
  while i <= nLen do
  begin
    i := PosEx(sFind, sSrc, i);
    if i > 0 then
      Inc(result)
    else break;

    Inc(i);
  end;
end;

function IsUTF8_AnsiChar(const str: PAnsiChar): Boolean;
var
  i, len, c, bits, b: Integer;
begin
  Result := True; // 기본적으로 True로 설정
  i := 0;
  len := StrLen(str);
  while i < len do
  begin
    c := Ord(str[i]);
    if c > 128 then
    begin
      if (c >= 254) then Exit(False)
      else if (c >= 252) then bits := 6
      else if (c >= 248) then bits := 5
      else if (c >= 240) then bits := 4
      else if (c >= 224) then bits := 3
      else if (c >= 192) then bits := 2
      else Exit(False);

      if (i + bits) > len then Exit(False);
      while bits > 1 do
      begin
        Inc(i);
        b := Ord(str[i]);
        if (b < 128) or (b > 191) then Exit(False);
        Dec(bits);
      end;
    end;
    Inc(i);
  end;
end;

function IsUTF8(const B: TBytes): Boolean;
var
  i, Len: Integer;
  C: Byte;
  nNeed: Integer;
  bMultiByte: Boolean;
begin
  Result := False;
  Len := Length(B);
  if Len = 0 then
    Exit;

  i := 0;
  nNeed := 0;
  bMultiByte := False;

  while i < Len do
  begin
    C := B[i];

    if nNeed = 0 then
    begin
      // 0xxxxxxx (ASCII)
      if (C and $80) = 0 then
      begin
        Inc(i);
        Continue;
      end;

      // 110xxxxx (2바이트 시작)
      if (C and $E0) = $C0 then
      begin
        nNeed := 1;
        bMultiByte := True;
      end
      // 1110xxxx (3바이트 시작)
      else if (C and $F0) = $E0 then
      begin
        nNeed := 2;
        bMultiByte := True;
      end
      // 11110xxx (4바이트 시작)
      else if (C and $F8) = $F0 then
      begin
        nNeed := 3;
        bMultiByte := True;
      end
      else
        Exit(False); // UTF-8 규칙 위반
    end
    else
    begin
      // 10xxxxxx (연속 바이트)
      if (C and $C0) <> $80 then
        Exit(False);
      Dec(nNeed);
    end;

    Inc(i);
  end;

  // 모든 멀티바이트 시퀀스가 완결되었고,
  // 최소 1개 이상의 멀티바이트 문자가 있었을 때만 UTF-8이라고 판단
  Result := (nNeed = 0) and bMultiByte;
end;

function LastIndexOf(const sFind, sSource: string): Integer;
var
  i, nFindLen, nSrcLen: Integer;
begin
  Result := 0;
  nFindLen := Length(sFind);
  nSrcLen := Length(sSource);
  if nFindLen > nSrcLen then
    exit;
  for i := nSrcLen - nFindLen downto 1 do
  begin
    if Copy(sSource, i, nFindLen) = sFind then
    begin
      Result := i;
      exit;
    end;
  end;
end;

end.
