/* Wayland compositor running on top of an X server.

Copyright (C) 2022 to various contributors.

This file is part of 12to11.

12to11 is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.

12to11 is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with 12to11.  If not, see <https://www.gnu.org/licenses/>.  */

#include <string.h>

#include "compositor.h"

#include "primary-selection-unstable-v1.h"

typedef struct _PDataDevice PDataDevice;
typedef struct _PDataOffer PDataOffer;

enum
  {
    WasSentOffer = 1,
  };

struct _PDataDevice
{
  /* The seat associated with this data device.  */
  Seat *seat;

  /* The destroy callback associated with that seat.  */
  void *seat_destroy_key;

  /* Some flags.  */
  int flags;

  /* The wl_resource associated with this data device.  */
  struct wl_resource *resource;

  /* The next and last data devices.  */
  PDataDevice *next, *last;
};

struct _PDataOffer
{
  /* The data source associated with this offer.  */
  PDataSource *source;

  /* The resource associated with this offer.  */
  struct wl_resource *resource;

  /* The next and last data offers associated with the data
     source.  */
  PDataOffer *next, *last;
};

struct _PDataSource
{
  /* The wl_resource associated with this data source.  */
  struct wl_resource *resource;

  /* List of all data offers attached to this source.  */
  PDataOffer offers;

  /* List of all MIME types provided by this source.  */
  XLList *mime_types;

  /* List of atoms provided by this source.  */
  XIDList *atom_types;

  /* Number of MIME types provided by this source.  */
  int n_mime_types;
};

/* The global primary selection manager.  */
static struct wl_global *global_primary_selection_device_manager;

/* The data source currently providing the primary selection.  */
static PDataSource *primary_selection;

/* The last time the primary selection changed.  */
static uint32_t last_change_serial;

/* All data devices.  */
static PDataDevice all_devices;

/* A special PDataSource meaning that the primary selection is a
   foreign selection.  */
static PDataSource foreign_selection_key;

/* Functions to call to obtain zwp_primary_selection_v1_offer
   resources and send associated data for foreign selections.  */
static CreateOfferFuncs foreign_selection_functions;

/* The time the foreign selection was set.  */
static Timestamp foreign_selection_time;

/* Forward declaration.  */

static void NoticeChanged (void);

/* Data offer implementation.  */


static void
Receive (struct wl_client *client, struct wl_resource *resource,
	 const char *mime_type, int32_t fd)
{
  PDataOffer *offer;

  offer = wl_resource_get_user_data (resource);

  if (!offer)
    {
      /* This data offer is inert.  */
      close (fd);
      return;
    }

  zwp_primary_selection_source_v1_send_send (offer->source->resource,
					     mime_type, fd);
  close (fd);
}

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

static struct zwp_primary_selection_offer_v1_interface offer_impl =
  {
    .receive = Receive,
    .destroy = DestroyOffer,
  };

static void
FreeDataOffer (PDataOffer *offer)
{
  /* Mark this offer as invalid by setting the resource's user_data to
     NULL.  */
  if (offer->resource)
    wl_resource_set_user_data (offer->resource, NULL);

  /* Unlink the offer.  */
  offer->last->next = offer->next;
  offer->next->last = offer->last;

  /* Free the offer.  */
  XLFree (offer);
}

static void
HandleOfferResourceDestroy (struct wl_resource *resource)
{
  PDataOffer *offer;

  offer = wl_resource_get_user_data (resource);

  if (!offer)
    /* The offer was made inert.  */
    return;

  offer->resource = NULL;
  FreeDataOffer (offer);
}

static struct wl_resource *
AddDataOffer (struct wl_client *client, PDataSource *source)
{
  PDataOffer *offer;
  struct wl_resource *resource;

  resource = wl_resource_create (client,
				 &zwp_primary_selection_offer_v1_interface,
				 /* 0 means to allocate a new resource
				    ID server-side.  */
				 1, 0);

  if (!resource)
    return NULL;

  offer = XLCalloc (1, sizeof *offer);
  offer->next = source->offers.next;
  offer->last = &source->offers;

  source->offers.next->last = offer;
  source->offers.next = offer;

  offer->resource = resource;
  offer->source = source;

  wl_resource_set_implementation (resource, &offer_impl,
				  offer, HandleOfferResourceDestroy);

  return resource;
}

/* Data source implementation.  */


static Bool
FindType (PDataSource *source, const char *mime_type)
{
  XLList *tem;

  for (tem = source->mime_types; tem; tem = tem->next)
    {
      if (!strcmp (tem->data, mime_type))
	return True;
    }

  return False;
}

static void
Offer (struct wl_client *client, struct wl_resource *resource,
       const char *mime_type)
{
  PDataSource *source;

  source = wl_resource_get_user_data (resource);

  /* If the type was already offered, simply return.  */
  if (FindType (source, mime_type))
    return;

  /* Otherwise, add the MIME type to the list of types provided by
     this source.  */
  source->mime_types = XLListPrepend (source->mime_types,
				      XLStrdup (mime_type));
  source->atom_types = XIDListPrepend (source->atom_types,
				       InternAtom (mime_type));
  source->n_mime_types++;
}

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

static struct zwp_primary_selection_source_v1_interface source_impl =
  {
    .offer = Offer,
    .destroy = DestroySource,
  };

static void
HandleSourceResourceDestroy (struct wl_resource *resource)
{
  PDataSource *source;
  PDataOffer *offer, *last;

  source = wl_resource_get_user_data (resource);

  /* Now free every data offer.  */
  offer = source->offers.next;
  while (offer != &source->offers)
    {
      last = offer;
      offer = offer->next;

      FreeDataOffer (last);
    }

  /* And free the MIME types offered by the source.  */
  XLListFree (source->mime_types, XLFree);
  XIDListFree (source->atom_types, NULL);

  /* Maybe disown the primary selection.  */
  XLNotePrimaryDestroyed (source);

  /* If source is the primary selection, clear it and send the change
     to all clients.  */
  if (source == primary_selection)
    {
      primary_selection = NULL;
      NoticeChanged ();
    }

  /* And the source itself.  */
  XLFree (source);
}

struct wl_resource *
XLResourceFromPDataSource (PDataSource *source)
{
  return source->resource;
}

Bool
XLPDataSourceHasAtomTarget (PDataSource *source, Atom target)
{
  XIDList *list;

  for (list = source->atom_types; list; list = list->next)
    {
      if (list->data == target)
	return True;
    }

  return False;
}

Bool
XLPDataSourceHasTarget (PDataSource *source, const char *mime_type)
{
  XLList *list;

  for (list = source->mime_types; list; list = list->next)
    {
      if (!strcmp (list->data, mime_type))
	return True;
    }

  return False;
}

int
XLPDataSourceTargetCount (PDataSource *source)
{
  return source->n_mime_types;
}

void
XLPDataSourceGetTargets (PDataSource *source, Atom *targets)
{
  int i;
  XIDList *list;

  list = source->atom_types;

  for (i = 0; i < source->n_mime_types; ++i)
    {
      targets[i] = list->data;
      list = list->next;
    }
}

/* Device implementation.  */


static void
UpdateSingleReferenceWithForeignOffer (struct wl_client *client,
				       PDataDevice *reference)
{
  struct wl_resource *scratch, *resource;
  Timestamp time;

  time = foreign_selection_time;
  resource = foreign_selection_functions.create_offer (client, time);
  scratch = reference->resource;

  if (!resource)
    return;

  /* Make the data offer known to the client.  */
  zwp_primary_selection_device_v1_send_data_offer (scratch, resource);

  /* Tell the foreign selection provider to send supported resources
     to the client.  */
  foreign_selection_functions.send_offers (resource, time);

  /* Finally, tell the client that the offer is a selection.  */
  zwp_primary_selection_device_v1_send_selection (scratch, resource);

  /* Set WasSentOffer since an offer was sent.  */
  reference->flags |= WasSentOffer;
}

static void
UpdateForSingleReference (PDataDevice *reference)
{
  struct wl_resource *scratch, *offer;
  struct wl_client *client;
  XLList *type;

  if (!reference->seat)
    /* This data device is inert... */
    return;

  client = wl_resource_get_client (reference->resource);

  if (!XLSeatIsClientFocused (reference->seat, client))
    /* The client is not focused.  */
    return;

  scratch = reference->resource;

  if (!primary_selection)
    {
      /* There is no primary selection.  Send a NULL selection.  */
      zwp_primary_selection_device_v1_send_selection (scratch, NULL);
      return;
    }

  /* If the primary selection is foreign, call the foreign selection
     functions.  */
  if (primary_selection == &foreign_selection_key)
    {
      UpdateSingleReferenceWithForeignOffer (client, reference);

      return;
    }

  /* Otherwise, create a data offer for that client.  */
  offer = AddDataOffer (client, primary_selection);

  if (!offer)
    /* Allocation of the offer failed.  */
    return;

  /* Introduce the offer to the client.  */
  zwp_primary_selection_device_v1_send_data_offer (scratch, offer);

  /* Send all the offered MIME types.  */

  type = primary_selection->mime_types;

  for (; type; type = type->next)
    zwp_primary_selection_offer_v1_send_offer (offer, type->data);

  /* And tell the client it is the primary selection.  */
  zwp_primary_selection_device_v1_send_selection (scratch, offer);

  /* Set or clear WasSentOffer based on whether or not an offer was
     sent.  */
  if (offer)
    reference->flags |= WasSentOffer;
  else
    reference->flags &= ~WasSentOffer;
}

void
XLPrimarySelectionHandleFocusChange (Seat *seat)
{
  PDataDevice *device;
  struct wl_client *client;
  struct wl_resource *scratch;

  /* The focus changed.  Send NULL data offers to any non-focused data
     device that was previously sent a valid data offer, and send data
     offers to any focused data device that was not.  */

  device = all_devices.next;
  while (device != &all_devices)
    {
      if (!device->seat)
	/* Inert device.  */
	goto next;

      scratch = device->resource;
      client = wl_resource_get_client (scratch);

      if (device->flags & WasSentOffer
	  && !XLSeatIsClientFocused (device->seat, client))
	{
	  /* The device was previously sent a data offer, but is no
	     longer focused.  Send NULL and clear WasSentOffer.  */
	  zwp_primary_selection_device_v1_send_selection (scratch,
							  NULL);
	  device->flags &= ~WasSentOffer;
	}
      else if (!(device->flags & WasSentOffer)
	       && primary_selection
	       && XLSeatIsClientFocused (device->seat, client))
	/* The device is now focused, there is a primary selection,
	   and the device was not sent a data offer.  Send the missing
	   data offer now.  */
	UpdateForSingleReference (device);

    next:
      device = device->next;
    }
}

static void
NoticeChanged (void)
{
  PDataDevice *device;

  /* Send data offers to each data device.  */
  device = all_devices.next;

  while (device != &all_devices)
    {
      UpdateForSingleReference (device);
      device = device->next;
    }
}

static void
SetSelection (struct wl_client *client, struct wl_resource *resource,
	      struct wl_resource *source_resource, uint32_t serial)
{
  PDataDevice *device;
  PDataSource *source;
  struct wl_resource *scratch;

  device = wl_resource_get_user_data (resource);

  if (!device->seat)
    /* This device is inert, since the seat has been deleted.  */
    return;

  /* Note that both serial and last_change_serial are unsigned.
     Remember two's complement arithmetic.  */
  if (serial - last_change_serial > UINT32_MAX / 2)
    /* This change is out of date.  Do nothing.  */
    return;

  /* Otherwise, set the primary selection.  */
  last_change_serial = serial;

  if (source_resource)
    source = wl_resource_get_user_data (source_resource);
  else
    source = NULL;

  /* Try to own the X primary selection.  If it fails, refrain from
     changing the current selection data.  */
  if (!XLNoteLocalPrimary (device->seat, source))
    return;

  if (primary_selection)
    {
      /* The primary selection already exists.  Send the cancelled
	 event and clear the primary selection variable.  */
      scratch = primary_selection->resource;

      if (primary_selection != &foreign_selection_key)
	/* The special foreign selection has no associated
	   wl_resource.  */
	zwp_primary_selection_source_v1_send_cancelled (scratch);

      /* Clear primary_selection.  */
      primary_selection = NULL;
    }

  if (source_resource)
    /* Make source_resource the new primary selection.  */
    primary_selection = source;

  /* Now, send the updated primary selection to clients.  */
  NoticeChanged ();
}

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

static struct zwp_primary_selection_device_v1_interface device_impl =
  {
    .set_selection = SetSelection,
    .destroy = DestroyDevice,
  };

static void
HandleSeatDestroy (void *data)
{
  PDataDevice *device;

  device = data;

  /* The seat destroy listener must be cancelled manually.  */
  XLSeatCancelDestroyListener (device->seat_destroy_key);
  device->seat_destroy_key = NULL;
  device->seat = NULL;
}

static void
HandleDeviceResourceDestroy (struct wl_resource *resource)
{
  PDataDevice *device;

  device = wl_resource_get_user_data (resource);

  /* Cancel the seat destroy listener should it exist.  */

  if (device->seat)
    XLSeatCancelDestroyListener (device->seat_destroy_key);

  /* Unlink the device.  */

  device->last->next = device->next;
  device->next->last = device->last;

  XLFree (device);
}

/* Device manager implementation.  */


static void
CreateSource (struct wl_client *client, struct wl_resource *resource,
	      uint32_t id)
{
  PDataSource *source;

  source = XLSafeMalloc (sizeof *source);

  if (!source)
    {
      wl_resource_post_no_memory (resource);
      return;
    }

  memset (source, 0, sizeof *source);
  source->resource
    = wl_resource_create (client, &zwp_primary_selection_source_v1_interface,
			  wl_resource_get_version (resource), id);

  if (!source->resource)
    {
      XLFree (source);
      wl_resource_post_no_memory (resource);
      return;
    }

  source->offers.next = &source->offers;
  source->offers.last = &source->offers;

  wl_resource_set_implementation (source->resource, &source_impl,
				  source, HandleSourceResourceDestroy);
}

static void
GetDevice (struct wl_client *client, struct wl_resource *resource,
	   uint32_t id, struct wl_resource *seat)
{
  PDataDevice *device;

  device = XLSafeMalloc (sizeof *device);

  if (!device)
    {
      wl_resource_post_no_memory (resource);
      return;
    }

  memset (device, 0, sizeof *device);
  device->resource
    = wl_resource_create (client,
			  &zwp_primary_selection_device_v1_interface,
			  wl_resource_get_version (resource), id);

  if (!device->resource)
    {
      XLFree (device);
      wl_resource_post_no_memory (resource);
      return;
    }

  device->seat = wl_resource_get_user_data (seat);

  if (XLSeatIsInert (device->seat))
    device->seat = NULL;
  else
    device->seat_destroy_key
      = XLSeatRunOnDestroy (device->seat, HandleSeatDestroy,
			    device);

  /* Link the device into the chain of all devices.  */
  device->next = all_devices.next;
  device->last = &all_devices;
  all_devices.next->last = device;
  all_devices.next = device;

  wl_resource_set_implementation (device->resource, &device_impl,
				  device, HandleDeviceResourceDestroy);

  /* If the primary selection is set and the client is focused, send
     the data offer now.  */
  if (primary_selection && device->seat
      && XLSeatIsClientFocused (device->seat, client))
    UpdateForSingleReference (device);
}

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

static struct zwp_primary_selection_device_manager_v1_interface manager_impl =
  {
    .create_source = CreateSource,
    .get_device = GetDevice,
    .destroy = Destroy,
  };

static void
HandleBind (struct wl_client *client, void *data, uint32_t version,
	    uint32_t id)
{
  struct wl_resource *resource;

  resource
    = wl_resource_create (client,
			  &zwp_primary_selection_device_manager_v1_interface,
			  version, id);

  if (!resource)
    {
      wl_client_post_no_memory (client);
      return;
    }

  wl_resource_set_implementation (resource, &manager_impl, NULL, NULL);
}



void
XLSetForeignPrimary (Timestamp time, CreateOfferFuncs functions)
{
  uint32_t serial;
  struct wl_resource *scratch;

  if (TimestampIs (time, Earlier, foreign_selection_time))
    return;

  serial = wl_display_next_serial (compositor.wl_display);

  /* Use this serial to prevent clients from changing the selection
     again until the next event is sent.  */
  last_change_serial = serial;

  foreign_selection_time = time;
  foreign_selection_functions = functions;

  if (primary_selection
      && primary_selection != &foreign_selection_key)
    {
      scratch = primary_selection->resource;
      zwp_primary_selection_source_v1_send_cancelled (scratch);
    }

  /* Use a special value of primary_selection to mean that foreign
     selections are in use.  */
  primary_selection = &foreign_selection_key;

  /* Send new data offers to current clients.  */
  NoticeChanged ();
}

void
XLClearForeignPrimary (Timestamp time)
{
  struct wl_resource *scratch;

  if (TimestampIs (time, Earlier, foreign_selection_time))
    return;

  if (primary_selection == &foreign_selection_key)
    {
      primary_selection = NULL;
      NoticeChanged ();
    }
  else if (primary_selection)
    {
      /* The foreign selection was cleared; that means PRIMARY has
	 been disowned after the last foreign or local selection was
	 set, which means the local selection must be cleared as
	 well.  */
      scratch = primary_selection->resource;
      zwp_primary_selection_source_v1_send_cancelled (scratch);

      /* Clear the primary selection and notice a change.  */
      primary_selection = NULL;
      NoticeChanged ();
    }

  foreign_selection_time = time;
}

void
XLInitPrimarySelection (void)
{
  global_primary_selection_device_manager
    = wl_global_create (compositor.wl_display,
			&zwp_primary_selection_device_manager_v1_interface,
			1, NULL, HandleBind);
  all_devices.next = &all_devices;
  all_devices.last = &all_devices;
}
