/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ClientWebGLContext.h"
#include "GLContext.h"
#include "WebGL2Context.h"
#include "WebGLBuffer.h"
#include "WebGLTransformFeedback.h"

namespace mozilla {

// -------------------------------------------------------------------------
// Buffer objects

void WebGL2Context::CopyBufferSubData(GLenum readTarget, GLenum writeTarget,
                                      uint64_t readOffset, uint64_t writeOffset,
                                      uint64_t size) const {
  const FuncScope funcScope(*this, "copyBufferSubData");
  if (IsContextLost()) return;

  const auto& readBuffer = ValidateBufferSelection(readTarget);
  if (!readBuffer) return;

  const auto& writeBuffer = ValidateBufferSelection(writeTarget);
  if (!writeBuffer) return;

  if (!CheckedInt<GLintptr>(readOffset).isValid() ||
      !CheckedInt<GLintptr>(writeOffset).isValid() ||
      !CheckedInt<GLsizeiptr>(size).isValid())
    return ErrorOutOfMemory("offset or size too large for platform.");

  const auto fnValidateOffsetSize = [&](const char* info, WebGLintptr offset,
                                        const WebGLBuffer* buffer) {
    const auto neededBytes = CheckedInt<uint64_t>(offset) + size;
    if (!neededBytes.isValid() || neededBytes.value() > buffer->ByteLength()) {
      ErrorInvalidValue("Invalid %s range.", info);
      return false;
    }
    return true;
  };

  if (!fnValidateOffsetSize("read", readOffset, readBuffer) ||
      !fnValidateOffsetSize("write", writeOffset, writeBuffer)) {
    return;
  }

  if (readBuffer == writeBuffer) {
    const bool separate =
        (readOffset + size <= writeOffset || writeOffset + size <= readOffset);
    if (!separate) {
      ErrorInvalidValue(
          "Ranges [readOffset, readOffset + size) and"
          " [writeOffset, writeOffset + size) overlap.");
      return;
    }
  }

  const auto& readType = readBuffer->Content();
  const auto& writeType = writeBuffer->Content();
  MOZ_ASSERT(readType != WebGLBuffer::Kind::Undefined);
  MOZ_ASSERT(writeType != WebGLBuffer::Kind::Undefined);
  if (writeType != readType) {
    ErrorInvalidOperation(
        "Can't copy %s data to %s data.",
        (readType == WebGLBuffer::Kind::OtherData) ? "other" : "element",
        (writeType == WebGLBuffer::Kind::OtherData) ? "other" : "element");
    return;
  }

  const ScopedLazyBind readBind(gl, readTarget, readBuffer);
  const ScopedLazyBind writeBind(gl, writeTarget, writeBuffer);
  gl->fCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset,
                         size);

  // Update the destination index cache if needed.
  if (writeBuffer->mIndexCache) {
    MOZ_ASSERT(readBuffer->mIndexCache);
    if (readBuffer->mIndexCache) {
      // The read and write ranges have been validated above by
      // fnValidateOffsetSize. If allocated, the size of mIndexCache is always
      // mByteLength.
      const auto* src =
          static_cast<const uint8_t*>(readBuffer->mIndexCache.get()) +
          readOffset;
      auto* dst =
          static_cast<uint8_t*>(writeBuffer->mIndexCache.get()) + writeOffset;
      memcpy(dst, src, size);
    }
    writeBuffer->InvalidateCacheRange(writeOffset, size);
  }

  writeBuffer->ResetLastUpdateFenceId();
}

bool WebGL2Context::GetBufferSubData(GLenum target, uint64_t srcByteOffset,
                                     const Range<uint8_t>& dest,
                                     uint64_t numRows, uint64_t rowDataWidth,
                                     uint64_t srcStride,
                                     uint64_t destStride) const {
  const FuncScope funcScope(*this, "getBufferSubData");
  if (IsContextLost()) return false;

  const auto& buffer = ValidateBufferSelection(target);
  if (!buffer) return false;

  uint64_t srcLen =
      numRows > 0 ? srcStride * (numRows - 1) + rowDataWidth : dest.length();
  uint64_t destLen =
      numRows > 0 ? destStride * (numRows - 1) + rowDataWidth : dest.length();
  if (!buffer->ValidateRange(srcByteOffset, srcLen)) return false;
  if (rowDataWidth > srcStride || rowDataWidth > destStride ||
      destLen > dest.length()) {
    ErrorInvalidValue("Destination is outside buffer.");
    return false;
  }

  ////

  if (!CheckedInt<GLintptr>(srcByteOffset).isValid() ||
      !CheckedInt<GLsizeiptr>(srcLen).isValid()) {
    ErrorOutOfMemory("Offset or size too large for platform.");
    return false;
  }
  const GLsizeiptr glByteLen(srcLen);

  ////

  switch (buffer->mUsage) {
    case LOCAL_GL_STATIC_READ:
    case LOCAL_GL_STREAM_READ:
    case LOCAL_GL_DYNAMIC_READ:
      if (mCompletedFenceId < buffer->mLastUpdateFenceId) {
        GenerateWarning(
            "Reading from a buffer without checking for previous"
            " command completion likely causes pipeline stalls."
            " Please use FenceSync.");
      }
      break;
    default:
      GenerateWarning(
          "Reading from a buffer with usage other than *_READ"
          " causes pipeline stalls. Copy through a STREAM_READ buffer.");
      break;
  }

  ////

  const ScopedLazyBind readBind(gl, target, buffer);

  if (srcLen) {
    const bool isTF = (target == LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER);
    GLenum mapTarget = target;
    if (isTF) {
      gl->fBindTransformFeedback(LOCAL_GL_TRANSFORM_FEEDBACK, mEmptyTFO);
      gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, buffer->mGLName);
      mapTarget = LOCAL_GL_ARRAY_BUFFER;
    }

    const void* mappedBytes = gl->fMapBufferRange(
        mapTarget, srcByteOffset, glByteLen, LOCAL_GL_MAP_READ_BIT);
    if (numRows > 0 && (destStride != srcStride || rowDataWidth != srcStride)) {
      const uint8_t* srcRow = (const uint8_t*)mappedBytes;
      uint8_t* destRow = dest.begin().get();
      while (numRows > 0) {
        memcpy(destRow, srcRow, rowDataWidth);
        srcRow += srcStride;
        destRow += destStride;
        --numRows;
      }
    } else {
      memcpy(dest.begin().get(), mappedBytes, srcLen);
    }
    gl->fUnmapBuffer(mapTarget);

    if (isTF) {
      const GLuint vbo = (mBoundArrayBuffer ? mBoundArrayBuffer->mGLName : 0);
      gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, vbo);
      const GLuint tfo =
          (mBoundTransformFeedback ? mBoundTransformFeedback->mGLName : 0);
      gl->fBindTransformFeedback(LOCAL_GL_TRANSFORM_FEEDBACK, tfo);
    }
  }
  return true;
}

}  // namespace mozilla
