/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "SVGTransform.h"

#include "mozilla/dom/SVGTransform.h"
#include "mozilla/dom/SVGMatrix.h"
#include "mozilla/dom/SVGTransformBinding.h"
#include "nsError.h"
#include "nsSVGAnimatedTransformList.h"
#include "nsSVGAttrTearoffTable.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/FloatingPoint.h"

namespace {
  const double kRadPerDegree = 2.0 * M_PI / 360.0;
} // namespace

namespace mozilla {
namespace dom {

static nsSVGAttrTearoffTable<SVGTransform, SVGMatrix>&
SVGMatrixTearoffTable()
{
  static nsSVGAttrTearoffTable<SVGTransform, SVGMatrix> sSVGMatrixTearoffTable;
  return sSVGMatrixTearoffTable;
}

//----------------------------------------------------------------------

// We could use NS_IMPL_CYCLE_COLLECTION(, except that in Unlink() we need to
// clear our list's weak ref to us to be safe. (The other option would be to
// not unlink and rely on the breaking of the other edges in the cycle, as
// NS_SVG_VAL_IMPL_CYCLE_COLLECTION does.)
NS_IMPL_CYCLE_COLLECTION_CLASS(SVGTransform)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SVGTransform)
  // We may not belong to a list, so we must null check tmp->mList.
  if (tmp->mList) {
    tmp->mList->mItems[tmp->mListIndex] = nullptr;
  }
NS_IMPL_CYCLE_COLLECTION_UNLINK(mList)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SVGTransform)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mList)
  SVGMatrix* matrix =
    SVGMatrixTearoffTable().GetTearoff(tmp);
  CycleCollectionNoteChild(cb, matrix, "matrix");
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(SVGTransform)
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(SVGTransform, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(SVGTransform, Release)

JSObject*
SVGTransform::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
  return SVGTransformBinding::Wrap(aCx, this, aGivenProto);
}

//----------------------------------------------------------------------
// Helper class: AutoChangeTransformNotifier
// Stack-based helper class to pair calls to WillChangeTransformList
// and DidChangeTransformList.
class MOZ_RAII AutoChangeTransformNotifier
{
public:
  explicit AutoChangeTransformNotifier(SVGTransform* aTransform MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
    : mTransform(aTransform)
  {
    MOZ_GUARD_OBJECT_NOTIFIER_INIT;
    MOZ_ASSERT(mTransform, "Expecting non-null transform");
    if (mTransform->HasOwner()) {
      mEmptyOrOldValue =
        mTransform->Element()->WillChangeTransformList();
    }
  }

  ~AutoChangeTransformNotifier()
  {
    if (mTransform->HasOwner()) {
      mTransform->Element()->DidChangeTransformList(mEmptyOrOldValue);
      if (mTransform->mList->IsAnimating()) {
        mTransform->Element()->AnimationNeedsResample();
      }
    }
  }

private:
  SVGTransform* const mTransform;
  nsAttrValue   mEmptyOrOldValue;
  MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};

//----------------------------------------------------------------------
// Ctors:

SVGTransform::SVGTransform(DOMSVGTransformList *aList,
                           uint32_t aListIndex,
                           bool aIsAnimValItem)
  : mList(aList)
  , mListIndex(aListIndex)
  , mIsAnimValItem(aIsAnimValItem)
  , mTransform(nullptr)
{
  // These shifts are in sync with the members in the header.
  MOZ_ASSERT(aList && aListIndex <= MaxListIndex(), "bad arg");

  MOZ_ASSERT(IndexIsValid(), "Bad index for DOMSVGNumber!");
}

SVGTransform::SVGTransform()
  : mList(nullptr)
  , mListIndex(0)
  , mIsAnimValItem(false)
  , mTransform(new nsSVGTransform()) // Default ctor for objects not in a list
                                     // initialises to matrix type with identity
                                     // matrix
{
}

SVGTransform::SVGTransform(const gfxMatrix &aMatrix)
  : mList(nullptr)
  , mListIndex(0)
  , mIsAnimValItem(false)
  , mTransform(new nsSVGTransform(aMatrix))
{
}

SVGTransform::SVGTransform(const nsSVGTransform &aTransform)
  : mList(nullptr)
  , mListIndex(0)
  , mIsAnimValItem(false)
  , mTransform(new nsSVGTransform(aTransform))
{
}

SVGTransform::~SVGTransform()
{
  SVGMatrix* matrix = SVGMatrixTearoffTable().GetTearoff(this);
  if (matrix) {
    SVGMatrixTearoffTable().RemoveTearoff(this);
    NS_RELEASE(matrix);
  }
  // Our mList's weak ref to us must be nulled out when we die. If GC has
  // unlinked us using the cycle collector code, then that has already
  // happened, and mList is null.
  if (mList) {
    mList->mItems[mListIndex] = nullptr;
  }
}

uint16_t
SVGTransform::Type() const
{
  return Transform().Type();
}

SVGMatrix*
SVGTransform::GetMatrix()
{
  SVGMatrix* wrapper =
    SVGMatrixTearoffTable().GetTearoff(this);
  if (!wrapper) {
    NS_ADDREF(wrapper = new SVGMatrix(*this));
    SVGMatrixTearoffTable().AddTearoff(this, wrapper);
  }
  return wrapper;
}

float
SVGTransform::Angle() const
{
  return Transform().Angle();
}

void
SVGTransform::SetMatrix(SVGMatrix& aMatrix, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }
  SetMatrix(aMatrix.GetMatrix());
}

void
SVGTransform::SetTranslate(float tx, float ty, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }

  if (Transform().Type() == SVG_TRANSFORM_TRANSLATE &&
      Matrixgfx()._31 == tx && Matrixgfx()._32 == ty) {
    return;
  }

  AutoChangeTransformNotifier notifier(this);
  Transform().SetTranslate(tx, ty);
}

void
SVGTransform::SetScale(float sx, float sy, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }

  if (Transform().Type() == SVG_TRANSFORM_SCALE &&
      Matrixgfx()._11 == sx && Matrixgfx()._22 == sy) {
    return;
  }
  AutoChangeTransformNotifier notifier(this);
  Transform().SetScale(sx, sy);
}

void
SVGTransform::SetRotate(float angle, float cx, float cy, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }

  if (Transform().Type() == SVG_TRANSFORM_ROTATE) {
    float currentCx, currentCy;
    Transform().GetRotationOrigin(currentCx, currentCy);
    if (Transform().Angle() == angle && currentCx == cx && currentCy == cy) {
      return;
    }
  }

  AutoChangeTransformNotifier notifier(this);
  Transform().SetRotate(angle, cx, cy);
}

void
SVGTransform::SetSkewX(float angle, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }

  if (Transform().Type() == SVG_TRANSFORM_SKEWX &&
      Transform().Angle() == angle) {
    return;
  }

  if (!IsFinite(tan(angle * kRadPerDegree))) {
    rv.ThrowRangeError<MSG_INVALID_TRANSFORM_ANGLE_ERROR>();
    return;
  }

  AutoChangeTransformNotifier notifier(this);
  DebugOnly<nsresult> result = Transform().SetSkewX(angle);
  MOZ_ASSERT(NS_SUCCEEDED(result), "SetSkewX unexpectedly failed");
}

void
SVGTransform::SetSkewY(float angle, ErrorResult& rv)
{
  if (mIsAnimValItem) {
    rv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
    return;
  }

  if (Transform().Type() == SVG_TRANSFORM_SKEWY &&
      Transform().Angle() == angle) {
    return;
  }

  if (!IsFinite(tan(angle * kRadPerDegree))) {
    rv.ThrowRangeError<MSG_INVALID_TRANSFORM_ANGLE_ERROR>();
    return;
  }

  AutoChangeTransformNotifier notifier(this);
  DebugOnly<nsresult> result = Transform().SetSkewY(angle);
  MOZ_ASSERT(NS_SUCCEEDED(result), "SetSkewY unexpectedly failed");
}

//----------------------------------------------------------------------
// List management methods:

void
SVGTransform::InsertingIntoList(DOMSVGTransformList *aList,
                                uint32_t aListIndex,
                                bool aIsAnimValItem)
{
  MOZ_ASSERT(!HasOwner(), "Inserting item that is already in a list");

  mList = aList;
  mListIndex = aListIndex;
  mIsAnimValItem = aIsAnimValItem;
  mTransform = nullptr;

  MOZ_ASSERT(IndexIsValid(), "Bad index for DOMSVGLength!");
}

void
SVGTransform::RemovingFromList()
{
  MOZ_ASSERT(!mTransform,
             "Item in list also has another non-list value associated with it");

  mTransform = new nsSVGTransform(InternalItem());
  mList = nullptr;
  mIsAnimValItem = false;
}

nsSVGTransform&
SVGTransform::InternalItem()
{
  nsSVGAnimatedTransformList *alist = Element()->GetAnimatedTransformList();
  return mIsAnimValItem && alist->mAnimVal ?
    (*alist->mAnimVal)[mListIndex] :
    alist->mBaseVal[mListIndex];
}

const nsSVGTransform&
SVGTransform::InternalItem() const
{
  return const_cast<SVGTransform*>(this)->InternalItem();
}

#ifdef DEBUG
bool
SVGTransform::IndexIsValid()
{
  nsSVGAnimatedTransformList *alist = Element()->GetAnimatedTransformList();
  return (mIsAnimValItem &&
          mListIndex < alist->GetAnimValue().Length()) ||
         (!mIsAnimValItem &&
          mListIndex < alist->GetBaseValue().Length());
}
#endif // DEBUG


//----------------------------------------------------------------------
// Interface for SVGMatrix's use

void
SVGTransform::SetMatrix(const gfxMatrix& aMatrix)
{
  MOZ_ASSERT(!mIsAnimValItem,
             "Attempting to modify read-only transform");

  if (Transform().Type() == SVG_TRANSFORM_MATRIX &&
      nsSVGTransform::MatricesEqual(Matrixgfx(), aMatrix)) {
    return;
  }

  AutoChangeTransformNotifier notifier(this);
  Transform().SetMatrix(aMatrix);
}

} // namespace dom
} // namespace mozilla
