{
 ***************************************************************************
 *                                                                         *
 *   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:
    Find all alternative declarations of an identifier.

  TCarbonControl = class(TCarbonWidget)
    procedure TCarbonControl.CreateWidget(const AParams: TCreateParams);
  TCarbonCustomCheckBox = class(TCarbonControl)
  TCarbonCheckBox = class(TCarbonCustomCheckBox)
    procedure TCarbonCheckBox.CreateWidget(const AParams: TCreateParams);

}
unit FindOverloadsDlg;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Laz_AVL_Tree,
  // LCL
  Forms, Controls, StdCtrls, ButtonPanel, ComCtrls,
  // LazUtils
  LazFileUtils, LazLoggerBase,
  // codetools
  FindDeclarationTool, PascalParserTool, CodeTree, CodeCache, CodeToolManager,
  FindOverloads,
  // IDE
  LazIDEIntf, ProjectIntf, SrcEditorIntf, IDEProcs;

type

  { TFOWFile }

  TFOWFile = class
  private
    FCode: TCodeBuffer;
    FFilename: string;
    FOnlyInterface: boolean;
    FScanned: boolean;
    procedure SetCode(const AValue: TCodeBuffer);
    procedure SetScanned(const AValue: boolean);
  public
    constructor Create(const TheFilename: string);
    destructor Destroy; override;
    property Filename: string read FFilename;
    property OnlyInterface: boolean read FOnlyInterface write FOnlyInterface;
    property Scanned: boolean read FScanned write SetScanned;
    property Code: TCodeBuffer read FCode write SetCode;
  end;

  TFindOverloadsScope = (
    fosProject,
    fosPackages,
    fosOtherSources
    );
  TFindOverloadsScopes = set of TFindOverloadsScope;

  TFOWStage = (
    fowsStart,
    fowsFinished
    );

  { TFindOverloadsWorker }

  TFindOverloadsWorker = class
  private
    FFiles: TAvlTree;
    FScanFiles: TAvlTree;
    FStagePosition: integer;
    FStagePosMax: integer;
    FStageTitle: string;
    procedure CollectProjectFiles;
    procedure CollectPackageFiles;
    procedure CollectOtherSourceFiles;
    procedure ScanSomeFiles;
    procedure ScanFile(AFile: TFOWFile);
    procedure CollectStartSource;
  public
    StartSourceScanned: boolean;
    Scopes: TFindOverloadsScopes;
    CompletedScopes: TFindOverloadsScopes;
    Graph: TDeclarationOverloadsGraph;
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    procedure Work;
    function Done: boolean;
    procedure StopSearching;
    function AddFileToScan(const Filename: string;
                           CheckExtension: boolean = true): TFOWFile;
    function FindFile(const Filename: string): TFOWFile;
    property Files: TAvlTree read FFiles; // tree of TFindOverloadsWorkerFile
    property ScanFiles: TAvlTree read FScanFiles;// tree of TFindOverloadsWorkerFile
    property StageTitle: string read FStageTitle write FStageTitle;
    property StagePosition: integer read FStagePosition write FStagePosition;
    property StagePosMax: integer read FStagePosMax write FStagePosMax;
  end;

  { TFindOverloadsDialog }

  TFindOverloadsDialog = class(TForm)
    ButtonPanel1: TButtonPanel;
    SearchAllCheckBox: TCheckBox;
    CurGroupBox: TGroupBox;
    ResultsProgressBar: TProgressBar;
    ResultsGroupBox: TGroupBox;
    CurTreeView: TTreeView;
    ResultsTreeView: TTreeView;
    procedure ButtonPanel1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure OnIdle(Sender: TObject; var Done: Boolean);
  private
    FIdleConnected: boolean;
    fCurTreeViewComplete: boolean;
    fWorker: TFindOverloadsWorker;
    procedure SetIdleConnected(const AValue: boolean);
    procedure UpdateProgress;
    procedure StopWorking;
    procedure UpdateCurTreeView;
  public
    property Worker: TFindOverloadsWorker read fWorker;
    property IdleConnected: boolean read FIdleConnected write SetIdleConnected;
  end;

function ShowFindOverloadsDialog: TModalResult;
function ShowFindOverloadsDialog(Code: TCodeBuffer; X, Y: integer): TModalResult;

function CompareFOWFiles(File1, File2: TFOWFile): integer;
function CompareFilenameWithFOWFile(FilenameAnsiString, FOWFile: Pointer): integer;

implementation

{$R *.lfm}

function ShowFindOverloadsDialog: TModalResult;
var
  SrcEdit: TSourceEditorInterface;
  Code: TCodeBuffer;
  XY: TPoint;
begin
  SrcEdit:=SourceEditorManagerIntf.ActiveEditor;
  if SrcEdit=nil then
    exit(mrCancel);
  Code:=TCodeBuffer(SrcEdit.CodeToolsBuffer);
  XY:=SrcEdit.CursorTextXY;
  Result:=ShowFindOverloadsDialog(Code,XY.X,XY.Y);
end;

function ShowFindOverloadsDialog(Code: TCodeBuffer; X, Y: integer): TModalResult;
var
  FindOverloadsDialog: TFindOverloadsDialog;
  Graph: TDeclarationOverloadsGraph;
begin
  if not LazarusIDE.BeginCodeTools then exit;
  Graph:=nil;
  FindOverloadsDialog:=nil;
  CodeToolBoss.ActivateWriteLock;
  try
    if not CodeToolBoss.GatherOverloads(Code,X,Y,Graph) then begin
      LazarusIDE.DoJumpToCodeToolBossError;
      exit(mrCancel);
    end;
    //DebugLn(['ShowFindOverloadsDialog ',Graph.StartCode.Filename,' ',Graph.StartX,',',Graph.StartY]);
    FindOverloadsDialog:=TFindOverloadsDialog.Create(nil);
    FindOverloadsDialog.Worker.Graph:=Graph;
    Result:=FindOverloadsDialog.ShowModal;
  finally
    CodeToolBoss.DeactivateWriteLock;
    FindOverloadsDialog.Free;
    Graph.Free;
  end;
end;

function CompareFOWFiles(File1, File2: TFOWFile): integer;
begin
  Result:=CompareFilenames(File1.Filename,File2.Filename);
end;

function CompareFilenameWithFOWFile(FilenameAnsiString, FOWFile: Pointer): integer;
begin
  Result:=CompareFilenames(ansistring(FilenameAnsiString),TFOWFile(FOWFile).Filename);
end;

{ TFindOverloadsDialog }

procedure TFindOverloadsDialog.FormCreate(Sender: TObject);
begin
  Caption:='Find overloads';
  CurGroupBox.Caption:='Current identifier';
  ResultsGroupBox.Caption:='Overloads';
  SearchAllCheckBox.Caption:='Search in other sources too';
  SearchAllCheckBox.ShowHint:=true;
  SearchAllCheckBox.Hint:='Enable this to search in system sources too. For example the RTL and FCL sources. This can take some minutes on slow machines.';

  ButtonPanel1.CancelButton.OnClick:=@ButtonPanel1Click;

  fWorker:=TFindOverloadsWorker.Create;
  IdleConnected:=true;
  UpdateProgress;
end;

procedure TFindOverloadsDialog.ButtonPanel1Click(Sender: TObject);
begin
  StopWorking;
end;

procedure TFindOverloadsDialog.FormDestroy(Sender: TObject);
begin
  IdleConnected:=false;
  FreeAndNil(fWorker);
end;

procedure TFindOverloadsDialog.OnIdle(Sender: TObject; var Done: Boolean);
begin
  fWorker.Work;
  Done:=fWorker.Done;
  if Done then
    IdleConnected:=false;
  UpdateProgress;
  if not fCurTreeViewComplete then
    UpdateCurTreeView;
end;

procedure TFindOverloadsDialog.SetIdleConnected(const AValue: boolean);
begin
  if FIdleConnected=AValue then exit;
  FIdleConnected:=AValue;
  if FIdleConnected then begin
    ButtonPanel1.CancelButton.Enabled:=true;
    ButtonPanel1.CloseButton.Enabled:=false;
    Application.AddOnIdleHandler(@OnIdle)
  end else begin
    ButtonPanel1.CancelButton.Enabled:=false;
    ButtonPanel1.CloseButton.Enabled:=true;
    Application.RemoveOnIdleHandler(@OnIdle);
  end;
end;

procedure TFindOverloadsDialog.UpdateProgress;
begin
  if Worker.Done then
    ResultsProgressBar.Visible:=false
  else begin
    ResultsProgressBar.Max:=Worker.StagePosMax;
    ResultsProgressBar.Position:=Worker.StagePosition;
    ResultsProgressBar.Visible:=true;
  end;
end;

procedure TFindOverloadsDialog.StopWorking;
begin
  IdleConnected:=false;
  Worker.StopSearching;
end;

procedure TFindOverloadsDialog.UpdateCurTreeView;
var
  s: String;
  Node: TCodeTreeNode;
  Tool: TFindDeclarationTool;
  ParentNode: TCodeTreeNode;
begin
  fCurTreeViewComplete:=true;
  CurTreeView.BeginUpdate;
  CurTreeView.Items.Clear;
  Node:=Worker.Graph.StartCodeNode;
  Tool:=Worker.Graph.StartTool;
  if Node<>nil then begin
    DebugLn(['TFindOverloadsDialog.UpdateCurTreeView ',Node.DescAsString,' ',dbgstr(copy(Tool.Src,Node.StartPos,20))]);
    // unit name
    s:=Tool.GetSourceName(false)+': ';
    // keyword
    case Node.Desc of
    ctnEnumIdentifier: s:=s+'enum';
    ctnVarDefinition: s:=s+'var';
    ctnConstDefinition: s:=s+'const';
    ctnTypeDefinition: s:=s+'type';
    ctnGenericType: s:=s+'generic';
    ctnProperty: s:=s+'property';
    ctnProcedure: s:=s+'procedure';
    ctnUseUnit: s:=s+'uses';
    ctnUnit: s:=s+'unit';
    ctnProgram: s:=s+'program';
    ctnPackage: s:=s+'package';
    ctnLibrary: s:=s+'library';
    end;
    s:=s+' ';
    // context
    if Node.Desc<>ctnEnumIdentifier then
    begin
      ParentNode:=Node.Parent;
      while ParentNode<>nil do begin
        case ParentNode.Desc of
        ctnTypeDefinition,ctnGenericType:
          s:=s+Tool.ExtractDefinitionName(Node)+'.';
        end;
        ParentNode:=ParentNode.Parent;
      end;
    end;
    // name
    case Node.Desc of
    ctnEnumIdentifier, ctnTypeDefinition, ctnConstDefinition, ctnVarDefinition,
    ctnGenericType:
      s:=s+Tool.ExtractDefinitionName(Node);
    ctnProperty:
      s:=s+Tool.ExtractPropName(Node,false);
    ctnProcedure:
      s:=s+Tool.ExtractProcName(Node,[phpWithoutClassName,phpCommentsToSpace]);
    ctnUseUnit:
      s:=s+Tool.ExtractNode(Node,[phpCommentsToSpace]);
    ctnUnit,ctnProgram,ctnPackage,ctnLibrary:
      s:=s+Tool.GetSourceName(false);
    end;
    // add node
    CurTreeView.Items.Add(nil,s);
  end;
  CurTreeView.EndUpdate;
end;

{ TFOWFile }

procedure TFOWFile.SetCode(const AValue: TCodeBuffer);
begin
  if FCode=AValue then exit;
  FCode:=AValue;
end;

procedure TFOWFile.SetScanned(const AValue: boolean);
begin
  if FScanned=AValue then exit;
  FScanned:=AValue;
end;

constructor TFOWFile.Create(const TheFilename: string);
begin
  FFilename:=TheFilename;
  FOnlyInterface:=true;
end;

destructor TFOWFile.Destroy;
begin
  inherited Destroy;
end;

{ TFindOverloadsWorker }

procedure TFindOverloadsWorker.CollectProjectFiles;
var
  AProject: TLazProject;
  i: Integer;
  ProjFile: TLazProjectFile;
begin
  AProject:=LazarusIDE.ActiveProject;
  if AProject<>nil then begin
    for i:=0 to AProject.FileCount-1 do begin
      ProjFile:=AProject.Files[i];
      if ProjFile.IsPartOfProject then
        AddFileToScan(ProjFile.Filename);
    end;
  end;
  Include(CompletedScopes,fosProject);
end;

procedure TFindOverloadsWorker.CollectPackageFiles;
begin

  Include(CompletedScopes,fosPackages);
end;

procedure TFindOverloadsWorker.CollectOtherSourceFiles;
begin

  Include(CompletedScopes,fosOtherSources);
end;

procedure TFindOverloadsWorker.ScanSomeFiles;
const
  MaxScanTime = 0.3/86400; // 0.3 seconds
var
  StartTime: TDateTime;
  CurFile: TFOWFile;
begin
  StartTime:=Now;
  while FScanFiles.Count>0 do begin
    CurFile:=TFOWFile(FScanFiles.FindLowest.Data);
    ScanFile(CurFile);
    if Now-StartTime>=MaxScanTime then
      break;
  end;
end;

procedure TFindOverloadsWorker.ScanFile(AFile: TFOWFile);
var
  Tool: TCodeTool;
  Filename: String;
  MainFile: TFOWFile;
  Code: TCodeBuffer;
begin
  FScanFiles.Remove(AFile);
  if AFile.Scanned then exit;
  AFile.Scanned:=true;
  //DebugLn(['TFindOverloadsWorker.ScanFile File=',AFile.Filename]);
  // get codetool
  Filename:=TrimFilename(AFile.Filename);
  Code:=CodeToolBoss.LoadFile(Filename,true,false);
  if Code=nil then begin
    DebugLn(['TFindOverloadsWorker.ScanFile file not readable: ',Filename]);
    exit;
  end;
  Tool:=TCodeTool(CodeToolBoss.GetCodeToolForSource(Code,true,false));
  if Tool=nil then begin
    DebugLn(['TFindOverloadsWorker.ScanFile file not in a unit: ',Filename]);
    exit;
  end;
  // check if AFile is an include file
  Filename:=Tool.MainFilename;
  MainFile:=FindFile(Filename);
  // get unit
  if MainFile=nil then begin
    MainFile:=TFOWFile.Create(Filename);
    FFiles.Add(MainFile);
  end;
  if (MainFile<>AFile) and MainFile.Scanned then begin
    //DebugLn(['TFindOverloadsWorker.ScanFile already scanned: ',Filename]);
    exit;
  end;
  // scan unit
  FScanFiles.Remove(MainFile);
  MainFile.Scanned:=true;
  if not AFile.OnlyInterface then
    MainFile.OnlyInterface:=false;
  //DebugLn(['TFindOverloadsWorker.ScanFile scanning: ',Tool.MainFilename]);
  Graph.ScanToolForIdentifier(Tool,MainFile.OnlyInterface);
end;

procedure TFindOverloadsWorker.CollectStartSource;
var
  Filename: String;
  aFile: TFOWFile;
begin
  Filename:=Graph.StartCode.Filename;
  aFile:=FindFile(Filename);
  if aFile=nil then begin
    aFile:=TFOWFile.Create(Filename);
    aFile.OnlyInterface:=false;
    FFiles.Add(aFile);
    FScanFiles.Add(aFile);
  end;
end;

function TFindOverloadsWorker.AddFileToScan(const Filename: string;
  CheckExtension: boolean): TFOWFile;
begin
  if CheckExtension and (not FilenameIsPascalSource(Filename)) then
    exit(nil);
  Result:=FindFile(Filename);
  if Result<>nil then exit;
  Result:=TFOWFile.Create(Filename);
  FFiles.Add(Result);
  FScanFiles.Add(Result);
end;

function TFindOverloadsWorker.FindFile(const Filename: string): TFOWFile;
var
  AVLNode: TAvlTreeNode;
begin
  AVLNode:=FFiles.FindKey(Pointer(Filename),@CompareFilenameWithFOWFile);
  if AVLNode<>nil then
    Result:=TFOWFile(AVLNode.Data)
  else
    Result:=nil;
end;

constructor TFindOverloadsWorker.Create;
begin
  Scopes:=[fosProject,fosPackages];
  FFiles:=TAvlTree.Create(TListSortCompare(@CompareFOWFiles));
  FScanFiles:=TAvlTree.Create(TListSortCompare(@CompareFOWFiles));
  FStagePosMax:=100;
end;

destructor TFindOverloadsWorker.Destroy;
begin
  Clear;
  FreeAndNil(FFiles);
  FreeAndNil(FScanFiles);
  inherited Destroy;
end;

procedure TFindOverloadsWorker.Clear;
begin
  FFiles.FreeAndClear;
  FScanFiles.Clear;
  FStageTitle:='Finished';
  FStagePosition:=0;
  FStagePosMax:=100;
  StartSourceScanned:=false;
end;

procedure TFindOverloadsWorker.Work;
begin
  DebugLn(['TFindOverloadsWorker.Work START']);
  if FScanFiles.Count>0 then begin
    // scan files
    ScanSomeFiles;
  end
  else if not StartSourceScanned then
  begin
    StageTitle:='Scanning start source ...';
    StagePosition:=10;
    StartSourceScanned:=true;
    CollectStartSource;
  end
  else if (fosProject in Scopes) and not (fosProject in CompletedScopes) then
  begin
    // collect project files
    StageTitle:='Scanning project ...';
    StagePosition:=20;
    CollectProjectFiles;
  end
  else if (fosPackages in Scopes) and not (fosPackages in CompletedScopes) then
  begin
    // collect package files
    StageTitle:='Scanning packages ...';
    StagePosition:=40;
    CollectPackageFiles;
  end
  else if (fosOtherSources in Scopes) and not (fosOtherSources in CompletedScopes)
  then begin
    // collect other sources
    StageTitle:='Scanning other sources ...';
    StagePosition:=60;
    CollectOtherSourceFiles;
  end else begin
    StageTitle:='Finished';
    StagePosition:=StagePosMax;
    Graph.ComputeShortestPaths;
  end;
  DebugLn(['TFindOverloadsWorker.Work END ',StageTitle,' ',StagePosition,'/',StagePosMax]);
end;

function TFindOverloadsWorker.Done: boolean;
begin
  Result:=(Scopes-CompletedScopes=[]) and (FScanFiles.Count=0);
end;

procedure TFindOverloadsWorker.StopSearching;
begin
  CompletedScopes:=Scopes;
  FScanFiles.Clear;
  Graph.ComputeShortestPaths;
end;

end.

