unit _Catalog;

interface

uses
  Windows, SysUtils, Classes,
  _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に特に意味はない。ただ、ちょっと読み過ぎでも大丈夫なように（ぉぃ）
}

type
  TCatalog = class
  private
    { Private 宣言 }
    CatalogFilePath:string;
    IndexFilename,DataFilename:string;
    CatalogIndexCount:integer;
    CatalogIndex:array of TCatalogIndex;
    Files:TFiles;
  public
    { Public 宣言 }
    procedure Init(sCatalogFilePath:string);
    procedure FilesClear(FullPath:string);
    function  GetFileCount:integer;
    procedure FilesAddMask(ext:string);
    procedure FilesLoadList;
    function  GetCatalogFilePath:string;
    function  GetIndexFilename:string;
    function  GetDataFilename:string;
    procedure LoadIndex;
    procedure SaveIndex(FilePath:string);
    function  LoadData(FilePath:string;var List:TStrings):boolean;
    procedure SaveData(FilePath:string;const List:TStrings);
  end;

implementation

uses _SimpleDialog, _m_Tools,_fsTools, _filebuf, _MDXWinINI;

procedure TCatalog.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 TCatalog.FilesClear(FullPath:string);
begin
  with Files do begin
    Directory:=FullPath;
    MaskCount:=0;
    FilenameCount:=0;
  end;
end;

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

procedure TCatalog.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 TCatalog.FilesLoadList;
var
  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+'\*.*', (faReadOnly or faHidden or faSysFile or faArchive), SearchRec);

  if res<>0 then exit;

  repeat
    fname:=SearchRec.Name;
    ext:=copy(lowercase(ExtractFileExt(fname)),2,255);
    with Files do begin
      extmatch:=False;
      for MaskNum:=0 to MaskCount-1 do begin
        if Mask[MaskNum]=ext then extmatch:=True;
      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:=0 to FilenameCount-1 do begin
      TestStr[S1Cnt]:=lowercase(Filename[S1Cnt]);
    end;
    for S1Cnt:=0 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 TCatalog.GetCatalogFilePath:string;
begin
  Result:=CatalogFilePath;
end;

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

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

procedure TCatalog.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 TCatalog.SaveIndex(FilePath:string);
var
  wfb:TWriteFileBuf;
  cnt:integer;
begin
  CatalogIndexCount:=0;

  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 TCatalog.LoadData(FilePath:string;var List:TStrings):boolean;
var
  MDXTitleChanged:boolean;
  Drive:string;
  VolumeSerial:dword;
  Datafs:TFileStream;
  Idx:integer;
  tmp:string;
  Segment,DataSize:dword;
  ReadFilename,ReadTitle:string;
  ExistFlag:boolean;
  FileCount:integer;
  FileUsedFlag:array of boolean;
  Filename:array of string;
  function SeekDataSegment(Serial:dword;Path:string):dword;
  var
    cnt:integer;
  begin
    Result:=0;
    for cnt:=0 to CatalogIndexCount-1 do begin
      if CatalogIndex[cnt].Serial=Serial then begin
        if CatalogIndex[cnt].Path=Path then Result:=CatalogIndex[cnt].Segment;
      end;
    end;
  end;
begin
  MDXTitleChanged:=False;

  FileCount:=Files.FilenameCount;
  SetLength(FileUsedFlag,FileCount);
  SetLength(Filename,FileCount);
  for Idx:=0 to FileCount-1 do begin
    FileUsedFlag[Idx]:=False;
    Filename[Idx]:=Files.Filename[Idx];
  end;

  VolumeSerial:=$00000000;
  Drive:=char(FilePath[1]);
  if ('0'<=Drive) and (Drive<='9') then begin
    VolumeSerial:=GetStrCRC16(MainINI.UserPath[ord(Drive[1])-ord('0')]);
  end;
  if (('a'<=Drive) and (Drive<='z')) or (('A'<=Drive) and (Drive<='Z')) then begin
    VolumeSerial:=GetDriveInfomation(Drive).VolumeSerial;
  end;

  Segment:=SeekDataSegment(VolumeSerial,FilePath);

  if Segment<>0 then begin
    if FileExists(DataFilename)=False then begin
      end else begin
      Datafs:=TFileStream.Create(DataFilename,fmOpenReadWrite or fmShareDenyWrite);
      if GetStreamMString(Datafs)<>CatalogDataHeadder then begin
        ShowMessage('MDXWin.exe','カタログデータヘッダが異常です。'+chr(13)+'メニューから最適化を行ってください。');
        end else begin
        Datafs.Position:=Segment;
        DataSize:=GetStreamDWord(Datafs);
        if GetStreamMString(Datafs)=FilePath then begin
          while (Datafs.Position-Segment)<DataSize do begin
            ReadFilename:=GetStreamMString(Datafs);
            ReadTitle:=GetStreamMString(Datafs);
            if ReadTitle='' then ReadTitle:='['+ReadFilename+']';
            if ReadFilename<>'' then begin
              ExistFlag:=False;
              for Idx:=0 to FileCount-1 do begin
                if ExistFlag=False then begin
                  if Filename[Idx]=ReadFilename then begin
                    ExistFlag:=True;
                    tmp:=Filename[Idx]+'|';
                    Filename[Idx]:=tmp+StringOfChar(' ',16-Length(tmp))+' '+ReadTitle;
                    FileUsedFlag[Idx]:=True;
                  end;
                end;
              end;
              if ExistFlag=False then MDXTitleChanged:=True;
            end;
          end;
        end;
      end;
      Datafs.Free;
    end;
  end;

  for Idx:=0 to FileCount-1 do begin
    List.Add('  '+Filename[Idx])
  end;

  Result:=MDXTitleChanged;
end;

procedure TCatalog.SaveData(FilePath:string;const List:TStrings);
var
  Drive:string;
  VolumeSerial:dword;
  cnt:integer;
  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;

  VolumeSerial:=$00000000;
  Drive:=char(FilePath[1]);
  if ('0'<=Drive) and (Drive<='9') then begin
    VolumeSerial:=GetStrCRC16(MainINI.UserPath[ord(Drive[1])-ord('0')]);
  end;
  if (('a'<=Drive) and (Drive<='z')) or (('A'<=Drive) and (Drive<='Z')) then begin
    VolumeSerial:=GetDriveInfomation(Drive).VolumeSerial;
  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;

  for cnt:=0 to CatalogIndexCount-1 do begin
    with CatalogIndex[cnt] do begin
      if Serial=VolumeSerial then begin
        if Path=FilePath then begin
          Path[1]:='.';
          Idxfs.Position:=Offset+8;
          SetStreamMString(Idxfs,Path);
        end;
      end;
    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:=VolumeSerial;
  CatalogIndex[CatalogIndexCount].Segment:=Segment;
  CatalogIndex[CatalogIndexCount].Path:=FilePath;
  inc(CatalogIndexCount);

  SetStreamDWord(Idxfs,VolumeSerial);
  SetStreamDWord(Idxfs,Segment);
  SetStreamMString(Idxfs,FilePath);

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

  Idxfs.Free;
  Datafs.Free;
end;

end.
