// Copyright 2014 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/platform/animation/timing_function.h"

#include <algorithm>

#include "base/notreached.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "ui/gfx/animation/keyframe/timing_function.h"

namespace blink {

String LinearTimingFunction::ToString() const {
  if (linear_->IsTrivial()) {
    return "linear";
  }
  StringBuilder builder;
  builder.Append("linear(");
  for (wtf_size_t i = 0; i < linear_->Points().size(); ++i) {
    if (i != 0) {
      builder.Append(", ");
    }
    builder.Append(String::NumberToStringEcmaScript(linear_->Point(i).output));
    builder.Append(" ");
    builder.Append(String::NumberToStringEcmaScript(linear_->Point(i).input));
    builder.Append("%");
  }
  builder.Append(")");
  return builder.ReleaseString();
}

double LinearTimingFunction::Evaluate(
    double fraction,
    TimingFunction::LimitDirection limit_direction) const {
  return linear_->GetValue(fraction, limit_direction);
}

void LinearTimingFunction::Range(double* min_value, double* max_value) const {
  if (IsTrivial()) {
    return;
  }
  //
  //        (min_it) # *               (max_it) ^ *
  //                 | | *                      | |
  //  (min_value) @  | | |   (max_value) %      | |
  //                 * | | *                    | |
  // ________________|_|_|_|____________________|_|_
  // @ - min_value.
  // % - max_value.
  // # - min_it is first of points with same input (and input >= min_value).
  // ^ - max_it.
  // for min_comp we want the first of points in case of input equality.
  // (e.g. begin of range).
  const auto min_comp = [](double value, const auto& point) {
    return value <= point.input;
  };
  // for max_comp we want the last of points in case of input equality.
  // (e.g. end of range).
  const auto max_comp = [](double value, const auto& point) {
    return value < point.input;
  };
  auto min_it = std::upper_bound(Points().cbegin(), Points().cend(),
                                 100 * *min_value, min_comp);
  min_it = min_it == Points().cend() ? std::prev(min_it) : min_it;
  auto max_it = std::upper_bound(Points().cbegin(), Points().cend(),
                                 100 * *max_value, max_comp);
  const auto [min, max] = std::minmax_element(
      min_it, max_it,
      [](const auto& a, const auto& b) { return a.output < b.output; });
  double min_val = Evaluate(*min_value);
  double max_val = Evaluate(*max_value);
  *min_value = std::min({min_val, max_val, min->output});
  *max_value = std::max({min_val, max_val, max->output});
}

std::unique_ptr<gfx::TimingFunction> LinearTimingFunction::CloneToCC() const {
  return linear_->Clone();
}

CubicBezierTimingFunction* CubicBezierTimingFunction::Preset(
    EaseType ease_type) {
  DEFINE_STATIC_REF(
      CubicBezierTimingFunction, ease,
      (base::AdoptRef(new CubicBezierTimingFunction(EaseType::EASE))));
  DEFINE_STATIC_REF(
      CubicBezierTimingFunction, ease_in,
      (base::AdoptRef(new CubicBezierTimingFunction(EaseType::EASE_IN))));
  DEFINE_STATIC_REF(
      CubicBezierTimingFunction, ease_out,
      (base::AdoptRef(new CubicBezierTimingFunction(EaseType::EASE_OUT))));
  DEFINE_STATIC_REF(
      CubicBezierTimingFunction, ease_in_out,
      (base::AdoptRef(new CubicBezierTimingFunction(EaseType::EASE_IN_OUT))));

  switch (ease_type) {
    case EaseType::EASE:
      return ease;
    case EaseType::EASE_IN:
      return ease_in;
    case EaseType::EASE_OUT:
      return ease_out;
    case EaseType::EASE_IN_OUT:
      return ease_in_out;
    default:
      NOTREACHED();
  }
}

String CubicBezierTimingFunction::ToString() const {
  switch (GetEaseType()) {
    case CubicBezierTimingFunction::EaseType::EASE:
      return "ease";
    case CubicBezierTimingFunction::EaseType::EASE_IN:
      return "ease-in";
    case CubicBezierTimingFunction::EaseType::EASE_OUT:
      return "ease-out";
    case CubicBezierTimingFunction::EaseType::EASE_IN_OUT:
      return "ease-in-out";
    case CubicBezierTimingFunction::EaseType::CUSTOM:
      return StrCat({"cubic-bezier(", String::NumberToStringEcmaScript(X1()),
                     ", ", String::NumberToStringEcmaScript(Y1()), ", ",
                     String::NumberToStringEcmaScript(X2()), ", ",
                     String::NumberToStringEcmaScript(Y2()), ")"});
    default:
      NOTREACHED();
  }
}

double CubicBezierTimingFunction::Evaluate(
    double fraction,
    TimingFunction::LimitDirection limit_direction) const {
  return bezier_->bezier().Solve(fraction);
}

void CubicBezierTimingFunction::Range(double* min_value,
                                      double* max_value) const {
  const double solution1 = bezier_->bezier().range_min();
  const double solution2 = bezier_->bezier().range_max();

  // We use the default epsilon of the gfx::CubicBezier which is 1e-7. This is
  // sufficient since in most cases, we expect newton's method to exceed the
  // given epsilon. Leaving several bits of room means that bisection fallback
  // won't suffer instabilities caused by floating-point imprecision.
  double epsilon = gfx::CubicBezier::GetDefaultEpsilon();

  // Since our input values can be out of the range 0->1 so we must also
  // consider the minimum and maximum points.
  double solution_min = bezier_->bezier().SolveWithEpsilon(*min_value, epsilon);
  double solution_max = bezier_->bezier().SolveWithEpsilon(*max_value, epsilon);
  *min_value = std::min(std::min(solution_min, solution_max), 0.0);
  *max_value = std::max(std::max(solution_min, solution_max), 1.0);
  *min_value = std::min(std::min(*min_value, solution1), solution2);
  *max_value = std::max(std::max(*max_value, solution1), solution2);
}

std::unique_ptr<gfx::TimingFunction> CubicBezierTimingFunction::CloneToCC()
    const {
  return bezier_->Clone();
}

String StepsTimingFunction::ToString() const {
  const char* position_string = nullptr;
  switch (GetStepPosition()) {
    case StepPosition::START:
      position_string = "start";
      break;

    case StepPosition::END:
      // do not specify step position in output
      break;

    case StepPosition::JUMP_BOTH:
      position_string = "jump-both";
      break;

    case StepPosition::JUMP_END:
      // do not specify step position in output
      break;

    case StepPosition::JUMP_NONE:
      position_string = "jump-none";
      break;

    case StepPosition::JUMP_START:
      position_string = "jump-start";
      break;
  }

  StringBuilder builder;
  builder.Append("steps(");
  builder.Append(String::NumberToStringEcmaScript(NumberOfSteps()));
  if (position_string) {
    builder.Append(", ");
    builder.Append(position_string);
  }
  builder.Append(')');
  return builder.ToString();
}

void StepsTimingFunction::Range(double* min_value, double* max_value) const {
  *min_value = 0;
  *max_value = 1;
}

double StepsTimingFunction::Evaluate(double fraction,
                                     LimitDirection limit_direction) const {
  return steps_->GetValue(fraction, limit_direction);
}

std::unique_ptr<gfx::TimingFunction> StepsTimingFunction::CloneToCC() const {
  return steps_->Clone();
}

scoped_refptr<TimingFunction> CreateCompositorTimingFunctionFromCC(
    const gfx::TimingFunction* timing_function) {
  if (!timing_function)
    return LinearTimingFunction::Shared();

  switch (timing_function->GetType()) {
    case gfx::TimingFunction::Type::CUBIC_BEZIER: {
      auto* cubic_timing_function =
          static_cast<const gfx::CubicBezierTimingFunction*>(timing_function);
      if (cubic_timing_function->ease_type() !=
          gfx::CubicBezierTimingFunction::EaseType::CUSTOM)
        return CubicBezierTimingFunction::Preset(
            cubic_timing_function->ease_type());

      const auto& bezier = cubic_timing_function->bezier();
      return CubicBezierTimingFunction::Create(bezier.GetX1(), bezier.GetY1(),
                                               bezier.GetX2(), bezier.GetY2());
    }

    case gfx::TimingFunction::Type::STEPS: {
      auto* steps_timing_function =
          static_cast<const gfx::StepsTimingFunction*>(timing_function);
      return StepsTimingFunction::Create(
          steps_timing_function->steps(),
          steps_timing_function->step_position());
    }

    case gfx::TimingFunction::Type::LINEAR: {
      auto* linear_timing_function =
          static_cast<const gfx::LinearTimingFunction*>(timing_function);
      if (linear_timing_function->IsTrivial()) {
        return LinearTimingFunction::Shared();
      }
      return LinearTimingFunction::Create(linear_timing_function->Points());
    }

    default:
      NOTREACHED();
  }
}

// Equals operators
bool operator==(const LinearTimingFunction& lhs, const TimingFunction& rhs) {
  if (auto* rhs_func = DynamicTo<LinearTimingFunction>(rhs)) {
    return lhs == *rhs_func;
  }
  return false;
}

bool operator==(const CubicBezierTimingFunction& lhs,
                const TimingFunction& rhs) {
  if (rhs.GetType() != TimingFunction::Type::CUBIC_BEZIER)
    return false;

  const auto& ctf = To<CubicBezierTimingFunction>(rhs);
  if ((lhs.GetEaseType() == CubicBezierTimingFunction::EaseType::CUSTOM) &&
      (ctf.GetEaseType() == CubicBezierTimingFunction::EaseType::CUSTOM))
    return (lhs.X1() == ctf.X1()) && (lhs.Y1() == ctf.Y1()) &&
           (lhs.X2() == ctf.X2()) && (lhs.Y2() == ctf.Y2());

  return lhs.GetEaseType() == ctf.GetEaseType();
}

bool operator==(const StepsTimingFunction& lhs, const TimingFunction& rhs) {
  if (rhs.GetType() != TimingFunction::Type::STEPS)
    return false;

  const auto& stf = To<StepsTimingFunction>(rhs);
  return (lhs.NumberOfSteps() == stf.NumberOfSteps()) &&
         (lhs.GetStepPosition() == stf.GetStepPosition());
}

// The generic operator== *must* come after the
// non-generic operator== otherwise it will end up calling itself.
bool operator==(const TimingFunction& lhs, const TimingFunction& rhs) {
  switch (lhs.GetType()) {
    case TimingFunction::Type::LINEAR: {
      const auto& linear = To<LinearTimingFunction>(lhs);
      return (linear == rhs);
    }
    case TimingFunction::Type::CUBIC_BEZIER: {
      const auto& cubic = To<CubicBezierTimingFunction>(lhs);
      return (cubic == rhs);
    }
    case TimingFunction::Type::STEPS: {
      const auto& step = To<StepsTimingFunction>(lhs);
      return (step == rhs);
    }
    default:
      NOTREACHED();
  }
}

}  // namespace blink
