{
 /***************************************************************************
                            helpfpcmessages.pas
                            -------------------


 ***************************************************************************/

 ***************************************************************************
 *                                                                         *
 *   This source is free software; you can redistribute it and/or modify   *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This code 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.  See the GNU     *
 *   General Public License for more details.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
 *                                                                         *
 ***************************************************************************
 
  Author: Mattias Gaertner
   
  Abstract:
    Help items for FPC messages.
}
unit HelpFPCMessages;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, fgl,
  // LCL
  LCLIntf, Dialogs, Forms, Controls, StdCtrls, ExtCtrls, Graphics,
  ButtonPanel, LazHelpHTML,
  // LazUtils
  LazConfigStorage, LazFileUtils, LazFileCache, LazUTF8, LazLoggerBase,
  // CodeTools
  FileProcs, CodeToolsFPCMsgs, CodeToolManager, CodeCache, DefineTemplates,
  // IdeIntf
  BaseIDEIntf, MacroIntf, HelpIntfs, IDEHelpIntf, IDEMsgIntf,
  IDEExternToolIntf, LazHelpIntf, IDEDialogs, TextTools,
  // IDE
  LazarusIDEStrConsts, EnvironmentOpts;

const
  lihcFPCMessages = 'Free Pascal Compiler messages';
  lihFPCMessagesURL = 'http://wiki.lazarus.freepascal.org/';
  lihFPCMessagesInternalURL = 'file://Build_messages#FreePascal_Compiler_messages';

type

  { TMessageHelpAddition }

  TMessageHelpAddition = class
  public
    Name: string;
    URL: string;
    RegEx: string;
    IDs: string; // comma separated
    procedure Assign(Source: TMessageHelpAddition);
    function IsEqual(Source: TMessageHelpAddition): boolean;
    function Fits(ID: integer; Msg: string): boolean;
  end;
  TBaseMessageHelpAdditions = specialize TFPGObjectList<TMessageHelpAddition>;

  { TMessageHelpAdditions }

  TMessageHelpAdditions = class(TBaseMessageHelpAdditions)
  public
    function FindWithName(Name: string): TMessageHelpAddition;
    function IsEqual(Source: TMessageHelpAdditions): boolean;
    procedure Clone(Source: TMessageHelpAdditions);
    procedure LoadFromConfig(Cfg: TConfigStorage);
    procedure SaveToConfig(Cfg: TConfigStorage);
    procedure LoadFromFile(Filename: string);
    procedure SaveToFile(Filename: string);
  end;

  { TFPCMessagesHelpDatabase }

  TFPCMessagesHelpDatabase = class(THTMLHelpDatabase)
  private
    fAdditions: TMessageHelpAdditions;
    FAdditionsChangeStep: integer;
    FAdditionsFile: string;
    FDefaultAdditionsFile: string;
    FFoundAddition: TMessageHelpAddition;
    FDefaultNode: THelpNode;
    FFoundComment: string;
    FLastMessage: string;
    FLoadedAdditionsFilename: string;
    FMsgFile: TFPCMsgFile;
    FMsgFileChangeStep: integer;
    FMsgFilename: string;
    function GetAdditions(Index: integer): TMessageHelpAddition;
    procedure SetAdditionsFile(AValue: string);
    procedure SetFoundComment(const AValue: string);
    procedure SetLastMessage(const AValue: string);
  public
    constructor Create(TheOwner: TComponent); override;
    destructor Destroy; override;
    function GetNodesForMessage(const AMessage: string; MessageParts: TStrings;
                                var ListOfNodes: THelpNodeQueryList;
                                var ErrMsg: string): TShowHelpResult; override;
    function ShowHelp(Query: THelpQuery; BaseNode, NewNode: THelpNode;
                      QueryItem: THelpQueryItem;
                      var ErrMsg: string): TShowHelpResult; override;
    procedure Load(Storage: TConfigStorage); override;
    procedure Save(Storage: TConfigStorage); override;
    property DefaultNode: THelpNode read FDefaultNode;
    property LastMessage: string read FLastMessage write SetLastMessage;
    property FoundComment: string read FFoundComment write SetFoundComment;
    property FoundAddition: TMessageHelpAddition read FFoundAddition;

    // the FPC message file
    function GetMsgFile: TFPCMsgFile;
    property MsgFile: TFPCMsgFile read FMsgFile;
    property MsgFilename: string read FMsgFilename;
    property MsgFileChangeStep: integer read FMsgFileChangeStep;

    // additional help for messages (they add an URL to the FPC comments)
    function AdditionsCount: integer;
    property Additions[Index: integer]: TMessageHelpAddition read GetAdditions;
    property AdditionsChangeStep: integer read FAdditionsChangeStep;
    property DefaultAdditionsFile: string read FDefaultAdditionsFile;
    property LoadedAdditionsFilename: string read FLoadedAdditionsFilename;
    procedure ClearAdditions;
    procedure LoadAdditions;
    procedure SaveAdditions;
    function GetAdditionsFilename: string;
  published
    property AdditionsFile: string read FAdditionsFile write SetAdditionsFile;
  end;

  { TEditIDEMsgHelpDialog }

  TEditIDEMsgHelpDialog = class(TForm)
    AddButton: TButton;
    AdditionFitsMsgLabel: TLabel;
    AdditionsFileEdit: TEdit;
    AdditionsFileLabel: TLabel;
    ButtonPanel1: TButtonPanel;
    CurGroupBox: TGroupBox;
    CurMsgGroupBox: TGroupBox;
    CurMsgMemo: TMemo;
    DeleteButton: TButton;
    FPCMsgFileValueLabel: TLabel;
    FPCMsgFileLabel: TLabel;
    GlobalOptionsGroupBox: TGroupBox;
    NameEdit: TEdit;
    NameLabel: TLabel;
    OnlyFPCMsgIDsLabel: TLabel;
    OnlyFPCMsgIDsEdit: TEdit;
    OnlyRegExEdit: TEdit;
    OnlyRegExLabel: TLabel;
    Splitter1: TSplitter;
    AllGroupBox: TGroupBox;
    TestURLButton: TButton;
    URLEdit: TEdit;
    URLLabel: TLabel;
    AllListBox: TListBox;
    procedure AddButtonClick(Sender: TObject);
    procedure AllListBoxSelectionChange(Sender: TObject; {%H-}User: boolean);
    procedure ButtonPanel1Click(Sender: TObject);
    procedure ButtonPanel1OKButtonClick(Sender: TObject);
    procedure DeleteButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure NameEditChange(Sender: TObject);
    procedure OnlyFPCMsgIDsEditChange(Sender: TObject);
    procedure OnlyRegExEditChange(Sender: TObject);
    procedure TestURLButtonClick(Sender: TObject);
    procedure URLEditChange(Sender: TObject);
  private
    fDefaultValue: string;
    procedure FillAdditionsList;
    procedure UpdateCurAddition;
    procedure UpdateCurMessage;
    procedure UpdateAdditionsFitsMsg;
    function IsIDListValid(IDs: string): boolean;
    function IsRegexValid(re: string): boolean;
    function IsURLValid(URL: string): boolean;
  public
    Additions: TMessageHelpAdditions;
    CurAddition: TMessageHelpAddition;
    CurMsg: string;
    CurFPCId: integer;
  end;

var
  FPCMsgHelpDB: TFPCMessagesHelpDatabase = nil;

function ShowMessageHelpEditor: TModalResult;

procedure CreateFPCMessagesHelpDB;

implementation

{$R *.lfm}

function ShowMessageHelpEditor: TModalResult;
var
  Editor: TEditIDEMsgHelpDialog;
begin
  Editor:=TEditIDEMsgHelpDialog.Create(nil);
  try
    Result:=Editor.ShowModal;
  finally
    Editor.Free;
  end;
end;

procedure CreateFPCMessagesHelpDB;
var
  StartNode: THelpNode;
begin
  FPCMessagesHelpDB:=HelpDatabases.CreateHelpDatabase(lihcFPCMessages,
                                                 TFPCMessagesHelpDatabase,true);
  FPCMsgHelpDB:=FPCMessagesHelpDB as TFPCMessagesHelpDatabase;
  FPCMsgHelpDB.DefaultBaseURL:=lihFPCMessagesURL;

  // HTML nodes
  StartNode:=THelpNode.CreateURLID(FPCMsgHelpDB, lisFreePascalCompilerMessages,
          lihFPCMessagesInternalURL,lihcFPCMessages);
  FPCMsgHelpDB.TOCNode:=THelpNode.Create(FPCMsgHelpDB,StartNode);// once as TOC
  FPCMsgHelpDB.RegisterItemWithNode(StartNode);// and once as normal page
end;

{ TMessageHelpAdditions }

function TMessageHelpAdditions.FindWithName(Name: string): TMessageHelpAddition;
var
  i: Integer;
begin
  for i:=0 to Count-1 do begin
    Result:=Items[i];
    if SysUtils.CompareText(Result.Name,Name)=0 then exit;
  end;
  Result:=nil;
end;

function TMessageHelpAdditions.IsEqual(Source: TMessageHelpAdditions): boolean;
var
  i: Integer;
begin
  Result:=false;
  if Source=nil then exit;
  if Source=Self then exit(true);
  if Count<>Source.Count then exit;
  for i:=0 to Count-1 do
    if not Items[i].IsEqual(Source[i]) then exit;
  Result:=true;
end;

procedure TMessageHelpAdditions.Clone(Source: TMessageHelpAdditions);
var
  i: Integer;
  Item: TMessageHelpAddition;
begin
  Clear;
  for i:=0 to Source.Count-1 do begin
    Item:=TMessageHelpAddition.Create;
    Item.Assign(Source[i]);
    Add(Item);
  end;
end;

procedure TMessageHelpAdditions.LoadFromConfig(Cfg: TConfigStorage);
var
  Cnt: Integer;
  i: Integer;
  Item: TMessageHelpAddition;
  SubPath: String;
begin
  Clear;
  Cfg.AppendBasePath('Additions');
  try
    Cnt:=Cfg.GetValue('Count',0);
    for i:=1 to Cnt do begin
      Item:=TMessageHelpAddition.Create;
      SubPath:='Item'+IntToStr(i)+'/';
      Item.Name:=Cfg.GetValue(SubPath+'Name','');
      if Item.Name='' then begin
        Item.Free;
      end else begin
        Add(Item);
        Item.IDs:=cfg.GetValue(SubPath+'IDs','');
        Item.RegEx:=cfg.GetValue(SubPath+'RegEx','');
        Item.URL:=cfg.GetValue(SubPath+'URL','');
      end;
    end;
  finally
    Cfg.UndoAppendBasePath;
  end;
end;

procedure TMessageHelpAdditions.SaveToConfig(Cfg: TConfigStorage);
var
  Cnt: Integer;
  i: Integer;
  Item: TMessageHelpAddition;
  SubPath: String;
begin
  Cfg.AppendBasePath('Additions');
  try
    Cnt:=0;
    for i:=0 to Count-1 do begin
      Item:=Items[i];
      if Item.Name='' then continue;
      inc(Cnt);
      SubPath:='Item'+IntToStr(Cnt)+'/';
      Cfg.SetDeleteValue(SubPath+'Name',Item.Name,'');
      cfg.SetDeleteValue(SubPath+'IDs',Item.IDs,'');
      cfg.SetDeleteValue(SubPath+'RegEx',Item.RegEx,'');
      cfg.SetDeleteValue(SubPath+'URL',Item.URL,'');
    end;
    Cfg.SetDeleteValue('Count',Cnt,0);
  finally
    Cfg.UndoAppendBasePath;
  end;
end;

procedure TMessageHelpAdditions.LoadFromFile(Filename: string);
var
  Cfg: TConfigStorage;
begin
  try
    Cfg:=GetIDEConfigStorage(Filename,true);
    try
      LoadFromConfig(Cfg);
    finally
      Cfg.Free;
    end;
  except
    on E: Exception do begin
      debugln(['TMessageHelpAdditions.LoadFromFile unable to load file "'+Filename+'": '+E.Message]);
    end;
  end;
end;

procedure TMessageHelpAdditions.SaveToFile(Filename: string);
var
  Cfg: TConfigStorage;
begin
  try
    Cfg:=GetIDEConfigStorage(Filename,false);
    try
      SaveToConfig(Cfg);
      Cfg.WriteToDisk;
    finally
      Cfg.Free;
    end;
  except
    on E: Exception do begin
      debugln(['TMessageHelpAdditions.SaveToFile unable to save file "'+Filename+'": '+E.Message]);
    end;
  end;
end;

{ TMessageHelpAddition }

procedure TMessageHelpAddition.Assign(Source: TMessageHelpAddition);
begin
  Name:=Source.Name;
  IDs:=Source.IDs;
  RegEx:=Source.RegEx;
  URL:=Source.URL;
end;

function TMessageHelpAddition.IsEqual(Source: TMessageHelpAddition): boolean;
begin
  Result:=(Name=Source.Name)
      and (IDs=Source.IDs)
      and (RegEx=Source.RegEx)
      and (URL=Source.URL);
end;

function TMessageHelpAddition.Fits(ID: integer; Msg: string): boolean;
var
  CurID: Integer;
  p: PChar;
begin
  Result:=false;
  if Msg='' then exit;
  if RegEx<>'' then begin
    try
      Result:=REMatches(Msg,RegEx,'I');
    except
    end;
    if not Result then exit;
  end;
  if IDs<>'' then begin
    Result:=false;
    p:=PChar(IDs);
    CurID:=0;
    while p^<>#0 do begin
      case p^ of
      ',':
        if (CurID>0) and (CurID=ID) then begin
          Result:=true;
          break;
        end;
      '0'..'9':
        begin
          CurID:=CurID*10+ord(p^)-ord('0');
          if CurID>100000 then exit;
        end;
      else exit;
      end;
      inc(p);
    end;
    if (CurID>0) and (CurID=ID) then Result:=true;
    if not Result then exit;
  end;
end;

{ TEditIDEMsgHelpDialog }

procedure TEditIDEMsgHelpDialog.FormCreate(Sender: TObject);
var
  s: String;
begin
  fDefaultValue:=lisDefaultPlaceholder;
  Caption:=lisEditAdditionalHelpForMessages;

  GlobalOptionsGroupBox.Caption:=lisGlobalSettings;
  FPCMsgFileLabel.Caption:=lisFPCMessageFile2;
  AdditionsFileLabel.Caption:=lisConfigFileOfAdditions;

  CurMsgGroupBox.Caption:=lisSelectedMessageInMessagesWindow;

  AllGroupBox.Caption:=lisAdditions;
  AddButton.Caption:=lisCreateNewAddition;

  NameLabel.Caption:=lisCodeToolsDefsName;
  OnlyFPCMsgIDsLabel.Caption:=lisOnlyMessagesWithTheseFPCIDsCommaSeparated;
  OnlyRegExLabel.Caption:=lisOnlyMessagesFittingThisRegularExpression;
  s:=(FPCMessagesHelpDB as THTMLHelpDatabase).GetEffectiveBaseURL;
  URLLabel.Caption:=Format(lisURLOnWikiTheBaseUrlIs, [s]);
  TestURLButton.Caption:=lisTestURL;

  DeleteButton.Caption:=lisDeleteThisAddition;

  ButtonPanel1.OKButton.OnClick:=@ButtonPanel1OKButtonClick;

  // global options
  FPCMsgFileValueLabel.Caption:=EnvironmentOptions.GetParsedCompilerMessagesFilename;
  AdditionsFileEdit.Text:=FPCMsgHelpDB.AdditionsFile;

  // fetch selected message
  UpdateCurMessage;

  // list of additions
  FPCMsgHelpDB.LoadAdditions;
  Additions:=TMessageHelpAdditions.Create;
  Additions.Clone(FPCMsgHelpDB.fAdditions);
  FillAdditionsList;

  // current addition
  if AllListBox.Items.Count>0 then
    AllListBox.ItemIndex:=0;
  UpdateCurAddition;
end;

procedure TEditIDEMsgHelpDialog.ButtonPanel1Click(Sender: TObject);
begin

end;

procedure TEditIDEMsgHelpDialog.AllListBoxSelectionChange(Sender: TObject;
  User: boolean);
begin
  UpdateCurAddition;
end;

procedure TEditIDEMsgHelpDialog.AddButtonClick(Sender: TObject);
var
  i: Integer;
  Prefix: String;
  NewName: String;
  Item: TMessageHelpAddition;
begin
  if CurFPCId>=0 then
    Prefix:='Msg'+IntToStr(CurFPCId)+'_'
  else
    Prefix:='Msg';
  i:=1;
  repeat
    NewName:=Prefix+IntToStr(i);
    if Additions.FindWithName(NewName)=nil then break;
    inc(i);
  until false;
  Item:=TMessageHelpAddition.Create;
  Item.Name:=NewName;
  if CurFPCId>=0 then
    Item.IDs:=IntToStr(CurFPCId);
  Additions.Add(Item);
  FillAdditionsList;
  AllListBox.ItemIndex:=AllListBox.Items.IndexOf(Item.Name);
  UpdateCurAddition;
end;

procedure TEditIDEMsgHelpDialog.ButtonPanel1OKButtonClick(Sender: TObject);
var
  Filename: TCaption;
  HasChanged: Boolean;
begin
  HasChanged:=false;

  Filename:=EnvironmentOptions.GetParsedCompilerMessagesFilename;
  if (Filename=fDefaultValue) then
    Filename:='';

  Filename:=AdditionsFileEdit.Text;
  if (Filename=fDefaultValue)
  or (Filename=FPCMsgHelpDB.FDefaultAdditionsFile) then
    Filename:='';
  if FPCMsgHelpDB.AdditionsFile<>Filename then begin
    FPCMsgHelpDB.AdditionsFile:=Filename;
    HasChanged:=true;
  end;

  if HasChanged then begin
    // ToDo: save changes
    ShowMessage('Saving global options is not yet supported');
  end;

  if not Additions.IsEqual(FPCMsgHelpDB.fAdditions) then
  begin
    FPCMsgHelpDB.fAdditions.Clone(Additions);
    FPCMsgHelpDB.SaveAdditions;
  end;
end;

procedure TEditIDEMsgHelpDialog.DeleteButtonClick(Sender: TObject);
var
  i: LongInt;
  NewIndex: Integer;
begin
  if CurAddition=nil then exit;
  if IDEMessageDialog(lisDelete2,
    Format(lisDeleteAddition, [CurAddition.Name]), mtConfirmation, [mbYes, mbNo]
      )<>mrYes
  then exit;
  NewIndex:=AllListBox.ItemIndex;
  i:=Additions.IndexOf(CurAddition);
  CurAddition:=nil;
  if i>=0 then
    Additions.Delete(i);
  FillAdditionsList;
  if NewIndex<0 then NewIndex:=0;
  if NewIndex>=AllListBox.Items.Count then dec(NewIndex);
  AllListBox.ItemIndex:=NewIndex;
  UpdateCurAddition;
end;

procedure TEditIDEMsgHelpDialog.FormDestroy(Sender: TObject);
begin
  FreeAndNil(Additions);
end;

procedure TEditIDEMsgHelpDialog.NameEditChange(Sender: TObject);
var
  NewName: TCaption;
  ConflictAddition: TMessageHelpAddition;
begin
  NewName:=NameEdit.Text;
  ConflictAddition:=Additions.FindWithName(NewName);
  if (NewName='') or (CurAddition=nil)
  or ((ConflictAddition<>nil) and (Additions.FindWithName(NewName)<>CurAddition))
  then begin
    // invalid name
    NameLabel.Font.Color:=clRed;
  end else begin
    NameLabel.Font.Color:=clDefault;
    CurAddition.Name:=NewName;
    AllListBox.Items[AllListBox.ItemIndex]:=NewName;
  end;
end;

procedure TEditIDEMsgHelpDialog.OnlyFPCMsgIDsEditChange(Sender: TObject);
var
  NewIDs: TCaption;
begin
  NewIDs:=OnlyFPCMsgIDsEdit.Text;
  if (CurAddition=nil) or (not IsIDListValid(NewIDs)) then begin
    OnlyFPCMsgIDsLabel.Font.Color:=clRed;
  end else begin
    OnlyFPCMsgIDsLabel.Font.Color:=clDefault;
    CurAddition.IDs:=NewIDs;
    UpdateAdditionsFitsMsg;
  end;
end;

procedure TEditIDEMsgHelpDialog.OnlyRegExEditChange(Sender: TObject);
var
  NewRE: TCaption;
begin
  NewRE:=OnlyRegExEdit.Text;
  if (CurAddition=nil) or (not IsRegexValid(NewRE)) then begin
    OnlyRegExLabel.Font.Color:=clRed;
  end else begin
    OnlyRegExLabel.Font.Color:=clDefault;
    CurAddition.RegEx:=NewRE;
    UpdateAdditionsFitsMsg;
  end;
end;

procedure TEditIDEMsgHelpDialog.TestURLButtonClick(Sender: TObject);
var
  URL: String;
begin
  if (CurAddition=nil) or (CurAddition.URL='') then exit;
  URL:=FPCMsgHelpDB.GetEffectiveBaseURL+CurAddition.URL;
  OpenURL(URL);
end;

procedure TEditIDEMsgHelpDialog.URLEditChange(Sender: TObject);
var
  NewURL: TCaption;
begin
  NewURL:=URLEdit.Text;
  if (CurAddition=nil) or (not IsURLValid(NewURL)) then begin
    URLLabel.Font.Color:=clRed;
  end else begin
    URLLabel.Font.Color:=clDefault;
    CurAddition.URL:=NewURL;
  end;
end;

procedure TEditIDEMsgHelpDialog.FillAdditionsList;
var
  sl: TStringListUTF8Fast;
  i: Integer;
begin
  sl:=TStringListUTF8Fast.Create;
  try
    for i:=0 to Additions.Count-1 do
      sl.Add(Additions[i].Name);
    sl.Sort;
    AllListBox.Items.Assign(sl);
  finally
    sl.Free;
  end;
end;

procedure TEditIDEMsgHelpDialog.UpdateCurAddition;
var
  i: Integer;
begin
  i:=AllListBox.ItemIndex;
  if i>=0 then
    CurAddition:=Additions.FindWithName(AllListBox.Items[i])
  else
    CurAddition:=nil;
  if CurAddition=nil then begin
    CurGroupBox.Caption:=lisNoneSelected;
    CurGroupBox.Enabled:=false;
    NameEdit.Text:='';
    OnlyFPCMsgIDsEdit.Text:='';
    OnlyRegExEdit.Text:='';
    URLEdit.Text:='';
    for i:=0 to CurGroupBox.ControlCount-1 do
      CurGroupBox.Controls[i].Enabled:=false;
    NameLabel.Font.Color:=clDefault;
    OnlyFPCMsgIDsEdit.Font.Color:=clDefault;
    OnlyRegExEdit.Font.Color:=clDefault;
    URLEdit.Font.Color:=clDefault;
  end else begin
    CurGroupBox.Caption:=lisSelectedAddition;
    CurGroupBox.Enabled:=true;
    NameEdit.Text:=CurAddition.Name;
    NameLabel.Font.Color:=clDefault;
    OnlyFPCMsgIDsEdit.Text:=CurAddition.IDs;
    if not IsIDListValid(CurAddition.IDs) then
      OnlyFPCMsgIDsLabel.Font.Color:=clRed
    else
      OnlyFPCMsgIDsLabel.Font.Color:=clDefault;
    OnlyRegExEdit.Text:=CurAddition.RegEx;
    if not IsRegexValid(CurAddition.RegEx) then
      OnlyRegExLabel.Font.Color:=clRed
    else
      OnlyRegExLabel.Font.Color:=clDefault;
    URLEdit.Text:=CurAddition.URL;
    if not IsURLValid(CurAddition.URL) then
      URLLabel.Font.Color:=clRed
    else
      URLLabel.Font.Color:=clDefault;
    for i:=0 to CurGroupBox.ControlCount-1 do
      CurGroupBox.Controls[i].Enabled:=true;
  end;
  UpdateAdditionsFitsMsg;
end;

procedure TEditIDEMsgHelpDialog.UpdateCurMessage;
var
  Line: TMessageLine;
  sl: TStringList;
  MsgFile: TFPCMsgFile;
  FPCMsg: TFPCMsgItem;
begin
  CurMsg:='';
  CurFPCId:=-1;
  Line:=IDEMessagesWindow.GetSelectedLine;
  if Line=nil then begin
    CurMsgMemo.Text:=lisNoMessageSelected;
    CurMsgMemo.Enabled:=false;
  end else begin
    CurMsg:=Line.Msg;
    sl:=TStringList.Create;
    try
      sl.Add('Msg='+Line.Msg);
      sl.Add('MsgID='+IntToStr(Line.MsgID));
      MsgFile:=FPCMsgHelpDB.GetMsgFile;
      if MsgFile<>nil then begin
        FPCMsg:=nil;
        if Line.MsgID>0 then
          FPCMsg:=MsgFile.FindWithID(Line.MsgID);
        if FPCMsg=nil then
          FPCMsg:=MsgFile.FindWithMessage(Line.Msg);
        if FPCMsg<>nil then begin
          CurFPCId:=FPCMsg.ID;
          sl.Add('FPC Msg='+FPCMsg.GetName);
        end;
      end;
      CurMsgMemo.Text:=sl.Text;
    finally
      sl.Free;
    end;
    CurMsgMemo.Enabled:=true;
  end;
end;

procedure TEditIDEMsgHelpDialog.UpdateAdditionsFitsMsg;
begin
  if (CurAddition=nil) or (CurMsg='') then
    AdditionFitsMsgLabel.Visible:=false
  else begin
    AdditionFitsMsgLabel.Visible:=true;
    if CurAddition.Fits(CurFPCId,CurMsg) then begin
      AdditionFitsMsgLabel.Caption:=lisAdditionFitsTheCurrentMessage;
    end else begin
      AdditionFitsMsgLabel.Caption:=lisAdditionDoesNotFitTheCurrentMessage;
    end;
  end;
end;

function TEditIDEMsgHelpDialog.IsIDListValid(IDs: string): boolean;
// comma separated decimal numbers
var
  p: PChar;
  id: Integer;
begin
  if IDs='' then exit(true);
  Result:=false;
  p:=PChar(IDs);
  id:=0;
  while p^<>#0 do begin
    case p^ of
    ',': id:=0;
    '0'..'9':
      begin
        id:=id*10+ord(p^)-ord('0');
        if id>100000 then begin
          debugln(['TEditIDEMsgHelpDialog.IsIDListValid id too big ',id]);
          exit;
        end;
      end;
    else
      debugln(['TEditIDEMsgHelpDialog.IsIDListValid invalid character ',ord(p^),'=',dbgstr(p[0])]);
      exit;
    end;
    inc(p);
  end;
  Result:=true;
end;

function TEditIDEMsgHelpDialog.IsRegexValid(re: string): boolean;
begin
  if re='' then exit(true);
  Result:=false;
  try
    REMatches('',re,'I');
    Result:=true;
  except
    on E: Exception do begin
      debugln(['TEditIDEMsgHelpDialog.IsRegexValid inalid Re="',re,'": ',E.Message]);
    end;
  end;
end;

function TEditIDEMsgHelpDialog.IsURLValid(URL: string): boolean;
var
  i: Integer;
begin
  Result:=false;
  if URL='' then exit;
  for i:=1 to length(URL) do begin
    if URL[i] in [#0..#32] then exit;
  end;
  Result:=true;
end;

{ TFPCMessagesHelpDatabase }

procedure TFPCMessagesHelpDatabase.SetFoundComment(const AValue: string);
begin
  if FFoundComment=AValue then exit;
  FFoundComment:=AValue;
end;

function TFPCMessagesHelpDatabase.GetAdditions(Index: integer
  ): TMessageHelpAddition;
begin
  Result:=fAdditions[Index];
end;

procedure TFPCMessagesHelpDatabase.SetAdditionsFile(AValue: string);
begin
  if FAdditionsFile=AValue then Exit;
  FAdditionsFile:=AValue;
  FAdditionsChangeStep:=CTInvalidChangeStamp;
  FLoadedAdditionsFilename:='';
end;

procedure TFPCMessagesHelpDatabase.SetLastMessage(const AValue: string);
begin
  if FLastMessage=AValue then exit;
  FLastMessage:=AValue;
end;

constructor TFPCMessagesHelpDatabase.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  FDefaultAdditionsFile:='$(LazarusDir)/docs/additionalmsghelp.xml';
  fAdditions:=TMessageHelpAdditions.Create;
  FAdditionsChangeStep:=CTInvalidChangeStamp;
  FMsgFileChangeStep:=CTInvalidChangeStamp;
  FDefaultNode:=THelpNode.CreateURL(Self, lisFPCMessagesAppendix,
     'http://lazarus-ccr.sourceforge.net/fpcdoc/user/userap3.html#x81-168000C');
end;

destructor TFPCMessagesHelpDatabase.Destroy;
begin
  FreeAndNil(fAdditions);
  FreeAndNil(FDefaultNode);
  FreeAndNil(FMsgFile);
  inherited Destroy;
end;

function TFPCMessagesHelpDatabase.GetNodesForMessage(const AMessage: string;
  MessageParts: TStrings; var ListOfNodes: THelpNodeQueryList;
  var ErrMsg: string): TShowHelpResult;
var
  MsgItem: TFPCMsgItem;
  i: Integer;
  FPCID: Integer;
  MsgId: Integer;
begin
  FFoundAddition:=nil;
  FFoundComment:='';
  FPCID:=-1;
  Result:=inherited GetNodesForMessage(AMessage, MessageParts, ListOfNodes,
                                       ErrMsg);
  if (ListOfNodes<>nil) and (ListOfNodes.Count>0) then exit;
  LastMessage:=AMessage;

  // search message in FPC message file
  GetMsgFile;
  MsgItem:=nil;
  if MsgFile<>nil then begin
    MsgId:=StrToIntDef(MessageParts.Values['MsgId'],0);
    if MsgId>0 then
      MsgItem:=MsgFile.FindWithID(MsgId);
    if MsgItem=nil then
      MsgItem:=MsgFile.FindWithMessage(AMessage);
    if MsgItem<>nil then begin
      FoundComment:=MsgItem.GetTrimmedComment(true,true);
      FPCID:=MsgItem.ID;
    end;
  end;

  // search message in additions
  LoadAdditions;
  FFoundAddition:=nil;
  for i:=0 to AdditionsCount-1 do begin
    if Additions[i].Fits(FPCID,AMessage) then begin
      FFoundAddition:=Additions[i];
      break;
    end;
  end;

  if (FoundComment<>'') or (FoundAddition<>nil) then begin
    Result:=shrSuccess;
    CreateNodeQueryListAndAdd(DefaultNode,nil,ListOfNodes,true);
    //DebugLn('TFPCMessagesHelpDatabase.GetNodesForMessage ',FoundComment);
  end;
end;

function TFPCMessagesHelpDatabase.ShowHelp(Query: THelpQuery; BaseNode,
  NewNode: THelpNode; QueryItem: THelpQueryItem; var ErrMsg: string
  ): TShowHelpResult;
var
  URL: String;
begin
  Result:=shrHelpNotFound;
  if NewNode<>DefaultNode then begin
    Result:=inherited ShowHelp(Query, BaseNode, NewNode, QueryItem, ErrMsg);
  end else begin
    URL:='';
    if (FoundAddition<>nil) and (FoundAddition.URL<>'') then
      URL:=GetEffectiveBaseURL+FoundAddition.URL;
    if FoundComment<>'' then begin
      if URL='' then begin
        IDEMessageDialog(lisHFMHelpForFreePascalCompilerMessage, FoundComment,
                   mtInformation,[mbOk]);
      end else begin
        if IDEQuestionDialog(lisHFMHelpForFreePascalCompilerMessage,
          Format(lisThereAreAdditionalNotesForThisMessageOn,
                 [FoundComment+LineEnding+LineEnding, LineEnding+URL]),
          mtInformation, [mrYes, lisOpenURL,
                          mrClose, lisClose]) = mrYes
        then begin
          if not OpenURL(URL) then
            exit(shrViewerError);
        end;
      end;
    end else if URL<>'' then begin
      if not OpenURL(URL) then
        exit(shrViewerError);
    end;
    Result:=shrSuccess;
  end;
end;

procedure TFPCMessagesHelpDatabase.Load(Storage: TConfigStorage);
begin
  inherited Load(Storage);
  AdditionsFile:=Storage.GetValue('Additions/Filename','');
end;

procedure TFPCMessagesHelpDatabase.Save(Storage: TConfigStorage);
begin
  inherited Save(Storage);
  Storage.SetDeleteValue('Additions/Filename',AdditionsFile,'');
end;

function TFPCMessagesHelpDatabase.GetMsgFile: TFPCMsgFile;
var
  Filename: String;
  FPCSrcDir: String;
  Code: TCodeBuffer;
  AltFilename: String;
  UnitSet: TFPCUnitSetCache;
  CfgCache: TPCTargetConfigCache;
begin
  Result:=nil;
  Filename:=EnvironmentOptions.GetParsedCompilerMessagesFilename;
  if Filename='' then
    FileName:='errore.msg';
  if not FilenameIsAbsolute(Filename) then
  begin
    // try in FPC sources and Compiler directory
    UnitSet:=CodeToolBoss.GetUnitSetForDirectory('');
    if UnitSet=nil then exit;

    // try in FPC sources
    FPCSrcDir:=UnitSet.FPCSourceDirectory;
    if (FPCSrcDir<>'') then begin
      AltFilename:=TrimFilename(AppendPathDelim(FPCSrcDir)
                +GetForcedPathDelims('compiler/msg/')+Filename);
      if FileExistsCached(AltFilename) then
        Filename:=AltFilename;
    end;

    if not FilenameIsAbsolute(Filename) then
    begin
      // try in compiler path
      CfgCache:=UnitSet.GetConfigCache(true);
      if CfgCache<>nil then begin
        // try in back end compiler path
        if FilenameIsAbsolute(CfgCache.RealCompiler) then
        begin
          AltFilename:=AppendPathDelim(ExtractFilePath(CfgCache.RealCompiler))
                      +'msg'+PathDelim+Filename;
          if FileExistsCached(AltFilename) then
            Filename:=AltFilename;
        end;
        // try in front end compiler path
        if (not FilenameIsAbsolute(Filename))
        and FilenameIsAbsolute(CfgCache.Compiler) then
        begin
          AltFilename:=AppendPathDelim(ExtractFilePath(CfgCache.Compiler))
                      +'msg'+PathDelim+Filename;
          if FileExistsCached(AltFilename) then
            Filename:=AltFilename;
        end;
      end;
    end;

    if not FilenameIsAbsolute(Filename) then exit;
  end;

  Code:=CodeToolBoss.LoadFile(Filename,true,false);
  if Code=nil then exit;

  // load MsgFile
  if (Filename<>MsgFilename) or (Code.ChangeStep<>MsgFileChangeStep) then begin
    fMsgFilename:=Filename;
    if FMsgFile=nil then
      FMsgFile:=TFPCMsgFile.Create;
    FMsgFileChangeStep:=Code.ChangeStep;
    try
      MsgFile.LoadFromText(Code.Source);
    except
      on E: Exception do begin
        debugln(['TFPCMessagesHelpDatabase failed to parse "'+MsgFilename+'": '+E.Message]);
        exit;
      end;
    end;
  end;
  Result:=MsgFile;
end;

function TFPCMessagesHelpDatabase.AdditionsCount: integer;
begin
  Result:=fAdditions.Count;
end;

procedure TFPCMessagesHelpDatabase.ClearAdditions;
begin
  fAdditions.Clear;
  FLoadedAdditionsFilename:='';
  FAdditionsChangeStep:=CTInvalidChangeStamp;
  FFoundAddition:=nil;
end;

procedure TFPCMessagesHelpDatabase.LoadAdditions;
var
  Filename: String;
  Code: TCodeBuffer;
begin
  Filename:=GetAdditionsFilename;
  if FLoadedAdditionsFilename<>Filename then
    FAdditionsChangeStep:=CTInvalidChangeStamp;
  Code:=CodeToolBoss.LoadFile(Filename,true,false);
  if Code<>nil then begin
    if Code.ChangeStep=AdditionsChangeStep then exit;
    fAdditionsChangeStep:=Code.ChangeStep;
  end else
    fAdditionsChangeStep:=CTInvalidChangeStamp;
  ClearAdditions;
  fAdditions.LoadFromFile(Filename);
  FLoadedAdditionsFilename:=Filename;
end;

procedure TFPCMessagesHelpDatabase.SaveAdditions;
var
  Code: TCodeBuffer;
  Filename: String;
begin
  Filename:=GetAdditionsFilename;
  fAdditions.SaveToFile(Filename);
  Code:=CodeToolBoss.LoadFile(Filename,true,false);
  if Code<>nil then
    fAdditionsChangeStep:=Code.ChangeStep;
  FLoadedAdditionsFilename:=Filename;
end;

function TFPCMessagesHelpDatabase.GetAdditionsFilename: string;
var
  LazDir: String;
begin
  Result:=AdditionsFile;
  IDEMacros.SubstituteMacros(Result);
  if Result='' then begin
    Result:=GetForcedPathDelims(FDefaultAdditionsFile);
    IDEMacros.SubstituteMacros(Result);
  end;
  Result:=TrimFilename(Result);
  if not FilenameIsAbsolute(Result) then begin
    LazDir:=EnvironmentOptions.GetParsedLazarusDirectory;
    Result:=TrimFilename(AppendPathDelim(LazDir)+Result);
  end;
end;

end.

