unit FpDebugValueConvertors;

{$mode objfpc}{$H+}
{$ModeSwitch typehelpers}

interface

uses
  Classes, SysUtils, FpDbgInfo, FpdMemoryTools, FpDbgCallContextInfo,
  FpPascalBuilder, FpErrorMessages, FpDbgClasses, FpDbgUtil, DbgIntfBaseTypes,
  LazClasses, Forms, StdCtrls, Controls, StrUtils, FpDebugDebuggerBase,
  FpDebugStringConstants, LazDebuggerValueConverter, LazDebuggerIntfBaseTypes,
  LazDebuggerIntf;

type
  (* TFpDbgValueConverter and descendants
     - A TFpDbgValueConverter should be immutable, once in the list.
       To change settings a new instance can be set to TDbgBackendConverterConfig
       This allows for TFpDbgValueConverter to be used outside the lock (reduces lock time)
     - Any setting that the IDE may need to store, should be published
  *)

  TFpDbgValueConverter = class(TRefCountedObject, ILazDbgValueConverterIntf)
  private
    FLastErrror: TFpError;
  protected
    function GetObject: TObject;
    function GetSettingsFrame: ILazDbgValueConverterSettingsFrameIntf; virtual;
    procedure Init; virtual;
  public
    class function GetName: String; virtual; abstract;
    function GetRegistryEntry: TLazDbgValueConvertRegistryEntryClass; virtual;
    constructor Create; virtual;
    procedure Assign(ASource: TFpDbgValueConverter); virtual;
    function CreateCopy: ILazDbgValueConverterIntf; virtual;
    function NeedConversionLimit: Boolean; virtual;
    (* CanHandleValue must return the SAME RESULT if called repeatedly (e.g. in an array)
       CanHandleValue must NOT depend on DATA, or anything that can change
       CanHandleValue must ONLY check type info (or other data that will not change)
       - For any other/uncertain case the converter should create an error (empty text) in
         ConvertValue, changing AnResData
    *)
    function CanHandleValue(ASourceValue: TFpValue; AnFpDebugger: TFpDebugDebuggerBase): Boolean; virtual;
    (* ConvertValue
       * AnResData = nil
         => Do nothing => ConvertValue must have done AnResData.Create....
       * Result <> nil  /  AnResData <> nil
         => Use Result (instead of ASourceValue) to build watch-data
         => use the new AnResData (in case it changed)
            If the new AnResData = nil, then do nothing
       * Result = nil
         - AnResData NOT-changed
         => Create an error (IDE will show ASourceValue)
         - AnResData CHANGED
         => Do nothing  => ConvertValue must have done AnResData.Create....
    *)
    function ConvertValue(ASourceValue: TFpValue;
                          AnFpDebugger: TFpDebugDebuggerBase;
                          AnExpressionScope: TFpDbgSymbolScope;
                          var AnResData: IDbgWatchDataIntf  // if changed, then the converter has done its job, and should return nil
                         ): TFpValue; virtual; abstract;
    procedure SetError(AnError: TFpError);
    property LastErrror: TFpError read FLastErrror;
  end;
  TFpDbgValueConverterClass = class of TFpDbgValueConverter;

  { TConverterSettingsFrameBase }

  TConverterSettingsFrameBase = class(TFrame, ILazDbgValueConverterSettingsFrameIntf)
  protected
    function GetFrame: TObject; virtual;
  public
    procedure ReadFrom(AConvertor: ILazDbgValueConverterIntf); virtual;
    function WriteTo(AConvertor: ILazDbgValueConverterIntf): Boolean; virtual;
  end;

  { TConverterWithFuncCallSettingsFrame }

  TConverterWithFuncCallSettingsFrame = class(TConverterSettingsFrameBase)
    chkRunAll: TCheckBox;
  public
    constructor Create(TheOwner: TComponent); override;
    procedure ReadFrom(AConvertor: ILazDbgValueConverterIntf); override;
    function WriteTo(AConvertor: ILazDbgValueConverterIntf): Boolean; override;
  end;

  { TFpDbgValueConverterWithFuncCall }

  TFpDbgValueConverterWithFuncCall = class(TFpDbgValueConverter)
  private
    FFuncCallRunAllThreads: Boolean;
  public
    procedure Assign(ASource: TFpDbgValueConverter); override;
  published
    property FuncCallRunAllThreads: Boolean read FFuncCallRunAllThreads write FFuncCallRunAllThreads;
  end;


  { TFpDbgValueConvertSelectorIntfHelper }

  TFpDbgValueConvertSelectorIntfHelper = type helper for ILazDbgValueConvertSelectorIntf
    function CheckMatch(AValue: TFpValue; IgnoreInstanceClass: boolean = False): Boolean;
    function CheckTypeMatch(AValue: TFpValue; IgnoreInstanceClass: boolean = False): Boolean;
  end;

  { TFpDbgValueConverterRegistryEntry }

  TFpDbgValueConverterRegistryEntry = class(TLazDbgValueConvertRegistryEntry)
  public
    class function CreateValueConvertorIntf: ILazDbgValueConverterIntf; override;
    class function GetName: String; override;
    class function GetDebuggerClass: TClass; override;
  end;

  (**** Call SysVarToLStr on variant ****)

  { TFpDbgValueConverterVariantToLStr }

  TFpDbgValueConverterVariantToLStr = class(TFpDbgValueConverterWithFuncCall)
  private
    function GetProcAddrFromMgr(AnFpDebugger: TFpDebugDebuggerBase; AnExpressionScope: TFpDbgSymbolScope): TDbgPtr;
  protected
    function GetSettingsFrame: ILazDbgValueConverterSettingsFrameIntf; override;
  public
    class function GetName: String; override;
    function GetRegistryEntry: TLazDbgValueConvertRegistryEntryClass; override;
    function ConvertValue(ASourceValue: TFpValue;
                          AnFpDebugger: TFpDebugDebuggerBase;
                          AnExpressionScope: TFpDbgSymbolScope;
                          var AnResData: IDbgWatchDataIntf
                         ): TFpValue; override;
  end;

  { TFpDbgValueConverterVariantToLStrRegistryEntry }

  TFpDbgValueConverterVariantToLStrRegistryEntry = class(TFpDbgValueConverterRegistryEntry)
  public
    class function GetConvertorClass: TClass; override;
  end;

implementation

{$R *.lfm}

{ TFpDbgValueConverterWithFuncCall }

procedure TFpDbgValueConverterWithFuncCall.Assign(ASource: TFpDbgValueConverter);
begin
  inherited Assign(ASource);
  if ASource is TFpDbgValueConverterWithFuncCall then begin
    FFuncCallRunAllThreads := TFpDbgValueConverterWithFuncCall(ASource).FFuncCallRunAllThreads;
  end;
end;

{ TFpDbgValueConverter }

function TFpDbgValueConverter.CreateCopy: ILazDbgValueConverterIntf;
var
  c: TFpDbgValueConverter;
begin
  c := TFpDbgValueConverterClass(ClassType).Create;
  c.Assign(Self);
  Result := c;
end;

function TFpDbgValueConverter.NeedConversionLimit: Boolean;
begin
  Result := True;
end;

function TFpDbgValueConverter.CanHandleValue(ASourceValue: TFpValue;
  AnFpDebugger: TFpDebugDebuggerBase): Boolean;
begin
  Result := True;
end;

procedure TFpDbgValueConverter.SetError(AnError: TFpError);
begin
  FLastErrror := AnError;
end;

function TFpDbgValueConverter.GetObject: TObject;
begin
  Result := Self;
end;

function TFpDbgValueConverter.GetSettingsFrame: ILazDbgValueConverterSettingsFrameIntf;
begin
  Result := nil;
end;

procedure TFpDbgValueConverter.Init;
begin
  //
end;

function TFpDbgValueConverter.GetRegistryEntry: TLazDbgValueConvertRegistryEntryClass;
begin
  Result := nil;
end;

constructor TFpDbgValueConverter.Create;
begin
  inherited Create;
  Init;
end;

procedure TFpDbgValueConverter.Assign(ASource: TFpDbgValueConverter);
begin
  //
end;

{ TConverterSettingsFrameBase }

function TConverterSettingsFrameBase.GetFrame: TObject;
begin
  Result := Self;
end;

procedure TConverterSettingsFrameBase.ReadFrom(
  AConvertor: ILazDbgValueConverterIntf);
begin
  //
end;

function TConverterSettingsFrameBase.WriteTo(
  AConvertor: ILazDbgValueConverterIntf): Boolean;
begin
  Result := False; // nothing changed
end;

{ TConverterWithFuncCallSettingsFrame }

procedure TConverterWithFuncCallSettingsFrame.ReadFrom(
  AConvertor: ILazDbgValueConverterIntf);
var
  c: TFpDbgValueConverterWithFuncCall;
begin
  if not (AConvertor.GetObject is TFpDbgValueConverterWithFuncCall) then
    exit;

  c := TFpDbgValueConverterWithFuncCall(AConvertor.GetObject);

  chkRunAll.Checked := c.FuncCallRunAllThreads;
end;

function TConverterWithFuncCallSettingsFrame.WriteTo(
  AConvertor: ILazDbgValueConverterIntf): Boolean;
var
  c: TFpDbgValueConverterWithFuncCall;
begin
  Result := False;
  if not (AConvertor.GetObject is TFpDbgValueConverterWithFuncCall) then
    exit;

  c := TFpDbgValueConverterWithFuncCall(AConvertor.GetObject);

  Result :=  chkRunAll.Checked <> c.FuncCallRunAllThreads;

  c.FuncCallRunAllThreads := chkRunAll.Checked;
end;

constructor TConverterWithFuncCallSettingsFrame.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  chkRunAll.Caption := drsRunAllThreadsWhileEval;
end;

{ TFpDbgValueConvertSelectorIntfHelper }

function TFpDbgValueConvertSelectorIntfHelper.CheckMatch(AValue: TFpValue;
  IgnoreInstanceClass: boolean): Boolean;
begin
  Result := //(AValue.Kind in (MatchKinds * GetConverter.GetSupportedKinds)) and
            CheckTypeMatch(AValue, IgnoreInstanceClass);
end;

function TFpDbgValueConvertSelectorIntfHelper.CheckTypeMatch(AValue: TFpValue;
  IgnoreInstanceClass: boolean): Boolean;
  function MatchPattern(const AName, APattern: String): Boolean;
  var
    NamePos, PatternPos, p: Integer;
  begin
    Result := False;
    if APattern = '' then
      exit;

    NamePos := 1;
    PatternPos := 1;

    while PatternPos <= Length(APattern) do begin
      if APattern[PatternPos] = '*' then begin
        inc(PatternPos);
      end
      else begin
        p := PatternPos;
        PatternPos := PosEx('*', APattern, p);
        if PatternPos < 1 then
          PatternPos := Length(APattern)+1;
        if PatternPos-p > Length(AName)+1 - NamePos then
          break;

        NamePos := PosEx(Copy(APattern, p, PatternPos-p), AName, NamePos);
        if (NamePos < 1) or
           ( (p = 1) and (NamePos <> 1) ) // APattern does not start with *
        then
          break;

        inc(NamePos, PatternPos-p);
      end;
    end;

    Result := (PatternPos = Length(APattern)+1) and
              ( (NamePos = Length(AName)+1) or
                ( (APattern[Length(APattern)] = '*') and
                  (NamePos <= Length(AName)+1)
                )
              );
  end;
var
  i, CnIdx: Integer;
  TpName, Pattern, ValClassName, ValUnitName: String;
  t: TFpSymbol;
  HasMaybeUnitDot: Boolean;
  MatchTypeNames: TStrings;
begin
  t := AValue.TypeInfo;
  Result := (t <> nil) and GetTypeName(TpName, t, [tnfNoSubstitute]);
  if not Result then
    exit;

  TpName := LowerCase(TpName);
  MatchTypeNames := AllowedTypeNames;
  i := MatchTypeNames.Count;
  while i > 0 do begin
    dec(i);
    Pattern := LowerCase(trim(MatchTypeNames[i]));

    HasMaybeUnitDot := (pos('.', Pattern) > 1) and
                       (AValue.Kind in [skClass]); // only class supports unitnames (via rtti)

    if AnsiStrLIComp('is:', @Pattern[1], 3) = 0 then begin
      Delete(Pattern, 1, 3);
      Pattern := trim(Pattern);

      if  (AValue.Kind in [skRecord, skClass, skObject, skInterface]) then begin
        ValClassName := TpName;
        while t <> nil do begin
          Result := MatchPattern(ValClassName, Pattern);
          if Result then
            exit;
          t := t.TypeInfo;
          if (t = nil) or not GetTypeName(ValClassName, t, [tnfNoSubstitute]) then
            break;
          ValClassName := LowerCase(ValClassName);
        end;

        if not IgnoreInstanceClass then begin
          CnIdx := 0;
          while AValue.GetInstanceClassName(@ValClassName, @ValUnitName, CnIdx) and
                (ValClassName <> '')
          do begin
            ValClassName := LowerCase(ValClassName);
            if (ValClassName = TpName) and (not HasMaybeUnitDot) then
              Break;
            Result := MatchPattern(ValClassName, Pattern);
            if Result then
              exit;

            if HasMaybeUnitDot and (ValUnitName <> '') then begin
              ValUnitName := LowerCase(ValUnitName);
              Result := MatchPattern(ValUnitName+'.'+ValClassName, Pattern);
              if Result then
                exit;
            end;

            inc(CnIdx);
          end;
        end;
        AValue.ResetError;

        Continue;
      end;
    end;

    Result := MatchPattern(TpName, Pattern);
    if Result then
      exit;
    if HasMaybeUnitDot then begin
      if AValue.GetInstanceClassName(@ValClassName, @ValUnitName) and
         (ValUnitName <> '') and (ValClassName <> '')
      then begin
        Result := MatchPattern(ValUnitName+'.'+ValClassName, Pattern);
        if Result then
          exit;
      end;
      AValue.ResetError;
    end;
  end;
end;

{ TFpDbgValueConverterRegistryEntry }

class function TFpDbgValueConverterRegistryEntry.CreateValueConvertorIntf: ILazDbgValueConverterIntf;
begin
  Result := TFpDbgValueConverterClass(GetConvertorClass).Create;
end;

class function TFpDbgValueConverterRegistryEntry.GetName: String;
begin
  Result := TFpDbgValueConverterClass(GetConvertorClass).GetName;
end;

class function TFpDbgValueConverterRegistryEntry.GetDebuggerClass: TClass;
begin
  Result := TFpDebugDebuggerBase;
end;

{ TFpDbgValueConverterVariantToLStr }

function TFpDbgValueConverterVariantToLStr.GetProcAddrFromMgr(
  AnFpDebugger: TFpDebugDebuggerBase; AnExpressionScope: TFpDbgSymbolScope
  ): TDbgPtr;
var
  ProcSym: TFpSymbol;
  MgrAddr, Fnd: TDBGPtr;
  CallContext: TFpDbgInfoCallContext;
  CurProc: TDbgProcess;
begin
  Result := 0;
  CurProc := AnFpDebugger.DbgController.CurrentProcess;
  if CurProc = nil then
    exit;

  ProcSym := nil;
  ProcSym := CurProc.FindProcSymbol('SYSTEM_$$_GETVARIANTMANAGER$TVARIANTMANAGER');
  try
    if (ProcSym = nil) or (not (ProcSym.Kind = skProcedure)) or
       (not IsTargetAddr(ProcSym.Address))
    then
      exit;

    CallContext := nil;
    MgrAddr := AnFpDebugger.DbgController.CurrentThread.AllocStackMem(1024); // enough space for the record
    CallContext := AnFpDebugger.DbgController.Call(ProcSym.Address, AnExpressionScope.LocationContext,
      AnFpDebugger.MemReader, AnFpDebugger.MemConverter);
    if CallContext = nil then
      exit;
    try
      CallContext.AddOrdinalParam(nil, MgrAddr);
      CallContext.FinalizeParams;
      AnFpDebugger.BeforeWatchEval(CallContext);
      AnFpDebugger.RunProcessLoop(True);

      if not CallContext.IsValid then
        exit;

      if CurProc.ReadAddress(MgrAddr + 8 * CurProc.PointerSize, Fnd) then
        Result := Fnd;

    finally
      AnFpDebugger.DbgController.AbortCurrentCommand(True);
      CallContext.ReleaseReference;
      AnFpDebugger.DbgController.CurrentThread.RestoreStackMem;
    end;
  finally
    ProcSym.ReleaseReference;
  end;
end;

function TFpDbgValueConverterVariantToLStr.GetSettingsFrame: ILazDbgValueConverterSettingsFrameIntf;
begin
  Result := TConverterWithFuncCallSettingsFrame.Create(nil);
end;

class function TFpDbgValueConverterVariantToLStr.GetName: String;
begin
  Result := drsCallSysVarToLStr;
end;

function TFpDbgValueConverterVariantToLStr.GetRegistryEntry: TLazDbgValueConvertRegistryEntryClass;
begin
  Result := TFpDbgValueConverterVariantToLStrRegistryEntry;
end;

function TFpDbgValueConverterVariantToLStr.ConvertValue(ASourceValue: TFpValue;
  AnFpDebugger: TFpDebugDebuggerBase; AnExpressionScope: TFpDbgSymbolScope;
  var AnResData: IDbgWatchDataIntf): TFpValue;
var
  NewResult, ProcVal, m: TFpValue;
  ProcSym: TFpSymbol;
  CallContext: TFpDbgInfoCallContext;
  StringAddr, ProcAddr, StringDecRefAddress: TDbgPtr;
  ProcLoc: TFpDbgMemLocation;
  r: Boolean;
begin
  Result := nil;
  if (ASourceValue.Kind <> skRecord) or
     (AnFpDebugger.DbgController.CurrentProcess = nil) or
     ( (AnFpDebugger.DbgController.CurrentProcess.Mode = dm32) and
       (ASourceValue.DataSize.Size <> 16)
     ) or
     ( (AnFpDebugger.DbgController.CurrentProcess.Mode = dm64) and
       (ASourceValue.DataSize.Size <> 24)
     )
  then begin
    SetError(CreateError(fpErrAnyError, ['Value not a variant']));
    exit;
  end;
  m := ASourceValue.MemberByName['vtype'];
  r := (m = nil) or (SizeToFullBytes(m.DataSize) <> 2);
  m.ReleaseReference;
  if r then begin
    SetError(CreateError(fpErrAnyError, ['Value not a variant']));
    exit;
  end;

  ProcVal := nil;
  ProcSym := nil;
  try
(*
    //VARIANTS_$$_SYSVARTOLSTR$ANSISTRING$VARIANT
    //U_$SYSTEM_$$_VARIANTMANAGER
    //SYSTEM_$$_GETVARIANTMANAGER$TVARIANTMANAGER

    ProcVal := AnExpressionScope.FindSymbol('sysvartolstr', 'variants');
    if ProcVal <> nil then begin
      ProcSym := ProcVal.DbgSymbol;
      if ProcSym <> nil then
        ProcSym.AddReference;
    end;
    if (ProcSym = nil) or (not (ProcSym.Kind = skProcedure)) or
       (not IsTargetAddr(ProcSym.Address))
    then
      ProcSym := AnFpDebugger.DbgController.CurrentProcess.FindProcSymbol('sysvartolstr');

    if (ProcSym = nil) or (not IsTargetAddr(ProcSym.Address)) then
      exit;

    ProcLoc := ProcSym.Address
*)

    if not IsTargetAddr(ASourceValue.Address) then begin
      SetError(CreateError(fpErrAnyError, ['Value not in memory']));
      exit;
    end;

    ProcAddr := AnFpDebugger.GetCachedData(pointer(TFpDbgValueConverterVariantToLStr));
    if ProcAddr = 0 then begin
      ProcAddr := GetProcAddrFromMgr(AnFpDebugger, AnExpressionScope);
      if ProcAddr = 0 then begin
        SetError(CreateError(fpErrAnyError, ['SysVarToLStr not found']));
        exit;
      end;
      AnFpDebugger.SetCachedData(pointer(TFpDbgValueConverterVariantToLStr), ProcAddr);
    end;
    ProcLoc := TargetLoc(ProcAddr);

    StringDecRefAddress := AnFpDebugger.GetCached_FPC_ANSISTR_DECR_REF;
    if (StringDecRefAddress = 0) then begin
      SetError(CreateError(fpErrAnyError, ['STRING_DEC_REF not found']));
      exit;
    end;

    StringAddr := 0;
    CallContext := AnFpDebugger.DbgController.Call(ProcLoc, AnExpressionScope.LocationContext,
      AnFpDebugger.MemReader, AnFpDebugger.MemConverter);
    if CallContext = nil then begin
      SetError(CreateError(fpErrAnyError, ['function call not possible']));
      exit;
    end;
    try
      CallContext.AddStringResult;
      CallContext.FinalizeParams; // force the string as first param (32bit) // TODO
      CallContext.AddOrdinalParam(nil, ASourceValue.DataAddress.Address);
      AnFpDebugger.BeforeWatchEval(CallContext);
      AnFpDebugger.RunProcessLoop(not FuncCallRunAllThreads);

      if not CallContext.IsValid then begin
        if (IsError(CallContext.LastError)) then
          SetError(CallContext.LastError)
        else
        if (CallContext.Message <> '') then
          SetError(CreateError(fpErrAnyError, [CallContext.Message]));
        exit;
      end;

      if not CallContext.GetStringResultAsPointer(StringAddr) then begin
        SetError(CallContext.LastError);
        exit;
      end;

      if not CallContext.GetStringResult(NewResult) then begin
        SetError(CallContext.LastError);
        exit;
      end;

      Result := NewResult;
    finally
      AnFpDebugger.DbgController.AbortCurrentCommand(True);
      CallContext.ReleaseReference;

      AnFpDebugger.CallTargetFuncStringDecRef(StringDecRefAddress, StringAddr, AnExpressionScope.LocationContext);
    end;
  finally
    ProcVal.ReleaseReference;
    ProcSym.ReleaseReference;
  end;
end;

{ TFpDbgValueConverterVariantToLStrRegistryEntry }

class function TFpDbgValueConverterVariantToLStrRegistryEntry.GetConvertorClass: TClass;
begin
  Result := TFpDbgValueConverterVariantToLStr;
end;

initialization
  ValueConverterRegistry.Add(TFpDbgValueConverterVariantToLStrRegistryEntry);

end.

