/*
 *  Copyright (C) 2017-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "SeatSelection.h"

#include "Connection.h"
#include "Registry.h"
#include "WinEventsWayland.h"
#include "utils/StringUtils.h"
#include "utils/log.h"

#include "platform/posix/utils/FileHandle.h"

#include <cerrno>
#include <chrono>
#include <cstring>
#include <mutex>
#include <system_error>
#include <utility>

#include <poll.h>
#include <unistd.h>

using namespace KODI::UTILS::POSIX;
using namespace KODI::WINDOWING::WAYLAND;

namespace
{

const std::vector<std::string> MIME_TYPES_PREFERENCE =
{
  "text/plain;charset=utf-8",
  "text/plain;charset=iso-8859-1",
  "text/plain;charset=us-ascii",
  "text/plain"
};

}

CSeatSelection::CSeatSelection(CConnection& connection, wayland::seat_t const& seat)
{
  wayland::data_device_manager_t manager;
  {
    CRegistry registry{connection};
    registry.RequestSingleton(manager, 1, 3, false);
    registry.Bind();
  }

  if (!manager)
  {
    CLog::Log(LOGWARNING, "No data device manager announced by compositor, clipboard will not be available");
    return;
  }

  m_dataDevice = manager.get_data_device(seat);

  // Class is created in response to seat add events - so no events can get lost
  m_dataDevice.on_data_offer() = [this](wayland::data_offer_t offer)
  {
    // We don't know yet whether this is drag-and-drop or selection, so collect
    // MIME types in either case
    m_currentOffer = std::move(offer);
    m_mimeTypeOffers.clear();
    m_currentOffer.on_offer() = [this](std::string mime)
    {
      m_mimeTypeOffers.push_back(std::move(mime));
    };
  };
  m_dataDevice.on_selection() = [this](const wayland::data_offer_t& offer)
  {
    std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
    m_matchedMimeType.clear();

    if (offer != m_currentOffer)
    {
      // Selection was not previously introduced by offer (could be NULL for example)
      m_currentSelection.proxy_release();
    }
    else
    {
      m_currentSelection = offer;
      std::string offers = StringUtils::Join(m_mimeTypeOffers, ", ");

      // Match MIME type by priority: Find first preferred MIME type that is in the
      // set of offered types
      // Charset is not case-sensitive in MIME type spec, so match case-insensitively
      auto mimeIt = std::find_first_of(MIME_TYPES_PREFERENCE.cbegin(), MIME_TYPES_PREFERENCE.cend(),
                                       m_mimeTypeOffers.cbegin(), m_mimeTypeOffers.cend(),
                                       // static_cast needed for overload resolution
                                       static_cast<bool (*)(std::string const&, std::string const&)> (&StringUtils::EqualsNoCase));
      if (mimeIt != MIME_TYPES_PREFERENCE.cend())
      {
        m_matchedMimeType = *mimeIt;
        CLog::Log(LOGDEBUG, "Chose selection MIME type {} out of offered {}", m_matchedMimeType,
                  offers);
      }
      else
      {
        CLog::Log(LOGDEBUG, "Could not find compatible MIME type for selection data (offered: {})",
                  offers);
      }
    }
  };
}

std::string CSeatSelection::GetSelectionText() const
{
  std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
  if (!m_currentSelection || m_matchedMimeType.empty())
  {
    return "";
  }

  std::array<int, 2> fds;
  if (pipe(fds.data()) != 0)
  {
    CLog::LogF(LOGERROR, "Could not open pipe for selection data transfer: {}",
               std::strerror(errno));
    return "";
  }

  CFileHandle readFd{fds[0]};
  CFileHandle writeFd{fds[1]};

  m_currentSelection.receive(m_matchedMimeType, writeFd);
  lock.unlock();
  // Make sure the other party gets the request as soon as possible
  CWinEventsWayland::Flush();
  // Fd now gets sent to the other party -> make sure our write end is closed
  // so we get POLLHUP when the other party closes its write fd
  writeFd.reset();

  pollfd fd =
  {
    .fd = readFd,
    .events = POLLIN,
    .revents = 0
  };

  // UI will block in this function when Ctrl+V is pressed, so timeout should be
  // rather short!
  const std::chrono::seconds TIMEOUT{1};
  const std::size_t MAX_SIZE{4096};
  std::array<char, MAX_SIZE> buffer;

  auto start = std::chrono::steady_clock::now();
  std::size_t totalBytesRead{0};

  do
  {
    auto now = std::chrono::steady_clock::now();
    // Do not permit negative timeouts (would cause infinitely long poll)
    auto remainingTimeout = std::max(std::chrono::milliseconds(0), std::chrono::duration_cast<std::chrono::milliseconds> (TIMEOUT - (now - start))).count();
    // poll() for changes until poll signals POLLHUP and the remaining data was read
    int ret{poll(&fd, 1, remainingTimeout)};
    if (ret == 0)
    {
      // Timeout
      CLog::LogF(LOGERROR, "Reading from selection data pipe timed out");
      return "";
    }
    else if (ret < 0 && errno == EINTR)
    {
      continue;
    }
    else if (ret < 0)
    {
      throw std::system_error(errno, std::generic_category(), "Error polling selection pipe");
    }
    else if (fd.revents & POLLNVAL || fd.revents & POLLERR)
    {
      CLog::LogF(LOGERROR, "poll() indicated error on selection pipe");
      return "";
    }
    else if (fd.revents & POLLIN)
    {
      if (totalBytesRead >= buffer.size())
      {
        CLog::LogF(LOGERROR, "Selection data is too big, aborting read");
        return "";
      }
      ssize_t readBytes{read(fd.fd, buffer.data() + totalBytesRead, buffer.size() - totalBytesRead)};
      if (readBytes < 0)
      {
        CLog::LogF(LOGERROR, "read() from selection pipe failed: {}", std::strerror(errno));
        return "";
      }
      totalBytesRead += readBytes;
    }
  }
  while (!(fd.revents & POLLHUP));

  return std::string(buffer.data(), totalBytesRead);
}
