// Copyright (c) 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 "ui/gfx/paint_throbber.h"

#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/skia_util.h"

namespace gfx {

namespace {

// The maximum size of the "spinning" state arc, in degrees.
const int64_t kMaxArcSize = 270;

// The amount of time it takes to grow the "spinning" arc from 0 to 270 degrees.
const int64_t kArcTimeMs = 666;

// The amount of time it takes for the "spinning" throbber to make a full
// rotation.
const int64_t kRotationTimeMs = 1568;

void PaintArc(Canvas* canvas,
              const Rect& bounds,
              SkColor color,
              SkScalar start_angle,
              SkScalar sweep) {
  // Stroke width depends on size.
  // . For size < 28:          3 - (28 - size) / 16
  // . For 28 <= size:         (8 + size) / 12
  SkScalar stroke_width = bounds.width() < 28
                              ? 3.0 - SkIntToScalar(28 - bounds.width()) / 16.0
                              : SkIntToScalar(bounds.width() + 8) / 12.0;
  Rect oval = bounds;
  // Inset by half the stroke width to make sure the whole arc is inside
  // the visible rect.
  int inset = SkScalarCeilToInt(stroke_width / 2.0);
  oval.Inset(inset, inset);

  SkPath path;
  path.arcTo(RectToSkRect(oval), start_angle, sweep, true);

  cc::PaintFlags flags;
  flags.setColor(color);
  flags.setStrokeCap(cc::PaintFlags::kRound_Cap);
  flags.setStrokeWidth(stroke_width);
  flags.setStyle(cc::PaintFlags::kStroke_Style);
  flags.setAntiAlias(true);
  canvas->DrawPath(path, flags);
}

void CalculateWaitingAngles(const base::TimeDelta& elapsed_time,
                            int64_t* start_angle,
                            int64_t* sweep) {
  // Calculate start and end points. The angles are counter-clockwise because
  // the throbber spins counter-clockwise. The finish angle starts at 12 o'clock
  // (90 degrees) and rotates steadily. The start angle trails 180 degrees
  // behind, except for the first half revolution, when it stays at 12 o'clock.
  base::TimeDelta revolution_time = base::TimeDelta::FromMilliseconds(1320);
  int64_t twelve_oclock = 90;
  int64_t finish_angle_cc =
      twelve_oclock + 360 * elapsed_time / revolution_time;
  int64_t start_angle_cc = std::max(finish_angle_cc - 180, twelve_oclock);

  // Negate the angles to convert to the clockwise numbers Skia expects.
  if (start_angle)
    *start_angle = -finish_angle_cc;
  if (sweep)
    *sweep = finish_angle_cc - start_angle_cc;
}

// This is a Skia port of the MD spinner SVG. The |start_angle| rotation
// here corresponds to the 'rotate' animation.
void PaintThrobberSpinningWithStartAngle(Canvas* canvas,
                                         const Rect& bounds,
                                         SkColor color,
                                         const base::TimeDelta& elapsed_time,
                                         int64_t start_angle) {
  // The sweep angle ranges from -270 to 270 over 1333ms. CSS
  // animation timing functions apply in between key frames, so we have to
  // break up the 1333ms into two keyframes (-270 to 0, then 0 to 270).
  base::TimeDelta arc_time = base::TimeDelta::FromMilliseconds(kArcTimeMs);
  double arc_size_progress = static_cast<double>(elapsed_time.InMicroseconds() %
                                                 arc_time.InMicroseconds()) /
                             arc_time.InMicroseconds();
  // This tween is equivalent to cubic-bezier(0.4, 0.0, 0.2, 1).
  double sweep = kMaxArcSize * Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN,
                                                     arc_size_progress);
  int64_t sweep_keyframe = (elapsed_time / arc_time) % 2;
  if (sweep_keyframe == 0)
    sweep -= kMaxArcSize;

  // This part makes sure the sweep is at least 5 degrees long. Roughly
  // equivalent to the "magic constants" in SVG's fillunfill animation.
  const double min_sweep_length = 5.0;
  if (sweep >= 0.0 && sweep < min_sweep_length) {
    start_angle -= (min_sweep_length - sweep);
    sweep = min_sweep_length;
  } else if (sweep <= 0.0 && sweep > -min_sweep_length) {
    start_angle += (-min_sweep_length - sweep);
    sweep = -min_sweep_length;
  }

  // To keep the sweep smooth, we have an additional rotation after each
  // |arc_time| period has elapsed. See SVG's 'rot' animation.
  int64_t rot_keyframe = (elapsed_time / (arc_time * 2)) % 4;
  PaintArc(canvas, bounds, color, start_angle + rot_keyframe * kMaxArcSize,
           sweep);
}

}  // namespace

void PaintThrobberSpinning(Canvas* canvas,
                           const Rect& bounds,
                           SkColor color,
                           const base::TimeDelta& elapsed_time) {
  base::TimeDelta rotation_time =
      base::TimeDelta::FromMilliseconds(kRotationTimeMs);
  int64_t start_angle = 270 + 360 * elapsed_time / rotation_time;
  PaintThrobberSpinningWithStartAngle(canvas, bounds, color, elapsed_time,
                                      start_angle);
}

void PaintThrobberWaiting(Canvas* canvas,
    const Rect& bounds, SkColor color, const base::TimeDelta& elapsed_time) {
  int64_t start_angle = 0, sweep = 0;
  CalculateWaitingAngles(elapsed_time, &start_angle, &sweep);
  PaintArc(canvas, bounds, color, start_angle, sweep);
}

void PaintThrobberSpinningAfterWaiting(Canvas* canvas,
                                       const Rect& bounds,
                                       SkColor color,
                                       const base::TimeDelta& elapsed_time,
                                       ThrobberWaitingState* waiting_state) {
  int64_t waiting_start_angle = 0, waiting_sweep = 0;
  CalculateWaitingAngles(waiting_state->elapsed_time, &waiting_start_angle,
                         &waiting_sweep);

  // |arc_time_offset| is the effective amount of time one would have to wait
  // for the "spinning" sweep to match |waiting_sweep|. Brute force calculation.
  if (waiting_state->arc_time_offset.is_zero()) {
    for (int64_t arc_time_it = 0; arc_time_it <= kArcTimeMs; ++arc_time_it) {
      double arc_size_progress = static_cast<double>(arc_time_it) / kArcTimeMs;
      if (kMaxArcSize * Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN,
                                              arc_size_progress) >=
          waiting_sweep) {
        // Add kArcTimeMs to sidestep the |sweep_keyframe == 0| offset below.
        waiting_state->arc_time_offset =
            base::TimeDelta::FromMilliseconds(arc_time_it + kArcTimeMs);
        break;
      }
    }
  }

  // Blend the color between "waiting" and "spinning" states.
  base::TimeDelta color_fade_time = base::TimeDelta::FromMilliseconds(900);
  double color_progress = 1.0;
  if (elapsed_time < color_fade_time) {
    color_progress = Tween::CalculateValue(
        Tween::LINEAR_OUT_SLOW_IN,
        static_cast<double>(elapsed_time.InMicroseconds()) /
            color_fade_time.InMicroseconds());
  }
  SkColor blend_color = color_utils::AlphaBlend(color, waiting_state->color,
                                                color_progress * 255);

  int64_t start_angle =
      waiting_start_angle +
      360 * elapsed_time / base::TimeDelta::FromMilliseconds(kRotationTimeMs);
  base::TimeDelta effective_elapsed_time =
      elapsed_time + waiting_state->arc_time_offset;

  PaintThrobberSpinningWithStartAngle(canvas, bounds, blend_color,
                                      effective_elapsed_time, start_angle);
}

}  // namespace gfx
