{%MainUnit ../graphics.pp}
{******************************************************************************
                                     TPen
 ******************************************************************************

 *****************************************************************************
  This file is part of the Lazarus Component Library (LCL)

  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************
}

type
  TExtPenAndPattern = record
    ExtPen: TExtLogPen;
    Pattern: TPenPattern;
  end;
  PExtPenAndPattern = ^TExtPenAndPattern;

function CompareExtPenAndPatternWithResDesc(Key: PExtPenAndPattern; Desc: TPenHandleCacheDescriptor): integer;
begin
  Result := CompareMemRange(@Key^.ExtPen, @Desc.ExtPen,
                          SizeOf(Key^.ExtPen));
  if Result <> 0 then
    Exit;

  Result := CompareValue(Length(Key^.Pattern), Length(Desc.Pattern));
  if Result <> 0 then
    Exit;

  if Length(Key^.Pattern) > 0 then
  begin
    Result := CompareMemRange(@Key^.Pattern[0], @Desc.Pattern[0],
                            SizeOf(Key^.Pattern[0]) * Length(Key^.Pattern));
  end;
end;

{ TPenHandleCache }

procedure TPenHandleCache.RemoveItem(Item: TResourceCacheItem);
begin
  DeleteObject(HGDIOBJ(Item.Handle));
  inherited RemoveItem(Item);
end;

constructor TPenHandleCache.Create;
begin
  inherited Create;
  FResourceCacheDescriptorClass := TPenHandleCacheDescriptor;
end;

function TPenHandleCache.CompareDescriptors(Tree: TAvlTree; Desc1,
  Desc2: Pointer): integer;
var
  Descriptor1: TPenHandleCacheDescriptor absolute Desc1;
  Descriptor2: TPenHandleCacheDescriptor absolute Desc2;
begin
  Result := CompareMemRange(@Descriptor1.ExtPen, @Descriptor2.ExtPen,
                          SizeOf(Descriptor1.ExtPen));
  if Result <> 0 then
    Exit;

  Result := CompareValue(Length(Descriptor1.Pattern), Length(Descriptor2.Pattern));
  if Result <> 0 then
    Exit;

  if Length(Descriptor1.Pattern) > 0 then
  begin
    Result := CompareMemRange(@Descriptor1.Pattern[0], @Descriptor2.Pattern[0],
                            SizeOf(Descriptor1.Pattern[0]) * Length(Descriptor1.Pattern));
  end;
end;

function TPenHandleCache.FindPen(APen: TLCLHandle): TResourceCacheItem;
var
  ANode: TAvlTreeNode;
begin
  ANode := FItems.FindKey(@APen,
                          TListSortCompare(@ComparePHandleWithResourceCacheItem));
  if ANode <> nil then
    Result := TResourceCacheItem(ANode.Data)
  else
    Result := nil;
end;

function TPenHandleCache.FindPenDesc(const AExtPen: TExtLogPen;
  const APattern: TPenPattern): TPenHandleCacheDescriptor;
var
  ExtPenAndPattern: TExtPenAndPattern;
  ANode: TAvlTreeNode;
begin
  ExtPenAndPattern.ExtPen := AExtPen;
  ExtPenAndPattern.Pattern := APattern;
  ANode := FDescriptors.Findkey(@ExtPenAndPattern,
                           TListSortCompare(@CompareExtPenAndPatternWithResDesc));
  if ANode <> nil then
    Result := TPenHandleCacheDescriptor(ANode.Data)
  else
    Result := nil;
end;

function TPenHandleCache.Add(APen: TLCLHandle; const AExtPen: TExtLogPen;
  const APattern: TPenPattern): TPenHandleCacheDescriptor;
var
  Item: TResourceCacheItem;
begin
  if FindPenDesc(AExtPen, APattern) <> nil then
    RaiseGDBException('TPenHandleCache.Add pen desc added twice');

  // find cache item with APen
  Item := FindPen(APen);
  if Item = nil then
  begin
    // create new item
    Item := TResourceCacheItem.Create(Self, APen);
    FItems.Add(Item);
  end;

  // create descriptor
  Result := TPenHandleCacheDescriptor.Create(Self, Item);
  Result.ExtPen := AExtPen;
  Result.Pattern := APattern;
  FDescriptors.Add(Result);
  if FindPenDesc(AExtPen, APattern) = nil then
  begin
    {$IFNDEF DisableChecks}
    DebugLn('TPenHandleCache.Add Added: %p', [Pointer(Result)]);
    {$ENDIF}
    RaiseGDBException('');
  end;
end;

{ TPen }

{------------------------------------------------------------------------------
  Method: TPen.SetColor
  Params: Value: the new value
  Returns:  nothing

  Sets the style of a pen
 ------------------------------------------------------------------------------}
procedure TPen.SetColor(Value : TColor);
begin
  if FColor <> Value then
    SetColor(Value, TColorToFPColor(Value));
end;

{------------------------------------------------------------------------------
  Method: TPen.SetStyle
  Params: Value: the new value
  Returns:  nothing

  Sets the style of a pen
 ------------------------------------------------------------------------------}
procedure TPen.SetStyle(Value : TPenStyle);
begin
  if Style <> Value then
  begin
    FreeReference;
    inherited SetStyle(Value);
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Method: TPen.SetMode
  Params: Value: the new value
  Returns:  nothing

  Sets the Mode of a pen
 ------------------------------------------------------------------------------}
procedure TPen.SetMode(Value : TPenMode);
begin
  if Mode <> Value then
  begin
    FreeReference;
    inherited SetMode(Value);
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Method: TPen.SetWidth
  Params: Value: the new value
  Returns:  nothing

  Sets the style of a pen
 ------------------------------------------------------------------------------}
procedure TPen.SetWidth(Value : Integer);
begin
  if (Width <> Value) then
  begin
    FreeReference;
    inherited SetWidth(Value);
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Method:  TPen.Create
  Params:  none
  Returns: Nothing

  Constructor for the class.
 ------------------------------------------------------------------------------}
constructor TPen.Create;
begin
  inherited Create;
  DelayAllocate := True;
  FCosmetic := True;
  {$IFDEF HasFPEndCap}
  inherited SetEndCap(pecRound);
  {$ELSE}
  FEndCap := pecRound;
  {$ENDIF}
  {$IFDEF HasFPJoinStyle}
  inherited SetJoinStyle(pjsRound);
  {$ELSE}
  FJoinStyle := pjsRound;
  {$ENDIF}
  inherited SetWidth(1);
  inherited SetStyle(psSolid);
  inherited SetMode(pmCopy);
  inherited SetFPColor(colBlack);
  Color := clBlack;
end;

{------------------------------------------------------------------------------
  Method: TPen.Destroy
  Params:  None
  Returns: Nothing

  Destructor for the class.
 ------------------------------------------------------------------------------}
destructor TPen.Destroy;
begin
  FreeReference;
  inherited Destroy;
end;

{------------------------------------------------------------------------------
  Method: TPen.Assign
  Params: Source: Another pen
  Returns:  nothing

  Copies the source pen to itself
 ------------------------------------------------------------------------------}
procedure TPen.Assign(Source : Tpersistent);
var
  APen: TPen absolute Source;
begin
  if Source is TPen then
  begin
    Width := APen.Width;
    SetColor(APen.Color, TFPCanvasHelper(Source).FPColor);
    Style := APen.Style;
    Mode := APen.Mode;
    Cosmetic := APen.Cosmetic;
    JoinStyle := APen.JoinStyle;
    EndCap := APen.EndCap;
    SetPattern(APen.GetPattern);
  end
  else
    inherited Assign(Source);
end;

function TPen.GetPattern: TPenPattern;
begin
  Result := FPattern;
end;

procedure TPen.SetPattern(APattern: TPenPattern);

  function PatternsDiffer: Boolean;
  var
    l1, l2, m: integer;
  begin
    l1 := Length(FPattern);
    l2 := Length(APattern);
    m := min(l1, l2);
    Result := (l1 <> l2) or
              ((m > 0) and not CompareMem(@APattern[0], @FPattern[0], m * SizeOf(LongWord)));
  end;

begin
  if PatternsDiffer then
  begin
    FreeReference;
    FPattern := APattern;
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Method: TPen.SetHandle
  Params:   a pen handle
  Returns:  nothing

  sets the pen to an external created pen
 ------------------------------------------------------------------------------}
procedure TPen.SetHandle(const Value: HPEN);
begin
  if HPEN(FReference.Handle) = Value then Exit;

  FreeReference;
  FReference._lclHandle := TLCLHandle(Value);
  Changed;
end;

procedure TPen.SetJoinStyle(AValue: TPenJoinStyle);
begin
  if JoinStyle <> AValue then
  begin
    FreeReference;
    {$IFDEF HasFPJoinStyle}
    inherited SetJoinStyle(AValue);
    {$ELSE}
    FJoinStyle := AValue;
    {$ENDIF}
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Function: TPen.GetHandle
  Params:   none
  Returns:  a handle to a pen gdiobject

  Creates a pen if needed
 ------------------------------------------------------------------------------}
function TPen.GetHandle: HPEN;
begin
  Result := HPEN(Reference.Handle);
end;

function TPen.GetReference: TWSPenReference;
begin
  ReferenceNeeded;
  Result := FReference;
end;

procedure TPen.ReferenceNeeded;
const
  PEN_STYLES: array[TPenStyle] of DWord = (
 { psSolid       } PS_SOLID,
 { psDash        } PS_DASH,
 { psDot         } PS_DOT,
 { psDashDot     } PS_DASHDOT,
 { psDashDotDot  } PS_DASHDOTDOT,
 { psinsideFrame } PS_INSIDEFRAME,
 { psPattern     } PS_USERSTYLE,
 { psClear       } PS_NULL
  );

  PEN_GEOMETRIC: array[Boolean] of DWord = (
  { false }  PS_COSMETIC,
  { true  }  PS_GEOMETRIC
  );

  PEN_ENDCAP: array[TPenEndCap] of DWord = (
  { pecRound  } PS_ENDCAP_ROUND,
  { pecSquare } PS_ENDCAP_SQUARE,
  { pecFlat   } PS_ENDCAP_FLAT
  );

  PEN_JOIN: array[TPenJoinStyle] of DWord = (
  { pjsRound } PS_JOIN_ROUND,
  { pjsBevel } PS_JOIN_BEVEL,
  { pjsMiter } PS_JOIN_MITER
  );
var
  ALogPen: TLogPen;
  AExtPen: TExtLogPen;
  ALogBrush: TLogBrush;
  CachedPen: TPenHandleCacheDescriptor;
  IsGeometric: Boolean;
begin
  if FReference.Allocated then Exit;

  IsGeometric := (Width > 1) or not Cosmetic;

  FillChar(AExtPen, SizeOf(AExtPen), 0);
  with AExtPen do
  begin
    elpPenStyle := PEN_STYLES[Style] or PEN_GEOMETRIC[IsGeometric];
    if IsGeometric then
      elpPenStyle := elpPenStyle or PEN_ENDCAP[EndCap] or PEN_JOIN[JoinStyle];
    if IsGeometric then
      elpWidth := Width
    else
    begin
      // issue #32465, regression from fixing #22646. Pure cosmetic
      // pen is created via TLogPen, not via TExtLogPen
      if ((elpPenStyle and PS_STYLE_MASK) = elpPenStyle) and
          (elpPenStyle <> PS_USERSTYLE) then
        elpWidth := 0
      else
        //(https://msdn.microsoft.com/en-us/library/windows/desktop/dd162705(v=vs.85).aspx
        //https://msdn.microsoft.com/en-us/library/windows/desktop/dd162711(v=vs.85).aspx
        //Issue #0022646
        elpWidth := 1;
    end;
    elpBrushStyle := BS_SOLID;
    elpColor := TColorRef(FColor);
  end;

  PenResourceCache.Lock;
  try
    if Style = psPattern then
      CachedPen := PenResourceCache.FindPenDesc(AExtPen, FPattern)
    else
      CachedPen := PenResourceCache.FindPenDesc(AExtPen, nil);

    if CachedPen <> nil then
    begin
      CachedPen.Item.IncreaseRefCount;
      FReference._lclHandle := CachedPen.Item.Handle;
    end else
    begin
      // choose which function to use: CreatePenIndirect or ExtCreatePen
      if ((AExtPen.elpPenStyle and PS_STYLE_MASK) = AExtPen.elpPenStyle) and
          (AExtPen.elpPenStyle <> PS_USERSTYLE) then
      begin
        // simple pen
        ALogPen.lopnStyle := AExtPen.elpPenStyle;
        ALogPen.lopnWidth := Point(AExtPen.elpWidth, 0);
        ALogPen.lopnColor := AExtPen.elpColor;
        FReference._lclHandle := TLCLHandle(CreatePenIndirect(ALogPen));
      end
      else
      begin
        // extended pen
        ALogBrush.lbStyle := AExtPen.elpBrushStyle;
        ALogBrush.lbColor := AExtPen.elpColor;
        ALogBrush.lbHatch := AExtPen.elpHatch;
        if (Style = psPattern) and (Length(FPattern) > 0) then
          FReference._lclHandle := TLCLHandle(ExtCreatePen(AExtPen.elpPenStyle,
            AExtPen.elpWidth, ALogBrush, Length(FPattern), @FPattern[0]))
        else
          FReference._lclHandle := TLCLHandle(ExtCreatePen(AExtPen.elpPenStyle,
            AExtPen.elpWidth, ALogBrush, 0, nil));
      end;

      if Style = psPattern then
        PenResourceCache.Add(FReference.Handle, AExtPen, FPattern)
      else
        PenResourceCache.Add(FReference.Handle, AExtPen, nil);
    end;
    FPenHandleCached := True;
  finally
    PenResourceCache.Unlock;
  end;
end;

procedure TPen.SetCosmetic(const AValue: Boolean);
begin
  if Cosmetic <> AValue then
  begin
    FreeReference;
    FCosmetic := AValue;
    Changed;
  end;
end;

procedure TPen.SetEndCap(AValue: TPenEndCap);
begin
  if EndCap <> AValue then
  begin
    FreeReference;
    {$IFDEF HasFPEndCap}
    inherited SetEndCap(AValue);
    {$ELSE}
    FEndCap := AValue;
    {$ENDIF}
    Changed;
  end;
end;

{------------------------------------------------------------------------------
  Method:  TPen.FreeReference
  Params:  none
  Returns: Nothing

  Frees a pen handle if needed
 ------------------------------------------------------------------------------}

procedure TPen.FreeReference;
begin
  if not FReference.Allocated then Exit;

  Changing;
  if FPenHandleCached then
  begin
    PenResourceCache.Lock;
    try
      PenResourceCache.FindPen(FReference.Handle).DecreaseRefCount;
      FPenHandleCached := False;
    finally
      PenResourceCache.Unlock;
    end;
  end else
    DeleteObject(HGDIOBJ(FReference.Handle));
  FReference._lclHandle := 0;
end;

procedure TPen.DoAllocateResources;
begin
  inherited DoAllocateResources;
  GetReference;
end;

procedure TPen.DoDeAllocateResources;
begin
  FreeReference;
  inherited DoDeAllocateResources;
end;

procedure TPen.DoCopyProps(From: TFPCanvasHelper);
var
  APen: TPen absolute From;
begin
  if From is TPen then
  begin
    FreeReference;
    inherited DoCopyProps(From);
    FCosmetic := APen.Cosmetic;
    EndCap := APen.EndCap;
    JoinStyle := APen.JoinStyle;
    //TODO: query new parameters
    Changed;
  end else
    inherited DoCopyProps(From);
end;

procedure TPen.SetColor(const NewColor: TColor; const NewFPColor: TFPColor);
begin
  if (NewColor = Color) and (NewFPColor = FPColor) then Exit;
  FreeReference;
  FColor := NewColor;
  inherited SetFPColor(NewFPColor);
  Changed;
end;

procedure TPen.SetFPColor(const AValue: TFPColor);
begin
  if FPColor <> AValue then
    SetColor(FPColorToTColor(AValue), AValue);
end;

