// Copyright 2016 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 "services/ui/ws/drag_controller.h"

#include <utility>

#include "base/logging.h"
#include "services/ui/public/interfaces/cursor.mojom.h"
#include "services/ui/ws/drag_cursor_updater.h"
#include "services/ui/ws/drag_source.h"
#include "services/ui/ws/drag_target_connection.h"
#include "services/ui/ws/event_dispatcher.h"
#include "services/ui/ws/server_window.h"

namespace ui {
namespace ws {

struct DragController::Operation {
  OperationType type;
  uint32_t event_flags;
  gfx::Point screen_position;
};

struct DragController::WindowState {
  // Set to true once we've observed the ServerWindow* that is the key to this
  // instance in |window_state_|.
  bool observed = false;

  // If we're waiting for a response, this is the type of message. NONE means
  // there's no outstanding
  OperationType waiting_on_reply = OperationType::NONE;

  // The operation that we'll send off if |waiting_on_reply| isn't NONE.
  Operation queued_operation = {OperationType::NONE, 0, gfx::Point()};

  // The current set of operations that this window accepts. This gets updated
  // on each return message.
  DropEffectBitmask bitmask = 0u;
};

DragController::DragController(
    DragCursorUpdater* cursor_updater,
    DragSource* source,
    ServerWindow* source_window,
    DragTargetConnection* source_connection,
    int32_t drag_pointer,
    mojo::Map<mojo::String, mojo::Array<uint8_t>> mime_data,
    DropEffectBitmask drag_operations)
    : source_(source),
      cursor_updater_(cursor_updater),
      drag_operations_(drag_operations),
      drag_pointer_id_(drag_pointer),
      current_cursor_(ui::mojom::Cursor::NO_DROP),
      source_window_(source_window),
      source_connection_(source_connection),
      mime_data_(std::move(mime_data)),
      weak_factory_(this) {
  SetCurrentTargetWindow(nullptr);
  EnsureWindowObserved(source_window_);
}

DragController::~DragController() {
  for (auto& pair : window_state_) {
    if (pair.second.observed)
      pair.first->RemoveObserver(this);
  }
}

void DragController::Cancel() {
  MessageDragCompleted(false, ui::mojom::kDropEffectNone);
  // |this| may be deleted now.
}

bool DragController::DispatchPointerEvent(const ui::PointerEvent& event,
                                          ServerWindow* current_target) {
  uint32_t event_flags =
      event.flags() &
      (ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN);
  gfx::Point screen_position = event.location();

  if (waiting_for_final_drop_response_) {
    // If we're waiting on a target window to respond to the final drag drop
    // call, don't process any more pointer events.
    return false;
  }

  if (event.pointer_id() != drag_pointer_id_)
    return false;

  // If |current_target| doesn't accept drags, walk its hierarchy up until we
  // find one that does (or set to nullptr at the top of the tree).
  while (current_target && !current_target->can_accept_drops())
    current_target = current_target->parent();

  if (current_target) {
    // If we're non-null, we're about to use |current_target| in some
    // way. Ensure that we receive notifications that this window has gone
    // away.
    EnsureWindowObserved(current_target);
  }

  if (current_target && current_target == current_target_window_ &&
      event.type() != ET_POINTER_UP) {
    QueueOperation(current_target, OperationType::OVER, event_flags,
                   screen_position);
  } else if (current_target != current_target_window_) {
    if (current_target_window_) {
      QueueOperation(current_target_window_, OperationType::LEAVE, event_flags,
                     screen_position);
    }

    if (current_target) {
      // TODO(erg): If we have a queued LEAVE operation, does this turn into a
      // noop?
      QueueOperation(current_target, OperationType::ENTER, event_flags,
                     screen_position);
    }

    SetCurrentTargetWindow(current_target);
  }

  if (event.type() == ET_POINTER_UP) {
    if (current_target) {
      QueueOperation(current_target, OperationType::DROP, event_flags,
                     screen_position);
      waiting_for_final_drop_response_ = true;
    } else {
      // The pointer was released over no window or a window that doesn't
      // accept drags.
      MessageDragCompleted(false, ui::mojom::kDropEffectNone);
    }
  }

  return true;
}

void DragController::OnWillDestroyDragTargetConnection(
    DragTargetConnection* connection) {
  called_on_drag_mime_types_.erase(connection);
}

void DragController::MessageDragCompleted(bool success,
                                          DropEffect action_taken) {
  for (DragTargetConnection* connection : called_on_drag_mime_types_)
    connection->PerformOnDragDropDone();
  called_on_drag_mime_types_.clear();

  source_->OnDragCompleted(success, action_taken);
  // |this| may be deleted now.
}

size_t DragController::GetSizeOfQueueForWindow(ServerWindow* window) {
  auto it = window_state_.find(window);
  if (it == window_state_.end())
    return 0u;
  if (it->second.waiting_on_reply == OperationType::NONE)
    return 0u;
  if (it->second.queued_operation.type == OperationType::NONE)
    return 1u;
  return 2u;
}

void DragController::SetWindowDropOperations(ServerWindow* window,
                                             DropEffectBitmask bitmask) {
  WindowState& state = window_state_[window];
  state.bitmask = bitmask;

  if (current_target_window_ == window) {
    current_cursor_ = CursorForEffectBitmask(bitmask);
    cursor_updater_->OnDragCursorUpdated();
  }
}

ui::mojom::Cursor DragController::CursorForEffectBitmask(
    DropEffectBitmask bitmask) {
  DropEffectBitmask combined = bitmask & drag_operations_;
  return combined == ui::mojom::kDropEffectNone
             ? ui::mojom::Cursor::NO_DROP
             : ui::mojom::Cursor::COPY;
}

void DragController::SetCurrentTargetWindow(ServerWindow* current_target) {
  current_target_window_ = current_target;

  if (current_target_window_) {
    // Immediately set the cursor to the last known set of operations (which
    // could be none).
    WindowState& state = window_state_[current_target_window_];
    current_cursor_ = CursorForEffectBitmask(state.bitmask);
  } else {
    // Can't drop in empty areas.
    current_cursor_ = ui::mojom::Cursor::NO_DROP;
  }

  cursor_updater_->OnDragCursorUpdated();
}

void DragController::EnsureWindowObserved(ServerWindow* window) {
  if (!window)
    return;

  WindowState& state = window_state_[window];
  if (!state.observed) {
    state.observed = true;
    window->AddObserver(this);
  }
}

void DragController::QueueOperation(ServerWindow* window,
                                    OperationType type,
                                    uint32_t event_flags,
                                    const gfx::Point& screen_position) {
  // If this window doesn't have the mime data, send it.
  DragTargetConnection* connection = source_->GetDragTargetForWindow(window);
  if (connection != source_connection_ &&
      !base::ContainsKey(called_on_drag_mime_types_, connection)) {
    connection->PerformOnDragDropStart(mime_data_.Clone());
    called_on_drag_mime_types_.insert(connection);
  }

  WindowState& state = window_state_[window];
  // Set the queued operation to the incoming.
  state.queued_operation = {type, event_flags, screen_position};

  if (state.waiting_on_reply == OperationType::NONE) {
    // Send the operation immediately.
    DispatchOperation(window, &state);
  }
}

void DragController::DispatchOperation(ServerWindow* target,
                                       WindowState* state) {
  DragTargetConnection* connection = source_->GetDragTargetForWindow(target);

  DCHECK_EQ(OperationType::NONE, state->waiting_on_reply);
  Operation& op = state->queued_operation;
  switch (op.type) {
    case OperationType::NONE: {
      // NONE case to silence the compiler.
      NOTREACHED();
      break;
    }
    case OperationType::ENTER: {
      connection->PerformOnDragEnter(
          target, op.event_flags, op.screen_position, drag_operations_,
          base::Bind(&DragController::OnDragStatusCompleted,
                     weak_factory_.GetWeakPtr(), target->id()));
      state->waiting_on_reply = OperationType::ENTER;
      break;
    }
    case OperationType::OVER: {
      connection->PerformOnDragOver(
          target, op.event_flags, op.screen_position, drag_operations_,
          base::Bind(&DragController::OnDragStatusCompleted,
                     weak_factory_.GetWeakPtr(), target->id()));
      state->waiting_on_reply = OperationType::OVER;
      break;
    }
    case OperationType::LEAVE: {
      connection->PerformOnDragLeave(target);
      state->waiting_on_reply = OperationType::NONE;
      break;
    }
    case OperationType::DROP: {
      connection->PerformOnCompleteDrop(
          target, op.event_flags, op.screen_position, drag_operations_,
          base::Bind(&DragController::OnDragDropCompleted,
                     weak_factory_.GetWeakPtr(), target->id()));
      state->waiting_on_reply = OperationType::DROP;
      break;
    }
  }

  state->queued_operation = {OperationType::NONE, 0, gfx::Point()};
}

void DragController::OnRespondToOperation(ServerWindow* window) {
  WindowState& state = window_state_[window];
  DCHECK_NE(OperationType::NONE, state.waiting_on_reply);
  state.waiting_on_reply = OperationType::NONE;
  if (state.queued_operation.type != OperationType::NONE)
    DispatchOperation(window, &state);
}

void DragController::OnDragStatusCompleted(const WindowId& id,
                                           DropEffectBitmask bitmask) {
  ServerWindow* window = source_->GetWindowById(id);
  if (!window) {
    // The window has been deleted and its queue is empty.
    return;
  }

  // We must remove the completed item.
  OnRespondToOperation(window);
  SetWindowDropOperations(window, bitmask);
}

void DragController::OnDragDropCompleted(const WindowId& id,
                                         DropEffect action) {
  ServerWindow* window = source_->GetWindowById(id);
  if (!window) {
    // The window has been deleted after we sent the drop message. It's really
    // hard to recover from this so just signal to the source that our drag
    // failed.
    MessageDragCompleted(false, ui::mojom::kDropEffectNone);
    return;
  }

  OnRespondToOperation(window);
  MessageDragCompleted(action != 0u, action);
}

void DragController::OnWindowDestroying(ServerWindow* window) {
  auto it = window_state_.find(window);
  if (it != window_state_.end()) {
    window->RemoveObserver(this);
    window_state_.erase(it);
  }

  if (current_target_window_ == window)
    SetCurrentTargetWindow(nullptr);

  if (source_window_ == window) {
    source_window_ = nullptr;
    // Our source window is being deleted, fail the drag.
    MessageDragCompleted(false, ui::mojom::kDropEffectNone);
  }
}

}  // namespace ws
}  // namespace ui
