/* -*- 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 "mozilla/EventListenerManager.h"
#include "mozilla/SMILInstanceTime.h"
#include "mozilla/SMILInterval.h"
#include "mozilla/SMILParserUtils.h"
#include "mozilla/SMILTimeContainer.h"
#include "mozilla/SMILTimedElement.h"
#include "mozilla/SMILTimeValueSpec.h"
#include "mozilla/SMILTimeValue.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "mozilla/dom/TimeEvent.h"
#include "nsString.h"
#include <limits>

using namespace mozilla::dom;

namespace mozilla {

//----------------------------------------------------------------------
// Nested class: EventListener

NS_IMPL_ISUPPORTS(SMILTimeValueSpec::EventListener, nsIDOMEventListener)

NS_IMETHODIMP
SMILTimeValueSpec::EventListener::HandleEvent(Event* aEvent) {
  if (mSpec) {
    mSpec->HandleEvent(aEvent);
  }
  return NS_OK;
}

//----------------------------------------------------------------------
// Implementation

SMILTimeValueSpec::SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin)
    : mOwner(&aOwner), mIsBegin(aIsBegin), mReferencedElement(this) {}

SMILTimeValueSpec::~SMILTimeValueSpec() {
  UnregisterFromReferencedElement(mReferencedElement.get());
  if (mEventListener) {
    mEventListener->Disconnect();
    mEventListener = nullptr;
  }
}

nsresult SMILTimeValueSpec::SetSpec(const nsAString& aStringSpec,
                                    Element& aContextElement) {
  SMILTimeValueSpecParams params;

  if (!SMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params))
    return NS_ERROR_FAILURE;

  mParams = params;

  // According to SMIL 3.0:
  //   The special value "indefinite" does not yield an instance time in the
  //   begin list. It will, however yield a single instance with the value
  //   "indefinite" in an end list. This value is not removed by a reset.
  if (mParams.mType == SMILTimeValueSpecParams::OFFSET ||
      (!mIsBegin && mParams.mType == SMILTimeValueSpecParams::INDEFINITE)) {
    mOwner->AddInstanceTime(new SMILInstanceTime(mParams.mOffset), mIsBegin);
  }

  // Fill in the event symbol to simplify handling later
  if (mParams.mType == SMILTimeValueSpecParams::REPEAT) {
    mParams.mEventSymbol = nsGkAtoms::repeatEvent;
  }

  ResolveReferences(aContextElement);

  return NS_OK;
}

void SMILTimeValueSpec::ResolveReferences(Element& aContextElement) {
  if (mParams.mType != SMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) {
    return;
  }

  // If we're not bound to the document yet, don't worry, we'll get called again
  // when that happens
  if (!aContextElement.IsInComposedDoc()) return;

  // Hold ref to the old element so that it isn't destroyed in between resetting
  // the referenced element and using the pointer to update the referenced
  // element.
  RefPtr<Element> oldReferencedElement = mReferencedElement.get();

  if (mParams.mDependentElemID) {
    mReferencedElement.ResetWithID(aContextElement, mParams.mDependentElemID);
  } else if (mParams.mType == SMILTimeValueSpecParams::EVENT) {
    Element* target = mOwner->GetTargetElement();
    mReferencedElement.ResetWithElement(target);
  } else {
    MOZ_ASSERT(false, "Syncbase or repeat spec without ID");
  }
  UpdateReferencedElement(oldReferencedElement, mReferencedElement.get());
}

bool SMILTimeValueSpec::IsEventBased() const {
  return mParams.mType == SMILTimeValueSpecParams::EVENT ||
         mParams.mType == SMILTimeValueSpecParams::REPEAT;
}

void SMILTimeValueSpec::HandleNewInterval(
    SMILInterval& aInterval, const SMILTimeContainer* aSrcContainer) {
  const SMILInstanceTime& baseInstance =
      mParams.mSyncBegin ? *aInterval.Begin() : *aInterval.End();
  SMILTimeValue newTime =
      ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer);

  // Apply offset
  if (!ApplyOffset(newTime)) {
    NS_WARNING("New time overflows SMILTime, ignoring");
    return;
  }

  // Create the instance time and register it with the interval
  RefPtr<SMILInstanceTime> newInstance = new SMILInstanceTime(
      newTime, SMILInstanceTime::SOURCE_SYNCBASE, this, &aInterval);
  mOwner->AddInstanceTime(newInstance, mIsBegin);
}

void SMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) {
  if (!IsEventBased() || mParams.mDependentElemID) return;

  mReferencedElement.ResetWithElement(aNewTarget);
}

void SMILTimeValueSpec::HandleChangedInstanceTime(
    const SMILInstanceTime& aBaseTime, const SMILTimeContainer* aSrcContainer,
    SMILInstanceTime& aInstanceTimeToUpdate, bool aObjectChanged) {
  // If the instance time is fixed (e.g. because it's being used as the begin
  // time of an active or postactive interval) we just ignore the change.
  if (aInstanceTimeToUpdate.IsFixedTime()) return;

  SMILTimeValue updatedTime =
      ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer);

  // Apply offset
  if (!ApplyOffset(updatedTime)) {
    NS_WARNING("Updated time overflows SMILTime, ignoring");
    return;
  }

  // The timed element that owns the instance time does the updating so it can
  // re-sort its array of instance times more efficiently
  if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) {
    mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin);
  }
}

void SMILTimeValueSpec::HandleDeletedInstanceTime(
    SMILInstanceTime& aInstanceTime) {
  mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin);
}

bool SMILTimeValueSpec::DependsOnBegin() const { return mParams.mSyncBegin; }

void SMILTimeValueSpec::Traverse(
    nsCycleCollectionTraversalCallback* aCallback) {
  mReferencedElement.Traverse(aCallback);
}

void SMILTimeValueSpec::Unlink() {
  UnregisterFromReferencedElement(mReferencedElement.get());
  mReferencedElement.Unlink();
}

//----------------------------------------------------------------------
// Implementation helpers

void SMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) {
  if (aFrom == aTo) return;

  UnregisterFromReferencedElement(aFrom);

  switch (mParams.mType) {
    case SMILTimeValueSpecParams::SYNCBASE: {
      SMILTimedElement* to = GetTimedElement(aTo);
      if (to) {
        to->AddDependent(*this);
      }
    } break;

    case SMILTimeValueSpecParams::EVENT:
    case SMILTimeValueSpecParams::REPEAT:
      RegisterEventListener(aTo);
      break;

    default:
      // not a referencing-type
      break;
  }
}

void SMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) {
  if (!aElement) return;

  if (mParams.mType == SMILTimeValueSpecParams::SYNCBASE) {
    SMILTimedElement* timedElement = GetTimedElement(aElement);
    if (timedElement) {
      timedElement->RemoveDependent(*this);
    }
    mOwner->RemoveInstanceTimesForCreator(this, mIsBegin);
  } else if (IsEventBased()) {
    UnregisterEventListener(aElement);
  }
}

SMILTimedElement* SMILTimeValueSpec::GetTimedElement(Element* aElement) {
  auto* animationElement = SVGAnimationElement::FromNodeOrNull(aElement);
  return animationElement ? &animationElement->TimedElement() : nullptr;
}

// Indicates whether we're allowed to register an event-listener
// when scripting is disabled.
bool SMILTimeValueSpec::IsEventAllowedWhenScriptingIsDisabled() {
  // The category of (SMIL-specific) "repeat(n)" events are allowed.
  if (mParams.mType == SMILTimeValueSpecParams::REPEAT) {
    return true;
  }

  // A specific list of other SMIL-related events are allowed, too.
  if (mParams.mType == SMILTimeValueSpecParams::EVENT &&
      (mParams.mEventSymbol == nsGkAtoms::repeat ||
       mParams.mEventSymbol == nsGkAtoms::repeatEvent ||
       mParams.mEventSymbol == nsGkAtoms::beginEvent ||
       mParams.mEventSymbol == nsGkAtoms::endEvent)) {
    return true;
  }

  return false;
}

void SMILTimeValueSpec::RegisterEventListener(Element* aTarget) {
  MOZ_ASSERT(IsEventBased(),
             "Attempting to register event-listener for unexpected "
             "SMILTimeValueSpec type");
  MOZ_ASSERT(mParams.mEventSymbol,
             "Attempting to register event-listener but there is no event "
             "name");

  if (!aTarget) return;

  // When script is disabled, only allow registration for limited events.
  if (!aTarget->GetOwnerDocument()->IsScriptEnabled() &&
      !IsEventAllowedWhenScriptingIsDisabled()) {
    return;
  }

  if (!mEventListener) {
    mEventListener = new EventListener(this);
  }

  EventListenerManager* elm = aTarget->GetOrCreateListenerManager();
  if (!elm) {
    return;
  }

  elm->AddEventListenerByType(mEventListener,
                              nsDependentAtomString(mParams.mEventSymbol),
                              AllEventsAtSystemGroupBubble());
}

void SMILTimeValueSpec::UnregisterEventListener(Element* aTarget) {
  if (!aTarget || !mEventListener) {
    return;
  }

  EventListenerManager* elm = aTarget->GetOrCreateListenerManager();
  if (!elm) {
    return;
  }

  elm->RemoveEventListenerByType(mEventListener,
                                 nsDependentAtomString(mParams.mEventSymbol),
                                 AllEventsAtSystemGroupBubble());
}

void SMILTimeValueSpec::HandleEvent(Event* aEvent) {
  MOZ_ASSERT(mEventListener, "Got event without an event listener");
  MOZ_ASSERT(IsEventBased(), "Got event for non-event SMILTimeValueSpec");
  MOZ_ASSERT(aEvent, "No event supplied");

  // XXX In the long run we should get the time from the event itself which will
  // store the time in global document time which we'll need to convert to our
  // time container
  SMILTimeContainer* container = mOwner->GetTimeContainer();
  if (!container) return;

  if (mParams.mType == SMILTimeValueSpecParams::REPEAT &&
      !CheckRepeatEventDetail(aEvent)) {
    return;
  }

  SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
  SMILTimeValue newTime(currentTime);
  if (!ApplyOffset(newTime)) {
    NS_WARNING("New time generated from event overflows SMILTime, ignoring");
    return;
  }

  RefPtr<SMILInstanceTime> newInstance =
      new SMILInstanceTime(newTime, SMILInstanceTime::SOURCE_EVENT);
  mOwner->AddInstanceTime(newInstance, mIsBegin);
}

bool SMILTimeValueSpec::CheckRepeatEventDetail(Event* aEvent) {
  TimeEvent* timeEvent = aEvent->AsTimeEvent();
  if (!timeEvent) {
    NS_WARNING("Received a repeat event that was not a DOMTimeEvent");
    return false;
  }

  int32_t detail = timeEvent->Detail();
  return detail > 0 && (uint32_t)detail == mParams.mRepeatIteration;
}

SMILTimeValue SMILTimeValueSpec::ConvertBetweenTimeContainers(
    const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer) {
  // If the source time is either indefinite or unresolved the result is going
  // to be the same
  if (!aSrcTime.IsDefinite()) return aSrcTime;

  // Convert from source time container to our parent time container
  const SMILTimeContainer* dstContainer = mOwner->GetTimeContainer();
  if (dstContainer == aSrcContainer) return aSrcTime;

  // If one of the elements is not attached to a time container then we can't do
  // any meaningful conversion
  if (!aSrcContainer || !dstContainer) return SMILTimeValue();  // unresolved

  SMILTimeValue docTime =
      aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis());

  if (docTime.IsIndefinite())
    // This will happen if the source container is paused and we have a future
    // time. Just return the indefinite time.
    return docTime;

  MOZ_ASSERT(docTime.IsDefinite(),
             "ContainerToParentTime gave us an unresolved or indefinite time");

  return dstContainer->ParentToContainerTime(docTime.GetMillis());
}

bool SMILTimeValueSpec::ApplyOffset(SMILTimeValue& aTime) const {
  // indefinite + offset = indefinite. Likewise for unresolved times.
  if (!aTime.IsDefinite()) {
    return true;
  }

  double resultAsDouble =
      (double)aTime.GetMillis() + mParams.mOffset.GetMillis();
  if (resultAsDouble > double(std::numeric_limits<SMILTime>::max()) ||
      resultAsDouble < double(std::numeric_limits<SMILTime>::min())) {
    return false;
  }
  aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis());
  return true;
}

}  // namespace mozilla