/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 et tw=78: */
/* 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/AsyncEventDispatcher.h"
#include "mozilla/dom/TextTrack.h"
#include "mozilla/dom/TextTrackBinding.h"
#include "mozilla/dom/TextTrackList.h"
#include "mozilla/dom/TextTrackCue.h"
#include "mozilla/dom/TextTrackCueList.h"
#include "mozilla/dom/TextTrackRegion.h"
#include "mozilla/dom/HTMLMediaElement.h"
#include "mozilla/dom/HTMLTrackElement.h"
#include "nsGlobalWindow.h"

namespace mozilla {
namespace dom {

NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrack,
                                   DOMEventTargetHelper,
                                   mCueList,
                                   mActiveCueList,
                                   mTextTrackList,
                                   mTrackElement)

NS_IMPL_ADDREF_INHERITED(TextTrack, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(TextTrack, DOMEventTargetHelper)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TextTrack)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow,
                     TextTrackKind aKind,
                     const nsAString& aLabel,
                     const nsAString& aLanguage,
                     TextTrackMode aMode,
                     TextTrackReadyState aReadyState,
                     TextTrackSource aTextTrackSource)
  : DOMEventTargetHelper(aOwnerWindow)
  , mKind(aKind)
  , mLabel(aLabel)
  , mLanguage(aLanguage)
  , mMode(aMode)
  , mReadyState(aReadyState)
  , mTextTrackSource(aTextTrackSource)
{
  SetDefaultSettings();
}

TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow,
                     TextTrackList* aTextTrackList,
                     TextTrackKind aKind,
                     const nsAString& aLabel,
                     const nsAString& aLanguage,
                     TextTrackMode aMode,
                     TextTrackReadyState aReadyState,
                     TextTrackSource aTextTrackSource)
  : DOMEventTargetHelper(aOwnerWindow)
  , mTextTrackList(aTextTrackList)
  , mKind(aKind)
  , mLabel(aLabel)
  , mLanguage(aLanguage)
  , mMode(aMode)
  , mReadyState(aReadyState)
  , mTextTrackSource(aTextTrackSource)
{
  SetDefaultSettings();
}

TextTrack::~TextTrack()
{
}

void
TextTrack::SetDefaultSettings()
{
  nsPIDOMWindowInner* ownerWindow = GetOwner();
  mCueList = new TextTrackCueList(ownerWindow);
  mActiveCueList = new TextTrackCueList(ownerWindow);
  mCuePos = 0;
  mDirty = false;
}

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

void
TextTrack::SetMode(TextTrackMode aValue)
{
  if (mMode != aValue) {
    mMode = aValue;
    if (aValue == TextTrackMode::Disabled) {
      // Remove all the cues in MediaElement.
      if (mTextTrackList) {
        HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
        if (mediaElement) {
          for (size_t i = 0; i < mCueList->Length(); ++i) {
            mediaElement->NotifyCueRemoved(*(*mCueList)[i]);
          }
        }
      }
      SetCuesInactive();
    } else {
      // Add all the cues into MediaElement.
      if (mTextTrackList) {
        HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
        if (mediaElement) {
          for (size_t i = 0; i < mCueList->Length(); ++i) {
            mediaElement->NotifyCueAdded(*(*mCueList)[i]);
          }
        }
      }
    }
    if (mTextTrackList) {
      mTextTrackList->CreateAndDispatchChangeEvent();
    }
    // Ensure the TimeMarchesOn is called in case that the mCueList
    // is empty.
    NotifyCueUpdated(nullptr);
  }
}

void
TextTrack::GetId(nsAString& aId) const
{
  // If the track has a track element then its id should be the same as the
  // track element's id.
  if (mTrackElement) {
    mTrackElement->GetAttribute(NS_LITERAL_STRING("id"), aId);
  }
}

void
TextTrack::AddCue(TextTrackCue& aCue)
{
  TextTrack* oldTextTrack = aCue.GetTrack();
  if (oldTextTrack) {
    ErrorResult dummy;
    oldTextTrack->RemoveCue(aCue, dummy);
  }
  mCueList->AddCue(aCue);
  aCue.SetTrack(this);
  if (mTextTrackList) {
    HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
    if (mediaElement && (mMode != TextTrackMode::Disabled)) {
      mediaElement->NotifyCueAdded(aCue);
    }
  }
  SetDirty();
}

void
TextTrack::RemoveCue(TextTrackCue& aCue, ErrorResult& aRv)
{
  // Bug1304948, check the aCue belongs to the TextTrack.
  mCueList->RemoveCue(aCue, aRv);
  if (aRv.Failed()) {
    return;
  }
  aCue.SetActive(false);
  aCue.SetTrack(nullptr);
  if (mTextTrackList) {
    HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
    if (mediaElement) {
      mediaElement->NotifyCueRemoved(aCue);
    }
  }
  SetDirty();
}

void
TextTrack::SetCuesDirty()
{
  for (uint32_t i = 0; i < mCueList->Length(); i++) {
    ((*mCueList)[i])->Reset();
  }
}

void
TextTrack::UpdateActiveCueList()
{
  if (!mTextTrackList) {
    return;
  }

  HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
  if (!mediaElement) {
    return;
  }

  // If we are dirty, i.e. an event happened that may cause the sorted mCueList
  // to have changed like a seek or an insert for a cue, than we need to rebuild
  // the active cue list from scratch.
  if (mDirty) {
    mCuePos = 0;
    mDirty = false;
    mActiveCueList->RemoveAll();
  }

  double playbackTime = mediaElement->CurrentTime();
  // Remove all the cues from the active cue list whose end times now occur
  // earlier then the current playback time.
  for (uint32_t i = mActiveCueList->Length(); i > 0; i--) {
    if ((*mActiveCueList)[i - 1]->EndTime() <= playbackTime) {
      mActiveCueList->RemoveCueAt(i - 1);
    }
  }
  // Add all the cues, starting from the position of the last cue that was
  // added, that have valid start and end times for the current playback time.
  // We can stop iterating safely once we encounter a cue that does not have
  // a valid start time as the cue list is sorted.
  for (; mCuePos < mCueList->Length() &&
         (*mCueList)[mCuePos]->StartTime() <= playbackTime; mCuePos++) {
    if ((*mCueList)[mCuePos]->EndTime() > playbackTime) {
      mActiveCueList->AddCue(*(*mCueList)[mCuePos]);
    }
  }
}

TextTrackCueList*
TextTrack::GetActiveCues() {
  if (mMode != TextTrackMode::Disabled) {
    return mActiveCueList;
  }
  return nullptr;
}

void
TextTrack::GetActiveCueArray(nsTArray<RefPtr<TextTrackCue> >& aCues)
{
  if (mMode != TextTrackMode::Disabled) {
    mActiveCueList->GetArray(aCues);
  }
}

TextTrackReadyState
TextTrack::ReadyState() const
{
  return mReadyState;
}

void
TextTrack::SetReadyState(uint32_t aReadyState)
{
  if (aReadyState <= TextTrackReadyState::FailedToLoad) {
    SetReadyState(static_cast<TextTrackReadyState>(aReadyState));
  }
}

void
TextTrack::SetReadyState(TextTrackReadyState aState)
{
  mReadyState = aState;

  if (!mTextTrackList) {
    return;
  }

  HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
  if (mediaElement && (mReadyState == TextTrackReadyState::Loaded||
      mReadyState == TextTrackReadyState::FailedToLoad)) {
    mediaElement->RemoveTextTrack(this, true);
    mediaElement->UpdateReadyState();
  }
}

TextTrackList*
TextTrack::GetTextTrackList()
{
  return mTextTrackList;
}

void
TextTrack::SetTextTrackList(TextTrackList* aTextTrackList)
{
  mTextTrackList = aTextTrackList;
}

HTMLTrackElement*
TextTrack::GetTrackElement() {
  return mTrackElement;
}

void
TextTrack::SetTrackElement(HTMLTrackElement* aTrackElement) {
  mTrackElement = aTrackElement;
}

void
TextTrack::SetCuesInactive()
{
  mCueList->SetCuesInactive();
}

void
TextTrack::NotifyCueUpdated(TextTrackCue *aCue)
{
  mCueList->NotifyCueUpdated(aCue);
  if (mTextTrackList) {
    HTMLMediaElement* mediaElement = mTextTrackList->GetMediaElement();
    if (mediaElement) {
      mediaElement->NotifyCueUpdated(aCue);
    }
  }
  SetDirty();
}

void
TextTrack::GetLabel(nsAString& aLabel) const
{
  if (mTrackElement) {
    mTrackElement->GetLabel(aLabel);
  } else {
    aLabel = mLabel;
  }
}
void
TextTrack::GetLanguage(nsAString& aLanguage) const
{
  if (mTrackElement) {
    mTrackElement->GetSrclang(aLanguage);
  } else {
    aLanguage = mLanguage;
  }
}

void
TextTrack::DispatchAsyncTrustedEvent(const nsString& aEventName)
{
  nsPIDOMWindowInner* win = GetOwner();
  if (!win) {
    return;
  }
  RefPtr<TextTrack> self = this;
  nsGlobalWindow::Cast(win)->Dispatch(
    TaskCategory::Other,
    NS_NewRunnableFunction(
      "dom::TextTrack::DispatchAsyncTrustedEvent",
      [self, aEventName]() { self->DispatchTrustedEvent(aEventName); }));
}

bool
TextTrack::IsLoaded()
{
  if (mMode == TextTrackMode::Disabled) {
    return true;
  }
  // If the TrackElement's src is null, we can not block the
  // MediaElement.
  if (mTrackElement) {
    nsAutoString src;
    if (!(mTrackElement->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src))) {
      return true;
    }
  }
  return (mReadyState >= Loaded);
}

} // namespace dom
} // namespace mozilla
