// SPDX-License-Identifier: LGPL-3.0-linking-exception
type
  PBGRAFaceVertexDescription = ^TBGRAFaceVertexDescription;
  TBGRAFaceVertexDescription = record
       Vertex: IBGRAVertex3D;
       Normal: IBGRANormal3D;
       Color: TBGRAPixel;
       TexCoord: TPointF;
       ColorOverride: boolean;
       TexCoordOverride: boolean;
       ActualColor: TBGRAPixel;
       ActualTexCoord: TPointF;
     end;

  { TBGRAFace3D }

  TBGRAFace3D = class(TInterfacedObject,IBGRAFace3D)
  private
    FVertices: packed array of TBGRAFaceVertexDescription;
    FVertexCount: integer;
    FTexture, FActualTexture: IBGRAScanner;
    FMaterial: IBGRAMaterial3D;
    FActualMaterial: TBGRAMaterial3D;
    FMaterialName: string;
    FParentTexture: boolean;
    FViewNormal: TPoint3D_128;
    FViewCenter: TPoint3D_128;
    FObject3D : IBGRAObject3D;
    FBiface: boolean;
    FLightThroughFactor: single;
    FLightThroughFactorOverride: boolean;
    FCustomFlags: LongWord;
    function GetCustomFlags: LongWord;
    function GetVertexDescription(AIndex : integer): PBGRAFaceVertexDescription;
    procedure SetCustomFlags(AValue: LongWord);
    procedure ComputeActualVertexColor(AIndex: integer);
    procedure ComputeActualTexCoord(AMinIndex, AMaxIndex: integer);
    procedure UpdateTexture;
  public
    function GetObject3D: IBGRAObject3D;
    constructor Create(AObject3D: IBGRAObject3D; AVertices: array of IBGRAVertex3D);
    destructor Destroy; override;
    procedure ComputeVertexColors;
    procedure UpdateMaterial;
    procedure FlipFace;
    function AddVertex(AVertex: IBGRAVertex3D): integer;
    function GetParentTexture: boolean;
    function GetTexture: IBGRAScanner;
    function GetVertex(AIndex: Integer): IBGRAVertex3D;
    function GetVertexColor(AIndex: Integer): TBGRAPixel;
    function GetVertexColorOverride(AIndex: Integer): boolean;
    function GetVertexCount: integer;
    function GetNormal(AIndex: Integer): IBGRANormal3D;
    function GetMaterial: IBGRAMaterial3D;
    function GetMaterialName: string;
    function GetTexCoord(AIndex: Integer): TPointF;
    function GetTexCoordOverride(AIndex: Integer): boolean;
    function GetViewNormal: TPoint3D;
    function GetViewNormal_128: TPoint3D_128;
    function GetViewCenter: TPoint3D;
    function GetViewCenter_128: TPoint3D_128;
    function GetViewCenterZ: single;
    function GetBiface: boolean;
    function GetLightThroughFactor: single;
    function GetLightThroughFactorOverride: boolean;
    procedure SetParentTexture(const AValue: boolean);
    procedure SetTexture(const AValue: IBGRAScanner);
    procedure SetColor(AColor: TBGRAPixel);
    procedure SetVertexColor(AIndex: Integer; const AValue: TBGRAPixel);
    procedure SetVertexColorOverride(AIndex: Integer; const AValue: boolean);
    procedure SetTexCoord(AIndex: Integer; const AValue: TPointF);
    procedure SetTexCoordOverride(AIndex: Integer; const AValue: boolean);
    procedure SetBiface(const AValue: boolean);
    procedure SetLightThroughFactor(const AValue: single);
    procedure SetLightThroughFactorOverride(const AValue: boolean);
    procedure SetVertex(AIndex: Integer; AValue: IBGRAVertex3D);
    procedure SetNormal(AIndex: Integer; AValue: IBGRANormal3D);
    procedure ComputeViewNormalAndCenter;
    procedure SetMaterial(const AValue: IBGRAMaterial3D);
    procedure SetMaterialName(const AValue: string);
    function GetAsObject: TObject;
    property Texture: IBGRAScanner read GetTexture write SetTexture;
    property ParentTexture: boolean read GetParentTexture write SetParentTexture;
    property VertexCount: integer read GetVertexCount;
    property Vertex[AIndex: Integer]: IBGRAVertex3D read GetVertex write SetVertex;
    property Normal[AIndex: Integer]: IBGRANormal3D read GetNormal write SetNormal;
    property VertexColor[AIndex: Integer]: TBGRAPixel read GetVertexColor write SetVertexColor;
    property VertexColorOverride[AIndex: Integer]: boolean read GetVertexColorOverride write SetVertexColorOverride;
    property TexCoord[AIndex: Integer]: TPointF read GetTexCoord write SetTexCoord;
    property TexCoordOverride[AIndex: Integer]: boolean read GetTexCoordOverride write SetTexCoordOverride;
    property ViewNormal: TPoint3D read GetViewNormal;
    property ViewNormal_128: TPoint3D_128 read GetViewNormal_128;
    property ViewCenter: TPoint3D read GetViewCenter;
    property ViewCenter_128: TPoint3D_128 read GetViewCenter_128;
    property ViewCenterZ: single read GetViewCenterZ;
    property Object3D: IBGRAObject3D read GetObject3D;
    property Biface: boolean read GetBiface write SetBiface;
    property LightThroughFactor: single read GetLightThroughFactor write SetLightThroughFactor;
    property LightThroughFactorOverride: boolean read GetLightThroughFactorOverride write SetLightThroughFactorOverride;
    property Material: IBGRAMaterial3D read GetMaterial write SetMaterial;
    property ActualMaterial: TBGRAMaterial3D read FActualMaterial;
    property ActualTexture: IBGRAScanner read FActualTexture;
    property VertexDescription[AIndex : integer]: PBGRAFaceVertexDescription read GetVertexDescription;
    property CustomFlags: LongWord read GetCustomFlags write SetCustomFlags;
  end;

{ TBGRAFace3D }

function TBGRAFace3D.GetVertexDescription(AIndex : integer
  ): PBGRAFaceVertexDescription;
begin
  result := @FVertices[AIndex];
end;

function TBGRAFace3D.GetCustomFlags: LongWord;
begin
  result := FCustomFlags;
end;

function TBGRAFace3D.GetNormal(AIndex: Integer): IBGRANormal3D;
begin
  result := FVertices[AIndex].Normal;
end;

procedure TBGRAFace3D.SetCustomFlags(AValue: LongWord);
begin
  FCustomFlags:= AValue;
end;

procedure TBGRAFace3D.ComputeActualVertexColor(AIndex: integer);
begin
  with FVertices[AIndex] do
  begin
    if ColorOverride then
      ActualColor := Color
    else
    if Vertex.ParentColor then
      ActualColor := FObject3D.Color
    else
      ActualColor := Vertex.Color;
  end;
end;

procedure TBGRAFace3D.ComputeActualTexCoord(AMinIndex, AMaxIndex: integer);
var
  i: Integer;
  zoom: TPointF;
  m: IBGRAMaterial3D;
begin
  m := ActualMaterial;
  if m <> nil then zoom := m.TextureZoom
  else zoom := PointF(1,1);
  for i := AMinIndex to AMaxIndex do
    with FVertices[i] do
    begin
      if TexCoordOverride then
        ActualTexCoord := TexCoord
      else
        ActualTexCoord := Vertex.TexCoord;
      ActualTexCoord.x := ActualTexCoord.x * zoom.x;
      ActualTexCoord.y := ActualTexCoord.y * zoom.y;
    end;
end;

procedure TBGRAFace3D.UpdateTexture;
begin
  if FParentTexture then
  begin
    FActualTexture := nil;
    if FActualMaterial <> nil then
      FActualTexture := FActualMaterial.GetTexture;
    if FActualTexture = nil then
      FActualTexture := FObject3D.Texture
  end
  else
    FActualTexture := FTexture;
end;

procedure TBGRAFace3D.SetNormal(AIndex: Integer; AValue: IBGRANormal3D);
begin
  FVertices[AIndex].Normal := AValue;
end;

function TBGRAFace3D.GetObject3D: IBGRAObject3D;
begin
  result := FObject3D;
end;

constructor TBGRAFace3D.Create(AObject3D: IBGRAObject3D;
  AVertices: array of IBGRAVertex3D);
var
  i: Integer;
begin
  FObject3D := AObject3D;
  FBiface := false;
  FParentTexture := True;
  FLightThroughFactor:= 0;
  FLightThroughFactorOverride:= false;

  UpdateMaterial;

  SetLength(FVertices, length(AVertices));
  for i:= 0 to high(AVertices) do
    AddVertex(AVertices[i]);
end;

destructor TBGRAFace3D.Destroy;
begin
  FMaterial := nil;
  fillchar(FTexture,sizeof(FTexture),0);
  fillchar(FActualTexture,sizeof(FActualTexture),0);
  inherited Destroy;
end;

procedure TBGRAFace3D.ComputeVertexColors;
var
  i: Integer;
begin
  for i := 0 to FVertexCount-1 do
    ComputeActualVertexColor(i);
end;

procedure TBGRAFace3D.UpdateMaterial;
begin
  if Material <> nil then
    FActualMaterial := TBGRAMaterial3D(Material.GetAsObject)
  else if FObject3D.Material <> nil then
    FActualMaterial := TBGRAMaterial3D(FObject3D.Material.GetAsObject)
  else if TBGRAScene3D(FObject3D.Scene).DefaultMaterial <> nil then
    FActualMaterial := TBGRAMaterial3D(TBGRAScene3D(FObject3D.Scene).DefaultMaterial.GetAsObject);

  UpdateTexture;

  ComputeActualTexCoord(0,FVertexCount-1);
end;

procedure TBGRAFace3D.FlipFace;
var i: integer;
  temp: TBGRAFaceVertexDescription;
begin
  for i := 0 to (VertexCount div 2)-1 do
  begin
    temp := FVertices[i];
    FVertices[i] := FVertices[VertexCount-1-i];
    FVertices[VertexCount-1-i] := temp;
  end;
end;

function TBGRAFace3D.AddVertex(AVertex: IBGRAVertex3D): integer;
begin
  if FVertexCount = length(FVertices) then
    setlength(FVertices, FVertexCount*2+3);
  result := FVertexCount;
  with FVertices[result] do
  begin
    Color := BGRAWhite;
    ColorOverride := false;
    TexCoord := PointF(0,0);
    TexCoordOverride := false;
    Vertex := AVertex;
    Normal := nil;
  end;
  ComputeActualVertexColor(result);
  ComputeActualTexCoord(result,result);
  inc(FVertexCount);
end;

function TBGRAFace3D.GetParentTexture: boolean;
begin
  result := FParentTexture;
end;

function TBGRAFace3D.GetTexture: IBGRAScanner;
begin
  result := FTexture;
end;

function TBGRAFace3D.GetVertex(AIndex: Integer): IBGRAVertex3D;
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  result := FVertices[AIndex].Vertex;
end;

procedure TBGRAFace3D.SetVertex(AIndex: Integer; AValue: IBGRAVertex3D);
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  FVertices[AIndex].Vertex := AValue;
  ComputeActualVertexColor(AIndex);
end;

function TBGRAFace3D.GetVertexColor(AIndex: Integer): TBGRAPixel;
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  result := FVertices[AIndex].ActualColor;
end;

function TBGRAFace3D.GetVertexColorOverride(AIndex: Integer): boolean;
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  result := FVertices[AIndex].ColorOverride;
end;

function TBGRAFace3D.GetVertexCount: integer;
begin
  result := FVertexCount;
end;

function TBGRAFace3D.GetMaterial: IBGRAMaterial3D;
begin
  result := FMaterial;
end;

function TBGRAFace3D.GetMaterialName: string;
begin
  result := FMaterialName;
end;

procedure TBGRAFace3D.SetParentTexture(const AValue: boolean);
begin
  FParentTexture := AValue;
  UpdateTexture;
end;

procedure TBGRAFace3D.SetTexture(const AValue: IBGRAScanner);
begin
  FTexture := AValue;
  FParentTexture := false;
  UpdateTexture;
end;

procedure TBGRAFace3D.SetColor(AColor: TBGRAPixel);
var i: integer;
begin
  for i := 0 to GetVertexCount-1 do
    SetVertexColor(i,AColor);
end;

procedure TBGRAFace3D.SetVertexColor(AIndex: Integer; const AValue: TBGRAPixel
  );
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  with FVertices[AIndex] do
  begin
    Color := AValue;
    ColorOverride := true;
  end;
  ComputeActualVertexColor(AIndex);
end;

procedure TBGRAFace3D.SetVertexColorOverride(AIndex: Integer;
  const AValue: boolean);
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  FVertices[AIndex].ColorOverride := AValue;
  ComputeActualVertexColor(AIndex);
end;

function TBGRAFace3D.GetTexCoord(AIndex: Integer): TPointF;
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  result := FVertices[AIndex].TexCoord;
end;

function TBGRAFace3D.GetTexCoordOverride(AIndex: Integer): boolean;
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  result := FVertices[AIndex].TexCoordOverride;
end;

procedure TBGRAFace3D.SetTexCoord(AIndex: Integer; const AValue: TPointF);
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  FVertices[AIndex].TexCoord := AValue;
  FVertices[AIndex].TexCoordOverride := true;
  ComputeActualTexCoord(AIndex, AIndex);
end;

procedure TBGRAFace3D.SetTexCoordOverride(AIndex: Integer; const AValue: boolean
  );
begin
  if (AIndex < 0) or (AIndex >= FVertexCount) then
    raise Exception.Create('Index out of bounds');
  FVertices[AIndex].TexCoordOverride := AValue;
end;

function TBGRAFace3D.GetViewNormal: TPoint3D;
begin
  result := Point3D(FViewNormal);
end;

function TBGRAFace3D.GetViewNormal_128: TPoint3D_128;
begin
  result := FViewNormal;
end;

function TBGRAFace3D.GetViewCenter: TPoint3D;
begin
  result := Point3D(FViewCenter);
end;

function TBGRAFace3D.GetViewCenter_128: TPoint3D_128;
begin
  result := FViewCenter;
end;

function TBGRAFace3D.GetViewCenterZ: single;
begin
  result := FViewCenter.Z;
end;

function TBGRAFace3D.GetBiface: boolean;
begin
  result := FBiface;
end;

procedure TBGRAFace3D.SetBiface(const AValue: boolean);
begin
  FBiface := AValue;
end;

function TBGRAFace3D.GetLightThroughFactor: single;
begin
  result := FLightThroughFactor;
end;

function TBGRAFace3D.GetLightThroughFactorOverride: boolean;
begin
  result := FLightThroughFactorOverride;
end;

procedure TBGRAFace3D.SetLightThroughFactor(const AValue: single);
begin
  if AValue < 0 then
    FLightThroughFactor := 0
  else
    FLightThroughFactor:= AValue;
  FLightThroughFactorOverride := true;
end;

procedure TBGRAFace3D.SetLightThroughFactorOverride(const AValue: boolean);
begin
  FLightThroughFactorOverride := AValue;
end;

procedure TBGRAFace3D.ComputeViewNormalAndCenter;
var v1,v2: TPoint3D_128;
  i: Integer;
  p0,p1,p2: IBGRAVertex3D;
begin
  if FVertexCount < 3 then
    ClearPoint3D_128(FViewNormal)
  else
  begin
    p0 := FVertices[0].Vertex;
    p1 := FVertices[1].Vertex;
    p2 := FVertices[2].Vertex;
    v1 := p1.ViewCoord_128 - p0.ViewCoord_128;
    v2 := p2.ViewCoord_128 - p1.ViewCoord_128;
    VectProduct3D_128(v2,v1,FViewNormal);
    Normalize3D_128(FViewNormal);
    for i := 0 to FVertexCount-1 do
      FVertices[i].Vertex.AddViewNormal(FViewNormal);
  end;
  ClearPoint3D_128(FViewCenter);
  if FVertexCount > 0 then
  begin
    for i := 0 to FVertexCount-1 do
      FViewCenter.Offset(FVertices[i].Vertex.ViewCoord_128);
    FViewCenter.Scale(1/FVertexCount);
  end;
end;

procedure TBGRAFace3D.SetMaterial(const AValue: IBGRAMaterial3D);
begin
  if AValue <> FMaterial then
  begin
    FMaterial := AValue;
    UpdateMaterial;
  end;
end;

procedure TBGRAFace3D.SetMaterialName(const AValue: string);
begin
  if AValue <> FMaterialName then
  begin
    FMaterialName := AValue;
    TBGRAScene3D(FObject3D.Scene).UseMaterial(FMaterialName, self);
  end;
end;

function TBGRAFace3D.GetAsObject: TObject;
begin
  result := self;
end;


