//{{{ TVertexBuffer private part

procedure TVertexBuffer.SetUseGPUStorage(UseGPUStorage: Boolean);
begin
  if UseGPUStorage = GetUseGPUStorage then
    Exit;
  Load_GL_version_1_5;

  if not UseGPUStorage then begin
    if FVertexArrayObject <> 0 then
      FVertexArrayObject := 0; // TODO glDeleteVertexArrays(1, @FVertexArrayObject);
    if FVertexBufferObject <> 0 then
      glDeleteBuffers(1, @FVertexBufferObject);
    FVertexArrayObject := 0;
    FVertexBufferObject := 0;
    Exit;
  end;

  if @glGenBuffers = nil then // OpenGL Version < 1.5?
    Exit;

  glGenBuffers(1, @FVertexBufferObject);

  // force VBO allocation and reserving
  glBindBuffer(GL_ARRAY_BUFFER, FVertexBufferObject);
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  FVertexArrayObject := 0;
  FVertexBufferObjectSize := 0;

  FGPUStorageDirty := True;
end;

function TVertexBuffer.GetUseGPUStorage: Boolean;
begin
  Exit(FVertexBufferObject <> 0);
end;

procedure DEngGL_BufferSubData(var GPUOffset: Pointer; Size: SizeUInt; Data: Pointer; out GPUData: Pointer);
begin
  // TODO alignment?
  GPUData := GPUOffset;
  if Size = 0 then
    Exit;
  glBufferSubData(GL_ARRAY_BUFFER, PtrInt(GPUOffset), Size, Data);
  Inc(GPUOffset, Size);
end;

procedure TVertexBuffer.SyncGPUStorage;
var
  Size: SizeUInt;
  GPUOffset: Pointer;
  I: LongInt;
begin
  if FVertexBufferObject = 0 then begin
    FGPUStorageDirty := False;
    Exit;
  end;

  glBindBuffer(GL_ARRAY_BUFFER, FVertexBufferObject);
  Size := GetAllArraysSize;
  if FVertexBufferObjectSize <> Size then begin
    Log('Allocating buffer for ' + IntToStr(Size) + ' bytes');
    glBufferData(GL_ARRAY_BUFFER, Size, nil, GL_STATIC_DRAW);
    FVertexBufferObjectSize := Size;
  end;

  GPUOffset := nil;
  DEngGL_BufferSubData(GPUOffset, FCount * GetInterleavedSize(FInterleavedUsed), FInterleaved.Data, FInterleaved.FGPUData);
  if not FInterleavedUsed then begin
    DEngGL_BufferSubData(GPUOffset, FCount * GetVertexSize(FVertexType), FVertices.Data, FVertices.FGPUData);
    DEngGL_BufferSubData(GPUOffset, FCount * GetColorSize(FColorType), FColors.Data, FColors.FGPUData);
    DEngGL_BufferSubData(GPUOffset, FCount * GetNormalSize(FNormalType), FNormals.Data, FNormals.FGPUData);
    DEngGL_BufferSubData(GPUOffset, FCount * GetTexCoordSize(FTexCoordType), FTexCoords.Data, FTexCoords.FGPUData);
    if FLayersArray <> nil then begin
      for I := 0 to High(TLayersArray) do begin
        if FLayersArray^[I].FType <> -1 then begin
          DEngGL_BufferSubData(GPUOffset, FLayersArray^[I].FElementSize * FCount,
                               FLayersArray^[I].FBuffer.Data, FLayersArray^[I].FBuffer.FGPUData);
        end;
      end;
    end;
    for I := 0 to High(FLayersMap) do begin
      DEngGL_BufferSubData(GPUOffset, FLayersMap[I].FElementSize * FCount,
                           FLayersMap[I].FBuffer.Data, FLayersMap[I].FBuffer.FGPUData);
    end;
  end else begin
    FVertices.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FVertices.Data);
    FColors.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FColors.Data);
    FNormals.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FNormals.Data);
    FTexCoords.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FTexCoords.Data);
    if FLayersArray <> nil then begin
      for I := 0 to High(TLayersArray) do begin
        if FLayersArray^[I].FType <> -1 then begin
          FLayersArray^[I].FBuffer.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FLayersArray^[I].FBuffer.Data);
        end;
      end;
    end;
    for I := 0 to High(FLayersMap) do begin
      FLayersMap[I].FBuffer.FGPUData := Pointer(FInterleaved.Data) + PtrUInt(FLayersMap[I].FBuffer.Data);
    end;
  end;

  FGPUStorageDirty := False;
end;

procedure TVertexBuffer.UploadToGPUStorage;
begin
end;

function TVertexBuffer.FindAndCreateLayer(LayerId: PtrUInt;
                                          out Layer: PVertexLayer): Boolean;
var
  I: LongInt;
begin
  if LayerId < Length(TLayersArray) then begin
    if FLayersArray = nil then begin
      New(FLayersArray);
      for I := 0 to High(TLayersArray) do begin
        FLayersArray^[I].FId := I;
        FLayersArray^[I].FType := -1;
      end;
    end;
    Layer := @FLayersArray^[LayerId];
    Exit(Layer^.FType <> -1);
  end else begin
    for I := 0 to High(FLayersMap) do begin
      if FLayersMap[I].FId = LayerId then begin
        Layer := @FLayersMap[I];
        Exit(True);
      end;
    end;
    SetLength(FLayersMap, Length(FLayersMap) + 1);
    Layer := @FLayersMap[High(FLayersMap)];
    Layer^.FId := LayerId;
    Exit(False);
  end;
end;

function TVertexBuffer.GetVertexSize(_Type: LongInt): LongInt;
begin
  case _Type of
    1: Result := SizeOf(TVec2i);
    2: Result := SizeOf(TVec2f);
    3: Result := SizeOf(TVec3f);
    4: Result := 2 * SizeOf(Int16);
  else
    Result := 0;
  end;
end;

procedure TVertexBuffer.SetVerticesType(NewType: LongInt);
var
  OldSize, NewSize: LongInt;
begin
  if NewType = 0 then begin
    if FVertexType <> 0 then begin
      FVertices.Done;
    end;
  end else begin
    if FVertexType = 0 then begin
      FVertices.Init(FCount * GetVertexSize(NewType));
    end else begin
      OldSize := GetVertexSize(FVertexType);
      NewSize := GetVertexSize(NewType);
      if NewSize > OldSize then begin
        FVertices.Done;
        FVertices.Init(FCount * NewSize);
      end;
    end;
  end;
  FVertexType := NewType;
end;

function TVertexBuffer.GetColorSize(_Type: LongInt): LongInt;
begin
  case _Type of
    1: Result := SizeOf(TColor);
    2: Result := SizeOf(TVec4f);
  else
    Result := 0;
  end;
end;

procedure TVertexBuffer.SetColorsType(NewType: LongInt);
var
  OldSize, NewSize: LongInt;
begin
  if NewType = 0 then begin
    if FColorType <> 0 then begin
      FColors.Done;
    end;
  end else begin
    if FColorType = 0 then begin
      FColors.Init(FCount * GetColorSize(NewType));
    end else begin
      OldSize := GetColorSize(FColorType);
      NewSize := GetColorSize(NewType);
      if NewSize > OldSize then begin
        FColors.Done;
        FColors.Init(FCount * NewSize);
      end;
    end;
  end;
  FColorType := NewType;
end;

function TVertexBuffer.GetNormalSize(_Type: LongInt): LongInt;
begin
  case _Type of
    1: Result := SizeOf(TVec3f);
  else
    Result := 0;
  end;
end;

procedure TVertexBuffer.SetNormalsType(NewType: LongInt);
var
  OldSize, NewSize: LongInt;
begin
  if NewType = 0 then begin
    if FNormalType <> 0 then begin
      FNormals.Done;
    end;
  end else begin
    if FNormalType = 0 then begin
      FNormals.Init(FCount * GetNormalSize(NewType));
    end else begin
      OldSize := GetNormalSize(FNormalType);
      NewSize := GetNormalSize(NewType);
      if NewSize > OldSize then begin
        FNormals.Done;
        FNormals.Init(FCount * NewSize);
      end;
    end;
  end;
  FNormalType := NewType;
end;

function TVertexBuffer.GetTexCoordSize(_Type: LongInt): LongInt;
begin
  case _Type of
    1: Result := SizeOf(TVec2f);
    2: Result := SizeOf(TVec3f);
  else
    Result := 0;
  end;
end;

procedure TVertexBuffer.SetTexCoordsType(NewType: LongInt);
var
  OldSize, NewSize: LongInt;
begin
  if NewType = 0 then begin
    if FTexCoordType <> 0 then begin
      FTexCoords.Done;
    end;
  end else begin
    if FTexCoordType = 0 then begin
      FTexCoords.Init(FCount * GetTexCoordSize(NewType));
    end else begin
      OldSize := GetTexCoordSize(FTexCoordType);
      NewSize := GetTexCoordSize(NewType);
      if NewSize > OldSize then begin
        FTexCoords.Done;
        FTexCoords.Init(FCount * NewSize);
      end;
    end;
  end;
  FTexCoordType := NewType;
end;

function TVertexBuffer.GetInterleavedSize(InterleavedUsed: Boolean): LongInt;
begin
  if InterleavedUsed then begin
    Exit(FInterleavedSize);
  end else
    Exit(0);
end;

procedure TVertexBuffer.EnableLayer(Layer: PVertexLayer);
begin
  glVertexAttribPointer(Layer^.FId,
                        Layer^.FComponents,
                        Layer^.FType,
                        GL_FALSE,
                        Layer^.FBuffer.FStride,
                        Layer^.FBuffer.FGPUData);
  glEnableVertexAttribArray(Layer^.FId);
end;

procedure TVertexBuffer.DisableLayer(Layer: PVertexLayer);
begin
  glDisableVertexAttribArray(Layer^.FId);
end;

procedure TVertexBuffer.Enable;
var
  I: LongInt;
begin
  if FVertexBufferObject <> 0 then begin
    if FGPUStorageDirty then
      SyncGPUStorage;
    glBindBuffer(GL_ARRAY_BUFFER, FVertexBufferObject);
  end;
  if FVertexType <> 0 then begin
    glEnableClientState(GL_VERTEX_ARRAY);
    case FVertexType of
      1: glVertexPointer(2, GL_INT, FVertices.FStride, FVertices.FGPUData);
      2: glVertexPointer(2, GL_FLOAT, FVertices.FStride, FVertices.FGPUData);
      3: glVertexPointer(3, GL_FLOAT, FVertices.FStride, FVertices.FGPUData);
      4: glVertexPointer(2, GL_SHORT, FVertices.FStride, FVertices.FGPUData);
    end;
  end;
  if FColorType <> 0 then begin
    glEnableClientState(GL_COLOR_ARRAY);
    case FColorType of
      1: glColorPointer(4, GL_UNSIGNED_BYTE, FColors.FStride, FColors.FGPUData);
      2: glColorPointer(4, GL_FLOAT, FColors.FStride, FColors.FGPUData);
    end;
  end;
  if FNormalType <> 0 then begin
    glEnableClientState(GL_NORMAL_ARRAY);
    case FNormalType of
      1: glNormalPointer(GL_FLOAT, FNormals.FStride, FNormals.FGPUData);
    end;
  end;
  if FTexCoordType <> 0 then begin
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    case FTexCoordType of
      1: begin
           glTexCoordPointer(2, GL_FLOAT, FTexCoords.FStride, FTexCoords.FGPUData);
         end;
      2: begin
           glTexCoordPointer(3, GL_FLOAT, FTexCoords.FStride, FTexCoords.FGPUData);
         end;
    end;
  end;
  if FLayersArray <> nil then begin
    for I := 0 to High(TLayersArray) do begin
      if FLayersArray^[I].FType <> -1 then begin
        EnableLayer(@FLayersArray^[I]);
      end;
    end;
  end;
  for I := 0 to High(FLayersMap) do begin
    EnableLayer(@FLayersMap[I]);
  end;
end;

procedure TVertexBuffer.Disable;
var
  I: LongInt;
begin
  if FVertexType <> 0 then
    glDisableClientState(GL_VERTEX_ARRAY);
  if FColorType <> 0 then
    glDisableClientState(GL_COLOR_ARRAY);
  if FNormalType <> 0 then
    glDisableClientState(GL_NORMAL_ARRAY);
  if FTexCoordType <> 0 then
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  if FLayersArray <> nil then begin
    for I := 0 to High(TLayersArray) do begin
      if FLayersArray^[I].FType <> -1 then begin
        DisableLayer(@FLayersArray^[I]);
      end;
    end;
  end;
  for I := 0 to High(FLayersMap) do begin
    DisableLayer(@FLayersMap[I]);
  end;
  if FVertexBufferObject <> 0 then begin
    glBindBuffer(GL_ARRAY_BUFFER, 0);
  end;
end;

//}}}
//{{{ TVertexBuffer get layers
procedure TVertexBuffer.DeleteVertices;
begin
  SetVerticesType(0);
end;

function TVertexBuffer.Vertices2i: PArrayOfVec2i;
begin
  if FVertexType <> 1 then
    SetVerticesType(1);
  Result := FVertices.Data;
end;

function TVertexBuffer.Vertices2f: PArrayOfVec2f;
begin
  if FVertexType <> 2 then
    SetVerticesType(2);
  Result := FVertices.Data;
end;

function TVertexBuffer.Vertices3f: PArrayOfVec3f;
begin
  if FVertexType <> 3 then
    SetVerticesType(3);
  Result := FVertices.Data;
end;

procedure TVertexBuffer.DeleteColors;
begin
  SetColorsType(0);
end;

function TVertexBuffer.Colors4ub: PArrayOfVec4ub;
begin
  if FColorType <> 1 then
    SetColorsType(1);
  Result := FColors.Data;
end;

function TVertexBuffer.Colors4f: PArrayOfVec4f;
begin
  if FColorType <> 2 then
    SetColorsType(2);
  Result := FColors.Data;
end;

procedure TVertexBuffer.DeleteNormals;
begin
  SetNormalsType(0);
end;

function TVertexBuffer.Normals: PArrayOfVec3f;
begin
  if FNormalType <> 1 then
    SetNormalsType(1);
  Result := FNormals.Data;
end;

procedure TVertexBuffer.DeleteTexCoords;
begin
  SetTexCoordsType(0);
end;

function TVertexBuffer.TexCoords2f: PArrayOfVec2f;
begin
  if FTexCoordType <> 1 then
    SetTexCoordsType(1);
  Result := FTexCoords.Data;
end;

function TVertexBuffer.TexCoords3f: PArrayOfVec3f;
begin
  if FTexCoordType <> 2 then
    SetTexCoordsType(2);
  Result := FTexCoords.Data;
end;

procedure TVertexBuffer.SetLayer(LayerId: PtrUInt;
                   Components: PtrUInt;
                   _Type: PtrUInt;
                   ElementSize: LongInt;
                   Data: Pointer);
var
  Layer: PVertexLayer;
begin
  if FindAndCreateLayer(LayerId, Layer) then
    Layer^.FBuffer.Done;
  Layer^.FComponents := Components;
  Layer^.FType := _Type;
  Layer^.FElementSize := ElementSize;
  Layer^.FBuffer.Init(Data);
end;

procedure TVertexBuffer.SetLayer(LayerId: PtrUInt;
                  const P: array of TVec2f);
begin
  SetLayer(LayerId, 2, GL_FLOAT, 2 * SizeOf(Single), @P[0]);
end;

procedure TVertexBuffer.SetLayer(LayerId: PtrUInt;
                  const P: array of TVec3f);
begin
  SetLayer(LayerId, 3, GL_FLOAT, 3 * SizeOf(Single), @P[0]);
end;

procedure TVertexBuffer.SetLayer(LayerId: PtrUInt;
                  const P: array of TVec4f);
begin
  SetLayer(LayerId, 4, GL_FLOAT, 4 * SizeOf(Single), @P[0]);
end;

function TVertexBuffer.GetLayer(LayerId: PtrUInt;
               Components: PtrUInt;
               _Type: PtrUInt;
               ElementSize: LongInt): Pointer;
var
  Layer: PVertexLayer;
begin
  if FindAndCreateLayer(LayerId, Layer) then begin
    if (Layer^.FComponents <> Components) or (Layer^.FType <> _Type) then begin
      Layer^.FBuffer.Done;
      Layer^.FComponents := Components;
      Layer^.FType := _Type;
      Layer^.FElementSize := ElementSize;
      Layer^.FBuffer.Init(FCount * Layer^.FElementSize);
    end;
  end else begin
    Layer^.FComponents := Components;
    Layer^.FType := _Type;
    Layer^.FElementSize := ElementSize;
    Layer^.FBuffer.Init(FCount * Layer^.FElementSize);
  end;
  Result := Layer^.FBuffer.Data;
end;

function TVertexBuffer.Layer2f(LayerId: PtrUInt): PArrayOfVec2f;
begin
  Result := GetLayer(LayerId, 2, GL_FLOAT, 2 * SizeOf(Single));
end;

function TVertexBuffer.Layer3f(LayerId: PtrUInt): PArrayOfVec3f;
begin
  Result := GetLayer(LayerId, 3, GL_FLOAT, 3 * SizeOf(Single));
end;

function TVertexBuffer.Layer4f(LayerId: PtrUInt): PArrayOfVec4f;
begin
  Result := GetLayer(LayerId, 4, GL_FLOAT, 4 * SizeOf(Single));
end;

procedure TVertexBuffer.DeleteLayer(LayerId: PtrUInt);
var
  I: LongInt;
  Layer: PVertexLayer;
begin
  if LayerId < Length(TLayersArray) then begin
    if FLayersArray = nil then
      Exit;
    Layer := @FLayersArray^[LayerId];
    if Layer^.FType <> -1 then begin
      Layer^.FBuffer.Done;
      Layer^.FType := -1;
      Exit;
    end;
  end else begin
    for I := 0 to High(FLayersMap) do begin
      if FLayersMap[I].FId = LayerId then begin
        Layer := @FLayersMap[I];
        Layer^.FBuffer.Done;
        Layer^ := FLayersMap[High(FLayersMap)];
        SetLength(FLayersMap, Length(FLayersMap) - 1);
        Exit;
      end;
    end;
  end;
end;
//}}}
//{{{ TVertexBuffer rendering
procedure TVertexBuffer.DrawPoints;
begin
  Enable;
  glDrawArrays(GL_POINTS, 0, FCount);
  Disable;
end;

function TVertexBuffer.GetAllArraysSize: SizeUInt;
var
  I: LongInt;
begin
  // TODO: alignment
  Result := 0;
  Inc(Result, FCount * GetVertexSize(FVertexType));
  Inc(Result, FCount * GetColorSize(FColorType));
  Inc(Result, FCount * GetNormalSize(FNormalType));
  Inc(Result, FCount * GetTexCoordSize(FTexCoordType));
  if FLayersArray <> nil then begin
    for I := 0 to High(TLayersArray) do begin
      if FLayersArray^[I].FType <> -1 then begin
        Inc(Result, FLayersArray^[I].FElementSize * FCount);
      end;
    end;
  end;
  for I := 0 to High(FLayersMap) do begin
    Inc(Result, FLayersMap[I].FElementSize * FCount);
  end;
end;

{$IFNDEF ANDROID}
procedure TVertexBuffer.DrawQuads;
begin
  Enable;
  glDrawArrays(GL_QUADS, 0, FCount);
  Disable;
end;
{$ENDIF}

procedure TVertexBuffer.DrawTriangles;
begin
  Enable;
  glDrawArrays(GL_TRIANGLES, 0, FCount);
  Disable;
end;

procedure TVertexBuffer.DrawLineLoop;
begin
  Enable;
  glDrawArrays(GL_LINE_LOOP, 0, FCount);
  Disable;
end;

procedure TVertexBuffer.DrawLines;
begin
  Enable;
  glDrawArrays(GL_LINES, 0, FCount);
  Disable;
end;

procedure TVertexBuffer.DrawPolygon;
var
  IB: TIndexBuffer;
  I: LongInt;
begin
  Enable;
  IB.Init;
  IB.SetCount(3 * (FCount - 2));
  for I := 0 to FCount - 3 do begin
    IB.Indices^[3 * I + 0] := 0;
    IB.Indices^[3 * I + 1] := I + 1;
    IB.Indices^[3 * I + 2] := I + 2;
  end;
  DrawTriangles(IB);
  IB.Done;
  Disable;
end;

procedure TVertexBuffer.DrawPoints(const Indices: TIndexBuffer);
begin
  Enable;
  glDrawElements(GL_POINTS, Indices.Count, GL_UNSIGNED_SHORT, Pointer(Indices.Indices));
  Disable;
end;

{$IFNDEF ANDROID}
procedure TVertexBuffer.DrawQuads(const Indices: TIndexBuffer);
begin
  Enable;
  glDrawElements(GL_QUADS, Indices.Count, GL_UNSIGNED_SHORT, Pointer(Indices.Indices));
  Disable;
end;
{$ENDIF}

procedure TVertexBuffer.DrawTriangles(const Indices: TIndexBuffer);
begin
  Enable;
  glDrawElements(GL_TRIANGLES, Indices.Count, GL_UNSIGNED_SHORT, Pointer(Indices.Indices));
  Disable;
end;

procedure TVertexBuffer.DrawTriangles(const Indices: TIndexBuffer; First, Count: PtrInt);
begin
  Enable;
  glDrawElements(GL_TRIANGLES, Count, GL_UNSIGNED_SHORT, Pointer(Indices.Indices) + First * SizeOf(TIndexBuffer.TIndex));
  Disable;
end;

procedure TVertexBuffer.BatchTriangles(First, Count: PtrInt); inline;
begin
  glDrawArrays(GL_TRIANGLES, First, Count);
end;
//}}}
