// 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/core/animation/interpolable_value.h"

#include <memory>

#include "third_party/blink/renderer/core/animation/css_color_interpolation_type.h"
#include "third_party/blink/renderer/core/animation/interpolable_style_color.h"
#include "third_party/blink/renderer/core/css/css_math_expression_node.h"
#include "third_party/blink/renderer/core/css/css_math_function_value.h"
#include "third_party/blink/renderer/core/css/css_numeric_literal_value.h"

namespace blink {

namespace {

using UnitType = CSSPrimitiveValue::UnitType;

CSSMathExpressionNode* NumberNode(double number,
                                  UnitType unit_type = UnitType::kNumber) {
  return CSSMathExpressionNumericLiteral::Create(
      CSSNumericLiteralValue::Create(number, unit_type));
}

}  // namespace

InterpolableNumber::InterpolableNumber(double value, UnitType unit_type) {
  SetDouble(value, unit_type);
}

InterpolableNumber::InterpolableNumber(
    const CSSMathExpressionNode& expression) {
  SetExpression(expression);
}

InterpolableNumber::InterpolableNumber(const CSSPrimitiveValue& value) {
  if (const auto* numeric = DynamicTo<CSSNumericLiteralValue>(value)) {
    SetDouble(numeric->DoubleValue(), numeric->GetType());
  } else {
    CHECK(value.IsMathFunctionValue());
    const auto& function = To<CSSMathFunctionValue>(value);
    SetExpression(*function.ExpressionNode());
  }
}

double InterpolableNumber::Value(
    const CSSLengthResolver& length_resolver) const {
  if (IsDoubleValue()) {
    return value_ *
           CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor(unit_type_);
  }
  std::optional<double> result =
      expression_->ComputeValueInCanonicalUnit(length_resolver);
  CHECK(result.has_value());
  return result.value();
}

void InterpolableNumber::SetExpression(
    const CSSMathExpressionNode& expression) {
  type_ = Type::kExpression;
  expression_ = &expression;
  unit_type_ = expression.ResolvedUnitType();
}

void InterpolableNumber::SetDouble(double value, UnitType unit_type) {
  type_ = Type::kDouble;
  value_ = value;
  unit_type_ = unit_type;
}

const CSSMathExpressionNode& InterpolableNumber::AsExpression() const {
  if (IsExpression()) {
    return *expression_;
  }
  return *NumberNode(value_, unit_type_);
}

bool InterpolableNumber::Equals(const InterpolableValue& other) const {
  const auto& other_number = To<InterpolableNumber>(other);
  if (IsDoubleValue() && other_number.IsDoubleValue() &&
      unit_type_ == other_number.unit_type_) {
    return value_ == To<InterpolableNumber>(other).value_;
  }
  return AsExpression() == other_number.AsExpression();
}

bool InterpolableList::Equals(const InterpolableValue& other) const {
  const auto& other_list = To<InterpolableList>(other);
  if (length() != other_list.length())
    return false;
  for (wtf_size_t i = 0; i < length(); i++) {
    if (!values_[i]->Equals(*other_list.values_[i]))
      return false;
  }
  return true;
}

void InterpolableNumber::AssertCanInterpolateWith(
    const InterpolableValue& other) const {
  DCHECK(other.IsNumber());
}

double InterpolableNumber::Interpolate(double from,
                                       double to,
                                       double progress) {
  if (progress == 0 || from == to) {
    return from;
  } else if (progress == 1) {
    return to;
  } else {
    return from * (1 - progress) + to * progress;
  }
}

void InterpolableNumber::Interpolate(const InterpolableValue& to,
                                     const double progress,
                                     InterpolableValue& result) const {
  const auto& to_number = To<InterpolableNumber>(to);
  auto& result_number = To<InterpolableNumber>(result);
  if (IsDoubleValue() && to_number.IsDoubleValue() &&
      unit_type_ == to_number.unit_type_) {
    result_number.SetDouble(Interpolate(value_, to_number.Value(), progress),
                            unit_type_);
    return;
  }
  const CSSMathExpressionNode* blended_from =
      CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          &AsExpression(), NumberNode(1 - progress),
          CSSMathOperator::kMultiply);
  const CSSMathExpressionNode* blended_to =
      CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          &to_number.AsExpression(), NumberNode(progress),
          CSSMathOperator::kMultiply);
  const CSSMathExpressionNode* result_expression =
      CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          blended_from, blended_to, CSSMathOperator::kAdd);
  result_number.SetExpression(*result_expression);
}

void InterpolableList::AssertCanInterpolateWith(
    const InterpolableValue& other) const {
#if DCHECK_IS_ON()
  DCHECK(other.IsList());
  DCHECK_EQ(To<InterpolableList>(other).length(), length());
  const auto& other_list = To<InterpolableList>(other);
  for (wtf_size_t i = 0; i < length(); i++) {
    values_[i]->AssertCanInterpolateWith(*other_list.values_[i]);
  }
#endif
}

void InterpolableList::Interpolate(const InterpolableValue& to,
                                   const double progress,
                                   InterpolableValue& result) const {
  const auto& to_list = To<InterpolableList>(to);
  auto& result_list = To<InterpolableList>(result);

  for (wtf_size_t i = 0; i < length(); i++) {
    DCHECK(values_[i]);
    DCHECK(to_list.values_[i]);
    if (values_[i]->IsStyleColor() || to_list.values_[i]->IsStyleColor() ||
        result_list.values_[i]->IsStyleColor()) {
      CSSColorInterpolationType::EnsureInterpolableStyleColor(result_list, i);
      InterpolableStyleColor::Interpolate(*values_[i], *(to_list.values_[i]),
                                          progress, *(result_list.values_[i]));
      continue;
    }
    values_[i]->Interpolate(*(to_list.values_[i]), progress,
                            *(result_list.values_[i]));
  }
}

InterpolableList* InterpolableList::RawCloneAndZero() const {
  auto* result = MakeGarbageCollected<InterpolableList>(length());
  for (wtf_size_t i = 0; i < length(); i++) {
    result->Set(i, values_[i]->CloneAndZero());
  }
  return result;
}

void InterpolableNumber::Scale(double scale) {
  if (IsDoubleValue()) {
    value_ *= scale;
    return;
  }
  SetExpression(
      *CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          &AsExpression(), NumberNode(scale), CSSMathOperator::kMultiply));
}

void InterpolableNumber::Scale(const InterpolableNumber& other) {
  if (IsDoubleValue() && other.IsDoubleValue() &&
      (unit_type_ == CSSPrimitiveValue::UnitType::kNumber ||
       other.unit_type_ == CSSPrimitiveValue::UnitType::kNumber)) {
    SetDouble(
        value_ * other.value_,
        (unit_type_ == CSSPrimitiveValue::UnitType::kNumber ? other.unit_type_
                                                            : unit_type_));
    return;
  }
  SetExpression(
      *CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          &AsExpression(), &other.AsExpression(), CSSMathOperator::kMultiply));
}

void InterpolableList::Scale(double scale) {
  for (wtf_size_t i = 0; i < length(); i++)
    values_[i]->Scale(scale);
}

void InterpolableNumber::Add(const InterpolableValue& other) {
  const auto& other_number = To<InterpolableNumber>(other);
  if (IsDoubleValue() && other_number.IsDoubleValue() &&
      unit_type_ == other_number.unit_type_) {
    value_ += other_number.value_;
    return;
  }
  const CSSMathExpressionNode* result =
      CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
          &AsExpression(), &other_number.AsExpression(), CSSMathOperator::kAdd);
  if (!result) {
    // An accumulation operation can fail if the units are incompatible. We
    // fallback to composite replace in this case by simply not adding the
    // accumulated part.
    return;
  }
  SetExpression(*result);
}

void InterpolableList::Add(const InterpolableValue& other) {
  const auto& other_list = To<InterpolableList>(other);
  CHECK_EQ(other_list.length(), length());
  for (wtf_size_t i = 0; i < length(); i++)
    values_[i]->Add(*other_list.values_[i]);
}

void InterpolableList::ScaleAndAdd(double scale,
                                   const InterpolableValue& other) {
  const auto& other_list = To<InterpolableList>(other);
  CHECK_EQ(other_list.length(), length());
  for (wtf_size_t i = 0; i < length(); i++)
    values_[i]->ScaleAndAdd(scale, *other_list.values_[i]);
}

}  // namespace blink
