// 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_requestor.h"

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

#include "base/run_loop.h"
#include "ui/base/x/selection_utils.h"
#include "ui/base/x/x11_util.h"
#include "ui/events/platform/platform_event_dispatcher.h"
#include "ui/events/platform/platform_event_source.h"
#include "ui/gfx/x/x11_types.h"

namespace ui {

namespace {

const char kChromeSelection[] = "CHROME_SELECTION";
const char kIncr[] = "INCR";

const char* kAtomsToCache[] = {
  kChromeSelection,
  kIncr,
  NULL
};

// The period of |abort_timer_|. Arbitrary but must be <= than
// kRequestTimeoutMs.
const int kTimerPeriodMs = 100;

// The amount of time to wait for a request to complete before aborting it.
const int kRequestTimeoutMs = 10000;

static_assert(kTimerPeriodMs <= kRequestTimeoutMs,
              "timer period must be <= request timeout");

// Combines |data| into a single RefCountedMemory object.
scoped_refptr<base::RefCountedMemory> CombineRefCountedMemory(
    const std::vector<scoped_refptr<base::RefCountedMemory> >& data) {
  if (data.size() == 1u)
    return data[0];

  size_t length = 0;
  for (size_t i = 0; i < data.size(); ++i)
    length += data[i]->size();
  std::vector<unsigned char> combined_data;
  combined_data.reserve(length);

  for (size_t i = 0; i < data.size(); ++i) {
    combined_data.insert(combined_data.end(),
                         data[i]->front(),
                         data[i]->front() + data[i]->size());
  }
  return base::RefCountedBytes::TakeVector(&combined_data);
}

}  // namespace

SelectionRequestor::SelectionRequestor(XDisplay* x_display,
                                       XID x_window,
                                       PlatformEventDispatcher* dispatcher)
    : x_display_(x_display),
      x_window_(x_window),
      x_property_(None),
      dispatcher_(dispatcher),
      current_request_index_(0u),
      atom_cache_(x_display_, kAtomsToCache) {
  x_property_ = atom_cache_.GetAtom(kChromeSelection);
}

SelectionRequestor::~SelectionRequestor() {}

bool SelectionRequestor::PerformBlockingConvertSelection(
    XAtom selection,
    XAtom target,
    scoped_refptr<base::RefCountedMemory>* out_data,
    size_t* out_data_items,
    XAtom* out_type) {
  base::TimeTicks timeout =
      base::TimeTicks::Now() +
      base::TimeDelta::FromMilliseconds(kRequestTimeoutMs);
  Request request(selection, target, timeout);
  requests_.push_back(&request);
  if (current_request_index_ == (requests_.size() - 1))
    ConvertSelectionForCurrentRequest();
  BlockTillSelectionNotifyForRequest(&request);

  std::vector<Request*>::iterator request_it = std::find(
      requests_.begin(), requests_.end(), &request);
  CHECK(request_it != requests_.end());
  if (static_cast<int>(current_request_index_) >
      request_it - requests_.begin()) {
    --current_request_index_;
  }
  requests_.erase(request_it);

  if (requests_.empty())
    abort_timer_.Stop();

  if (request.success) {
    if (out_data)
      *out_data = CombineRefCountedMemory(request.out_data);
    if (out_data_items)
      *out_data_items = request.out_data_items;
    if (out_type)
      *out_type = request.out_type;
  }
  return request.success;
}

void SelectionRequestor::PerformBlockingConvertSelectionWithParameter(
    XAtom selection,
    XAtom target,
    const std::vector<XAtom>& parameter) {
  SetAtomArrayProperty(x_window_, kChromeSelection, "ATOM", parameter);
  PerformBlockingConvertSelection(selection, target, NULL, NULL, NULL);
}

SelectionData SelectionRequestor::RequestAndWaitForTypes(
    XAtom selection,
    const std::vector<XAtom>& types) {
  for (std::vector<XAtom>::const_iterator it = types.begin();
       it != types.end(); ++it) {
    scoped_refptr<base::RefCountedMemory> data;
    XAtom type = None;
    if (PerformBlockingConvertSelection(selection,
                                        *it,
                                        &data,
                                        NULL,
                                        &type) &&
        type == *it) {
      return SelectionData(type, data);
    }
  }

  return SelectionData();
}

void SelectionRequestor::OnSelectionNotify(const XEvent& event) {
  Request* request = GetCurrentRequest();
  XAtom event_property = event.xselection.property;
  if (!request ||
      request->completed ||
      request->selection != event.xselection.selection ||
      request->target != event.xselection.target) {
    // ICCCM requires us to delete the property passed into SelectionNotify.
    if (event_property != None)
      XDeleteProperty(x_display_, x_window_, event_property);
    return;
  }

  bool success = false;
  if (event_property == x_property_) {
    scoped_refptr<base::RefCountedMemory> out_data;
    success = ui::GetRawBytesOfProperty(x_window_,
                                        x_property_,
                                        &out_data,
                                        &request->out_data_items,
                                        &request->out_type);
    if (success) {
      request->out_data.clear();
      request->out_data.push_back(out_data);
    }
  }
  if (event_property != None)
    XDeleteProperty(x_display_, x_window_, event_property);

  if (request->out_type == atom_cache_.GetAtom(kIncr)) {
    request->data_sent_incrementally = true;
    request->out_data.clear();
    request->out_data_items = 0u;
    request->out_type = None;
    request->timeout = base::TimeTicks::Now() +
        base::TimeDelta::FromMilliseconds(kRequestTimeoutMs);
  } else {
    CompleteRequest(current_request_index_, success);
  }
}

bool SelectionRequestor::CanDispatchPropertyEvent(const XEvent& event) {
  return event.xproperty.window == x_window_ &&
      event.xproperty.atom == x_property_ &&
      event.xproperty.state == PropertyNewValue;
}

void SelectionRequestor::OnPropertyEvent(const XEvent& event) {
  Request* request = GetCurrentRequest();
  if (!request || !request->data_sent_incrementally)
    return;

  scoped_refptr<base::RefCountedMemory> out_data;
  size_t out_data_items = 0u;
  Atom out_type = None;
  bool success = ui::GetRawBytesOfProperty(x_window_,
                                           x_property_,
                                           &out_data,
                                           &out_data_items,
                                           &out_type);
  if (!success) {
    CompleteRequest(current_request_index_, false);
    return;
  }

  if (request->out_type != None && request->out_type != out_type) {
    CompleteRequest(current_request_index_, false);
    return;
  }

  request->out_data.push_back(out_data);
  request->out_data_items += out_data_items;
  request->out_type = out_type;

  // Delete the property to tell the selection owner to send the next chunk.
  XDeleteProperty(x_display_, x_window_, x_property_);

  request->timeout = base::TimeTicks::Now() +
      base::TimeDelta::FromMilliseconds(kRequestTimeoutMs);

  if (out_data->size() == 0u)
    CompleteRequest(current_request_index_, true);
}

void SelectionRequestor::AbortStaleRequests() {
  base::TimeTicks now = base::TimeTicks::Now();
  for (size_t i = current_request_index_; i < requests_.size(); ++i) {
    if (requests_[i]->timeout <= now)
      CompleteRequest(i, false);
  }
}

void SelectionRequestor::CompleteRequest(size_t index, bool success) {
   if (index >= requests_.size())
     return;

  Request* request = requests_[index];
  if (request->completed)
    return;
  request->success = success;
  request->completed = true;

  if (index == current_request_index_) {
    while (GetCurrentRequest() && GetCurrentRequest()->completed)
      ++current_request_index_;
    ConvertSelectionForCurrentRequest();
  }

  if (!request->quit_closure.is_null())
    request->quit_closure.Run();
}

void SelectionRequestor::ConvertSelectionForCurrentRequest() {
  Request* request = GetCurrentRequest();
  if (request) {
    XConvertSelection(x_display_,
                      request->selection,
                      request->target,
                      x_property_,
                      x_window_,
                      CurrentTime);
  }
}

void SelectionRequestor::BlockTillSelectionNotifyForRequest(Request* request) {
  if (PlatformEventSource::GetInstance()) {
    if (!abort_timer_.IsRunning()) {
      abort_timer_.Start(FROM_HERE,
                         base::TimeDelta::FromMilliseconds(kTimerPeriodMs),
                         this,
                         &SelectionRequestor::AbortStaleRequests);
    }

    base::MessageLoop::ScopedNestableTaskAllower allow_nested(
        base::MessageLoopForUI::current());
    base::RunLoop run_loop;
    request->quit_closure = run_loop.QuitClosure();
    run_loop.Run();

    // We cannot put logic to process the next request here because the RunLoop
    // might be nested. For instance, request 'B' may start a RunLoop while the
    // RunLoop for request 'A' is running. It is not possible to end the RunLoop
    // for request 'A' without first ending the RunLoop for request 'B'.
  } else {
    // This occurs if PerformBlockingConvertSelection() is called during
    // shutdown and the PlatformEventSource has already been destroyed.
    while (!request->completed &&
           request->timeout > base::TimeTicks::Now()) {
      if (XPending(x_display_)) {
        XEvent event;
        XNextEvent(x_display_, &event);
        dispatcher_->DispatchEvent(&event);
      }
    }
  }
}

SelectionRequestor::Request* SelectionRequestor::GetCurrentRequest() {
  return current_request_index_ == requests_.size() ?
      NULL : requests_[current_request_index_];
}

SelectionRequestor::Request::Request(XAtom selection,
                                     XAtom target,
                                     base::TimeTicks timeout)
    : selection(selection),
      target(target),
      data_sent_incrementally(false),
      out_data_items(0u),
      out_type(None),
      success(false),
      timeout(timeout),
      completed(false) {
}

SelectionRequestor::Request::~Request() {
}

}  // namespace ui
