unit ChmLangRef;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, chmreader, chmsitemap,
  Dialogs, LazHelpIntf, HelpIntfs,
  FileUtil, LazFileUtils, LazStringUtils, LazUTF8,
  IDEHelpIntf, MacroIntf;

const
  sFPCLangRef = 'FPC Language Reference';

type

  { TLangRefHelpDatabase }

  TLangRefHelpDatabase = class(THelpDatabase)
  private
    FCHMSearchPath: string;
    FKeywordNodes: TList;
    FKeyWordsList: TStringListUTF8Fast;
    FRTLIndex: TStringList;
    procedure ClearKeywordNodes;
    procedure LoadChmIndex(const Path, ChmFileName: string;
      IndexStrings: TStrings; const Filter: string = '');
  public
    constructor Create(TheOwner: TComponent); override;
    destructor Destroy; override;
    procedure LoadKeywordList(const Path: string);
    function GetNodesForKeyword(const HelpKeyword: string;
      var ListOfNodes: THelpNodeQueryList; var ErrMsg: string
      ): TShowHelpResult; override;
    function ShowHelp(Query: THelpQuery; {%H-}BaseNode, NewNode: THelpNode;
      {%H-}QueryItem: THelpQueryItem;
      var ErrMsg: string): TShowHelpResult; override;
    property CHMSearchPath: string read FCHMSearchPath write FCHMSearchPath;
  end;

procedure RegisterLangRefHelpDatabase;

var
  LangRefHelpDatabase: TLangRefHelpDatabase = nil;

implementation

procedure RegisterLangRefHelpDatabase;
begin
  if not Assigned(LangRefHelpDatabase) then
    LangRefHelpDatabase := TLangRefHelpDatabase(HelpDatabases.CreateHelpDatabase(sFPCLangRef,
    TLangRefHelpDatabase, true));
end;

{ TLangRefHelpDatabase }

procedure TLangRefHelpDatabase.ClearKeywordNodes;
var
  i: Integer;
begin
  for i := 0 to FKeywordNodes.Count - 1 do
    TObject(FKeywordNodes[i]).Free;
  FKeywordNodes.Clear;
end;

constructor TLangRefHelpDatabase.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  FKeywordNodes := TList.Create;
  FKeyWordsList := TStringListUTF8Fast.Create;
  FKeyWordsList.CaseSensitive := False;
  FRTLIndex := TStringList.Create;
  FRTLIndex.CaseSensitive := False;
end;

destructor TLangRefHelpDatabase.Destroy;
begin
  ClearKeywordNodes;
  FKeywordNodes.Free;
  FKeyWordsList.Free;
  FRTLIndex.Free;
  inherited Destroy;
end;

procedure TLangRefHelpDatabase.LoadKeywordList(const Path: string);
begin
  FRTLIndex.Clear; // Path has been changed
  LoadChmIndex(Path, 'ref.chm', FKeyWordsList);
end;

procedure TLangRefHelpDatabase.LoadChmIndex(const Path, ChmFileName: string;
  IndexStrings: TStrings; const Filter: string = '');
var
  chm: TChmFileList;
  fchm: TChmReader;
  SM: TChmSiteMap;
  X, Y: Integer;
  s: string;
  Filename: String;
  SMItem: TChmSiteMapItem;
begin
  fCHMSearchPath := Path;
  if fCHMSearchPath = '' then
  begin
    fCHMSearchPath := '$(LazarusDir)/docs/chm;$(LazarusDir)/docs/html';
    IDEMacros.SubstituteMacros(fCHMSearchPath);
    fCHMSearchPath := MinimizeSearchPath(GetForcedPathDelims(fCHMSearchPath));
  end;
  Filename:=SearchFileInPath(ChmFileName,'',fCHMSearchPath,';',[]);

  IndexStrings.Clear;
  if (Filename<>'') then
  begin
    chm := TChmFileList.Create(Utf8ToSys(Filename));
    try
      if chm.Count = 0 then Exit;
      fchm := chm.Chm[0];
      SM := fChm.GetIndexSitemap;
      if SM <> nil then
      begin
        for X := 0 to SM.Items.Count - 1 do
        begin
          SMItem:=SM.Items.Item[X];
          s := SMItem.Name;
          if SMItem.Children.Count = 0 then
          begin
            if (SMItem.Local<>'')
                and ((Filter = '') or (Pos(Filter, SMItem.Local) > 0)) then
              IndexStrings.Add(s + '=' + SMItem.Local)
          end else begin
            with SMItem.Children do
              for Y := 0 to Count - 1 do
              begin
                if (Item[Y].Local<>'')
                and ((Filter = '') or (Pos(Filter, Item[Y].Local) > 0)) then
                  IndexStrings.Add(s + '=' + Item[Y].Local)
              end;
          end;
          for Y:=0 to SMItem.SubItemcount-1 do begin
            if (SMItem.SubItem[Y].Local<>'')
            and ((Filter = '') or (Pos(Filter, SMItem.SubItem[Y].Local) > 0)) then
              IndexStrings.Add(s + '=' + SMItem.SubItem[Y].Local)
          end;
        end;
        SM.Free;
      end;
      fchm.Free;
    finally
      chm.Free;
    end;
  end;
end;

function TLangRefHelpDatabase.GetNodesForKeyword(const HelpKeyword: string;
  var ListOfNodes: THelpNodeQueryList; var ErrMsg: string): TShowHelpResult;
var
  KeyWord, s: String;
  i, n: Integer;
  KeywordNode: THelpNode;
begin
  Result := shrHelpNotFound;
  if (csDesigning in ComponentState) then Exit;
  if (FPCKeyWordHelpPrefix<>'')
  and (LeftStr(HelpKeyword,length(FPCKeyWordHelpPrefix))=FPCKeyWordHelpPrefix) then
  begin
    if FKeyWordsList.Count = 0 then LoadKeywordList(fCHMSearchPath);
    if FKeyWordsList.Count = 0 then
    begin
      Result := shrDatabaseNotFound;
      ErrMsg := Format('ref.chm not found. Please put ref.chm help file in '+ LineEnding
        + '%s' +  LineEnding
        +'or set the path to it with "HelpFilesPath" in '
        +' Environment Options -> Help -> Help Options ->' + LineEnding
        +'under Viewers - CHM Help Viewer', [fCHMSearchPath]);
      Exit;
    end;
    // HelpKeyword starts with KeywordPrefix
    KeyWord := Copy(HelpKeyword, Length(FPCKeyWordHelpPrefix) + 1, Length(HelpKeyword));
    ClearKeywordNodes;
    n := 0;
    for i := 0 to FKeyWordsList.Count - 1 do
    begin
      if SameText(FKeyWordsList.Names[i], KeyWord) then
      begin
        Inc(n);
        KeywordNode := THelpNode.CreateURL(Self,KeyWord,'ref.chm://' + FKeyWordsList.ValueFromIndex[i]);
        KeywordNode.Title := Format('Pascal keyword "%s"', [KeyWord]);
        if n > 1 then
          KeywordNode.Title := KeywordNode.Title + ' (' + IntToStr(n) + ')';
        FKeywordNodes.Add(KeywordNode);
        CreateNodeQueryListAndAdd(KeywordNode,nil,ListOfNodes,true);
        Result := shrSuccess;
      end;
    end;
    if (Result = shrSuccess) and (SameText(KeyWord, 'for') or SameText(KeyWord, 'in')) then
    begin  { for => +forin, in => +forin }
      i := FKeyWordsList.IndexOfName('forin');
      if i < 0 then Exit;
      KeywordNode := THelpNode.CreateURL(Self,KeyWord,'ref.chm://' + FKeyWordsList.ValueFromIndex[i]);
      KeywordNode.Title := Format('Pascal keyword "%s"', ['for..in']);
      FKeywordNodes.Add(KeywordNode);
      CreateNodeQueryListAndAdd(KeywordNode, nil, ListOfNodes, True);
    end;
    if Result <> shrSuccess then
    begin
      { it can be predefined procedure/function from RTL }
      if FRTLIndex.Count = 0 then
        LoadChmIndex(FCHMSearchPath, 'rtl.chm', FRTLIndex, 'system/');
      for i := 0 to FRTLIndex.Count - 1 do
      begin
        s := FRTLIndex.Names[i];
        if LazStartsText(KeyWord, s) and
          ((Length(s) = Length(KeyWord)) or (s[Length(KeyWord) + 1] = ' ')) then
        begin
          KeywordNode := THelpNode.CreateURL(Self,KeyWord,'rtl.chm://' + FRTLIndex.ValueFromIndex[i]);
          KeywordNode.Title := Format('RTL - Free Pascal Run Time Library: "%s"', [KeyWord]);
          FKeywordNodes.Add(KeywordNode);
          CreateNodeQueryListAndAdd(KeywordNode, nil, ListOfNodes, True);
          Exit(shrSuccess); // only first match
        end;
      end;
    end;
  end;
end;

function TLangRefHelpDatabase.ShowHelp(Query: THelpQuery; BaseNode,
  NewNode: THelpNode; QueryItem: THelpQueryItem; var ErrMsg: string
  ): TShowHelpResult;
var
  Viewer: THelpViewer;
begin
  Result:=shrHelpNotFound;
  if not (Query is THelpQueryKeyword) then exit;
  Result := FindViewer('text/html', ErrMsg, Viewer);
  if Result <> shrSuccess then Exit;
  Result := Viewer.ShowNode(NewNode, ErrMsg);
end;

end.

