// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/xr/xr_canvas_input_provider.h"

#include "device/vr/public/mojom/vr_service.mojom-blink.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/events/pointer_event.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
#include "third_party/blink/renderer/modules/xr/xr_frame_provider.h"
#include "third_party/blink/renderer/modules/xr/xr_input_source.h"
#include "third_party/blink/renderer/modules/xr/xr_session.h"
#include "third_party/blink/renderer/modules/xr/xr_system.h"
#include "third_party/blink/renderer/modules/xr/xr_view.h"

namespace blink {

namespace {

class XRCanvasInputEventListener : public NativeEventListener {
 public:
  explicit XRCanvasInputEventListener(XRCanvasInputProvider* input_provider)
      : input_provider_(input_provider) {}

  void Invoke(ExecutionContext* execution_context, Event* event) override {
    if (!input_provider_->ShouldProcessEvents())
      return;

    if (!event->isTrusted()) {
      return;
    }

    auto* pointer_event = To<PointerEvent>(event);
    DCHECK(pointer_event);
    if (!pointer_event->isPrimary())
      return;

    if (event->type() == event_type_names::kPointerdown) {
      input_provider_->OnPointerDown(pointer_event);
    } else if (event->type() == event_type_names::kPointerup ||
               event->type() == event_type_names::kPointercancel) {
      input_provider_->OnPointerUp(pointer_event);
    }
  }

  void Trace(Visitor* visitor) const override {
    visitor->Trace(input_provider_);
    EventListener::Trace(visitor);
  }

 private:
  Member<XRCanvasInputProvider> input_provider_;
};

}  // namespace

XRCanvasInputProvider::XRCanvasInputProvider(XRSession* session,
                                             HTMLCanvasElement* canvas)
    : session_(session), canvas_(canvas) {
  listener_ = MakeGarbageCollected<XRCanvasInputEventListener>(this);
  canvas->addEventListener(event_type_names::kPointerdown, listener_,
                           /*use_capture=*/false);
  canvas->addEventListener(event_type_names::kPointerup, listener_,
                           /*use_capture=*/false);
  canvas->addEventListener(event_type_names::kPointercancel, listener_,
                           /*use_capture=*/false);
}

XRCanvasInputProvider::~XRCanvasInputProvider() {}

void XRCanvasInputProvider::Stop() {
  if (!listener_) {
    return;
  }
  canvas_->removeEventListener(event_type_names::kPointerdown, listener_,
                               /*use_capture=*/false);
  canvas_->removeEventListener(event_type_names::kPointerup, listener_,
                               /*use_capture=*/false);
  canvas_->removeEventListener(event_type_names::kPointercancel, listener_,
                               /*use_capture=*/false);
  canvas_ = nullptr;
  listener_ = nullptr;
}

bool XRCanvasInputProvider::ShouldProcessEvents() {
  // Don't process canvas gestures if there's an active immersive session.
  return !(session_->xr()->frameProvider()->immersive_session());
}

void XRCanvasInputProvider::OnPointerDown(PointerEvent* event) {
  UpdateInputSource(event);
  input_source_->OnSelectStart();
}

void XRCanvasInputProvider::OnPointerUp(PointerEvent* event) {
  UpdateInputSource(event);
  input_source_->OnSelect();
  ClearInputSource();
}

XRInputSource* XRCanvasInputProvider::GetInputSource() {
  return input_source_.Get();
}

void XRCanvasInputProvider::UpdateInputSource(PointerEvent* event) {
  if (!canvas_)
    return;

  if (!input_source_) {
    // XRSession doesn't like source ID's of 0.  We should only be processing
    // Canvas Input events in non-immersive sessions anyway, where we don't
    // expect other controllers, so this number is somewhat arbitrary anyway.
    input_source_ = MakeGarbageCollected<XRInputSource>(
        session_, 1, device::mojom::XRTargetRayMode::TAPPING);
    session_->AddTransientInputSource(input_source_);
  }

  // Get the event location relative to the canvas element.
  double element_x = event->pageX() - canvas_->OffsetLeft();
  double element_y = event->pageY() - canvas_->OffsetTop();

  // Unproject the event location into a pointer matrix. This takes the 2D
  // position of the screen interaction and shoves it backwards through the
  // projection matrix to get a 3D point in space, which is then returned in
  // matrix form so we can use it as an XRInputSource's pointerMatrix.
  XRViewData* view =
      session_->ViewDataForEye(device::mojom::blink::XREye::kNone);
  gfx::Transform viewer_from_pointer = view->UnprojectPointer(
      element_x, element_y, canvas_->OffsetWidth(), canvas_->OffsetHeight());

  // Update the pointer pose in input space. For screen tapping, input
  // space is equivalent to viewer space.
  input_source_->SetInputFromPointer(&viewer_from_pointer);
}

void XRCanvasInputProvider::ClearInputSource() {
  session_->RemoveTransientInputSource(input_source_);
  input_source_ = nullptr;
}

void XRCanvasInputProvider::Trace(Visitor* visitor) const {
  visitor->Trace(session_);
  visitor->Trace(canvas_);
  visitor->Trace(listener_);
  visitor->Trace(input_source_);
}

}  // namespace blink
