/*
 * Copyright © 2011 Kristian Høgsberg
 *             2020 Red Hat Inc.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that copyright
 * notice and this permission notice appear in supporting documentation, and
 * that the name of the copyright holders not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  The copyright holders make no representations
 * about the suitability of this software for any purpose.  It is provided "as
 * is" without express or implied warranty.
 *
 * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
 * OF THIS SOFTWARE.
 */

#include "config.h"

#include <gio/gunixoutputstream.h>
#include <glib-unix.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "meta/meta-selection.h"
#include "wayland/meta-wayland-data-device.h"
#include "wayland/meta-wayland-private.h"

#include "meta-wayland-data-offer.h"

#define ALL_ACTIONS (WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | \
                     WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE | \
                     WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK)

static void
data_offer_accept (struct wl_client *client,
                   struct wl_resource *resource,
                   guint32 serial,
                   const char *mime_type)
{
  MetaWaylandDataOffer *offer = wl_resource_get_user_data (resource);

  /* FIXME: Check that client is currently focused by the input
   * device that is currently dragging this data source.  Should
   * this be a wl_data_device request? */

  if (offer->source)
    {
      meta_wayland_data_source_target (offer->source, mime_type);
      meta_wayland_data_source_set_has_target (offer->source,
                                               mime_type != NULL);
    }

  offer->accepted = mime_type != NULL;
}

static void
transfer_cb (MetaSelection *selection,
             GAsyncResult  *res,
             GOutputStream *stream)
{
  GError *error = NULL;

  if (!meta_selection_transfer_finish (selection, res, &error))
    {
      g_warning ("Could not fetch selection data: %s", error->message);
      g_error_free (error);
    }

  g_output_stream_close (stream, NULL, NULL);
  g_object_unref (stream);
}

static void
data_offer_receive (struct wl_client *client, struct wl_resource *resource,
                    const char *mime_type, int32_t fd)
{
  MetaWaylandDataOffer *offer = wl_resource_get_user_data (resource);
  MetaDisplay *display = meta_get_display ();
  MetaSelectionType selection_type;
  GList *mime_types;
  gboolean found;

  selection_type = offer->selection_type;
  mime_types = meta_selection_get_mimetypes (meta_display_get_selection (display),
                                             selection_type);
  found = g_list_find_custom (mime_types, mime_type, (GCompareFunc) g_strcmp0) != NULL;
  g_list_free_full (mime_types, g_free);

  if (found)
    {
      GOutputStream *stream;

      stream = g_unix_output_stream_new (fd, TRUE);
      meta_selection_transfer_async (meta_display_get_selection (display),
                                     selection_type,
                                     mime_type,
                                     -1,
                                     stream,
                                     NULL,
                                     (GAsyncReadyCallback) transfer_cb,
                                     stream);
    }
  else
    {
      close (fd);
    }
}

static void
data_offer_destroy (struct wl_client   *client,
                    struct wl_resource *resource)
{
  wl_resource_destroy (resource);
}

static void
data_offer_finish (struct wl_client   *client,
                   struct wl_resource *resource)
{
  MetaWaylandDataOffer *offer = wl_resource_get_user_data (resource);
  enum wl_data_device_manager_dnd_action current_action;

  if (!offer->source ||
      offer != meta_wayland_data_source_get_current_offer (offer->source))
    return;

  if (!offer->accepted || !offer->action_sent)
    {
      wl_resource_post_error (offer->resource,
                              WL_DATA_OFFER_ERROR_INVALID_FINISH,
                              "premature finish request");
      return;
    }

  current_action = meta_wayland_data_source_get_current_action (offer->source);

  if (current_action == WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE ||
      current_action == WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK)
    {
      wl_resource_post_error (offer->resource,
                              WL_DATA_OFFER_ERROR_INVALID_OFFER,
                              "offer finished with an invalid action");
      return;
    }

  meta_wayland_data_source_notify_finish (offer->source);
}

static void
data_offer_set_actions (struct wl_client   *client,
                        struct wl_resource *resource,
                        uint32_t            dnd_actions,
                        uint32_t            preferred_action)
{
  MetaWaylandDataOffer *offer = wl_resource_get_user_data (resource);

  if (dnd_actions & ~(ALL_ACTIONS))
    {
      wl_resource_post_error (offer->resource,
                              WL_DATA_OFFER_ERROR_INVALID_ACTION_MASK,
                              "invalid actions mask %x", dnd_actions);
      return;
    }

  if (preferred_action &&
      (!(preferred_action & dnd_actions) ||
       __builtin_popcount (preferred_action) > 1))
    {
      wl_resource_post_error (offer->resource,
                              WL_DATA_OFFER_ERROR_INVALID_ACTION,
                              "invalid action %x", preferred_action);
      return;
    }

  offer->dnd_actions = dnd_actions;
  offer->preferred_dnd_action = preferred_action;

  meta_wayland_data_offer_update_action (offer);
}

static const struct wl_data_offer_interface data_offer_interface = {
  data_offer_accept,
  data_offer_receive,
  data_offer_destroy,
  data_offer_finish,
  data_offer_set_actions,
};

static void
destroy_data_offer (struct wl_resource *resource)
{
  MetaWaylandDataOffer *offer = wl_resource_get_user_data (resource);
  MetaWaylandSeat *seat;

  if (offer->source)
    {
      seat = meta_wayland_data_source_get_seat (offer->source);

      if (offer == meta_wayland_data_source_get_current_offer (offer->source))
        {
          if (seat->data_device.dnd_data_source == offer->source)
            {
              if (wl_resource_get_version (offer->resource) <
                  WL_DATA_OFFER_ACTION_SINCE_VERSION)
                meta_wayland_data_source_notify_finish (offer->source);
              else if (meta_wayland_data_source_get_drop_performed (offer->source))
                meta_wayland_data_source_cancel (offer->source);
            }
          else
            {
              meta_wayland_data_source_set_current_offer (offer->source, NULL);
              meta_wayland_data_source_set_has_target (offer->source, FALSE);
            }
        }

      g_object_remove_weak_pointer (G_OBJECT (offer->source),
                                    (gpointer *)&offer->source);
      offer->source = NULL;
    }

  meta_display_sync_wayland_input_focus (meta_get_display ());
  g_free (offer);
}

MetaWaylandDataOffer *
meta_wayland_data_offer_new (MetaSelectionType      selection_type,
                             MetaWaylandDataSource *source,
                             struct wl_resource    *target)
{
  MetaWaylandDataOffer *offer;

  offer = g_new0 (MetaWaylandDataOffer, 1);
  offer->selection_type = selection_type;
  offer->resource = wl_resource_create (wl_resource_get_client (target),
                                        &wl_data_offer_interface,
                                        wl_resource_get_version (target), 0);
  wl_resource_set_implementation (offer->resource,
                                  &data_offer_interface,
                                  offer,
                                  destroy_data_offer);
  if (source)
    {
      offer->source = source;
      g_object_add_weak_pointer (G_OBJECT (source), (gpointer *)&offer->source);
    }

  return offer;
}

static enum wl_data_device_manager_dnd_action
data_offer_choose_action (MetaWaylandDataOffer *offer)
{
  MetaWaylandDataSource *source = offer->source;
  uint32_t actions, user_action, available_actions;

  if (wl_resource_get_version (offer->resource) <
      WL_DATA_OFFER_ACTION_SINCE_VERSION)
    return WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY;

  meta_wayland_data_source_get_actions (source, &actions);
  user_action = meta_wayland_data_source_get_user_action (source);

  available_actions = actions & offer->dnd_actions;

  if (!available_actions)
    return WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE;

  /* If the user is forcing an action, go for it */
  if ((user_action & available_actions) != 0 &&
      !(user_action == WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK &&
        meta_wayland_data_source_get_drop_performed (source)))
    return user_action;

  /* If the dest side has a preferred DnD action, use it */
  if ((offer->preferred_dnd_action & available_actions) != 0)
    return offer->preferred_dnd_action;

  /* Use the first found action, in bit order */
  return 1 << (ffs (available_actions) - 1);
}

void
meta_wayland_data_offer_update_action (MetaWaylandDataOffer *offer)
{
  enum wl_data_device_manager_dnd_action current_action, action;
  MetaWaylandDataSource *source;

  if (!offer->source)
    return;

  source = offer->source;
  current_action = meta_wayland_data_source_get_current_action (source);
  action = data_offer_choose_action (offer);

  if (current_action == action)
    return;

  meta_wayland_data_source_set_current_action (source, action);

  if (!meta_wayland_data_source_get_in_ask (source) &&
      wl_resource_get_version (offer->resource) >=
      WL_DATA_OFFER_ACTION_SINCE_VERSION)
    {
      wl_data_offer_send_action (offer->resource, action);
      offer->action_sent = TRUE;
    }
}

struct wl_resource *
meta_wayland_data_offer_get_resource (MetaWaylandDataOffer *offer)
{
  return offer->resource;
}

MetaWaylandDataSource *
meta_wayland_data_offer_get_source (MetaWaylandDataOffer *offer)
{
  return offer->source;
}
