// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/x/selection_owner.h"

#include <algorithm>
#include <X11/Xlib.h>
#include <X11/Xatom.h>

#include "base/logging.h"
#include "ui/base/x/selection_utils.h"
#include "ui/base/x/x11_util.h"
#include "ui/base/x/x11_window_event_manager.h"
#include "ui/events/platform/x11/x11_event_source.h"

namespace ui {

namespace {

const char kAtomPair[] = "ATOM_PAIR";
const char kIncr[] = "INCR";
const char kMultiple[] = "MULTIPLE";
const char kSaveTargets[] = "SAVE_TARGETS";
const char kTargets[] = "TARGETS";
const char kTimestamp[] = "TIMESTAMP";

const char* kAtomsToCache[] = {kAtomPair, kIncr,      kMultiple, kSaveTargets,
                               kTargets,  kTimestamp, NULL};

// The period of |incremental_transfer_abort_timer_|. Arbitrary but must be <=
// than kIncrementalTransferTimeoutMs.
const int kTimerPeriodMs = 1000;

// The amount of time to wait for the selection requestor to process the data
// sent by the selection owner before aborting an incremental data transfer.
const int kIncrementalTransferTimeoutMs = 10000;

static_assert(kTimerPeriodMs <= kIncrementalTransferTimeoutMs,
              "timer period must be <= transfer timeout");

// Returns a conservative max size of the data we can pass into
// XChangeProperty(). Copied from GTK.
size_t GetMaxRequestSize(XDisplay* display) {
  long extended_max_size = XExtendedMaxRequestSize(display);
  long max_size =
      (extended_max_size ? extended_max_size : XMaxRequestSize(display)) - 100;
  return std::min(static_cast<long>(0x40000),
                  std::max(static_cast<long>(0), max_size));
}

// Gets the value of an atom pair array property. On success, true is returned
// and the value is stored in |value|.
bool GetAtomPairArrayProperty(XID window,
                              XAtom property,
                              std::vector<std::pair<XAtom,XAtom> >* value) {
  XAtom type = None;
  int format = 0;  // size in bits of each item in 'property'
  unsigned long num_items = 0;
  unsigned char* properties = NULL;
  unsigned long remaining_bytes = 0;

  int result = XGetWindowProperty(gfx::GetXDisplay(),
                                  window,
                                  property,
                                  0,          // offset into property data to
                                              // read
                                  (~0L),      // entire array
                                  False,      // deleted
                                  AnyPropertyType,
                                  &type,
                                  &format,
                                  &num_items,
                                  &remaining_bytes,
                                  &properties);
  gfx::XScopedPtr<unsigned char> scoped_properties(properties);

  if (result != Success)
    return false;

  // GTK does not require |type| to be kAtomPair.
  if (format != 32 || num_items % 2 != 0)
    return false;

  XAtom* atom_properties = reinterpret_cast<XAtom*>(properties);
  value->clear();
  for (size_t i = 0; i < num_items; i+=2)
    value->push_back(std::make_pair(atom_properties[i], atom_properties[i+1]));
  return true;
}

}  // namespace

SelectionOwner::SelectionOwner(XDisplay* x_display,
                               XID x_window,
                               XAtom selection_name)
    : x_display_(x_display),
      x_window_(x_window),
      selection_name_(selection_name),
      max_request_size_(GetMaxRequestSize(x_display)),
      atom_cache_(x_display_, kAtomsToCache) {
}

SelectionOwner::~SelectionOwner() {
  // If we are the selection owner, we need to release the selection so we
  // don't receive further events. However, we don't call ClearSelectionOwner()
  // because we don't want to do this indiscriminately.
  if (XGetSelectionOwner(x_display_, selection_name_) == x_window_)
    XSetSelectionOwner(x_display_, selection_name_, None, CurrentTime);
}

void SelectionOwner::RetrieveTargets(std::vector<XAtom>* targets) {
  for (SelectionFormatMap::const_iterator it = format_map_.begin();
       it != format_map_.end(); ++it) {
    targets->push_back(it->first);
  }
}

void SelectionOwner::TakeOwnershipOfSelection(
    const SelectionFormatMap& data) {
  acquired_selection_timestamp_ = X11EventSource::GetInstance()->GetTimestamp();
  XSetSelectionOwner(x_display_, selection_name_, x_window_,
                     acquired_selection_timestamp_);

  if (XGetSelectionOwner(x_display_, selection_name_) == x_window_) {
    // The X server agrees that we are the selection owner. Commit our data.
    format_map_ = data;
  }
}

void SelectionOwner::ClearSelectionOwner() {
  XSetSelectionOwner(x_display_, selection_name_, None, CurrentTime);
  format_map_ = SelectionFormatMap();
}

void SelectionOwner::OnSelectionRequest(const XEvent& event) {
  XID requestor = event.xselectionrequest.requestor;
  XAtom requested_target = event.xselectionrequest.target;
  XAtom requested_property = event.xselectionrequest.property;

  // Incrementally build our selection. By default this is a refusal, and we'll
  // override the parts indicating success in the different cases.
  XEvent reply;
  reply.xselection.type = SelectionNotify;
  reply.xselection.requestor = requestor;
  reply.xselection.selection = event.xselectionrequest.selection;
  reply.xselection.target = requested_target;
  reply.xselection.property = None;  // Indicates failure
  reply.xselection.time = event.xselectionrequest.time;

  if (requested_target == atom_cache_.GetAtom(kMultiple)) {
    // The contents of |requested_property| should be a list of
    // <target,property> pairs.
    std::vector<std::pair<XAtom,XAtom> > conversions;
    if (GetAtomPairArrayProperty(requestor,
                                 requested_property,
                                 &conversions)) {
      std::vector<XAtom> conversion_results;
      for (size_t i = 0; i < conversions.size(); ++i) {
        bool conversion_successful = ProcessTarget(conversions[i].first,
                                                   requestor,
                                                   conversions[i].second);
        conversion_results.push_back(conversions[i].first);
        conversion_results.push_back(
            conversion_successful ? conversions[i].second : None);
      }

      // Set the property to indicate which conversions succeeded. This matches
      // what GTK does.
      XChangeProperty(
          x_display_,
          requestor,
          requested_property,
          atom_cache_.GetAtom(kAtomPair),
          32,
          PropModeReplace,
          reinterpret_cast<const unsigned char*>(&conversion_results.front()),
          conversion_results.size());

      reply.xselection.property = requested_property;
    }
  } else {
    if (ProcessTarget(requested_target, requestor, requested_property))
      reply.xselection.property = requested_property;
  }

  // Send off the reply.
  XSendEvent(x_display_, requestor, False, 0, &reply);
}

void SelectionOwner::OnSelectionClear(const XEvent& event) {
  DLOG(ERROR) << "SelectionClear";

  // TODO(erg): If we receive a SelectionClear event while we're handling data,
  // we need to delay clearing.
}

bool SelectionOwner::CanDispatchPropertyEvent(const XEvent& event) {
  return event.xproperty.state == PropertyDelete &&
         FindIncrementalTransferForEvent(event) != incremental_transfers_.end();
}

void SelectionOwner::OnPropertyEvent(const XEvent& event) {
  std::vector<IncrementalTransfer>::iterator it =
      FindIncrementalTransferForEvent(event);
  if (it == incremental_transfers_.end())
    return;

  ProcessIncrementalTransfer(&(*it));
  if (!it->data.get())
    CompleteIncrementalTransfer(it);
}

bool SelectionOwner::ProcessTarget(XAtom target,
                                   XID requestor,
                                   XAtom property) {
  XAtom multiple_atom = atom_cache_.GetAtom(kMultiple);
  XAtom save_targets_atom = atom_cache_.GetAtom(kSaveTargets);
  XAtom targets_atom = atom_cache_.GetAtom(kTargets);
  XAtom timestamp_atom = atom_cache_.GetAtom(kTimestamp);

  if (target == multiple_atom || target == save_targets_atom)
    return false;

  if (target == timestamp_atom) {
    XChangeProperty(
        x_display_, requestor, property, XA_INTEGER, 32, PropModeReplace,
        reinterpret_cast<unsigned char*>(&acquired_selection_timestamp_), 1);
    return true;
  }

  if (target == targets_atom) {
    // We have been asked for TARGETS. Send an atom array back with the data
    // types we support.
    std::vector<XAtom> targets;
    targets.push_back(timestamp_atom);
    targets.push_back(targets_atom);
    targets.push_back(save_targets_atom);
    targets.push_back(multiple_atom);
    RetrieveTargets(&targets);

    XChangeProperty(x_display_, requestor, property, XA_ATOM, 32,
                    PropModeReplace,
                    reinterpret_cast<unsigned char*>(&targets.front()),
                    targets.size());
    return true;
  }

  // Try to find the data type in map.
  SelectionFormatMap::const_iterator it = format_map_.find(target);
  if (it != format_map_.end()) {
    if (it->second->size() > max_request_size_) {
      // We must send the data back in several chunks due to a limitation in
      // the size of X requests. Notify the selection requestor that the data
      // will be sent incrementally by returning data of type "INCR".
      long length = it->second->size();
      XChangeProperty(x_display_,
                      requestor,
                      property,
                      atom_cache_.GetAtom(kIncr),
                      32,
                      PropModeReplace,
                      reinterpret_cast<unsigned char*>(&length),
                      1);

      // Wait for the selection requestor to indicate that it has processed
      // the selection result before sending the first chunk of data. The
      // selection requestor indicates this by deleting |property|.
      base::TimeTicks timeout =
          base::TimeTicks::Now() +
          base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs);
      requestor_events_.reset(
          new ui::XScopedEventSelector(requestor, PropertyChangeMask));
      incremental_transfers_.push_back(IncrementalTransfer(
          requestor, target, property, it->second, 0, timeout));

      // Start a timer to abort the data transfer in case that the selection
      // requestor does not support the INCR property or gets destroyed during
      // the data transfer.
      if (!incremental_transfer_abort_timer_.IsRunning()) {
        incremental_transfer_abort_timer_.Start(
            FROM_HERE,
            base::TimeDelta::FromMilliseconds(kTimerPeriodMs),
            this,
            &SelectionOwner::AbortStaleIncrementalTransfers);
      }
    } else {
      XChangeProperty(
          x_display_,
          requestor,
          property,
          target,
          8,
          PropModeReplace,
          const_cast<unsigned char*>(it->second->front()),
          it->second->size());
    }
    return true;
  }

  // I would put error logging here, but GTK ignores TARGETS and spams us
  // looking for its own internal types.
  return false;
}

void SelectionOwner::ProcessIncrementalTransfer(IncrementalTransfer* transfer) {
  size_t remaining = transfer->data->size() - transfer->offset;
  size_t chunk_length = std::min(remaining, max_request_size_);
  XChangeProperty(
      x_display_,
      transfer->window,
      transfer->property,
      transfer->target,
      8,
      PropModeReplace,
      const_cast<unsigned char*>(transfer->data->front() + transfer->offset),
      chunk_length);
  transfer->offset += chunk_length;
  transfer->timeout = base::TimeTicks::Now() +
      base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs);

  // When offset == data->size(), we still need to transfer a zero-sized chunk
  // to notify the selection requestor that the transfer is complete. Clear
  // transfer->data once the zero-sized chunk is sent to indicate that state
  // related to this data transfer can be cleared.
  if (chunk_length == 0)
    transfer->data = NULL;
}

void SelectionOwner::AbortStaleIncrementalTransfers() {
  base::TimeTicks now = base::TimeTicks::Now();
  for (int i = static_cast<int>(incremental_transfers_.size()) - 1;
       i >= 0; --i) {
    if (incremental_transfers_[i].timeout <= now)
      CompleteIncrementalTransfer(incremental_transfers_.begin() + i);
  }
}

void SelectionOwner::CompleteIncrementalTransfer(
    std::vector<IncrementalTransfer>::iterator it) {
  requestor_events_.reset();

  incremental_transfers_.erase(it);

  if (incremental_transfers_.empty())
    incremental_transfer_abort_timer_.Stop();
}

std::vector<SelectionOwner::IncrementalTransfer>::iterator
    SelectionOwner::FindIncrementalTransferForEvent(const XEvent& event) {
  for (std::vector<IncrementalTransfer>::iterator it =
           incremental_transfers_.begin();
       it != incremental_transfers_.end();
       ++it) {
    if (it->window == event.xproperty.window &&
        it->property == event.xproperty.atom) {
      return it;
    }
  }
  return incremental_transfers_.end();
}

SelectionOwner::IncrementalTransfer::IncrementalTransfer(
    XID window,
    XAtom target,
    XAtom property,
    const scoped_refptr<base::RefCountedMemory>& data,
    int offset,
    base::TimeTicks timeout)
    : window(window),
      target(target),
      property(property),
      data(data),
      offset(offset),
      timeout(timeout) {}

SelectionOwner::IncrementalTransfer::IncrementalTransfer(
    const IncrementalTransfer& other) = default;

SelectionOwner::IncrementalTransfer::~IncrementalTransfer() {
}

}  // namespace ui
