/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "SVGAnimatedTransformList.h"

#include <utility>

#include "DOMSVGAnimatedTransformList.h"
#include "SVGTransform.h"
#include "SVGTransformListSMILType.h"
#include "mozilla/SMILValue.h"
#include "mozilla/SVGContentUtils.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsContentUtils.h"

using namespace mozilla::dom;
using namespace mozilla::dom::SVGTransform_Binding;

namespace mozilla {

nsresult SVGAnimatedTransformList::SetBaseValueString(const nsAString& aValue,
                                                      SVGElement* aSVGElement) {
  SVGTransformList newBaseValue;
  nsresult rv = newBaseValue.SetValueFromString(aValue);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return SetBaseValue(newBaseValue, aSVGElement);
}

nsresult SVGAnimatedTransformList::SetBaseValue(const SVGTransformList& aValue,
                                                SVGElement* aSVGElement) {
  DOMSVGAnimatedTransformList* domWrapper =
      DOMSVGAnimatedTransformList::GetDOMWrapperIfExists(this);
  if (domWrapper) {
    // We must send this notification *before* changing mBaseVal! If the length
    // of our baseVal is being reduced, our baseVal's DOM wrapper list may have
    // to remove DOM items from itself, and any removed DOM items need to copy
    // their internal counterpart values *before* we change them.
    //
    domWrapper->InternalBaseValListWillChangeLengthTo(aValue.Length());
  }

  // (This bool will be copied to our member-var, if attr-change succeeds.)
  bool hadTransform = HasTransform();

  // We don't need to call DidChange* here - we're only called by
  // SVGElement::ParseAttribute under Element::SetAttr,
  // which takes care of notifying.

  nsresult rv = mBaseVal.CopyFrom(aValue);
  if (NS_FAILED(rv) && domWrapper) {
    // Attempting to increase mBaseVal's length failed - reduce domWrapper
    // back to the same length:
    domWrapper->InternalBaseValListWillChangeLengthTo(mBaseVal.Length());
  } else {
    mIsBaseSet = true;
    // We only need to treat this as a creation or removal of a transform if the
    // frame already exists and it didn't have an existing one.
    mCreatedOrRemovedOnLastChange =
        aSVGElement->GetPrimaryFrame() && !hadTransform;
  }
  return rv;
}

void SVGAnimatedTransformList::ClearBaseValue() {
  mCreatedOrRemovedOnLastChange = !HasTransform();

  DOMSVGAnimatedTransformList* domWrapper =
      DOMSVGAnimatedTransformList::GetDOMWrapperIfExists(this);
  if (domWrapper) {
    // We must send this notification *before* changing mBaseVal! (See above.)
    domWrapper->InternalBaseValListWillChangeLengthTo(0);
  }
  mBaseVal.Clear();
  mIsBaseSet = false;
  // Caller notifies
}

nsresult SVGAnimatedTransformList::SetAnimValue(const SVGTransformList& aValue,
                                                SVGElement* aElement) {
  bool prevSet = HasTransform() || aElement->GetAnimateMotionTransform();
  DOMSVGAnimatedTransformList* domWrapper =
      DOMSVGAnimatedTransformList::GetDOMWrapperIfExists(this);
  if (domWrapper) {
    // A new animation may totally change the number of items in the animVal
    // list, replacing what was essentially a mirror of the baseVal list, or
    // else replacing and overriding an existing animation. When this happens
    // we must try and keep our animVal's DOM wrapper in sync (see the comment
    // in DOMSVGAnimatedTransformList::InternalBaseValListWillChangeLengthTo).
    //
    // It's not possible for us to reliably distinguish between calls to this
    // method that are setting a new sample for an existing animation, and
    // calls that are setting the first sample of an animation that will
    // override an existing animation. Happily it's cheap to just blindly
    // notify our animVal's DOM wrapper of its internal counterpart's new value
    // each time this method is called, so that's what we do.
    //
    // Note that we must send this notification *before* setting or changing
    // mAnimVal! (See the comment in SetBaseValueString above.)
    //
    domWrapper->InternalAnimValListWillChangeLengthTo(aValue.Length());
  }
  if (!mAnimVal) {
    mAnimVal = std::make_unique<SVGTransformList>();
  }
  nsresult rv = mAnimVal->CopyFrom(aValue);
  if (NS_FAILED(rv)) {
    // OOM. We clear the animation, and, importantly, ClearAnimValue() ensures
    // that mAnimVal and its DOM wrapper (if any) will have the same length!
    ClearAnimValue(aElement);
    return rv;
  }
  mCreatedOrRemovedOnLastChange = !prevSet;
  aElement->DidAnimateTransformList();
  return NS_OK;
}

void SVGAnimatedTransformList::ClearAnimValue(SVGElement* aElement) {
  DOMSVGAnimatedTransformList* domWrapper =
      DOMSVGAnimatedTransformList::GetDOMWrapperIfExists(this);
  if (domWrapper) {
    // When all animation ends, animVal simply mirrors baseVal, which may have
    // a different number of items to the last active animated value. We must
    // keep the length of our animVal's DOM wrapper list in sync, and again we
    // must do that before touching mAnimVal. See comments above.
    //
    domWrapper->InternalAnimValListWillChangeLengthTo(mBaseVal.Length());
  }
  mAnimVal = nullptr;
  mCreatedOrRemovedOnLastChange =
      !HasTransform() && !aElement->GetAnimateMotionTransform();
  aElement->DidAnimateTransformList();
}

bool SVGAnimatedTransformList::IsExplicitlySet() const {
  // Like other methods of this name, we need to know when a transform value has
  // been explicitly set.
  //
  // There are three ways an animated list can become set:
  // 1) Markup -- we set mIsBaseSet to true on any successful call to
  //    SetBaseValueString and clear it on ClearBaseValue (as called by
  //    SVGElement::UnsetAttr or a failed SVGElement::ParseAttribute)
  // 2) DOM call -- simply fetching the baseVal doesn't mean the transform value
  //    has been set. It is set if that baseVal has one or more transforms in
  //    the list.
  // 3) Animation -- which will cause the mAnimVal member to be allocated
  return mIsBaseSet || !mBaseVal.IsEmpty() || mAnimVal;
}

std::unique_ptr<SMILAttr> SVGAnimatedTransformList::ToSMILAttr(
    SVGElement* aSVGElement) {
  return std::make_unique<SMILAnimatedTransformList>(this, aSVGElement);
}

static uint16_t ToTransformType(const nsAtom* aTransformType) {
  if (aTransformType == nsGkAtoms::translate) {
    return SVG_TRANSFORM_TRANSLATE;
  }
  if (aTransformType == nsGkAtoms::scale) {
    return SVG_TRANSFORM_SCALE;
  }
  if (aTransformType == nsGkAtoms::rotate) {
    return SVG_TRANSFORM_ROTATE;
  }
  if (aTransformType == nsGkAtoms::skewX) {
    return SVG_TRANSFORM_SKEWX;
  }
  if (aTransformType == nsGkAtoms::skewY) {
    return SVG_TRANSFORM_SKEWY;
  }
  if (aTransformType == nsGkAtoms::matrix) {
    return SVG_TRANSFORM_MATRIX;
  }
  return SVG_TRANSFORM_UNKNOWN;
}

nsresult SVGAnimatedTransformList::SMILAnimatedTransformList::ValueFromString(
    const nsAString& aStr, const dom::SVGAnimationElement* aSrcElement,
    SMILValue& aValue, bool& aPreventCachingOfSandwich) const {
  NS_ENSURE_TRUE(aSrcElement, NS_ERROR_FAILURE);
  MOZ_ASSERT(aValue.IsNull(),
             "aValue should have been cleared before calling ValueFromString");

  const nsAttrValue* typeAttr = aSrcElement->GetParsedAttr(nsGkAtoms::type);
  uint16_t transformType = SVG_TRANSFORM_TRANSLATE;  // default val
  if (typeAttr) {
    if (typeAttr->Type() != nsAttrValue::eAtom) {
      // Recognized values of |type| are parsed as an atom -- so if we have
      // something other than an atom, then we know already our |type| is
      // invalid.
      return NS_ERROR_FAILURE;
    }
    transformType = ToTransformType(typeAttr->GetAtomValue());
  }

  ParseValue(aStr, transformType, aValue);
  return aValue.IsNull() ? NS_ERROR_FAILURE : NS_OK;
}

void SVGAnimatedTransformList::SMILAnimatedTransformList::ParseValue(
    const nsAString& aSpec, uint16_t aTransformType, SMILValue& aResult) {
  MOZ_ASSERT(aResult.IsNull(), "Unexpected type for SMIL value");

  static_assert(SVGTransformSMILData::kNumSimpleParams == 3,
                "SVGSMILTransform constructor should be expecting array "
                "with 3 params");

  SVGTransformSMILData::SimpleParams params = {0.f};
  int32_t numParsed = ParseParameterList(aSpec, params);

  switch (aTransformType) {
    case SVG_TRANSFORM_TRANSLATE:
      // tx [ty=0]
      if (numParsed != 1 && numParsed != 2) {
        return;
      }
      break;
    case SVG_TRANSFORM_SCALE:
      // sx [sy=sx]
      if (numParsed != 1 && numParsed != 2) {
        return;
      }
      if (numParsed == 1) {
        params[1] = params[0];
      }
      break;
    case SVG_TRANSFORM_ROTATE:
      // r [cx=0 cy=0]
      if (numParsed != 1 && numParsed != 3) {
        return;
      }
      break;
    case SVG_TRANSFORM_SKEWX:
    case SVG_TRANSFORM_SKEWY:
      // x-angle or y-angle
      if (numParsed != 1) {
        return;
      }
      break;
    default:
      return;
  }

  SMILValue val(SVGTransformListSMILType::Singleton());
  SVGTransformSMILData transform(aTransformType, params);
  if (NS_FAILED(SVGTransformListSMILType::AppendTransform(transform, val))) {
    return;  // OOM
  }

  // Success! Populate our outparam with parsed value.
  aResult = std::move(val);
}

int32_t SVGAnimatedTransformList::SMILAnimatedTransformList::ParseParameterList(
    const nsAString& aSpec, SVGTransformSMILData::SimpleParams& aParams) {
  size_t numArgsFound = 0;

  for (const auto& token :
       nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace,
                                        nsTokenizerFlags::SeparatorOptional>(
           aSpec, ',')
           .ToRange()) {
    float f;
    if (!SVGContentUtils::ParseNumber(token, f)) {
      return -1;
    }
    if (numArgsFound < aParams.size()) {
      aParams[numArgsFound] = f;
    }
    numArgsFound++;
  }
  return numArgsFound;
}

SMILValue SVGAnimatedTransformList::SMILAnimatedTransformList::GetBaseValue()
    const {
  // To benefit from Return Value Optimization and avoid copy constructor calls
  // due to our use of return-by-value, we must return the exact same object
  // from ALL return points. This function must only return THIS variable:
  SMILValue val(SVGTransformListSMILType::Singleton());
  if (!SVGTransformListSMILType::AppendTransforms(mVal->mBaseVal, val)) {
    val = SMILValue();
  }

  return val;
}

nsresult SVGAnimatedTransformList::SMILAnimatedTransformList::SetAnimValue(
    const SMILValue& aNewAnimValue) {
  MOZ_ASSERT(aNewAnimValue.mType == SVGTransformListSMILType::Singleton(),
             "Unexpected type to assign animated value");
  SVGTransformList animVal;
  if (!SVGTransformListSMILType::GetTransforms(aNewAnimValue, animVal.mItems)) {
    return NS_ERROR_FAILURE;
  }

  return mVal->SetAnimValue(animVal, mElement);
}

void SVGAnimatedTransformList::SMILAnimatedTransformList::ClearAnimValue() {
  if (mVal->mAnimVal) {
    mVal->ClearAnimValue(mElement);
  }
}

}  // namespace mozilla
