/* 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 <xf86drm.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <gbm.h>
#include <mutex>

#include "DMABufDevice.h"
#include "DMABufFormats.h"
#include "WidgetUtilsGtk.h"
#ifdef MOZ_WAYLAND
#  include "nsWaylandDisplay.h"
#  include "mozilla/widget/mozwayland.h"
#  include "mozilla/widget/linux-dmabuf-unstable-v1-client-protocol.h"
#endif
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/gfx/Logging.h"  // for gfxCriticalNote

// drm_fourcc.h defines DRM_FORMAT_MOD_INVALID without a guard; undef the
// fallback from DMABufFormats.h first so the unified build doesn't see a
// redefinition (same pattern as DMABufSurface.cpp).
#ifdef DRM_FORMAT_MOD_INVALID
#  undef DRM_FORMAT_MOD_INVALID
#endif
#include <libdrm/drm_fourcc.h>
#include "GfxInfo.h"
#include "mozilla/Components.h"

using namespace mozilla::gfx;

#ifndef GBM_FORMAT_P010
#  define GBM_FORMAT_P010 \
    __gbm_fourcc_code('P', '0', '1', '0') /* 2x2 subsampled Cr:Cb plane */
#endif

namespace mozilla::widget {

// Table of all supported DRM formats, every format is stored as
// FOURCC format + modifier pair and
// one FOURCC format can be stored with more modifiers.
// The format table data are provided via. mapped memory
// so we don't copy it, just map fd and read.
class DMABufFormatTable final {
 public:
  bool IsSet() { return mData && mSize && mData != MAP_FAILED; }

  void Set(int32_t aFd, uint32_t aSize) {
    MOZ_DIAGNOSTIC_ASSERT(!mData && !mSize);
    mSize = aSize;
    mData = (DRMFormatTableEntry*)mmap(nullptr, aSize, PROT_READ, MAP_PRIVATE,
                                       aFd, 0);
    close(aFd);
  }

  bool GetFormat(uint16_t aIndex, uint32_t* aFormat, uint64_t* aModifier) {
    if (aIndex >= mSize / sizeof(DRMFormatTableEntry)) {
      gfxCriticalNote << "Wrong DRM DMABuf format index!";
      return false;
    }
    *aFormat = mData[aIndex].mFormat;
    *aModifier = mData[aIndex].mModifier;
    return true;
  }

  ~DMABufFormatTable() {
    if (mData && mData != MAP_FAILED) {
      munmap(mData, mSize);
    }
  }

 private:
  unsigned int mSize = 0;
  struct DRMFormatTableEntry {
    uint32_t mFormat;
    uint32_t padding; /* unused */
    uint64_t mModifier;
  }* mData = nullptr;
};

class DMABufFeedbackTranche final {
 public:
#ifdef MOZ_WAYLAND
  void SetFormats(DMABufFormatTable* aFormatTable, wl_array* aIndices) {
    // Formats are reported as array with appropriate modifiers.
    // Modifiers are sorted from the most preffered.
    // There's a sample output of weston-simple-dmabuf-feedback utility
    // which prints the format table:
    //
    // format ABGR16161616F, modifier
    // AMD_GFX10_RBPLUS,64KB_R_X,PIPE_XOR_BITS=3... format ABGR16161616F,
    // modifier AMD_GFX10,64KB_S_X,PIPE_XOR_BITS=3 format ABGR16161616F,
    // modifier AMD_GFX9,64KB_D format ABGR16161616F, modifier AMD_GFX9,64KB_S
    // format ABGR16161616F, modifier LINEAR

    RefPtr<DRMFormat> currentDrmFormat;

    // We need to use such ugly constructions because wl_array_for_each
    // is not C++ compliant
    // (https://gitlab.freedesktop.org/wayland/wayland/-/issues/34)
    uint16_t* index = (uint16_t*)aIndices->data;
    uint16_t* lastIndex =
        (uint16_t*)((const char*)aIndices->data + aIndices->size);

    for (; index < lastIndex; index++) {
      uint32_t format;
      uint64_t modifier;
      if (!aFormatTable->GetFormat(*index, &format, &modifier)) {
        return;
      }
      LOGDMABUF(("DMABufFeedbackTranche [%p] format 0x%x modifier %" PRIx64,
                 this, format, modifier));
      if (!currentDrmFormat || !currentDrmFormat->Matches(format)) {
        currentDrmFormat = new DRMFormat(format, modifier);
        mFormats.AppendElement(currentDrmFormat);
        continue;
      }
      currentDrmFormat->AddModifier(modifier);
    }
  }
#endif
  void SetScanout(bool aIsScanout) { mIsScanout = aIsScanout; }
  bool IsScanout() { return mIsScanout; }

  void AddFormat(uint32_t aFormat, uint64_t aModifier) {
    DRMFormat* format = GetFormat(aFormat);
    if (format) {
      format->AddModifier(aModifier);
      return;
    }
    mFormats.AppendElement(new DRMFormat(aFormat, aModifier));
  }

  DRMFormat* GetFormat(uint32_t aFormat) {
    for (const auto& format : mFormats) {
      if (format->Matches(aFormat)) {
        return format.get();
      }
    }
    return nullptr;
  }

 private:
  bool mIsScanout = false;
  nsTArray<RefPtr<DRMFormat>> mFormats;
};

class DMABufFeedback final {
 public:
  DMABufFormatTable* FormatTable() { return &mFormatTable; }
  DMABufFeedbackTranche* PendingTranche() {
    if (!mPendingTranche) {
      mPendingTranche = MakeUnique<DMABufFeedbackTranche>();
    }
    return mPendingTranche.get();
  }
  void PendingTrancheDone() {
    // It's possible that Wayland compositor doesn't send us any format,
    // so !mPendingTranche
    if (mPendingTranche) {
      mTranches.AppendElement(std::move(mPendingTranche));
    }
  }
  DRMFormat* GetFormat(uint32_t aFormat, bool aRequestScanoutFormat) {
    for (const auto& tranche : mTranches) {
      if (aRequestScanoutFormat && !tranche->IsScanout()) {
        continue;
      }
      if (DRMFormat* format = tranche->GetFormat(aFormat)) {
        return format;
      }
    }
    return nullptr;
  }

 private:
  DMABufFormatTable mFormatTable;
  UniquePtr<DMABufFeedbackTranche> mPendingTranche;
  nsTArray<UniquePtr<DMABufFeedbackTranche>> mTranches;
};

DMABufFeedback* DMABufFormats::GetPendingDMABufFeedback() {
  if (!mPendingDMABufFeedback) {
    mPendingDMABufFeedback = MakeUnique<DMABufFeedback>();
  }
  return mPendingDMABufFeedback.get();
}

void DMABufFormats::PendingDMABufFeedbackDone() {
  mDMABufFeedback = std::move(mPendingDMABufFeedback);
  if (mFormatRefreshCallback) {
    mFormatRefreshCallback(this);
  }
}

#ifdef MOZ_WAYLAND
static void dmabuf_feedback_format_table(
    void* data,
    struct zwp_linux_dmabuf_feedback_v1* zwp_linux_dmabuf_feedback_v1,
    int32_t fd, uint32_t size) {
  auto* dmabuf = static_cast<DMABufFormats*>(data);
  if (!dmabuf) {
    return;
  }
  DMABufFormatTable* formats =
      dmabuf->GetPendingDMABufFeedback()->FormatTable();
  formats->Set(fd, size);
}

static void dmabuf_feedback_tranche_target_device(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback,
    struct wl_array* dev) {
  // We're getting device from GL
}

static void dmabuf_feedback_tranche_formats(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback,
    struct wl_array* indices) {
  auto* dmabuf = static_cast<DMABufFormats*>(data);
  if (!dmabuf) {
    return;
  }
  DMABufFeedback* feedback = dmabuf->GetPendingDMABufFeedback();
  DMABufFormatTable* formatTable = feedback->FormatTable();
  if (!formatTable->IsSet()) {
    formatTable = dmabuf->GetDMABufFeedback()
                      ? dmabuf->GetDMABufFeedback()->FormatTable()
                      : nullptr;
    if (!formatTable || !formatTable->IsSet()) {
      gfxCriticalNote << "Missing DMABuf format table!";
      return;
    }
  }
  feedback->PendingTranche()->SetFormats(formatTable, indices);
}

static void dmabuf_feedback_tranche_flags(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback,
    uint32_t flags) {
  if (flags & ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT) {
    auto* dmabuf = static_cast<DMABufFormats*>(data);
    if (!dmabuf) {
      return;
    }
    LOGDMABUF(("DMABufFeedbackTranche [%p] is scanout tranche",
               dmabuf->GetPendingDMABufFeedback()->PendingTranche()));
    dmabuf->GetPendingDMABufFeedback()->PendingTranche()->SetScanout(true);
  }
}

static void dmabuf_feedback_tranche_done(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback) {
  auto* dmabuf = static_cast<DMABufFormats*>(data);
  if (!dmabuf) {
    return;
  }
  LOGDMABUF(("DMABufFeedbackTranche [%p] is done",
             dmabuf->GetPendingDMABufFeedback()));
  dmabuf->GetPendingDMABufFeedback()->PendingTrancheDone();
}

static void dmabuf_feedback_done(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback) {
  auto* dmabuf = static_cast<DMABufFormats*>(data);
  if (!dmabuf) {
    return;
  }
  dmabuf->PendingDMABufFeedbackDone();
}

static void dmabuf_feedback_main_device(
    void* data, struct zwp_linux_dmabuf_feedback_v1* dmabuf_feedback,
    struct wl_array* dev) {
  // We're getting DRM device from GL.
}

static const struct zwp_linux_dmabuf_feedback_v1_listener
    dmabuf_feedback_listener = {
        .done = dmabuf_feedback_done,
        .format_table = dmabuf_feedback_format_table,
        .main_device = dmabuf_feedback_main_device,
        .tranche_done = dmabuf_feedback_tranche_done,
        .tranche_target_device = dmabuf_feedback_tranche_target_device,
        .tranche_formats = dmabuf_feedback_tranche_formats,
        .tranche_flags = dmabuf_feedback_tranche_flags,
};

static void dmabuf_v3_modifiers(void* data,
                                struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf,
                                uint32_t format, uint32_t modifier_hi,
                                uint32_t modifier_lo) {
  // skip modifiers marked as invalid
  if (modifier_hi == (DRM_FORMAT_MOD_INVALID >> 32) &&
      modifier_lo == (DRM_FORMAT_MOD_INVALID & 0xffffffff)) {
    return;
  }
  auto* dmabuf = static_cast<DMABufFormats*>(data);
  if (!dmabuf) {
    return;
  }

  uint64_t modifier = ((uint64_t)modifier_hi << 32) | modifier_lo;
  LOGDMABUF(("DMABuf format 0x%x modifier %" PRIx64, format, modifier));

  dmabuf->GetPendingDMABufFeedback()->PendingTranche()->AddFormat(format,
                                                                  modifier);
}

static void dmabuf_v3_format(void* data,
                             struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf,
                             uint32_t format) {
  // XXX: deprecated
}

static const struct zwp_linux_dmabuf_v1_listener dmabuf_v3_listener = {
    dmabuf_v3_format, dmabuf_v3_modifiers};

void DMABufFormats::InitFeedback(zwp_linux_dmabuf_v1* aDMABuf,
                                 const DMABufFormatsCallback& aFormatRefreshCB,
                                 wl_surface* aSurface) {
  LOGDMABUF(("DMABufFormats::Init() feedback wl_surface %p", aSurface));
  if (aSurface) {
    mWaylandFeedback =
        zwp_linux_dmabuf_v1_get_surface_feedback(aDMABuf, aSurface);
  } else {
    mWaylandFeedback = zwp_linux_dmabuf_v1_get_default_feedback(aDMABuf);
  }
  zwp_linux_dmabuf_feedback_v1_add_listener(mWaylandFeedback,
                                            &dmabuf_feedback_listener, this);
  mFormatRefreshCallback = aFormatRefreshCB;
}

void DMABufFormats::InitV3(zwp_linux_dmabuf_v1* aDMABuf) {
  LOGDMABUF(("DMABufFormats::Init() v.3"));
  zwp_linux_dmabuf_v1_add_listener(aDMABuf, &dmabuf_v3_listener, this);
}

void DMABufFormats::InitV3Done() {
  LOGDMABUF(("DMABufFormats::Init() v.3 Done"));
  GetPendingDMABufFeedback()->PendingTrancheDone();
  PendingDMABufFeedbackDone();
}
#endif

DRMFormat* DMABufFormats::GetFormat(uint32_t aFormat,
                                    bool aRequestScanoutFormat) {
  MOZ_DIAGNOSTIC_ASSERT(mDMABufFeedback);
  return mDMABufFeedback->GetFormat(aFormat, aRequestScanoutFormat);
}

void DMABufFormats::EnsureBasicFormat(uint32_t aDrmFourcc) {
  MOZ_DIAGNOSTIC_ASSERT(mDMABufFeedback);
  if (!GetFormat(aDrmFourcc)) {
    LOGDMABUF(("DMABufFormats::EnsureBasicFormat(): %x is missing, adding.",
               aDrmFourcc));
    mDMABufFeedback->PendingTranche()->AddFormat(aDrmFourcc,
                                                 DRM_FORMAT_MOD_INVALID);
  }
}

void DMABufFormats::EnsureBasicFormats() {
  MOZ_DIAGNOSTIC_ASSERT(!mPendingDMABufFeedback,
                        "Can't add extra formats during init!");
  if (!mDMABufFeedback) {
    mDMABufFeedback = MakeUnique<DMABufFeedback>();
  }

  EnsureBasicFormat(GBM_FORMAT_XRGB8888);
  EnsureBasicFormat(GBM_FORMAT_ARGB8888);
  EnsureBasicFormat(GBM_FORMAT_P010);
  EnsureBasicFormat(GBM_FORMAT_NV12);
  EnsureBasicFormat(GBM_FORMAT_YUV420);

  mDMABufFeedback->PendingTrancheDone();
}

DMABufFormats::DMABufFormats() = default;

DMABufFormats::~DMABufFormats() {
#ifdef MOZ_WAYLAND
  if (mWaylandFeedback) {
    zwp_linux_dmabuf_feedback_v1_destroy(mWaylandFeedback);
  }
#endif
}

#ifdef MOZ_WAYLAND
RefPtr<DMABufFormats> CreateDMABufFeedbackFormats(
    wl_surface* aSurface, const DMABufFormatsCallback& aFormatRefreshCB) {
  if (!WaylandDisplayGet()->HasDMABufFeedback()) {
    return nullptr;
  }
  auto formats = MakeRefPtr<DMABufFormats>();
  formats->InitFeedback(WaylandDisplayGet()->GetDmabuf(), aFormatRefreshCB,
                        aSurface);
  return formats.forget();
}
#endif

bool GlobalDMABufFormats::ConfigureFormat(RefPtr<DMABufFormats> aFormats,
                                          RefPtr<DRMFormat>& aTargetFormat,
                                          uint32_t aDrmFourcc) {
  DRMFormat* format = aFormats->GetFormat(aDrmFourcc);
  if (!format) {
    LOGDMABUF(
        ("GlobalDMABufFormats::ConfigureFormat(): missing %x fourcc format.",
         aDrmFourcc));
    return false;
  }
  nsTArray<uint64_t> mods;
  if (!format->UseModifiers()) {
    const nsCOMPtr<nsIGfxInfo> gfxInfo = components::GfxInfo::Service();
    if (gfxInfo) {
      auto* gfx = static_cast<GfxInfo*>(gfxInfo.get());
      mods.AppendElements(gfx->GetDMABufEGLModifiers(aDrmFourcc));
    }
    LOGDMABUF(
        ("GlobalDMABufFormats::ConfigureFormat(): Adding %x fourcc format EGL "
         "modifiers num [%d].",
         aDrmFourcc, (int)mods.Length()));
  }
  if (mods.IsEmpty()) {
    mods.Assign(*format->GetModifiers());
    LOGDMABUF(
        ("GlobalDMABufFormats::ConfigureFormat(): Adding %x fourcc format "
         "dmabuf "
         "modifiers num [%d].",
         aDrmFourcc, (int)mods.Length()));
  }
  aTargetFormat = new DRMFormat(aDrmFourcc, mods);
  return true;
}

void GlobalDMABufFormats::SetModifiersToGfxVars() {
  RefPtr<DMABufFormats> formats;
#ifdef MOZ_WAYLAND
  if (GdkIsWaylandDisplay()) {
    formats = WaylandDisplayGet()->GetDMABufFormats();
  }
#endif
  if (!formats) {
    formats = new DMABufFormats();
  }
  formats->EnsureBasicFormats();

  if (!ConfigureFormat(formats, mFormatRGBX, GBM_FORMAT_XRGB8888)) {
    MOZ_DIAGNOSTIC_CRASH("Missing GBM_FORMAT_XRGB8888 dmabuf format!");
  }
  gfxVars::SetDMABufModifiersXRGB(*mFormatRGBX->GetModifiers());

  if (!ConfigureFormat(formats, mFormatRGBA, GBM_FORMAT_ARGB8888)) {
    MOZ_DIAGNOSTIC_CRASH("Missing GBM_FORMAT_ARGB8888 dmabuf format!");
  }
  gfxVars::SetDMABufModifiersARGB(*mFormatRGBA->GetModifiers());

  if (ConfigureFormat(formats, mFormatP010, GBM_FORMAT_P010)) {
    gfxVars::SetDMABufModifiersP010(*mFormatP010->GetModifiers());
  }

  if (ConfigureFormat(formats, mFormatNV12, GBM_FORMAT_NV12)) {
    gfxVars::SetDMABufModifiersNV12(*mFormatNV12->GetModifiers());
  }

  ConfigureFormat(formats, mFormatYUV420, GBM_FORMAT_YUV420);
}

void GlobalDMABufFormats::GetModifiersFromGfxVars() {
  mFormatRGBX =
      new DRMFormat(GBM_FORMAT_XRGB8888, gfxVars::DMABufModifiersXRGB());
  mFormatRGBA =
      new DRMFormat(GBM_FORMAT_ARGB8888, gfxVars::DMABufModifiersARGB());
  if (!gfxVars::DMABufModifiersP010().IsEmpty()) {
    mFormatP010 =
        new DRMFormat(GBM_FORMAT_P010, gfxVars::DMABufModifiersP010());
  }
  if (!gfxVars::DMABufModifiersNV12().IsEmpty()) {
    mFormatNV12 =
        new DRMFormat(GBM_FORMAT_NV12, gfxVars::DMABufModifiersNV12());
  }
}

DRMFormat* GlobalDMABufFormats::GetDRMFormat(int32_t aFOURCCFormat) {
  switch (aFOURCCFormat) {
    case GBM_FORMAT_XRGB8888:
      MOZ_DIAGNOSTIC_ASSERT(mFormatRGBX, "Missing RGBX dmabuf format!");
      return mFormatRGBX;
    case GBM_FORMAT_ARGB8888:
      MOZ_DIAGNOSTIC_ASSERT(mFormatRGBA, "Missing RGBA dmabuf format!");
      return mFormatRGBA;
    case GBM_FORMAT_P010:
      return mFormatP010;
    case GBM_FORMAT_NV12:
      return mFormatNV12;
    case GBM_FORMAT_YUV420:
      return mFormatYUV420;
    default:
      gfxCriticalNoteOnce << "DMABufDevice::GetDRMFormat() unknow format: "
                          << aFOURCCFormat;
      return nullptr;
  }
}

bool GlobalDMABufFormats::SupportsDirectComposition(
    mozilla::gfx::SurfaceFormat aFormat) const {
  switch (aFormat) {
    case gfx::SurfaceFormat::R8G8B8X8:
    case gfx::SurfaceFormat::R8G8B8A8:
    case gfx::SurfaceFormat::B8G8R8A8:
    case gfx::SurfaceFormat::B8G8R8X8:
      return true;
    case gfx::SurfaceFormat::NV12:
      return !!mFormatNV12;
    case gfx::SurfaceFormat::YUV420:
      return !!mFormatYUV420;
    case gfx::SurfaceFormat::P010:
      return !!mFormatP010;
    default:
      MOZ_ASSERT_UNREACHABLE("unexpected to be called");
      return false;
  }
}

void GlobalDMABufFormats::LoadFormatModifiers() {
  if (XRE_IsParentProcess()) {
    MOZ_ASSERT(NS_IsMainThread());
    SetModifiersToGfxVars();
  } else {
    GetModifiersFromGfxVars();
  }
}

GlobalDMABufFormats::GlobalDMABufFormats() { LoadFormatModifiers(); }

GlobalDMABufFormats* GetGlobalDMABufFormats() {
  static StaticAutoPtr<GlobalDMABufFormats> sGlobalDMABufFormats;
  static std::once_flag onceFlag;
  std::call_once(onceFlag, [] {
    sGlobalDMABufFormats = new GlobalDMABufFormats();
    if (NS_IsMainThread()) {
      ClearOnShutdown(&sGlobalDMABufFormats);
    } else {
      NS_DispatchToMainThread(NS_NewRunnableFunction(
          "ClearGlobalDMABufFormats",
          [] { ClearOnShutdown(&sGlobalDMABufFormats); }));
    }
  });
  return sGlobalDMABufFormats.get();
}

}  // namespace mozilla::widget
