{
 ***************************************************************************
 *                                                                         *
 *   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:
    A dialog showing the empty methods of the current class
    (at cursor in source editor).
    With the ability to remove them automatically.
}
unit EmptyMethodsDlg;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, TypInfo,
  // LCL
  Forms, Controls, Dialogs, StdCtrls, ButtonPanel,
  //LazUtils
  LazLoggerBase,
  // SynEdit
  SynEdit, SynHighlighterPas,
  // CodeTools
  CodeToolsStructs, CodeCache, CodeToolManager, PascalParserTool, CodeTree,
  // IdeIntf
  IdeIntfStrConsts, SrcEditorIntf, LazIDEIntf, PropEdits, IDEDialogs,
  // IDE
  CustomFormEditor, JitForms, Project, LazarusIDEStrConsts, EditorOptions;

type

  { TEmptyMethodsDialog }

  TEmptyMethodsDialog = class(TForm)
    AllButton: TButton;
    PublishedButton: TButton;
    ButtonPanel1: TButtonPanel;
    PrivateCheckBox: TCheckBox;
    ProtectedCheckBox: TCheckBox;
    PublicCheckBox: TCheckBox;
    PublishedCheckBox: TCheckBox;
    SectionsGroupBox: TGroupBox;
    MethodsGroupBox: TGroupBox;
    MethodsSynEdit: TSynEdit;
    SynPasSyn1: TSynPasSyn;
    procedure AllButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure OKButtonClick(Sender: TObject);
    procedure PrivateCheckBoxChange(Sender: TObject);
    procedure PublishedButtonClick(Sender: TObject);
  private
    FCaret: TPoint;
    FCode: TCodeBuffer;
    function GetSections: TPascalClassSections;
    procedure SetCaret(const AValue: TPoint);
    procedure SetCode(const AValue: TCodeBuffer);
    procedure SetSections(const AValue: TPascalClassSections);
    procedure UpdateList;
  public
    property Sections: TPascalClassSections read GetSections write SetSections;
    property Code: TCodeBuffer read FCode write SetCode;
    property Caret: TPoint read FCaret write SetCaret;
  end;

function ShowEmptyMethodsDialog: TModalResult;

function RemoveEmptyMethodsInUnit(Code: TCodeBuffer; AClassName: string;
  X, Y: integer; Sections: TPascalClassSections): TModalResult;


implementation

{$R *.lfm}

function ShowEmptyMethodsDialog: TModalResult;
var
  EmptyMethodsDialog: TEmptyMethodsDialog;
  ErrMsg: String;
  SrcEdit: TSourceEditorInterface;
  Code: TCodeBuffer;
  Caret: TPoint;
  ListOfPCodeXYPosition: TFPList;
  AllEmpty: boolean;
begin
  Result:=mrCancel;
  ListOfPCodeXYPosition:=TFPList.Create;
  try
    // init codetools
    ErrMsg:=lisSAMIDEIsBusy;
    if not LazarusIDE.BeginCodeTools then exit;

    // get cursor position
    ErrMsg:=lisSAMCursorIsNotInAClassDeclaration;
    SrcEdit:=SourceEditorManagerIntf.ActiveEditor;
    if SrcEdit=nil then exit;
    Code:=TCodeBuffer(SrcEdit.CodeToolsBuffer);
    if Code=nil then exit;
    Caret:=SrcEdit.CursorTextXY;
    ErrMsg:='';

    // check cursor is in a class
    if not CodeToolBoss.FindEmptyMethods(Code,'',Caret.X,Caret.Y,
      AllPascalClassSections,ListOfPCodeXYPosition,AllEmpty)
    then begin
      DebugLn(['ShowEmptyMethodsDialog CodeToolBoss.FindEmptyMethods failed']);
      if CodeToolBoss.ErrorMessage<>'' then begin
        ErrMsg:='';
        LazarusIDE.DoJumpToCodeToolBossError;
      end else begin
        IDEMessageDialog(lisEMDNoClass,
          Format(lisEMDNoClassAt, [Code.Filename, IntToStr(Caret.Y), IntToStr(
            Caret.X)]),
          mtError,[mbCancel]);
      end;
      exit;
    end;
    CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);

    EmptyMethodsDialog:=TEmptyMethodsDialog.Create(nil);
    try
      EmptyMethodsDialog.Code:=Code;
      EmptyMethodsDialog.Caret:=Caret;
      EmptyMethodsDialog.UpdateList;
      Result:=EmptyMethodsDialog.ShowModal;
    finally
      EmptyMethodsDialog.Free;
    end;
  finally
    CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);
    if ErrMsg<>'' then begin
      IDEMessageDialog(lisCCOErrorCaption,
        Format(lisEMDUnableToShowEmptyMethodsOfTheCurrentClassBecause,
               [LineEnding, ErrMsg]), mtError, [mbCancel]);
    end;
  end;
end;

function GetInheritedMethod(APersistent: TPersistent; PropInfo: PPropInfo): TMethod;
var
  AncestorRoot, AncestorComponent: TComponent;
  AncestorMethod: TMethod;
  Comp: TComponent;
begin
  FillByte(Result{%H-}, SizeOf(Result), 0);
  if APersistent is TComponent then
  begin
    Comp := TComponent(APersistent);
    if csAncestor in Comp.ComponentState then
    begin
      // search for ancestor component
      if Assigned(Comp.Owner) then
      begin
        AncestorRoot := BaseFormEditor1.GetAncestorLookupRoot(Comp);
        if Assigned(AncestorRoot) then
          AncestorComponent := AncestorRoot.FindComponent(Comp.Name)
        else
          AncestorComponent := nil;
      end
      else
        AncestorComponent := BaseFormEditor1.GetAncestorInstance(Comp);

      if Assigned(AncestorComponent) then
      begin
        AncestorMethod := GetMethodProp(AncestorComponent, PropInfo);
        if IsJITMethod(AncestorMethod) then
          Result := AncestorMethod
      end;
    end;
  end;
end;

function RemoveEmptyMethodsInUnit(Code: TCodeBuffer; AClassName: string;
  X, Y: integer; Sections: TPascalClassSections): TModalResult;
var
  RemovedProcHeads: TStrings;
  PropChanged: boolean;

  function ExtractClassName: string;
  var
    ProcName: string;
    p: LongInt;
    i: Integer;
  begin
    Result:='';
    for i:=RemovedProcHeads.Count-1 downto 0 do
    begin
      ProcName:=RemovedProcHeads[i];
      p:=System.Pos('.',ProcName);
      if p<1 then
        RemovedProcHeads.Delete(i)
      else begin
        Result:=copy(ProcName,1,p-1);
        RemovedProcHeads[i]:=copy(ProcName,p+1,length(ProcName));
      end;
    end;
  end;

  procedure CheckEvents(APersistent: TPersistent);
  // Read properties and remove event handlers which were removed from source by Codetools.
  var
    TypeInfo: PTypeInfo;
    PropInfo: PPropInfo;
    PropList: PPropList;
    PropCount, ic, i: integer;
    AMethod: TMethod;
    AMethodName: String;
    Coll: TCollection;
  begin
    TypeInfo:=PTypeInfo(APersistent.ClassInfo);
    PropCount:=GetPropList(TypeInfo,PropList); // List of properties and their count
    try
      for ic:=0 to PropCount-1 do              // iterate properties
      begin
        PropInfo:=PropList^[ic];
        if PropInfo^.PropType^.Kind=tkMethod then
        begin
          AMethod:=GetMethodProp(APersistent,PropInfo);   // event
          AMethodName:=GlobalDesignHook.GetMethodName(AMethod,nil);
          if AMethodName<>'' then
          begin
            i:=RemovedProcHeads.Count-1;
            while (i>=0) and (CompareText(RemovedProcHeads[i],AMethodName)<>0) do
              dec(i);
            if i>=0 then
            begin
              //DebugLn([' CheckEvents Clearing Property=',PropInfo^.Name,' AMethodName=',AMethodName]);
              AMethod := GetInheritedMethod(APersistent, PropInfo);
              SetMethodProp(APersistent, PropInfo, AMethod);
              PropChanged:=true;
            end;
          end;
        end
        else if PropInfo^.PropType^.Kind=tkClass then
        begin
          Coll := TCollection(GetObjectProp(APersistent, PropInfo, TCollection));
          if Assigned(Coll) then
            for i := 0 to Coll.Count - 1 do // CollectionItem can have events.
              CheckEvents(Coll.Items[i]); // Recurse also because collections can be nested.
        end;
      end;
    finally
      FreeMem(PropList);
    end;
  end;

var
  AllEmpty: boolean;
  AnUnitInfo: TUnitInfo;
  i: Integer;
  LookupRoot: TComponent;
  CurClassName: String;
begin
  Result:=mrCancel;
  RemovedProcHeads:=nil;
  try
    if not CodeToolBoss.RemoveEmptyMethods(Code,AClassName,X,Y,Sections,AllEmpty,
      [phpAddClassName,phpDoNotAddSemicolon,phpWithoutParamList,
       phpWithoutBrackets,phpWithoutClassKeyword,phpWithoutSemicolon],
      RemovedProcHeads)
    then begin
      DebugLn(['RemoveEmptyMethods failed']);
      exit;
    end;
    if (RemovedProcHeads<>nil) and (RemovedProcHeads.Count>0) then begin
      // RemovedProcHeads contains a list of classname.procname, remove classname from the list
      CurClassName:=ExtractClassName;
      if (CurClassName<>'') and (Project1<>nil) then
      begin
        AnUnitInfo:=Project1.UnitInfoWithFilename(Code.Filename);
        if AnUnitInfo<>nil then
        begin
          // fix events of designer components
          LookupRoot:=AnUnitInfo.Component;
          if (LookupRoot<>nil) and (CompareText(LookupRoot.ClassName,CurClassName)=0) then
          begin
            PropChanged:=false;
            CheckEvents(LookupRoot);
            for i:=0 to LookupRoot.ComponentCount-1 do
              CheckEvents(LookupRoot.Components[i]);
            // update objectinspector
            if PropChanged and (GlobalDesignHook.LookupRoot=LookupRoot) then
              GlobalDesignHook.RefreshPropertyValues;
          end;
        end;
      end;
    end;
  finally
    RemovedProcHeads.Free;
  end;
  Result:=mrOk;
end;

{ TEmptyMethodsDialog }

procedure TEmptyMethodsDialog.FormCreate(Sender: TObject);
begin
  Caption:=lisEMDEmptyMethods;
  SectionsGroupBox.Caption:=lisEMDSearchInTheseClassSections;
  PrivateCheckBox.Caption:=lisPrivate;
  ProtectedCheckBox.Caption:=lisProtected;
  PublicCheckBox.Caption:=lisEMDPublic;
  PublishedCheckBox.Caption:=lisEMDPublished;
  AllButton.Caption:=lisEMDAll;
  PublishedButton.Caption:=lisEMDOnlyPublished;
  MethodsGroupBox.Caption:=lisEMDFoundEmptyMethods;
  Sections:=AllPascalClassSections;
  
  ButtonPanel1.OKButton.Caption:=lisEMDRemoveMethods;
  ButtonPanel1.CancelButton.Caption:=lisCancel;

  EditorOpts.GetSynEditSettings(MethodsSynEdit);
end;

procedure TEmptyMethodsDialog.OKButtonClick(Sender: TObject);
begin
  if LazarusIDE.BeginCodeTools
  and (RemoveEmptyMethodsInUnit(Code,'',Caret.X,Caret.Y,Sections)=mrOk) then
    ModalResult:=mrOk;
end;

procedure TEmptyMethodsDialog.PrivateCheckBoxChange(Sender: TObject);
begin
  UpdateList;
end;

procedure TEmptyMethodsDialog.PublishedButtonClick(Sender: TObject);
begin
  Sections:=[pcsPublished];
end;

procedure TEmptyMethodsDialog.SetSections(const AValue: TPascalClassSections);
begin
  PrivateCheckBox.Checked:=pcsPrivate in AValue;
  ProtectedCheckBox.Checked:=pcsProtected in AValue;
  PublicCheckBox.Checked:=pcsPublic in AValue;
  PublishedCheckBox.Checked:=pcsPublished in AValue;
end;

procedure TEmptyMethodsDialog.SetCaret(const AValue: TPoint);
begin
  FCaret:=AValue;
end;

function TEmptyMethodsDialog.GetSections: TPascalClassSections;
begin
  Result:=[];
  if PrivateCheckBox.Checked then Include(Result,pcsPrivate);
  if ProtectedCheckBox.Checked then Include(Result,pcsProtected);
  if PublicCheckBox.Checked then Include(Result,pcsPublic);
  if PublishedCheckBox.Checked then Include(Result,pcsPublished);
end;

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

procedure TEmptyMethodsDialog.UpdateList;
var
  CurSections: TPascalClassSections;
  ListOfPCodeXYPosition: TFPList;
  i: Integer;
  CodePos: TCodeXYPosition;
  Tool: TCodeTool;
  CleanPos: integer;
  Node: TCodeTreeNode;
  NodeText: String;
  AllEmpty: boolean;
  NewTxt: String;
begin
  if (Code=nil) or (Caret.X<1) or (Caret.Y<1) then begin
    MethodsSynEdit.Text:='';
    exit;
  end;

  CurSections:=Sections;
  ListOfPCodeXYPosition:=TFPList.Create;
  try
    if (not CodeToolBoss.FindEmptyMethods(Code,'',Caret.X,Caret.Y,
      CurSections,ListOfPCodeXYPosition,AllEmpty))
    or (not CodeToolBoss.Explore(Code,Tool,false))
    then begin
      MethodsSynEdit.Text:='CodeToolBoss.FindEmptyMethods failed'#10
        +CodeToolBoss.ErrorMessage;
      exit;
    end;

    NewTxt:='';
    for i:=0 to ListOfPCodeXYPosition.Count-1 do begin
      CodePos:=PCodeXYPosition(ListOfPCodeXYPosition[i])^;
      //DebugLn(['TEmptyMethodsDialog.UpdateList ',i,' ',DbgsCXY(CodePos)]);
      if Tool.CaretToCleanPos(CodePos,CleanPos)<>0 then begin
        DebugLn(['TEmptyMethodsDialog.UpdateList Tool.CaretToCleanPos failed']);
        continue;
      end;
      Node:=Tool.FindDeepestNodeAtPos(CleanPos,false);
      if Node=nil then begin
        DebugLn(['TEmptyMethodsDialog.UpdateList Tool.FindDeepestNodeAtPos failed']);
        continue;
      end;
      NodeText:=Tool.ExtractProcHead(Node,[phpWithStart,phpWithParameterNames,
        phpWithVarModifiers,phpWithDefaultValues,phpWithResultType,
        phpWithCallingSpecs,phpWithProcModifiers]);
      NewTxt:=NewTxt+NodeText+#10;
    end;
    MethodsSynEdit.Text:=NewTxt;
  finally
    CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);
  end;
end;

procedure TEmptyMethodsDialog.AllButtonClick(Sender: TObject);
begin
  Sections:=AllPascalClassSections;
end;

end.

