unit _NetworkCatalog;

interface

uses
  Windows, SysUtils, Classes, StdCtrls,
  _SimpleDialog, _MDXWin_const;

{$WARN UNIT_PLATFORM OFF}
{$WARN SYMBOL_PLATFORM OFF}

{
MDXWin Catalog File Ver1.20 ファイルフォーマット仕様書

文字列は、全て\0を含まない文字数(byte)が先頭に付く。(256文字以上は扱えない）
文字数+1が、次のデータへのオフセットになる。

ex> "0123" $04,'0','1','2','3'
    ""     $00

文字数以外の数値は全てDWORD(4byte)で格納される。
余った領域は前方向に$00が追加される。

ex> $12345678 $78,$56,$34,$12
    $1234     $00,$00,$34,$12

-- catalog.idx

$20,"MDXWin Catalog IndexFile Ver1.20"
dword(シリアル),dword(ポインタ),byte(文字数),"パス名"
dword(シリアル),dword(ポインタ),byte(文字数),"パス名"
dword(シリアル),dword(ポインタ),byte(文字数),"パス名"

以降、ファイル最後まで続く。ファイル終端は無し。
追加したいときは、最終ポインタに書き込むだけでＯＫ。
パス名の最初の文字が'.'の時は、無効なエントリを指す。
無効エントリがたまってきたら最適化をするようにする。（予定）

-- catalog.dat

$1f,"MDXWin Catalog DataFile Ver1.20"
dword(サイズ),byte,"パス名"
byte,"ファイル名",byte,"タイトル"
byte,"ファイル名",byte,"タイトル"
byte,"ファイル名",byte,"タイトル"
(dword)$00000000
dword(サイズ),byte,"パス名"
byte,"ファイル名",byte,"タイトル"
byte,"ファイル名",byte,"タイトル"
byte,"ファイル名",byte,"タイトル"
(dword)$00000000

以降、ファイル最後まで続く。ファイル終端は無し。
追加したいときは、最終ポインタに書き込むだけでＯＫ。
dword(サイズ)は、dword(サイズ)を含めないパッケージ終端$00までのバイト数を表す。
つまり、dword(ポインタ)+dword(サイズ)で得られるオフセットからは$00000000となる。
(dword)$00000000に特に意味はない。ただ、ちょっと読み過ぎでも大丈夫なように（ぉぃ）

NetworkCatalogのみ：ファイル名の最後が[\]の時はフォルダ名
}

type
  TNetworkCatalog = class
  private
    { Private 宣言 }
    CatalogFilePath:string;
    IndexFilename,DataFilename:string;
    Files:TFiles;
    function SeekDataSegment(Path:string):dword;
  public
    { Public 宣言 }
    CatalogIndexCount:integer;
    CatalogIndex:array of TCatalogIndex;
    CatalogIndexPathHashLoaded:boolean;
    CatalogIndexPathHash:array of word;
    procedure Init(sCatalogFilePath:string);
    procedure FilesClear(FullPath:string);
    function  GetFileCount:integer;
    procedure FilesAddMask(ext:string);
    procedure FilesDirLoadList(LoadSZipFiles:boolean);
    function  GetCatalogFilePath:string;
    function  GetIndexFilename:string;
    function  GetDataFilename:string;
    procedure LoadIndex;
    procedure SaveIndex(FilePath:string);
    function  LoadData(FilePath:string;var List:TStrings;cnvSJIS:boolean):boolean;
    function  LoadDataFromSegment(Segment:dword;var List:TStrings;cnvSJIS:boolean):boolean;
    function  MakeData(var List:TStrings):boolean;
    procedure AddData(FilePath:string;const List:TStrings);
    function  GetTitle(const path,filename:string;cnvSJIS:boolean):string;
    procedure GetTitlePackStart(var Datafs:TFileStream);
    function  GetTitlePackSeek(var Datafs:TFileStream;const path,filename:string;cnvSJIS:boolean):string;
    procedure GetTitlePackEnd(var Datafs:TFileStream);
  end;

implementation

uses _m_Tools, _fsTools,_filebuf;

procedure TNetworkCatalog.Init(sCatalogFilePath:string);
begin
  CatalogFilePath:=sCatalogFilePath;
  IndexFilename:=CatalogFilePath+'\catalog.idx';
  DataFilename:=CatalogFilePath+'\catalog.dat';
  with Files do begin
    Directory:='';
    MaskCount:=0;
    SetLength(Mask,0);
    FilenameCount:=0;
    SetLength(Filename,128);
  end;
end;

procedure TNetworkCatalog.FilesClear(FullPath:string);
begin
  with Files do begin
    Directory:=FullPath;
    MaskCount:=0;
    FilenameCount:=0;
  end;
end;

function TNetworkCatalog.GetFileCount:integer;
begin
  Result:=Files.FilenameCount;
end;

procedure TNetworkCatalog.FilesAddMask(ext:string);
begin
  if copy(ext,1,1)='.' then ext:=copy(ext,2,255);
  with Files do begin
    SetLength(Mask,MaskCount+1);
    Mask[MaskCount]:=lowercase(ext);
    inc(MaskCount);
  end;
end;

procedure TNetworkCatalog.FilesDirLoadList(LoadSZipFiles:boolean);
var
  DirOffset:integer;
  res:integer;
  SearchRec: TSearchRec;
  fname:string;
  MaskNum:integer;
  ext:string;
  extmatch:boolean;

  S1Cnt,S2Cnt:integer; // Sort
  SStr:string;
  TestStr:array[0..16384] of string;
begin
  if Files.MaskCount=0 then exit;

  res:=FindFirst(Files.Directory+'\*.*', (faDirectory or faReadOnly or faHidden or faSysFile or faArchive), SearchRec);

  if res<>0 then exit;

  repeat
    if (SearchRec.Attr and faDirectory)<>0 then begin
      with Files do begin
        fname:=SearchRec.Name;
        if LoadSZipFiles=False then begin
          if (fname<>'.') and (fname<>'..') then begin
            if (FilenameCount mod 128)=0 then SetLength(Filename,FilenameCount+128);
            Filename[FilenameCount]:=fname+'\';
            inc(FilenameCount);
          end;
          end else begin
          if (fname<>'.') and (fname<>'..') and (fname<>'_cgi-bin') and (fname<>'FMPPMD_PCM') and (fname<>'MXDRV_PDX') then begin
            if (FilenameCount mod 128)=0 then SetLength(Filename,FilenameCount+128);
            Filename[FilenameCount]:=fname+'\';
            inc(FilenameCount);
          end;
        end;
      end;
    end;
    res:=FindNext(SearchRec);
  until (res<>0);

  FindClose(SearchRec);

  DirOffset:=Files.FilenameCount;

  res:=FindFirst(Files.Directory+'\*.*', (faReadOnly or faHidden or faSysFile or faArchive), SearchRec);

  if res<>0 then exit;

  repeat
    fname:=SearchRec.Name;
    ext:=copy(lowercase(ExtractFileExt(fname)),2,255);
    if (LoadSZipFiles=True) and (ext='szip') then begin
      ext:=copy(lowercase(ExtractFileExt(ChangeFileExt(fname,''))),2,255);
    end;
    with Files do begin
      extmatch:=False;
      if MaskCount=1 then begin
        if Mask[0]='*' then extmatch:=True;
        if Mask[0]=ext then extmatch:=True;
        end else begin
        for MaskNum:=0 to MaskCount-1 do begin
          if Mask[MaskNum]=ext then extmatch:=True;
        end;
      end;
      if extmatch=True then begin
        if (FilenameCount mod 128)=0 then SetLength(Filename,FilenameCount+128);
        Filename[FilenameCount]:=fname;
        inc(FilenameCount);
      end;
    end;
    res:=FindNext(SearchRec);
  until (res<>0);

  FindClose(SearchRec);

  with Files do begin
    for S1Cnt:=DirOffset to FilenameCount-1 do begin
      TestStr[S1Cnt]:=lowercase(Filename[S1Cnt]);
    end;
    for S1Cnt:=DirOffset to FilenameCount-2 do begin
      for S2Cnt:=S1Cnt to FilenameCount-1 do begin
        if TestStr[S1Cnt]>TestStr[S2Cnt] then begin
          SStr:=Filename[S1Cnt];
          Filename[S1Cnt]:=Filename[S2Cnt];
          Filename[S2Cnt]:=SStr;
          SStr:=TestStr[S1Cnt];
          TestStr[S1Cnt]:=TestStr[S2Cnt];
          TestStr[S2Cnt]:=SStr;
        end;
      end;
    end;
  end;
end;

function TNetworkCatalog.GetCatalogFilePath:string;
begin
  Result:=CatalogFilePath;
end;

function TNetworkCatalog.GetIndexFilename:string;
begin
  Result:=IndexFilename;
end;

function TNetworkCatalog.GetDataFilename:string;
begin
  Result:=DataFilename;
end;

procedure TNetworkCatalog.LoadIndex;
var
  rfb:TReadFileBuf;
  Disable:integer;
begin
  CatalogIndexCount:=0;
  SetLength(CatalogIndex,128);
  if FileExists(IndexFilename)=False then exit;

  rfb:=TReadFileBuf.Create;
  rfb.Init(IndexFilename);
  rfb.LoadFile;
  if rfb.GetMString<>CatalogIndexHeadder then begin
    ShowMessage('MDXWin.exe','カタログインデックスヘッダが異常です');
    rfb.Free;
    exit;
  end;

  Disable:=0;

  while rfb.GetPosition<rfb.GetCount do begin
    if (CatalogIndexCount mod 128)=0 then SetLength(CatalogIndex,CatalogIndexCount+128);
    with CatalogIndex[CatalogIndexCount] do begin
      Offset:=rfb.GetPosition;
      rfb.GetVDWord(Serial);
      rfb.GetVDWord(Segment);
      rfb.GetVMString(Path);
      if Path[1]='.' then inc(Disable)
    end;
    inc(CatalogIndexCount);
  end;
  rfb.Free;

  if Disable>=100 then ShowMessage('MDXWin.exe','無効なフォルダが１００を超えました。'+chr(13)+'カタログファイルの最適化をおすすめします。'+chr(13)+'メニューの「カタログファイルの最適化」で行えます。');
end;

procedure TNetworkCatalog.SaveIndex(FilePath:string);
var
  wfb:TWriteFileBuf;
  cnt:integer;
begin
  wfb:=TWriteFileBuf.Create;
  wfb.Init(IndexFilename);
  wfb.SetMString(CatalogIndexHeadder);

  for cnt:=0 to CatalogIndexCount-1 do begin
    with CatalogIndex[cnt] do begin
      wfb.SetVDWord(Serial);
      wfb.SetVDWord(Segment);
      wfb.SetVMString(Path);
    end;
  end;

  wfb.SaveFile;
  wfb.Free;
end;

function TNetworkCatalog.MakeData(var List:TStrings):boolean;
var
  Idx:integer;
begin
  for Idx:=0 to Files.FilenameCount-1 do begin
    List.Add(Files.Filename[Idx]);
  end;

  Result:=True;
end;

function TNetworkCatalog.SeekDataSegment(Path:string):dword;
var
  cnt:integer;
  PathHash:word;
begin
  if CatalogIndexPathHashLoaded=False then begin
    CatalogIndexPathHashLoaded:=True;
    SetLength(CatalogIndexPathHash,CatalogIndexCount);
    for cnt:=0 to CatalogIndexCount-1 do begin
      CatalogIndexPathHash[cnt]:=GetStrCRC16(CatalogIndex[cnt].Path);
    end;
  end;

  PathHash:=GetStrCRC16(Path);
  Result:=0;
  for cnt:=0 to CatalogIndexCount-1 do begin
    if CatalogIndexPathHash[cnt]=PathHash then begin
      if CatalogIndex[cnt].Path=Path then begin
        Result:=CatalogIndex[cnt].Segment;
        exit;
      end;
    end;
  end;
end;

function TNetworkCatalog.LoadData(FilePath:string;var List:TStrings;cnvSJIS:boolean):boolean;
begin
  Result:=LoadDataFromSegment(SeekDataSegment(FilePath),List,cnvSJIS);
end;

function TNetworkCatalog.LoadDataFromSegment(Segment:dword;var List:TStrings;cnvSJIS:boolean):boolean;
var
  Datafs:TFileStream;
  DataSize:dword;
  ReadFilename,ReadTitle:string;
  tmp:string;
begin
  if Segment=0 then begin
    Result:=False;
    end else begin
    if FileExists(DataFilename)=False then begin
      Result:=False;
      end else begin
      Datafs:=TFileStream.Create(DataFilename,fmOpenRead or fmShareDenyWrite);
      if GetStreamMString(Datafs)<>CatalogDataHeadder then begin
        Result:=False;
        ShowMessage('MDXWin.exe','ネットワークカタログデータヘッダが異常です。');
        end else begin
        Datafs.Position:=Segment;
        DataSize:=GetStreamDWord(Datafs);
        GetStreamMString(Datafs);
        while (Datafs.Position-Segment)<DataSize do begin
          ReadFilename:=GetStreamMString(Datafs);
          ReadTitle:=GetStreamMString(Datafs);
          if cnvSJIS=True then ReadTitle:=strCnvAnk2to1byte(strGetCompact(strCnvX68kPlusSJIS(strClearESC(ReadTitle))));
          if ReadFilename<>'' then begin
            if copy(ReadFilename,Length(ReadFilename),1)='\' then begin // Directory
              List.Add('_ < Directory >  '+copy(ReadFilename,1,Length(ReadFilename)-1));
              end else begin
              tmp:=ReadFilename+'|';
//              List.Add('  '+tmp+StringOfChar(' ',16-Length(tmp))+' '+ReadTitle);
              List.Add('  '+ReadFilename+'|'+ReadTitle);
            end;
          end;
        end;
        Result:=True;
      end;
      Datafs.Free;
    end;
  end;

end;

procedure TNetworkCatalog.AddData(FilePath:string;const List:TStrings);
var
  tmp:string;
  Idxfs,Datafs:TFileStream;
  WriteIdx,WriteStr:string;
  Idx,TitlePos:integer;
  Segment,DataSize:dword;
begin
  if ForceDirectories(CatalogFilePath)=False then begin
    ShowMessage('MDXWin.exe','カタログファイルが作れません。フォルダ設定を確認してください。');
    exit;
  end;

  if FileExists(IndexFilename)=False then begin
    Idxfs:=TFileStream.Create(IndexFilename,fmCreate or fmShareDenyWrite);
    tmp:=CatalogIndexHeadder;
    SetStreamMString(Idxfs,tmp);
    end else begin
    Idxfs:=TFileStream.Create(IndexFilename,fmOpenReadWrite or fmShareDenyWrite);
    if GetStreamMString(Idxfs)<>CatalogIndexHeadder then begin
      ShowMessage('MDXWin.exe','カタログインデックスヘッダが異常です'+chr(13)+'メニューから最適化を行ってください。');
      Idxfs.Free;
      exit;
    end;
  end;

  if FileExists(DataFilename)=False then begin
    Datafs:=TFileStream.Create(DataFilename,fmCreate or fmShareDenyWrite);
    tmp:=CatalogDataHeadder;
    SetStreamMString(Datafs,tmp);
    end else begin
    Datafs:=TFileStream.Create(DataFilename,fmOpenReadWrite or fmShareDenyWrite);
    if GetStreamMString(Datafs)<>CatalogDataHeadder then begin
      ShowMessage('MDXWin.exe','カタログデータヘッダが異常です。'+chr(13)+'メニューから最適化を行ってください。');
      Idxfs.Free;
      Datafs.Free;
      exit;
    end;
    Datafs.Position:=Datafs.Size;
  end;
  Segment:=Datafs.Position;

  Idxfs.Position:=Idxfs.Size;

  SetLength(CatalogIndex,CatalogIndexCount+1);
  CatalogIndex[CatalogIndexCount].Offset:=Idxfs.Position;
  CatalogIndex[CatalogIndexCount].Serial:=$00000000;
  CatalogIndex[CatalogIndexCount].Segment:=Segment;
  CatalogIndex[CatalogIndexCount].Path:=FilePath;
  inc(CatalogIndexCount);

  SetStreamDWord(Idxfs,$00000000);
  SetStreamDWord(Idxfs,Segment);
  SetStreamMString(Idxfs,FilePath);

  SetStreamDWord(Datafs,0);
  SetStreamMString(Datafs,FilePath);
  for Idx:=0 to List.Count-1 do begin
    WriteIdx:=List[Idx];
    TitlePos:=ansipos('|',WriteIdx);
    WriteStr:=copy(WriteIdx,1,TitlePos-1);
    SetStreamMString(Datafs,WriteStr);
    WriteStr:=copy(WriteIdx,TitlePos+1,255);
    SetStreamMString(Datafs,WriteStr);
  end;
  DataSize:=Datafs.Position-Segment;
  SetStreamDWord(Datafs,0);
  Datafs.Position:=Segment;
  SetStreamDWord(Datafs,DataSize);

  Idxfs.Free;
  Datafs.Free;
end;

function TNetworkCatalog.GetTitle(const path,filename:string;cnvSJIS:boolean):string;
var
  Datafs:TFileStream;
begin
  GetTitlePackStart(Datafs);
  Result:=GetTitlePackSeek(Datafs,path,filename,cnvSJIS);
  GetTitlePackEnd(Datafs);
end;

procedure TNetworkCatalog.GetTitlePackStart(var Datafs:TFileStream);
begin
  Datafs:=nil;

  if FileExists(DataFilename)=False then begin
    exit;
  end;

  Datafs:=TFileStream.Create(DataFilename,fmOpenRead or fmShareDenyWrite);

  Datafs.Position:=0;
  if GetStreamMString(Datafs)<>CatalogDataHeadder then begin
    ShowMessage('MDXWin.exe','ネットワークカタログデータヘッダが異常です。');
    Datafs.Free;
    Datafs:=nil;
    exit;
  end;
end;

function  TNetworkCatalog.GetTitlePackSeek(var Datafs:TFileStream;const path,filename:string;cnvSJIS:boolean):string;
var
  Segment,DataSize:dword;
  ReadFilename,ReadTitle:string;
begin
  Result:='';

  Segment:=SeekDataSegment(path);

  if Segment=0 then begin
    exit;
  end;

  Datafs.Position:=Segment;
  DataSize:=GetStreamDWord(Datafs);
  if GetStreamMString(Datafs)=path then begin
    while (Datafs.Position-Segment)<DataSize do begin
      ReadFilename:=GetStreamMString(Datafs);
      ReadTitle:=GetStreamMString(Datafs);
      if filename<>'' then begin
        if ReadFilename=filename then begin
          if cnvSJIS=True then ReadTitle:=strCnvAnk2to1byte(strGetCompact(strCnvX68kPlusSJIS(strClearESC(ReadTitle))));
          Result:=ReadTitle;
          break;
        end;
        end else begin
        if copy(ReadFilename,Length(ReadFilename),1)<>'\' then begin // Directory
          if cnvSJIS=True then ReadTitle:=strCnvAnk2to1byte(strGetCompact(strCnvX68kPlusSJIS(strClearESC(ReadTitle))));
          Result:=ReadTitle;
          break;
        end;
      end;
    end;
  end;
end;

procedure TNetworkCatalog.GetTitlePackEnd(var Datafs:TFileStream);
begin
  Datafs.Free;
end;

end.
