/*
 * 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 "WaylandBuffer.h"
#include "WaylandSurface.h"
#include "WaylandSurfaceLock.h"

#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>

#include "gfx2DGlue.h"
#include "gfxPlatform.h"
#include "mozilla/WidgetUtilsGtk.h"
#include "mozilla/gfx/Tools.h"
#include "mozilla/gfx/Logging.h"
#include "mozilla/ipc/SharedMemoryHandle.h"
#include "nsGtkUtils.h"
#include "nsPrintfCString.h"
#include "prenv.h"  // For PR_GetEnv

#ifdef MOZ_LOGGING
#  include "mozilla/Logging.h"
#  include "mozilla/ScopeExit.h"
#  include "Units.h"
extern mozilla::LazyLogModule gWidgetWaylandLog;
#  define LOGWAYLAND(...) \
    MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
#else
#  define LOGWAYLAND(...)
#endif /* MOZ_LOGGING */

using namespace mozilla::gl;

namespace mozilla::widget {

#define BUFFER_BPP 4

#ifdef MOZ_LOGGING
MOZ_RUNINIT int WaylandBuffer::mDumpSerial =
    PR_GetEnv("MOZ_WAYLAND_DUMP_WL_BUFFERS") ? 1 : 0;
MOZ_RUNINIT char* WaylandBuffer::mDumpDir = PR_GetEnv("MOZ_WAYLAND_DUMP_DIR");
#endif

/* static */
RefPtr<WaylandShmPool> WaylandShmPool::Create(nsWaylandDisplay* aWaylandDisplay,
                                              int aSize) {
  if (!aWaylandDisplay->GetShm()) {
    NS_WARNING("WaylandShmPool: Missing Wayland shm interface!");
    return nullptr;
  }

  RefPtr<WaylandShmPool> shmPool = new WaylandShmPool();

  auto handle = ipc::shared_memory::Create(aSize);
  if (!handle) {
    NS_WARNING("WaylandShmPool: Unable to allocate shared memory!");
    return nullptr;
  }

  shmPool->mShmHandle = handle.Clone();
  shmPool->mShmPool =
      wl_shm_create_pool(aWaylandDisplay->GetShm(),
                         handle.Clone().TakePlatformHandle().get(), aSize);
  if (!shmPool->mShmPool) {
    NS_WARNING("WaylandShmPool: Unable to allocate shared memory pool!");
    return nullptr;
  }

  return shmPool;
}

void* WaylandShmPool::GetImageData() {
  if (!mShm) {
    mShm = mShmHandle.Map();
    if (!mShm) {
      NS_WARNING("WaylandShmPool: Failed to map Shm!");
      return nullptr;
    }
  }
  return mShm.Address();
}

WaylandShmPool::~WaylandShmPool() {
  MozClearPointer(mShmPool, wl_shm_pool_destroy);
}

WaylandBuffer::WaylandBuffer(const LayoutDeviceIntSize& aSize) : mSize(aSize) {}

bool WaylandBuffer::IsAttached(const WaylandSurfaceLock& aSurfaceLock) const {
  for (const auto& transaction : mBufferTransactions) {
    if (transaction->IsAttached()) {
      return true;
    }
  }
  return false;
}

BufferTransaction* WaylandBuffer::GetTransaction(
    const WaylandSurfaceLock& aSurfaceLock) {
  for (const auto& transaction : mBufferTransactions) {
    if (transaction->CanRecycle(aSurfaceLock.GetWaylandSurface())) {
      LOGWAYLAND("WaylandBuffer::GetTransaction() [%p] reuse transaction [%d]",
                 (void*)this, (int)mBufferTransactions.Length());
      return transaction;
    }
  }

  wl_buffer* buffer = mExternalWlBuffer;
  if (!buffer) {
    buffer = CreateWlBuffer();
  }
  if (!buffer) {
    gfxCriticalError()
        << "WaylandBuffer::GetTransaction() failed to create wl_buffer!";
    return nullptr;
  }

  LOGWAYLAND(
      "WaylandBuffer::GetTransaction() create new [%p] wl_buffer [%p] "
      "transactions [%d] external buffer [%d] WaylandSurface [%p]",
      (void*)this, buffer, (int)mBufferTransactions.Length(),
      !!mExternalWlBuffer, aSurfaceLock.GetWaylandSurface());

  auto* transaction = new BufferTransaction(this, buffer, !!mExternalWlBuffer);
  mBufferTransactions.AppendElement(transaction);
  return transaction;
}

void WaylandBuffer::RemoveTransaction(const WaylandSurfaceLock& aSurfaceLock,
                                      RefPtr<BufferTransaction> aTransaction) {
  LOGWAYLAND("WaylandBuffer::RemoveTransaction() [%p]", (void*)aTransaction);
  [[maybe_unused]] bool removed =
      mBufferTransactions.RemoveElement(aTransaction);
  MOZ_DIAGNOSTIC_ASSERT(removed);
  MOZ_DIAGNOSTIC_ASSERT(!mBufferTransactions.Contains(aTransaction));
}

void WaylandBuffer::SetExternalWLBuffer(wl_buffer* aWLBuffer) {
  LOGWAYLAND("WaylandBuffer::SetExternalWLBuffer() [%p] wl_buffer %p",
             (void*)this, aWLBuffer);
  MOZ_DIAGNOSTIC_ASSERT(!mExternalWlBuffer);
  mExternalWlBuffer = aWLBuffer;
}

/* static */
RefPtr<WaylandBufferSHM> WaylandBufferSHM::Create(
    const LayoutDeviceIntSize& aSize) {
  RefPtr<WaylandBufferSHM> buffer = new WaylandBufferSHM(aSize);
  nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();

  LOGWAYLAND("WaylandBufferSHM::Create() [%p] [%d x %d]", (void*)buffer,
             aSize.width, aSize.height);

  int size = aSize.width * aSize.height * BUFFER_BPP;
  buffer->mShmPool = WaylandShmPool::Create(waylandDisplay, size);
  if (!buffer->mShmPool) {
    LOGWAYLAND("  failed to create shmPool");
    return nullptr;
  }

  LOGWAYLAND("  created [%p] WaylandDisplay [%p]\n", buffer.get(),
             waylandDisplay);

  return buffer;
}

wl_buffer* WaylandBufferSHM::CreateWlBuffer() {
  MOZ_DIAGNOSTIC_ASSERT(!mExternalWlBuffer);

  auto* buffer = wl_shm_pool_create_buffer(
      mShmPool->GetShmPool(), 0, mSize.width, mSize.height,
      mSize.width * BUFFER_BPP, WL_SHM_FORMAT_ARGB8888);

  LOGWAYLAND("WaylandBufferSHM::CreateWlBuffer() [%p] wl_buffer [%p]",
             (void*)this, buffer);

  return buffer;
}

WaylandBufferSHM::WaylandBufferSHM(const LayoutDeviceIntSize& aSize)
    : WaylandBuffer(aSize) {
  LOGWAYLAND("WaylandBufferSHM::WaylandBufferSHM() [%p]\n", (void*)this);
}

WaylandBufferSHM::~WaylandBufferSHM() {
  LOGWAYLAND("WaylandBufferSHM::~WaylandBufferSHM() [%p]\n", (void*)this);
  MOZ_RELEASE_ASSERT(mBufferTransactions.IsEmpty());
}

already_AddRefed<gfx::DrawTarget> WaylandBufferSHM::Lock() {
  LOGWAYLAND("WaylandBufferSHM::lock() [%p]\n", (void*)this);
  return gfxPlatform::CreateDrawTargetForData(
      static_cast<unsigned char*>(mShmPool->GetImageData()),
      mSize.ToUnknownSize(), BUFFER_BPP * mSize.width, GetSurfaceFormat());
}

void WaylandBufferSHM::Clear() {
  LOGWAYLAND("WaylandBufferSHM::Clear() [%p]\n", (void*)this);
  memset(mShmPool->GetImageData(), 0xff,
         mSize.height * mSize.width * BUFFER_BPP);
}

#ifdef MOZ_LOGGING
void WaylandBufferSHM::DumpToFile(const char* aHint) {
  if (!mDumpSerial) {
    NS_WARNING("mDumpSerial is not set!");
    return;
  }

  cairo_surface_t* surface = nullptr;
  auto unmap = MakeScopeExit([&] {
    if (surface) {
      cairo_surface_destroy(surface);
    }
  });
  surface = cairo_image_surface_create_for_data(
      (unsigned char*)mShmPool->GetImageData(), CAIRO_FORMAT_ARGB32,
      mSize.width, mSize.height, BUFFER_BPP * mSize.width);
  if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
    nsCString filename;
    if (mDumpDir) {
      filename.Append(mDumpDir);
      filename.Append('/');
    }
    filename.Append(nsPrintfCString("firefox-wl-sw-buffer-%.5d-%s.png",
                                    mDumpSerial++, aHint));
    cairo_surface_write_to_png(surface, filename.get());
    LOGWAYLAND("Dumped wl_buffer to %s\n", filename.get());
  }
}
#endif

/* static */
already_AddRefed<WaylandBufferDMABUF> WaylandBufferDMABUF::CreateRGBA(
    const LayoutDeviceIntSize& aSize, GLContext* aGL,
    RefPtr<DRMFormat> aFormat) {
  RefPtr<WaylandBufferDMABUF> buffer = new WaylandBufferDMABUF(aSize);

  buffer->mDMABufSurface = DMABufSurfaceRGBA::CreateDMABufSurface(
      aGL, aSize.width, aSize.height, DMABUF_SCANOUT | DMABUF_USE_MODIFIERS,
      aFormat);
  if (!buffer->mDMABufSurface || !buffer->mDMABufSurface->CreateTexture(aGL)) {
    LOGWAYLAND("  failed to create texture");
    return nullptr;
  }

  LOGWAYLAND("WaylandBufferDMABUF::CreateRGBA() [%p] UID %d [%d x %d]",
             (void*)buffer, buffer->mDMABufSurface->GetUID(), aSize.width,
             aSize.height);
  return buffer.forget();
}

/* static */
already_AddRefed<WaylandBufferDMABUF> WaylandBufferDMABUF::CreateExternal(
    RefPtr<DMABufSurface> aSurface) {
  const auto size =
      LayoutDeviceIntSize(aSurface->GetWidth(), aSurface->GetHeight());
  RefPtr<WaylandBufferDMABUF> buffer = new WaylandBufferDMABUF(size);

  LOGWAYLAND("WaylandBufferDMABUF::CreateExternal() [%p] UID %d [%d x %d]",
             (void*)buffer, aSurface->GetUID(), size.width, size.height);

  buffer->mDMABufSurface = aSurface;
  return buffer.forget();
}

wl_buffer* WaylandBufferDMABUF::CreateWlBuffer() {
  MOZ_DIAGNOSTIC_ASSERT(mDMABufSurface);
  MOZ_DIAGNOSTIC_ASSERT(!mExternalWlBuffer);

  auto* buffer = mDMABufSurface->CreateWlBuffer();

  LOGWAYLAND("WaylandBufferDMABUF::CreateWlBuffer() [%p] UID %d wl_buffer [%p]",
             (void*)this, mDMABufSurface->GetUID(), buffer);

  return buffer;
}

WaylandBufferDMABUF::WaylandBufferDMABUF(const LayoutDeviceIntSize& aSize)
    : WaylandBuffer(aSize) {
  LOGWAYLAND("WaylandBufferDMABUF::WaylandBufferDMABUF [%p]\n", (void*)this);
}

WaylandBufferDMABUF::~WaylandBufferDMABUF() {
  LOGWAYLAND("WaylandBufferDMABUF::~WaylandBufferDMABUF [%p] UID %d\n",
             (void*)this, mDMABufSurface ? mDMABufSurface->GetUID() : -1);
  MOZ_RELEASE_ASSERT(mBufferTransactions.IsEmpty());
}

#ifdef MOZ_LOGGING
void WaylandBufferDMABUF::DumpToFile(const char* aHint) {
  if (!mDumpSerial) {
    NS_WARNING("mDumpSerial is not set!");
    return;
  }
  nsCString filename;
  if (mDumpDir) {
    filename.Append(mDumpDir);
    filename.Append('/');
  }
  filename.AppendPrintf("firefox-wl-buffer-dmabuf-%.5d-%s.png", mDumpSerial++,
                        aHint);
  mDMABufSurface->DumpToFile(filename.get());
  LOGWAYLAND("Dumped wl_buffer to %s\n", filename.get());
}
#endif

WaylandBufferDMABUFHolder::WaylandBufferDMABUFHolder(DMABufSurface* aSurface,
                                                     wl_buffer* aWLBuffer)
    : mWLBuffer(aWLBuffer) {
  mUID = aSurface->GetUID();
  mPID = aSurface->GetPID();
  LOGWAYLAND(
      "WaylandBufferDMABUFHolder::WaylandBufferDMABUFHolder wl_buffer [%p] UID "
      "%d PID %d",
      mWLBuffer, mUID, mPID);
}

WaylandBufferDMABUFHolder::~WaylandBufferDMABUFHolder() {
  LOGWAYLAND(
      "WaylandBufferDMABUFHolder::~WaylandBufferDMABUFHolder wl_buffer [%p] "
      "UID %d PID %d",
      mWLBuffer, mUID, mPID);
  MozClearPointer(mWLBuffer, wl_buffer_destroy);
}

bool WaylandBufferDMABUFHolder::Matches(DMABufSurface* aSurface) const {
  return mUID == aSurface->GetUID() && mPID == aSurface->GetPID();
}

wl_buffer* BufferTransaction::BufferBorrowLocked(
    const WaylandSurfaceLock& aSurfaceLock) {
  LOGWAYLAND(
      "BufferTransaction::BufferBorrow() [%p] widget [%p] WaylandSurface [%p] "
      "WaylandBuffer [%p]",
      this, aSurfaceLock.GetWaylandSurface()->GetLoggingWidget(),
      aSurfaceLock.GetWaylandSurface(), mBuffer.get());

  MOZ_DIAGNOSTIC_ASSERT(
      !mSurface || mSurface == aSurfaceLock.GetWaylandSurface(),
      "Can't transfer transaction between WaylandSurfaces!");
  MOZ_DIAGNOSTIC_ASSERT(mBufferState == BufferState::Detached);

  mSurface = aSurfaceLock.GetWaylandSurface();

  // We don't take reference to this. Some compositors doesn't send
  // buffer release callback and we may leak BufferTransaction then.
  // Rather we destroy wl_buffer at end which makes sure no release callback
  // comes after BufferTransaction release.
  if (wl_proxy_get_listener((wl_proxy*)mWLBuffer)) {
    wl_proxy_set_user_data((wl_proxy*)mWLBuffer, this);
  } else {
    static const struct wl_buffer_listener listener{
        [](void* aData, wl_buffer* aBuffer) {
          auto transaction = static_cast<BufferTransaction*>(aData);
          if (transaction) {
            // BufferDetachCallback can delete transaction
            RefPtr grip{transaction};
            transaction->BufferDetachCallback();
          }
        }};
    if (wl_buffer_add_listener(mWLBuffer, &listener, this) < 0) {
      gfxCriticalError() << "wl_buffer_add_listener() failed";
    }
  }

  mBufferState = BufferState::WaitingForDetach;
  return mWLBuffer;
}

void BufferTransaction::BufferDetachCallback() {
  WaylandSurfaceLock lock(mSurface);
  LOGWAYLAND(
      "BufferTransaction::BufferDetach() [%p] WaylandBuffer [%p] attached to "
      "WaylandSurface %d",
      this, (void*)mBuffer, mSurface->IsBufferAttached(mBuffer));

  if (mBufferState != BufferState::WaitingForDelete) {
    mBufferState = BufferState::Detached;

    // Delete this transaction if WaylandSurface uses different WaylandBuffer
    // already, we don't need to keep it for recycling.
    if (!mSurface->IsBufferAttached(mBuffer)) {
      DeleteTransactionLocked(lock);
    }
  }
}

void BufferTransaction::BufferDeleteCallback() {
  LOGWAYLAND("BufferTransaction::DeleteCallback() [%p] WaylandBuffer [%p] ",
             this, (void*)mBuffer);
  WaylandSurfaceLock lock(mSurface);
  mBufferState = BufferState::Deleted;
  DeleteLocked(lock);
}

void BufferTransaction::WlBufferDeleteLocked(
    const WaylandSurfaceLock& aSurfaceLock) {
  LOGWAYLAND(
      "BufferTransaction::WlBufferDeleteLocked() [%p] WaylandBuffer [%p] ",
      this, (void*)mBuffer);
  MOZ_DIAGNOSTIC_ASSERT(mWLBuffer);
  if (mIsExternalBuffer) {
    wl_proxy_set_user_data((wl_proxy*)mWLBuffer, nullptr);
    mWLBuffer = nullptr;
  } else {
    MozClearPointer(mWLBuffer, wl_buffer_destroy);
  }
}

void BufferTransaction::DeleteTransactionLocked(
    const WaylandSurfaceLock& aSurfaceLock) {
  // It's possible that the transaction is already deleted. It happens
  // if one WaylandBuffer is attached to WaylandSurface,
  // then detached/deleted from WaylandSurface::UnmapLocked() where all buffers
  // are removed and then attached to another WaylandSurface.
  if (mBufferState == BufferState::WaitingForDelete ||
      mBufferState == BufferState::Deleted) {
    return;
  }

  LOGWAYLAND(
      "BufferTransaction::BufferDelete() [%p] WaylandBuffer [%p] wl_buffer "
      "[%p] "
      "external %d state %d",
      this, (void*)mBuffer, mWLBuffer, mIsExternalBuffer, (int)mBufferState);

  WlBufferDeleteLocked(aSurfaceLock);

  // wl_buffer is detached so we can't get any release event so
  // delete the transaction now.
  if (mBufferState == BufferState::Detached) {
    mBufferState = BufferState::Deleted;
    DeleteLocked(aSurfaceLock);
    return;
  }

  mBufferState = BufferState::WaitingForDelete;

  // There are various Wayland queues processed for every thread.
  // It's possible that wl_buffer release event is pending in any
  // queue while we already asked for wl_buffer delete.
  // We need to finish wl_buffer removal when all events from this
  // point are processed so we use sync callback.
  //
  // When wl_display_sync comes back to us (from main thread)
  // we know all events are processed and there isn't any
  // wl_buffer operation pending so we can safely release WaylandSurface
  // and WaylandBuffer objects.
  // We need to wait with WaylandMonitor
  AddRef();
  static const struct wl_callback_listener listener{
      [](void* aData, struct wl_callback* callback, uint32_t time) {
        RefPtr t = dont_AddRef(static_cast<BufferTransaction*>(aData));
        t->BufferDeleteCallback();
      }};
  wl_callback_add_listener(wl_display_sync(WaylandDisplayGetWLDisplay()),
                           &listener, this);
}

void BufferTransaction::DeleteLocked(const WaylandSurfaceLock& aSurfaceLock) {
  LOGWAYLAND("BufferTransaction::DeleteLocked() [%p] WaylandBuffer [%p]", this,
             (void*)mBuffer);
  MOZ_DIAGNOSTIC_ASSERT(mBufferState == BufferState::Deleted);
  MOZ_DIAGNOSTIC_ASSERT(mSurface);

  // Unlink from Surface
  mSurface->RemoveTransactionLocked(aSurfaceLock, this);
  mSurface = nullptr;

  // This can destroy us
  RefPtr grip{this};
  mBuffer->RemoveTransaction(aSurfaceLock, this);
  mBuffer = nullptr;
}

BufferTransaction::BufferTransaction(WaylandBuffer* aBuffer,
                                     wl_buffer* aWLBuffer,
                                     bool aIsExternalBuffer)
    : mBuffer(aBuffer),
      mWLBuffer(aWLBuffer),
      mIsExternalBuffer(aIsExternalBuffer) {
  MOZ_COUNT_CTOR(BufferTransaction);
  LOGWAYLAND(
      "BufferTransaction::BufferTransaction() [%p] WaylandBuffer [%p] "
      "wl_buffer [%p] external [%d]",
      this, aBuffer, mWLBuffer, mIsExternalBuffer);
}

BufferTransaction::~BufferTransaction() {
  MOZ_COUNT_DTOR(BufferTransaction);
  LOGWAYLAND("BufferTransaction::~BufferTransaction() [%p] ", this);
  MOZ_DIAGNOSTIC_ASSERT(mBufferState == BufferState::Deleted);
  MOZ_DIAGNOSTIC_ASSERT(!mWLBuffer);
}

}  // namespace mozilla::widget
