{
    This file is part of the Free Pascal run time library.
    Copyright (c) 2021 by the Free Pascal development team.

    Helper RTL functions for The WebAssembly System Interface (WASI).

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}

{$IFNDEF FPC_DOTTEDUNITS}
unit wasiutil;
{$ENDIF FPC_DOTTEDUNITS}

{$mode objfpc}

interface

{$IFDEF FPC_DOTTEDUNITS}
uses
  WASIApi.WASIApi;
{$ELSE FPC_DOTTEDUNITS}
uses
  wasiapi;
{$ENDIF FPC_DOTTEDUNITS}

type
  PWasiSearchRec = ^TWasiSearchRec;
  TWasiSearchRec = record
    SearchPos  : UInt64;             {directory position}
    SearchNum  : LongInt;            {to track which search this is}
    DirFD      : __wasi_fd_t;        {directory fd handle for reading directory}
    SearchType : Byte;               {0=normal, 1=open will close, 2=only 1 file}
    SearchAttr : Byte;               {attribute we are searching for}
    Attr       : Byte;               {attribute of found file}
    Time       : __wasi_timestamp_t; {last modify date of found file}
    Size       : __wasi_filesize_t;  {file size of found file}
    Name       : RawByteString;      {name of found file}
    SearchSpec : RawByteString;      {search pattern}
    NamePos    : Word;               {end of path, start of name position}
  End;

function ConvertToFdRelativePath(path: RawByteString; out fd: LongInt; out relfd_path: RawByteString): Word; external name 'FPC_WASI_CONVERTTOFDRELATIVEPATH';
function fpc_wasi_path_readlink_ansistring(fd: __wasi_fd_t; const path: PAnsiChar; path_len: size_t; out link: rawbytestring): __wasi_errno_t; external name 'FPC_WASI_PATH_READLINK_ANSISTRING';
function FNMatch(const Pattern,Name:rawbytestring):Boolean;
function WasiFindFirst(const Path: RawByteString; Attr: Word; var f: TWasiSearchRec): longint;
function WasiFindNext(var f: TWasiSearchRec): longint;
procedure WasiFindClose(var f: TWasiSearchRec);
Function UniversalToEpoch(year,month,day,hour,minute,second:Word):int64;
Function LocalToEpoch(year,month,day,hour,minute,second:Word):int64;
Procedure EpochToUniversal(epoch:int64;var year,month,day,hour,minute,second:Word);
Procedure EpochToLocal(epoch:int64;var year,month,day,hour,minute,second:Word);

implementation

const
  {Bitmasks for file attribute}
  readonly  = $01;
  hidden    = $02;
  sysfile   = $04;
  volumeid  = $08;
  directory = $10;
  archive   = $20;
  anyfile   = $3F;

Const
  RtlFindSize = 15;
Type
  RtlFindRecType = Record
    DirFD    : LongInt;
    SearchNum,
    LastUsed : LongInt;
  End;
Var
  RtlFindRecs   : Array[1..RtlFindSize] of RtlFindRecType;
  CurrSearchNum : LongInt;

Function FNMatch(const Pattern,Name:rawbytestring):Boolean;
Var
  LenPat,LenName : longint;

  Function DoFNMatch(i,j:longint):Boolean;
  Var
    Found : boolean;
  Begin
  Found:=true;
  While Found and (i<=LenPat) Do
   Begin
     Case Pattern[i] of
      '?' : Found:=(j<=LenName);
      '*' : Begin
            {find the next character in pattern, different of ? and *}
              while Found do
                begin
                inc(i);
                if i>LenPat then Break;
                case Pattern[i] of
                  '*' : ;
                  '?' : begin
                          if j>LenName then begin DoFNMatch:=false; Exit; end;
                          inc(j);
                        end;
                else
                  Found:=false;
                end;
               end;
              Assert((i>LenPat) or ( (Pattern[i]<>'*') and (Pattern[i]<>'?') ));
            {Now, find in name the character which i points to, if the * or ?
             wasn't the last character in the pattern, else, use up all the
             chars in name}
              Found:=false;
              if (i<=LenPat) then
              begin
                repeat
                  {find a letter (not only first !) which maches pattern[i]}
                  while (j<=LenName) and (name[j]<>pattern[i]) do
                    inc (j);
                  if (j<LenName) then
                  begin
                    if DoFnMatch(i+1,j+1) then
                    begin
                      i:=LenPat;
                      j:=LenName;{we can stop}
                      Found:=true;
                      Break;
                    end else
                      inc(j);{We didn't find one, need to look further}
                  end else
                  if j=LenName then
                  begin
                    Found:=true;
                    Break;
                  end;
                  { This 'until' condition must be j>LenName, not j>=LenName.
                    That's because when we 'need to look further' and
                    j = LenName then loop must not terminate. }
                until (j>LenName);
              end else
              begin
                j:=LenName;{we can stop}
                Found:=true;
              end;
            end;
     else {not a wildcard character in pattern}
       Found:=(j<=LenName) and (pattern[i]=name[j]);
     end;
     inc(i);
     inc(j);
   end;
  DoFnMatch:=Found and (j>LenName);
  end;

Begin {start FNMatch}
  LenPat:=Length(Pattern);
  LenName:=Length(Name);
  FNMatch:=DoFNMatch(1,1);
End;

Procedure WasiFindClose(Var f: TWasiSearchRec);
{
  Closes dirfd if it is open
}
Var
  res: __wasi_errno_t;
  i : longint;
Begin
  if f.SearchType=0 then
   begin
     i:=1;
     repeat
       if (RtlFindRecs[i].SearchNum=f.SearchNum) then
        break;
       inc(i);
     until (i>RtlFindSize);
     If i<=RtlFindSize Then
      Begin
        RtlFindRecs[i].SearchNum:=0;
        if f.dirfd<>-1 then
          repeat
            res:=__wasi_fd_close(f.dirfd);
          until (res=__WASI_ERRNO_SUCCESS) or (res<>__WASI_ERRNO_INTR);
      End;
   end;
  f.dirfd:=-1;
End;


Function FindGetFileInfo(const s:rawbytestring;var f:TWasiSearchRec):boolean;
var
  st   : __wasi_filestat_t;
  fd   : __wasi_fd_t;
  pr   : RawByteString;
  Info : record
    FMode: LongInt;
    FSize: __wasi_filesize_t;
    FMTime: __wasi_timestamp_t;
  end;
begin
  FindGetFileInfo:=false;
  if ConvertToFdRelativePath(s,fd,pr)<>0 then
    exit;
  { todo: __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW??? }
  if __wasi_path_filestat_get(fd,0,PAnsiChar(pr),Length(pr),@st)<>__WASI_ERRNO_SUCCESS then
    exit;
  info.FSize:=st.size;
  info.FMTime:=st.mtim;
  if st.filetype=__WASI_FILETYPE_DIRECTORY then
   info.fmode:=$10
  else
   info.fmode:=$0;
  {if (st.st_mode and STAT_IWUSR)=0 then
   info.fmode:=info.fmode or 1;}
  if s[f.NamePos+1]='.' then
   info.fmode:=info.fmode or $2;

  If ((Info.FMode and Not(f.searchattr))=0) Then
   Begin
     f.Name:=Copy(s,f.NamePos+1);
     f.Attr:=Info.FMode;
     f.Size:=Info.FSize;
     f.Time:=Info.FMTime;
     FindGetFileInfo:=true;
   End;
end;


Function  FindLastUsed: Longint;
{
  Find unused or least recently used dirpointer slot in findrecs array
}
Var
  BestMatch,i : Longint;
  Found       : Boolean;
Begin
  BestMatch:=1;
  i:=1;
  Found:=False;
  While (i <= RtlFindSize) And (Not Found) Do
   Begin
     If (RtlFindRecs[i].SearchNum = 0) Then
      Begin
        BestMatch := i;
        Found := True;
      End
     Else
      Begin
        If RtlFindRecs[i].LastUsed > RtlFindRecs[BestMatch].LastUsed Then
         BestMatch := i;
      End;
     Inc(i);
   End;
  FindLastUsed := BestMatch;
End;



function WasiFindNext(var f: TWasiSearchRec): longint;
{
  re-opens dir if not already in array and calls FindWorkProc
}
Var
  fd,ourfd: __wasi_fd_t;
  pr: RawByteString;
  res: __wasi_errno_t;
  DirName  : RawByteString;
  i,
  ArrayPos : Longint;
  FName,
  SName    : RawByteString;
  Found,
  Finished : boolean;
  Buf: array [0..SizeOf(__wasi_dirent_t)+256-1] of Byte;
  BufUsed: __wasi_size_t;
Begin
  If f.SearchType=0 Then
   Begin
     ArrayPos:=0;
     For i:=1 to RtlFindSize Do
      Begin
        If RtlFindRecs[i].SearchNum = f.SearchNum Then
         ArrayPos:=i;
        Inc(RtlFindRecs[i].LastUsed);
      End;
     If ArrayPos=0 Then
      Begin
        If f.NamePos = 0 Then
         DirName:='./'
        Else
         DirName:=Copy(f.SearchSpec,1,f.NamePos);
        if ConvertToFdRelativePath(DirName,fd,pr)=0 then
         begin
           { WasmTime doesn't like opening an empty path }
           if pr='' then
             pr:='.';
           repeat
             res:=__wasi_path_open(fd,
                                   0,
                                   PAnsiChar(pr),
                                   length(pr),
                                   __WASI_OFLAGS_DIRECTORY,
                                   __WASI_RIGHTS_FD_READDIR,
                                   __WASI_RIGHTS_FD_READDIR,
                                   0,
                                   @ourfd);
           until (res=__WASI_ERRNO_SUCCESS) or (res<>__WASI_ERRNO_INTR);
           If res=__WASI_ERRNO_SUCCESS Then
            begin
              f.DirFD := ourfd;
              ArrayPos:=FindLastUsed;
              If RtlFindRecs[ArrayPos].SearchNum > 0 Then
                repeat
                  res:=__wasi_fd_close(RtlFindRecs[arraypos].DirFD);
                until (res=__WASI_ERRNO_SUCCESS) or (res<>__WASI_ERRNO_INTR);
              RtlFindRecs[ArrayPos].SearchNum := f.SearchNum;
              RtlFindRecs[ArrayPos].DirFD := f.DirFD;
            end
           else
            f.DirFD:=-1;
         end
        else
         f.DirFD:=-1;
      End;
     if ArrayPos>0 then
       RtlFindRecs[ArrayPos].LastUsed:=0;
   end;
{Main loop}
  SName:=Copy(f.SearchSpec,f.NamePos+1);
  Found:=False;
  Finished:=(f.DirFD=-1);
  While Not Finished Do
   Begin
     res:=__wasi_fd_readdir(f.DirFD,
                            @buf,
                            SizeOf(buf),
                            f.searchpos,
                            @bufused);
     if (res<>__WASI_ERRNO_SUCCESS) or (bufused<=SizeOf(__wasi_dirent_t)) then
      FName:=''
     else
      begin
        SetLength(FName,P__wasi_dirent_t(@buf)^.d_namlen);
        Move(buf[SizeOf(__wasi_dirent_t)],FName[1],Length(FName));
        f.searchpos:=P__wasi_dirent_t(@buf)^.d_next;
      end;
     If FName='' Then
      Finished:=True
     Else
      Begin
        If FNMatch(SName,FName) Then
         Begin
           Found:=FindGetFileInfo(Copy(f.SearchSpec,1,f.NamePos)+FName,f);
           if Found then
            Finished:=true;
         End;
      End;
   End;
{Shutdown}
  If Found Then
   result:=0
  Else
   Begin
     WasiFindClose(f);
     result:=18;
   End;
End;


function WasiFindFirst(const Path: RawByteString; Attr: Word; var f: TWasiSearchRec): longint;
{
  opens dir and calls FindWorkProc
}
Begin
  fillchar(f,sizeof(f),0);
  if Path='' then
   begin
     result:=3;
     exit;
   end;
{Create Info}
  f.SearchSpec := Path;
  {We always also search for readonly and archive, regardless of Attr:}
  f.SearchAttr := Attr or archive or readonly;
  f.SearchPos  := 0;
  f.NamePos := Length(f.SearchSpec);
  while (f.NamePos>0) and not (f.SearchSpec[f.NamePos] in AllowDirectorySeparators) do
   dec(f.NamePos);
{Wildcards?}
  if (Pos('?',Path)=0)  and (Pos('*',Path)=0) then
   begin
     if FindGetFileInfo(Path,f) then
      result:=0
     else
      begin
        { According to tdos2 test it should return 18
        if ErrNo=Sys_ENOENT then
         result:=3
        else }
         result:=18;
      end;
     f.DirFD:=-1;
     f.SearchType:=1;
     f.searchnum:=-1;
   end
  else
{Find Entry}
   begin
     Inc(CurrSearchNum);
     f.SearchNum:=CurrSearchNum;
     f.SearchType:=0;
     result:=WasiFindNext(f);
   end;
End;


Function UniversalToEpoch(year,month,day,hour,minute,second:Word):int64;
const
  days_in_month: array [boolean, 1..12] of Byte =
    ((31,28,31,30,31,30,31,31,30,31,30,31),
     (31,29,31,30,31,30,31,31,30,31,30,31));
  days_before_month: array [boolean, 1..12] of Word =
    ((0,
      0+31,
      0+31+28,
      0+31+28+31,
      0+31+28+31+30,
      0+31+28+31+30+31,
      0+31+28+31+30+31+30,
      0+31+28+31+30+31+30+31,
      0+31+28+31+30+31+30+31+31,
      0+31+28+31+30+31+30+31+31+30,
      0+31+28+31+30+31+30+31+31+30+31,
      0+31+28+31+30+31+30+31+31+30+31+30),
     (0,
      0+31,
      0+31+29,
      0+31+29+31,
      0+31+29+31+30,
      0+31+29+31+30+31,
      0+31+29+31+30+31+30,
      0+31+29+31+30+31+30+31,
      0+31+29+31+30+31+30+31+31,
      0+31+29+31+30+31+30+31+31+30,
      0+31+29+31+30+31+30+31+31+30+31,
      0+31+29+31+30+31+30+31+31+30+31+30));
var
  leap: Boolean;
  days_in_year: LongInt;
  y,m: LongInt;
begin
  if (year<1970) or (month<1) or (month>12) or (day<1) or (day>31) or
     (hour>=24) or (minute>=60) or (second>=60) then
  begin
    result:=-1;
    exit;
  end;
  leap:=((year mod 4)=0) and (((year mod 100)<>0) or ((year mod 400)=0));
  if day>days_in_month[leap,month] then
  begin
    result:=-1;
    exit;
  end;
  result:=0;
  for y:=1970 to year-1 do
    if ((y mod 4)=0) and (((y mod 100)<>0) or ((y mod 400)=0)) then
      Inc(result,366)
    else
      Inc(result,365);
  Inc(result,days_before_month[leap,month]);
  Inc(result,day-1);
  result:=(((result*24+hour)*60+minute)*60)+second;
end;

Function LocalToEpoch(year,month,day,hour,minute,second:Word):int64;
begin
  { todo: convert UTC to local time, as soon as we can get the local timezone
    from WASI: https://github.com/WebAssembly/WASI/issues/239 }
  result:=UniversalToEpoch(year,month,day,hour,minute,second);
end;


Procedure EpochToUniversal(epoch:int64;var year,month,day,hour,minute,second:Word);
const
  days_in_month: array [boolean, 1..12] of Byte =
    ((31,28,31,30,31,30,31,31,30,31,30,31),
     (31,29,31,30,31,30,31,31,30,31,30,31));
var
  leap: Boolean;
  days_in_year: LongInt;
begin
  if epoch<0 then
  begin
    year:=0;
    month:=0;
    day:=0;
    hour:=0;
    minute:=0;
    second:=0;
    exit;
  end;
  second:=epoch mod 60;
  epoch:=epoch div 60;
  minute:=epoch mod 60;
  epoch:=epoch div 60;
  hour:=epoch mod 24;
  epoch:=epoch div 24;
  year:=1970;
  leap:=false;
  days_in_year:=365;
  while epoch>=days_in_year do
  begin
    Dec(epoch,days_in_year);
    Inc(year);
    leap:=((year mod 4)=0) and (((year mod 100)<>0) or ((year mod 400)=0));
    if leap then
      days_in_year:=366
    else
      days_in_year:=365;
  end;
  month:=1;
  Inc(epoch);
  while epoch>days_in_month[leap,month] do
  begin
    Dec(epoch,days_in_month[leap,month]);
    Inc(month);
  end;
  day:=Word(epoch);
end;


Procedure EpochToLocal(epoch:int64;var year,month,day,hour,minute,second:Word);
begin
  { todo: convert UTC to local time, as soon as we can get the local timezone
    from WASI: https://github.com/WebAssembly/WASI/issues/239 }
  EpochToUniversal(epoch,year,month,day,hour,minute,second);
end;

end.
