// Copyright 2015 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 "components/exo/touch.h"

#include "components/exo/surface.h"
#include "components/exo/touch_delegate.h"
#include "components/exo/touch_stylus_delegate.h"
#include "components/exo/wm_helper.h"
#include "ui/aura/window.h"
#include "ui/events/event.h"

namespace exo {
namespace {

// Helper function that returns an iterator to the first item in |vector|
// with |value|.
template <typename T, typename U>
typename T::iterator FindVectorItem(T& vector, U value) {
  return std::find(vector.begin(), vector.end(), value);
}

// Helper function that returns true if |vector| contains an item with |value|.
template <typename T, typename U>
bool VectorContainsItem(T& vector, U value) {
  return FindVectorItem(vector, value) != vector.end();
}

gfx::PointF EventLocationInWindow(ui::TouchEvent* event, aura::Window* window) {
  ui::Layer* root = window->GetRootWindow()->layer();
  ui::Layer* target = window->layer();

  gfx::Transform transform;
  target->GetTargetTransformRelativeTo(root, &transform);
  auto point = gfx::Point3F(event->root_location_f());
  transform.TransformPointReverse(&point);
  return point.AsPointF();
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// Touch, public:

Touch::Touch(TouchDelegate* delegate) : delegate_(delegate) {
  WMHelper::GetInstance()->AddPreTargetHandler(this);
}

Touch::~Touch() {
  delegate_->OnTouchDestroying(this);
  if (focus_)
    focus_->RemoveSurfaceObserver(this);
  WMHelper::GetInstance()->RemovePreTargetHandler(this);
}

void Touch::SetStylusDelegate(TouchStylusDelegate* delegate) {
  stylus_delegate_ = delegate;
}

bool Touch::HasStylusDelegate() const {
  return !!stylus_delegate_;
}

////////////////////////////////////////////////////////////////////////////////
// ui::EventHandler overrides:

void Touch::OnTouchEvent(ui::TouchEvent* event) {
  bool send_details = false;

  switch (event->type()) {
    case ui::ET_TOUCH_PRESSED: {
      // Early out if event doesn't contain a valid target for touch device.
      Surface* target = GetEffectiveTargetForEvent(event);
      if (!target)
        return;

      // If this is the first touch point then target becomes the focus surface
      // until all touch points have been released.
      if (touch_points_.empty()) {
        DCHECK(!focus_);
        focus_ = target;
        focus_->AddSurfaceObserver(this);
      }

      DCHECK(!VectorContainsItem(touch_points_, event->touch_id()));
      touch_points_.push_back(event->touch_id());

      // Convert location to focus surface coordinate space.
      DCHECK(focus_);
      gfx::PointF location = EventLocationInWindow(event, focus_->window());

      // Generate a touch down event for the focus surface. Note that this can
      // be different from the target surface.
      delegate_->OnTouchDown(focus_, event->time_stamp(), event->touch_id(),
                             location);
      if (stylus_delegate_ &&
          event->pointer_details().pointer_type !=
              ui::EventPointerType::POINTER_TYPE_TOUCH) {
        stylus_delegate_->OnTouchTool(event->touch_id(),
                                      event->pointer_details().pointer_type);
      }
      send_details = true;
    } break;
    case ui::ET_TOUCH_RELEASED: {
      auto it = FindVectorItem(touch_points_, event->touch_id());
      if (it == touch_points_.end())
        return;
      touch_points_.erase(it);

      // Reset focus surface if this is the last touch point.
      if (touch_points_.empty()) {
        DCHECK(focus_);
        focus_->RemoveSurfaceObserver(this);
        focus_ = nullptr;
      }

      delegate_->OnTouchUp(event->time_stamp(), event->touch_id());
    } break;
    case ui::ET_TOUCH_MOVED: {
      auto it = FindVectorItem(touch_points_, event->touch_id());
      if (it == touch_points_.end())
        return;

      DCHECK(focus_);
      // Convert location to focus surface coordinate space.
      gfx::PointF location = EventLocationInWindow(event, focus_->window());
      delegate_->OnTouchMotion(event->time_stamp(), event->touch_id(),
                               location);
      send_details = true;
    } break;
    case ui::ET_TOUCH_CANCELLED: {
      auto it = FindVectorItem(touch_points_, event->touch_id());
      if (it == touch_points_.end())
        return;

      DCHECK(focus_);
      focus_->RemoveSurfaceObserver(this);
      focus_ = nullptr;

      // Cancel the full set of touch sequences as soon as one is canceled.
      touch_points_.clear();
      delegate_->OnTouchCancel();
    } break;
    default:
      NOTREACHED();
      return;
  }
  if (send_details) {
    delegate_->OnTouchShape(event->touch_id(),
                            event->pointer_details().radius_x,
                            event->pointer_details().radius_y);

    if (stylus_delegate_ &&
        event->pointer_details().pointer_type !=
            ui::EventPointerType::POINTER_TYPE_TOUCH) {
      if (!std::isnan(event->pointer_details().force)) {
        stylus_delegate_->OnTouchForce(event->time_stamp(), event->touch_id(),
                                       event->pointer_details().force);
      }
      stylus_delegate_->OnTouchTilt(
          event->time_stamp(), event->touch_id(),
          gfx::Vector2dF(event->pointer_details().tilt_x,
                         event->pointer_details().tilt_y));
    }
  }
  // TODO(denniskempin): Extend ui::TouchEvent to signal end of sequence of
  // touch events to send TouchFrame once after all touches have been updated.
  delegate_->OnTouchFrame();
}

////////////////////////////////////////////////////////////////////////////////
// SurfaceObserver overrides:

void Touch::OnSurfaceDestroying(Surface* surface) {
  DCHECK(surface == focus_);
  focus_ = nullptr;
  surface->RemoveSurfaceObserver(this);

  // Cancel touch sequences.
  DCHECK_NE(touch_points_.size(), 0u);
  touch_points_.clear();
  delegate_->OnTouchCancel();
}

////////////////////////////////////////////////////////////////////////////////
// Touch, private:

Surface* Touch::GetEffectiveTargetForEvent(ui::Event* event) const {
  Surface* target =
      Surface::AsSurface(static_cast<aura::Window*>(event->target()));
  if (!target)
    return nullptr;

  return delegate_->CanAcceptTouchEventsForSurface(target) ? target : nullptr;
}

}  // namespace exo
