// Copyright (c) 2012 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 "remoting/host/linux/x_server_clipboard.h"

#include <X11/extensions/Xfixes.h>

#include "base/basictypes.h"
#include "base/callback.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/base/util.h"

namespace remoting {

XServerClipboard::XServerClipboard()
    : display_(NULL),
      clipboard_window_(BadValue),
      xfixes_event_base_(-1),
      clipboard_atom_(None),
      large_selection_atom_(None),
      selection_string_atom_(None),
      targets_atom_(None),
      timestamp_atom_(None),
      utf8_string_atom_(None),
      large_selection_property_(None) {
}

XServerClipboard::~XServerClipboard() {
}

void XServerClipboard::Init(Display* display,
                            const ClipboardChangedCallback& callback) {
  display_ = display;
  callback_ = callback;

  // If any of these X API calls fail, an X Error will be raised, crashing the
  // process.  This is unlikely to occur in practice, and even if it does, it
  // would mean the X server is in a bad state, so it's not worth trying to
  // trap such errors here.

  // TODO(lambroslambrou): Consider using ScopedXErrorHandler here, or consider
  // placing responsibility for handling X Errors outside this class, since
  // X Error handlers are global to all X connections.
  int xfixes_error_base;
  if (!XFixesQueryExtension(display_, &xfixes_event_base_,
                            &xfixes_error_base)) {
    HOST_LOG << "X server does not support XFixes.";
    return;
  }

  clipboard_window_ = XCreateSimpleWindow(display_,
                                          DefaultRootWindow(display_),
                                          0, 0, 1, 1,  // x, y, width, height
                                          0, 0, 0);

  // TODO(lambroslambrou): Use ui::X11AtomCache for this, either by adding a
  // dependency on ui/ or by moving X11AtomCache to base/.
  static const char* const kAtomNames[] = {
    "CLIPBOARD",
    "INCR",
    "SELECTION_STRING",
    "TARGETS",
    "TIMESTAMP",
    "UTF8_STRING"
  };
  static const int kNumAtomNames = arraysize(kAtomNames);

  Atom atoms[kNumAtomNames];
  if (XInternAtoms(display_, const_cast<char**>(kAtomNames), kNumAtomNames,
                   False, atoms)) {
    clipboard_atom_ = atoms[0];
    large_selection_atom_ = atoms[1];
    selection_string_atom_ = atoms[2];
    targets_atom_ = atoms[3];
    timestamp_atom_ = atoms[4];
    utf8_string_atom_ = atoms[5];
    COMPILE_ASSERT(kNumAtomNames >= 6, kAtomNames_too_small);
  } else {
    LOG(ERROR) << "XInternAtoms failed";
  }

  XFixesSelectSelectionInput(display_, clipboard_window_, clipboard_atom_,
                             XFixesSetSelectionOwnerNotifyMask);
}

void XServerClipboard::SetClipboard(const std::string& mime_type,
                                    const std::string& data) {
  DCHECK(display_);

  if (clipboard_window_ == BadValue)
    return;

  // Currently only UTF-8 is supported.
  if (mime_type != kMimeTypeTextUtf8)
    return;
  if (!StringIsUtf8(data.c_str(), data.length())) {
    LOG(ERROR) << "ClipboardEvent: data is not UTF-8 encoded.";
    return;
  }

  data_ = data;

  AssertSelectionOwnership(XA_PRIMARY);
  AssertSelectionOwnership(clipboard_atom_);
}

void XServerClipboard::ProcessXEvent(XEvent* event) {
  if (clipboard_window_ == BadValue ||
      event->xany.window != clipboard_window_) {
    return;
  }

  switch (event->type) {
    case PropertyNotify:
      OnPropertyNotify(event);
      break;
    case SelectionNotify:
      OnSelectionNotify(event);
      break;
    case SelectionRequest:
      OnSelectionRequest(event);
      break;
    case SelectionClear:
      OnSelectionClear(event);
      break;
    default:
      break;
  }

  if (event->type == xfixes_event_base_ + XFixesSetSelectionOwnerNotify) {
    XFixesSelectionNotifyEvent* notify_event =
        reinterpret_cast<XFixesSelectionNotifyEvent*>(event);
    OnSetSelectionOwnerNotify(notify_event->selection,
                              notify_event->selection_timestamp);
  }
}

void XServerClipboard::OnSetSelectionOwnerNotify(Atom selection,
                                                 Time timestamp) {
  // Protect against receiving new XFixes selection notifications whilst we're
  // in the middle of waiting for information from the current selection owner.
  // A reasonable timeout allows for misbehaving apps that don't respond
  // quickly to our requests.
  if (!get_selections_time_.is_null() &&
      (base::TimeTicks::Now() - get_selections_time_) <
          base::TimeDelta::FromSeconds(5)) {
    // TODO(lambroslambrou): Instead of ignoring this notification, cancel any
    // pending request operations and ignore the resulting events, before
    // dispatching new requests here.
    return;
  }

  // Only process CLIPBOARD selections.
  if (selection != clipboard_atom_)
    return;

  // If we own the selection, don't request details for it.
  if (IsSelectionOwner(selection))
    return;

  get_selections_time_ = base::TimeTicks::Now();

  // Before getting the value of the chosen selection, request the list of
  // target formats it supports.
  RequestSelectionTargets(selection);
}

void XServerClipboard::OnPropertyNotify(XEvent* event) {
  if (large_selection_property_ != None &&
      event->xproperty.atom == large_selection_property_ &&
      event->xproperty.state == PropertyNewValue) {
    Atom type;
    int format;
    unsigned long item_count, after;
    unsigned char *data;
    XGetWindowProperty(display_, clipboard_window_, large_selection_property_,
                       0, ~0L, True, AnyPropertyType, &type, &format,
                       &item_count, &after, &data);
    if (type != None) {
      // TODO(lambroslambrou): Properly support large transfers -
      // http://crbug.com/151447.
      XFree(data);

      // If the property is zero-length then the large transfer is complete.
      if (item_count == 0)
        large_selection_property_ = None;
    }
  }
}

void XServerClipboard::OnSelectionNotify(XEvent* event) {
  if (event->xselection.property != None) {
    Atom type;
    int format;
    unsigned long item_count, after;
    unsigned char *data;
    XGetWindowProperty(display_, clipboard_window_,
                       event->xselection.property, 0, ~0L, True,
                       AnyPropertyType, &type, &format,
                       &item_count, &after, &data);
    if (type == large_selection_atom_) {
      // Large selection - just read and ignore these for now.
      large_selection_property_ = event->xselection.property;
    } else {
      // Standard selection - call the selection notifier.
      large_selection_property_ = None;
      if (type != None) {
        HandleSelectionNotify(&event->xselection, type, format, item_count,
                              data);
        XFree(data);
        return;
      }
    }
  }
  HandleSelectionNotify(&event->xselection, 0, 0, 0, 0);
}

void XServerClipboard::OnSelectionRequest(XEvent* event) {
  XSelectionEvent selection_event;
  selection_event.type = SelectionNotify;
  selection_event.display = event->xselectionrequest.display;
  selection_event.requestor = event->xselectionrequest.requestor;
  selection_event.selection = event->xselectionrequest.selection;
  selection_event.time = event->xselectionrequest.time;
  selection_event.target = event->xselectionrequest.target;
  if (event->xselectionrequest.property == None)
    event->xselectionrequest.property = event->xselectionrequest.target;
  if (!IsSelectionOwner(selection_event.selection)) {
    selection_event.property = None;
  } else {
    selection_event.property = event->xselectionrequest.property;
    if (selection_event.target == targets_atom_) {
      SendTargetsResponse(selection_event.requestor, selection_event.property);
    } else if (selection_event.target == timestamp_atom_) {
      SendTimestampResponse(selection_event.requestor,
                            selection_event.property);
    } else if (selection_event.target == utf8_string_atom_ ||
               selection_event.target == XA_STRING) {
      SendStringResponse(selection_event.requestor, selection_event.property,
                         selection_event.target);
    }
  }
  XSendEvent(display_, selection_event.requestor, False, 0,
             reinterpret_cast<XEvent*>(&selection_event));
}

void XServerClipboard::OnSelectionClear(XEvent* event) {
  selections_owned_.erase(event->xselectionclear.selection);
}

void XServerClipboard::SendTargetsResponse(Window requestor, Atom property) {
  // Respond advertising XA_STRING, UTF8_STRING and TIMESTAMP data for the
  // selection.
  Atom targets[3];
  targets[0] = timestamp_atom_;
  targets[1] = utf8_string_atom_;
  targets[2] = XA_STRING;
  XChangeProperty(display_, requestor, property, XA_ATOM, 32, PropModeReplace,
                  reinterpret_cast<unsigned char*>(targets), 3);
}

void XServerClipboard::SendTimestampResponse(Window requestor, Atom property) {
  // Respond with the timestamp of our selection; we always return
  // CurrentTime since our selections are set by remote clients, so there
  // is no associated local X event.

  // TODO(lambroslambrou): Should use a proper timestamp here instead of
  // CurrentTime.  ICCCM recommends doing a zero-length property append,
  // and getting a timestamp from the subsequent PropertyNotify event.
  Time time = CurrentTime;
  XChangeProperty(display_, requestor, property, XA_INTEGER, 32,
                  PropModeReplace, reinterpret_cast<unsigned char*>(&time), 1);
}

void XServerClipboard::SendStringResponse(Window requestor, Atom property,
                                          Atom target) {
  if (!data_.empty()) {
    // Return the actual string data; we always return UTF8, regardless of
    // the configured locale.
    XChangeProperty(display_, requestor, property, target, 8, PropModeReplace,
                    reinterpret_cast<unsigned char*>(
                        const_cast<char*>(data_.data())),
                    data_.size());
  }
}

void XServerClipboard::HandleSelectionNotify(XSelectionEvent* event,
                                             Atom type,
                                             int format,
                                             int item_count,
                                             void* data) {
  bool finished = false;

  if (event->target == targets_atom_) {
    finished = HandleSelectionTargetsEvent(event, format, item_count, data);
  } else if (event->target == utf8_string_atom_ ||
             event->target == XA_STRING) {
    finished = HandleSelectionStringEvent(event, format, item_count, data);
  }

  if (finished)
    get_selections_time_ = base::TimeTicks();
}

bool XServerClipboard::HandleSelectionTargetsEvent(XSelectionEvent* event,
                                                   int format,
                                                   int item_count,
                                                   void* data) {
  if (event->property == targets_atom_) {
    if (data && format == 32) {
      // The XGetWindowProperty man-page specifies that the returned
      // property data will be an array of |long|s in the case where
      // |format| == 32.  Although the items are 32-bit values (as stored and
      // sent over the X protocol), Xlib presents the data to the client as an
      // array of |long|s, with zero-padding on a 64-bit system where |long|
      // is bigger than 32 bits.
      const long* targets = static_cast<const long*>(data);
      for (int i = 0; i < item_count; i++) {
        if (targets[i] == static_cast<long>(utf8_string_atom_)) {
          RequestSelectionString(event->selection, utf8_string_atom_);
          return false;
        }
      }
    }
  }
  RequestSelectionString(event->selection, XA_STRING);
  return false;
}

bool XServerClipboard::HandleSelectionStringEvent(XSelectionEvent* event,
                                                  int format,
                                                  int item_count,
                                                  void* data) {
  if (event->property != selection_string_atom_ || !data || format != 8)
    return true;

  std::string text(static_cast<char*>(data), item_count);

  if (event->target == XA_STRING || event->target == utf8_string_atom_)
    NotifyClipboardText(text);

  return true;
}

void XServerClipboard::NotifyClipboardText(const std::string& text) {
  data_ = text;
  callback_.Run(kMimeTypeTextUtf8, data_);
}

void XServerClipboard::RequestSelectionTargets(Atom selection) {
  XConvertSelection(display_, selection, targets_atom_, targets_atom_,
                    clipboard_window_, CurrentTime);
}

void XServerClipboard::RequestSelectionString(Atom selection, Atom target) {
  XConvertSelection(display_, selection, target, selection_string_atom_,
                    clipboard_window_, CurrentTime);
}

void XServerClipboard::AssertSelectionOwnership(Atom selection) {
  XSetSelectionOwner(display_, selection, clipboard_window_, CurrentTime);
  if (XGetSelectionOwner(display_, selection) == clipboard_window_) {
    selections_owned_.insert(selection);
  } else {
    LOG(ERROR) << "XSetSelectionOwner failed for selection " << selection;
  }
}

bool XServerClipboard::IsSelectionOwner(Atom selection) {
  return selections_owned_.find(selection) != selections_owned_.end();
}

}  // namespace remoting
