unit RegTestInsight;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  LazLoggerBase, Forms, Controls,
  IDECommands, MenuIntf, IDEWindowIntf, LazIDEIntf, IDEMsgIntf,
  IDEOptEditorIntf, IDEOptionsIntf, IDEExternToolIntf,
  Projectintf,
  CodeTree, CodeCache, CodeToolManager, FindDeclarationTool,
  frmTestInsight, TestInsightServer, StrTestCaseOpts,
  TestInsightController, fraTestInsightOpts;

Type

  { TLazTestInsightForm }

  TLazTestInsightForm = class(TTestInsightForm)
  private
  Public
    procedure ShowMessage(aType: TInsightMessageType; Const Msg : String); override;
    Function GetTestProject : String; override;
    procedure RunTestProject(aExecutable : string; SendNamesOnly : Boolean); override;
    procedure NavigateTo(const aClass, aMethod, aUnit, aLocationFile: String; aLocationLine: Integer); override;
    procedure ConfigureServer(aServer: TTestInsightServer); override;
    Function AutoFetchTests: Boolean; override;
    function ShowRefreshTestproject: Boolean; override;
  end;



procedure register;

implementation

const
  TestInsightFormName = 'TestInsightForm';

var
  TestInsightOptionsFrameID: integer = 1001;

Type

  { TFormFreeNotifier }

  TFormFreeNotifier = Class(TComponent)
  Public
  Type
    TFreeNotificationProc = Procedure(aForm : TCustomForm);
  Private
    FOnNotify : TFreeNotificationProc;
    FForm : TCustomForm;
  private
    procedure SetForm(AValue: TCustomForm);
  Protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    function DoProjectChanged(Sender: TObject; {%H-}AProject: TLazProject): TModalResult;
  Public
    Constructor CustomCreate(aOwner : TComponent; aOnNotify : TFreeNotificationProc);
    Property Form : TCustomForm Read FForm Write SetForm;
  end;

var
  FreeNotifier : TFormFreeNotifier;
  TestInsightForm: TTestInsightForm;


Function ShowFileAt(aLocationFile : String; aLocationLine : integer) : Boolean;

var
  F : TLazProjectFile;

begin
  F:=LazarusIDE.ActiveProject.FindFile(aLocationFile,[]);
  Result:=Assigned(F);
  if Result then
    Result:=(mrOK=LazarusIDE.DoOpenFileAndJumpToPos(F.GetFullFilename,Point(aLocationLine-1,0),0,0,0,0,0,[ofOnlyIfExists]));
end;


Function ShowMethod(aClass,aMethod, aUnit : String) : Boolean;

var
  F : TLazProjectFile;
  Tool : TCodeTool;
  ClassTool : TFindDeclarationTool;
  C : TCodeBuffer;
  ClassNode,NewNode,Node : TCodeTreeNode;
  NewTopLine : Integer;
  Params: TFindDeclarationParams;
  LProj : TLazProject;
  Ctx : TFindContext;
  caret : TCodeXYPosition;

begin
  Result:=False;
  LProj:=LazarusIDE.ActiveProject;
  if (aUnit<>'') then
    begin
    F:=LProj.FindFile(aunit+'.pp',[pfsfOnlyProjectFiles]);
    if Not Assigned(F) then
      F:=LProj.FindFile(aunit+'.pas',[pfsfOnlyProjectFiles]);
    if Not Assigned(F) then
      F:=LProj.FindFile(aunit+'.p',[pfsfOnlyProjectFiles]);
    // Writeln('Found unit "',aunit,'" : ',Assigned(F));
    end;
  // Search in unit, if available
  Tool:=Nil;
  Node:=Nil;
  if Assigned(F) then
    begin
    C:=CodeToolBoss.LoadFile(F.GetFullFilename,False,false);
    if CodetoolBoss.Explore(C,Tool,False,false) then
      Node:=Tool.FindImplementationNode;
    end;
  // If not found
  if not Assigned(Node) then
    begin
    C:=CodeToolBoss.LoadFile(LProj.MainFile.GetFullFilename,False,false);
    if CodetoolBoss.Explore(C,Tool,False,false) then
      Node:=Tool.FindMainBeginEndNode;
    end;
  // Writeln('Node found for unit "',aunit,'" : ',Assigned(Node));
  if Node=nil then
    exit;
  Params:=TFindDeclarationParams.Create;
  Params.ContextNode:=Node;
  Params.Flags:=[fdfSearchInParentNodes];
  Params.SetIdentifier(Tool,PChar(aClass),nil);
  if not Tool.FindIdentifierInContext(Params) then
    begin
    // Writeln('Class not found for unit "',aunit,'", identifier ',aClass);
    exit;
    end;
  NewNode:=Params.NewNode;
  ClassTool:=Params.NewCodeTool;
  ClassNode:=NewNode.FirstChild;
  if (ClassNode=nil)
     or (NewNode.Desc<>ctnTypeDefinition)
     or (ClassNode.Desc<>ctnClass) then
       begin
       // Writeln('Class identifier in unit "',aunit,'", is not a class identifier ',aClass);
       Exit;
       end;
  Ctx:=ClassTool.FindClassMember(ClassNode,aMethod,true);
  if not Assigned(Ctx.Node) then
    begin
    // Writeln('Method ',aMethod,' for class ',aClass,' not found in unit "',aunit);
    exit;
    end;
  Ctx.Tool.CleanPosToCaretAndTopLine(Ctx.Node.StartPos,Caret,NewTopLine);
  Result:=(mrOK=LazarusIDE.DoOpenFileAndJumpToPos(Caret.Code.Filename,Point(Caret.X,Caret.Y),NewTopLine,-1,-1,[ofRegularFile]));
end;

Var
  TestInsightWindowCreator: TIDEWindowCreator;
  ViewTestInsightWindowCommand: TIDECommand;
  Controller : TTestInsightController;

procedure ShowTestInsightWindow(Sender: TObject);

var
  F : TCustomForm;

begin
  if TestInsightForm=nil then
    begin
    F:=IDEWindowCreators.ShowForm(TestInsightWindowCreator.FormName,true);
    FreeNotifier.Form:=F;
    end
  else
    IDEWindowCreators.ShowForm(TestInsightForm,true);
end;

procedure CreateTestInsightWindow(Sender: TObject; aFormName: string;
  var AForm: TCustomForm; DoDisableAutoSizing: boolean);
begin
  if not SameText(aFormName,TestInsightFormName) then
    begin
    debugln(['ERROR: TestInsightFormName : there is already a form with this name']);
    exit;
    end;
  IDEWindowCreators.CreateForm(AForm,TLazTestInsightForm,DoDisableAutoSizing,
    LazarusIDE.OwningComponent);
  AForm.Name:=aFormName;
  TestInsightForm:=TTestInsightForm(AForm);
end;

procedure ClearTestInsightForm(aForm: TCustomForm);
begin
  if aForm=TestInsightForm then
    TestInsightForm:=nil;
end;

procedure register;

var
  CmdCatView: TIDECommandCategory;

begin
  Controller:=TTestInsightController.Create;
  Controller.Options.LoadFromFile(TestInsightConfig);
  FreeNotifier:=TFormFreeNotifier.CustomCreate(Nil,@ClearTestInsightForm);

  CmdCatView:=IDECommandList.FindCategoryByName(CommandCategoryViewName);
  if CmdCatView=nil then
    raise Exception.Create('simplewebsrv: command category '+CommandCategoryViewName+' not found');


 // Windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  TestInsightWindowCreator:=IDEWindowCreators.Add(TestInsightFormName,
      @CreateTestInsightWindow,nil,'20%','20%','+50%','+20%');


 // Windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  ViewTestInsightWindowCommand:=RegisterIDECommand(CmdCatView, 'ViewTestInsightWindow',
   rsTestInsightTitle, CleanIDEShortCut, CleanIDEShortCut, nil, @ ShowTestInsightWindow);
 RegisterIDEMenuCommand(itmViewMainWindows, 'ViewTestInsightWindow',
   rsTestInsightTitle, nil, nil,  ViewTestInsightWindowCommand);

 // Options Frame - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

  TestInsightOptionsFrameID:=RegisterIDEOptionsEditor(GroupEnvironment,TTestInsightOptionsFrame,TestInsightOptionsFrameID)^.Index;
  LazarusIDE.AddHandlerOnProjectOpened(@FreeNotifier.DoProjectChanged);

end;

{ TFormFreeNotifier }

function TFormFreeNotifier.DoProjectChanged(Sender: TObject; AProject: TLazProject): TModalResult;
begin
  Result:=mrOK;
  if Assigned(FForm) and (FForm is TLazTestInsightForm) then
    TLazTestInsightForm(FForm).MaybeStartTestProject;
end;

procedure TFormFreeNotifier.SetForm(AValue: TCustomForm);
begin
  if FForm=AValue then Exit;
  if Assigned(FForm) then FForm.RemoveFreeNotification(Self);
  FForm:=AValue;
  if Assigned(FForm) then FForm.FreeNotification(Self);
end;

procedure TFormFreeNotifier.Notification(AComponent: TComponent; Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation=opRemove) and (aComponent=FForm) then
    begin
    FOnNotify(FForm);
    FForm:=Nil;
    end;
end;

constructor TFormFreeNotifier.CustomCreate(aOwner: TComponent; aOnNotify: TFreeNotificationProc);

begin
  Inherited Create(aOwner);
  FForm:=Nil;
  FOnNotify:=aOnNotify;
end;

{ TLazTestInsightForm }

procedure TLazTestInsightForm.ShowMessage(aType: TInsightMessageType; const Msg: String);

Const
  MLU : Array[TInsightMessageType] of TMessageLineUrgency = (TMessageLineUrgency.mluImportant,TMessageLineUrgency.mluError);

begin
  if Assigned(IDEMessagesWindow) then

    IDEMessagesWindow.AddCustomMessage(MLU[aType],Msg,'',0,0,rsTestInsightTitle)
end;

function TLazTestInsightForm.GetTestProject: String;

begin
  // When the IDE restores windows, the ActiveProject is not yet there, so we need to check.
  if Assigned(LazarusIDE)
     and Assigned(LazarusIDE.ActiveProject)
     and assigned(LazarusIDE.ActiveProject.LazCompilerOptions) then
  Result:=LazarusIDE.ActiveProject.LazCompilerOptions.CreateTargetFilename;
end;

procedure TLazTestInsightForm.RunTestProject(aExecutable : string; SendNamesOnly: Boolean);
begin
  // Probably the controller should do this.
  inherited RunTestProject(aExecutable,SendNamesOnly);
end;


procedure TLazTestInsightForm.ConfigureServer(aServer: TTestInsightServer);
begin
  aServer.Port:=Controller.Options.Port;
  aServer.BasePath:=Controller.Options.BasePath;
end;

function TLazTestInsightForm.AutoFetchTests: Boolean;
begin
  Result:=Controller.Options.AutoFetchTests
end;


procedure TLazTestInsightForm.NavigateTo(const aClass,aMethod, aUnit, aLocationFile: String; aLocationLine: Integer);

var
  NavOK : Boolean;

begin
  NavOK:=False;
  if (aLocationFile<>'')  then
    NavOK:=ShowFileAt(aLocationFile,aLocationLine);
  if not NavOK then
    NavOK:=ShowMethod(aClass,aMethod,aUnit);
  if not NavOK then
    ShowMessage(imtError,Format('Failed to navigate to test %s.%s in unit %s',[aClass,aMethod,aUnit]));
end;

function TLazTestInsightForm.ShowRefreshTestproject: Boolean;
begin
  Result:=True;
end;

finalization
  FreeAndNil(Controller);
  FreeAndNil(FreeNotifier);
end.

