// Copyright 2018 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 "content/browser/media/capture/mouse_cursor_overlay_controller.h"

#include <cmath>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/check_op.h"

namespace content {

// static
constexpr base::TimeDelta MouseCursorOverlayController::kIdleTimeout;

void MouseCursorOverlayController::Start(
    std::unique_ptr<Overlay> overlay,
    scoped_refptr<base::SequencedTaskRunner> task_runner) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);
  DCHECK(overlay);
  DCHECK(task_runner);

  Stop();
  overlay_ = std::move(overlay);
  overlay_task_runner_ = std::move(task_runner);
}

void MouseCursorOverlayController::Stop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);

  if (overlay_) {
    overlay_task_runner_->DeleteSoon(FROM_HERE, overlay_.release());
    overlay_task_runner_ = nullptr;
  }
}

bool MouseCursorOverlayController::IsUserInteractingWithView() const {
  return mouse_move_behavior() == kRecentlyMovedOrClicked;
}

base::WeakPtr<MouseCursorOverlayController>
MouseCursorOverlayController::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void MouseCursorOverlayController::OnMouseMoved(const gfx::PointF& location) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);

  switch (mouse_move_behavior()) {
    case kNotMoving:
      set_mouse_move_behavior(kStartingToMove);
      mouse_move_start_location_ = location;
      mouse_activity_ended_timer_.Start(
          FROM_HERE, kIdleTimeout,
          base::BindOnce(&MouseCursorOverlayController::OnMouseHasGoneIdle,
                         base::Unretained(this)));
      break;
    case kStartingToMove:
      if (std::abs(location.x() - mouse_move_start_location_.x()) >
              kMinMovementPixels ||
          std::abs(location.y() - mouse_move_start_location_.y()) >
              kMinMovementPixels) {
        set_mouse_move_behavior(kRecentlyMovedOrClicked);
        mouse_activity_ended_timer_.Reset();
      }
      break;
    case kRecentlyMovedOrClicked:
      mouse_activity_ended_timer_.Reset();
      break;
  }

  if (mouse_move_behavior() == kRecentlyMovedOrClicked) {
    UpdateOverlay(location);
  }
}

void MouseCursorOverlayController::OnMouseClicked(const gfx::PointF& location) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);

  if (mouse_activity_ended_timer_.IsRunning()) {
    mouse_activity_ended_timer_.Reset();
  } else {
    mouse_activity_ended_timer_.Start(
        FROM_HERE, kIdleTimeout,
        base::BindOnce(&MouseCursorOverlayController::OnMouseHasGoneIdle,
                       base::Unretained(this)));
  }
  set_mouse_move_behavior(kRecentlyMovedOrClicked);

  UpdateOverlay(location);
}

void MouseCursorOverlayController::OnMouseHasGoneIdle() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);

  // Note that the following is not redundant since callers other than the timer
  // may have invoked this method.
  mouse_activity_ended_timer_.Stop();

  set_mouse_move_behavior(kNotMoving);

  UpdateOverlay(gfx::PointF());
}

void MouseCursorOverlayController::UpdateOverlay(const gfx::PointF& location) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);

  if (!overlay_) {
    return;
  }

  // Breaking out of the following do-block indicates one or more prerequisites
  // are not met and the cursor should be(come) hidden.
  do {
    // If the mouse has not recently moved, hide the overlay.
    if (mouse_move_behavior() != kRecentlyMovedOrClicked) {
      break;
    }

    const gfx::NativeCursor cursor = GetCurrentCursorOrDefault();
    const gfx::RectF relative_bounds =
        ComputeRelativeBoundsForOverlay(cursor, location);

    // If the cursor (and, by implication, the cursor image) has not changed,
    // just move the overlay to its new position, if any.
    if (cursor == last_cursor_) {
      if (bounds_ != relative_bounds) {
        bounds_ = relative_bounds;
        overlay_task_runner_->PostTask(
            FROM_HERE,
            base::BindOnce(&Overlay::SetBounds,
                           base::Unretained(overlay_.get()), bounds_));
      }
      return;
    }

    // The cursor image has changed. Edge-case: If the platform does not provide
    // a cursor image (e.g., this can occur at browser shutdown), just hide the
    // overlay.
    const SkBitmap cursor_image = GetCursorImage(cursor);
    if (cursor_image.drawsNothing()) {
      last_cursor_ = gfx::NativeCursor();
      break;
    }
    last_cursor_ = cursor;
    bounds_ = relative_bounds;
    overlay_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&Overlay::SetImageAndBounds,
                                  base::Unretained(overlay_.get()),
                                  cursor_image, bounds_));
    return;
  } while (false);

  // If this point has been reached, then the overlay should be hidden.
  if (!bounds_.IsEmpty()) {
    bounds_ = gfx::RectF();
    overlay_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&Overlay::SetBounds,
                                  base::Unretained(overlay_.get()), bounds_));
  }
}

}  // namespace content
