content: /*
   * Copyright 2014 Jacob Klinker
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.mms.dom.smil;
  
  import com.android.mms.logs.LogTag;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashSet;
  
  import org.w3c.dom.NodeList;
  import org.w3c.dom.events.DocumentEvent;
  import org.w3c.dom.events.Event;
  import org.w3c.dom.events.EventTarget;
  import org.w3c.dom.smil.ElementParallelTimeContainer;
  import org.w3c.dom.smil.ElementSequentialTimeContainer;
  import org.w3c.dom.smil.ElementTime;
  import org.w3c.dom.smil.Time;
  import org.w3c.dom.smil.TimeList;
  
  import com.klinker.android.logger.Log;
  
  /**
   * The SmilPlayer is responsible for playing, stopping, pausing and resuming a SMIL tree.
   * <li>It creates a whole timeline before playing.</li>
   * <li>The player runs in a different thread which intends not to block the main thread.</li>
   */
  public class SmilPlayer implements Runnable {
      private static final String TAG = LogTag.TAG;
      private static final boolean DEBUG = false;
      private static final boolean LOCAL_LOGV = false;
      private static final int TIMESLICE = 200;
  
      private static enum SmilPlayerState {
          INITIALIZED,
          PLAYING,
          PLAYED,
          PAUSED,
          STOPPED,
      }
  
      private static enum SmilPlayerAction {
          NO_ACTIVE_ACTION,
          RELOAD,
          STOP,
          PAUSE,
          START,
          NEXT,
          PREV
      }
  
      public static final String MEDIA_TIME_UPDATED_EVENT = "mediaTimeUpdated";
  
      private static final Comparator<TimelineEntry> sTimelineEntryComparator =
          new Comparator<TimelineEntry>() {
          public int compare(TimelineEntry o1, TimelineEntry o2) {
              return Double.compare(o1.getOffsetTime(), o2.getOffsetTime());
          }
      };
  
      private static SmilPlayer sPlayer;
  
      private long mCurrentTime;
      private int mCurrentElement;
      private int mCurrentSlide;
      private ArrayList<TimelineEntry> mAllEntries;
      private ElementTime mRoot;
      private Thread mPlayerThread;
      private SmilPlayerState mState = SmilPlayerState.INITIALIZED;
      private SmilPlayerAction mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
      private ArrayList<ElementTime> mActiveElements;
      private Event mMediaTimeUpdatedEvent;
  
      private static ArrayList<TimelineEntry> getParTimeline(
              ElementParallelTimeContainer par, double offset, double maxOffset) {
          ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
  
          // Set my begin at first
          TimeList myBeginList = par.getBegin();
          /*
           * Begin list only contain 1 begin time which has been resolved.
           * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getBegin()
           */
          Time begin = myBeginList.item(0);
          double beginOffset = begin.getResolvedOffset() + offset;
          if (beginOffset > maxOffset) {
              // This element can't be started.
              return timeline;
          }
          TimelineEntry myBegin = new TimelineEntry(beginOffset, par, TimelineEntry.ACTION_BEGIN);
          timeline.add(myBegin);
  
          TimeList myEndList = par.getEnd();
          /*
           * End list only contain 1 end time which has been resolved.
           * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getEnd()
           */
          Time end = myEndList.item(0);
          double endOffset = end.getResolvedOffset() + offset;
          if (endOffset > maxOffset) {
              endOffset = maxOffset;
          }
          TimelineEntry myEnd = new TimelineEntry(endOffset, par, TimelineEntry.ACTION_END);
  
          maxOffset = endOffset;
  
          NodeList children = par.getTimeChildren();
          for (int i = 0; i < children.getLength(); ++i) {
              ElementTime child = (ElementTime) children.item(i);
              ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset);
              timeline.addAll(childTimeline);
          }
  
          Collections.sort(timeline, sTimelineEntryComparator);
  
          // Add end-event to timeline for all active children
          NodeList activeChildrenAtEnd = par.getActiveChildrenAt(
                  (float) (endOffset - offset) * 1000);
          for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) {
              timeline.add(new TimelineEntry(endOffset,
                      (ElementTime) activeChildrenAtEnd.item(i),
                      TimelineEntry.ACTION_END));
          }
  
          // Set my end at last
          timeline.add(myEnd);
  
          return timeline;
      }
  
      private static ArrayList<TimelineEntry> getSeqTimeline(
              ElementSequentialTimeContainer seq, double offset, double maxOffset) {
          ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
          double orgOffset = offset;
  
          // Set my begin at first
          TimeList myBeginList = seq.getBegin();
          /*
           * Begin list only contain 1 begin time which has been resolved.
           * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getBegin()
           */
          Time begin = myBeginList.item(0);
          double beginOffset = begin.getResolvedOffset() + offset;
          if (beginOffset > maxOffset) {
              // This element can't be started.
              return timeline;
          }
          TimelineEntry myBegin = new TimelineEntry(beginOffset, seq, TimelineEntry.ACTION_BEGIN);
          timeline.add(myBegin);
  
          TimeList myEndList = seq.getEnd();
          /*
           * End list only contain 1 end time which has been resolved.
           * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getEnd()
           */
          Time end = myEndList.item(0);
          double endOffset = end.getResolvedOffset() + offset;
          if (endOffset > maxOffset) {
              endOffset = maxOffset;
          }
          TimelineEntry myEnd = new TimelineEntry(endOffset, seq, TimelineEntry.ACTION_END);
  
          maxOffset = endOffset;
  
          // Get children's timelines
          NodeList children = seq.getTimeChildren();
          for (int i = 0; i < children.getLength(); ++i) {
              ElementTime child = (ElementTime) children.item(i);
              ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset);
              timeline.addAll(childTimeline);
  
              // Since the child timeline has been sorted, the offset of the last one is the biggest.
              offset = childTimeline.get(childTimeline.size() - 1).getOffsetTime();
          }
  
          // Add end-event to timeline for all active children
          NodeList activeChildrenAtEnd = seq.getActiveChildrenAt(
                  (float) (endOffset - orgOffset));
          for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) {
              timeline.add(new TimelineEntry(endOffset,
                      (ElementTime) activeChildrenAtEnd.item(i),
                      TimelineEntry.ACTION_END));
          }
  
          // Set my end at last
          timeline.add(myEnd);
  
          return timeline;
      }
  
      private static ArrayList<TimelineEntry> getTimeline(ElementTime element,
              double offset, double maxOffset) {
          if (element instanceof ElementParallelTimeContainer) {
              return getParTimeline((ElementParallelTimeContainer) element, offset, maxOffset);
          } else if (element instanceof ElementSequentialTimeContainer) {
              return getSeqTimeline((ElementSequentialTimeContainer) element, offset, maxOffset);
          } else {
              // Not ElementTimeContainer here
              ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
  
              TimeList beginList = element.getBegin();
              for (int i = 0; i < beginList.getLength(); ++i) {
                  Time begin = beginList.item(i);
                  if (begin.getResolved()) {
                      double beginOffset = begin.getResolvedOffset() + offset;
                      if (beginOffset <= maxOffset) {
                          TimelineEntry entry = new TimelineEntry(beginOffset,
                                  element, TimelineEntry.ACTION_BEGIN);
                          timeline.add(entry);
                      }
                  }
              }
  
              TimeList endList = element.getEnd();
              for (int i = 0; i < endList.getLength(); ++i) {
                  Time end = endList.item(i);
                  if (end.getResolved()) {
                      double endOffset = end.getResolvedOffset() + offset;
                      if (endOffset <= maxOffset) {
                          TimelineEntry entry = new TimelineEntry(endOffset,
                                  element, TimelineEntry.ACTION_END);
                          timeline.add(entry);
                      }
                  }
              }
  
              Collections.sort(timeline, sTimelineEntryComparator);
  
              return timeline;
          }
      }
  
      private SmilPlayer() {
          // Private constructor
      }
  
      public static SmilPlayer getPlayer() {
          if (sPlayer == null) {
              sPlayer = new SmilPlayer();
          }
          return sPlayer;
      }
  
      public synchronized boolean isPlayingState() {
          return mState == SmilPlayerState.PLAYING;
      }
  
      public synchronized boolean isPlayedState() {
          return mState == SmilPlayerState.PLAYED;
      }
  
      public synchronized boolean isPausedState() {
          return mState == SmilPlayerState.PAUSED;
      }
  
      public synchronized boolean isStoppedState() {
          return mState == SmilPlayerState.STOPPED;
      }
  
      private synchronized boolean isPauseAction() {
          return mAction == SmilPlayerAction.PAUSE;
      }
  
      private synchronized boolean isStartAction() {
          return mAction == SmilPlayerAction.START;
      }
  
      private synchronized boolean isStopAction() {
          return mAction == SmilPlayerAction.STOP;
      }
  
      private synchronized boolean isReloadAction() {
          return mAction == SmilPlayerAction.RELOAD;
      }
  
      private synchronized boolean isNextAction() {
        return mAction == SmilPlayerAction.NEXT;
      }
  
      private synchronized boolean isPrevAction() {
        return mAction == SmilPlayerAction.PREV;
      }
  
      public synchronized void init(ElementTime root) {
          mRoot = root;
          mAllEntries = getTimeline(mRoot, 0, Long.MAX_VALUE);
          mMediaTimeUpdatedEvent = ((DocumentEvent) mRoot).createEvent("Event");
          mMediaTimeUpdatedEvent.initEvent(MEDIA_TIME_UPDATED_EVENT, false, false);
          mActiveElements = new ArrayList<ElementTime>();
      }
  
      public synchronized void play() {
          if (!isPlayingState()) {
              mCurrentTime = 0;
              mCurrentElement = 0;
              mCurrentSlide = 0;
              mPlayerThread = new Thread(this, "SmilPlayer thread");
              mState = SmilPlayerState.PLAYING;
              mPlayerThread.start();
          } else {
              Log.w(TAG, "Error State: Playback is playing!");
          }
      }
  
      public synchronized void pause() {
          if (isPlayingState()) {
              mAction = SmilPlayerAction.PAUSE;
              notifyAll();
          } else {
              Log.w(TAG, "Error State: Playback is not playing!");
          }
      }
  
      public synchronized void start() {
          if (isPausedState()) {
              resumeActiveElements();
              mAction = SmilPlayerAction.START;
              notifyAll();
          } else if (isPlayedState()) {
              play();
          } else {
              Log.w(TAG, "Error State: Playback can not be started!");
          }
      }
  
      public synchronized void stop() {
          if (isPlayingState() || isPausedState()) {
              mAction = SmilPlayerAction.STOP;
              notifyAll();
          } else if (isPlayedState()) {
              actionStop();
          }
      }
  
      public synchronized void stopWhenReload() {
          endActiveElements();
      }
  
      public synchronized void reload() {
          if (isPlayingState() || isPausedState()) {
              mAction = SmilPlayerAction.RELOAD;
              notifyAll();
          } else if (isPlayedState()) {
              actionReload();
          }
      }
  
      public synchronized void next() {
        if (isPlayingState() || isPausedState()) {
          mAction = SmilPlayerAction.NEXT;
          notifyAll();
        }
      }
  
      public synchronized void prev() {
        if (isPlayingState() || isPausedState()) {
          mAction = SmilPlayerAction.PREV;
          notifyAll();
        }
      }
  
      private synchronized boolean isBeginOfSlide(TimelineEntry entry) {
          return (TimelineEntry.ACTION_BEGIN == entry.getAction())
                      && (entry.getElement() instanceof SmilParElementImpl);
      }
  
      private synchronized void reloadActiveSlide() {
          mActiveElements.clear();
          beginSmilDocument();
  
          for (int i = mCurrentSlide; i < mCurrentElement; i++) {
              TimelineEntry entry = mAllEntries.get(i);
              actionEntry(entry);
          }
          seekActiveMedia();
      }
  
      private synchronized void beginSmilDocument() {
          TimelineEntry entry = mAllEntries.get(0);
          actionEntry(entry);
      }
  
      private synchronized double getOffsetTime(ElementTime element) {
          for (int i = mCurrentSlide; i < mCurrentElement; i++) {
              TimelineEntry entry = mAllEntries.get(i);
              if (element.equals(entry.getElement())) {
                  return entry.getOffsetTime() * 1000;  // in ms
              }
          }
          return -1;
      }
  
      private synchronized void seekActiveMedia() {
          for (int i = mActiveElements.size() - 1; i >= 0; i--) {
              ElementTime element = mActiveElements.get(i);
              if (element instanceof SmilParElementImpl) {
                  return;
              }
              double offset = getOffsetTime(element);
              if ((offset >= 0) && (offset <= mCurrentTime)) {
                  if (LOCAL_LOGV) {
                      Log.v(TAG, "[SEEK]  " + " at " + mCurrentTime
                              + " " + element);
                  }
                  element.seekElement( (float) (mCurrentTime - offset) );
              }
          }
      }
  
      private synchronized void waitForEntry(long interval)
              throws InterruptedException {
          if (LOCAL_LOGV) {
              Log.v(TAG, "Waiting for " + interval + "ms.");
          }
  
          long overhead = 0;
  
          while (interval > 0) {
              long startAt = System.currentTimeMillis();
              long sleep = Math.min(interval, TIMESLICE);
              if (overhead < sleep) {
                  wait(sleep - overhead);
                  mCurrentTime += sleep;
              } else {
                  sleep = 0;
                  mCurrentTime += overhead;
              }
  
              if (isStopAction() || isReloadAction() || isPauseAction() || isNextAction() ||
                  isPrevAction()) {
                  return;
              }
  
              ((EventTarget) mRoot).dispatchEvent(mMediaTimeUpdatedEvent);
  
              interval -= TIMESLICE;
              overhead = System.currentTimeMillis() - startAt - sleep;
          }
      }
  
      public synchronized int getDuration() {
           if ((mAllEntries != null) && !mAllEntries.isEmpty()) {
               return (int) mAllEntries.get(mAllEntries.size() - 1).mOffsetTime * 1000;
           }
           return 0;
      }
  
      public synchronized int getCurrentPosition() {
          return (int) mCurrentTime;
      }
  
      private synchronized void endActiveElements() {
          for (int i = mActiveElements.size() - 1; i >= 0; i--) {
              ElementTime element = mActiveElements.get(i);
              if (LOCAL_LOGV) {
                  Log.v(TAG, "[STOP]  " + " at " + mCurrentTime
                          + " " + element);
              }
              element.endElement();
          }
      }
  
      private synchronized void pauseActiveElements() {
          for (int i = mActiveElements.size() - 1; i >= 0; i--) {
              ElementTime element = mActiveElements.get(i);
              if (LOCAL_LOGV) {
                  Log.v(TAG, "[PAUSE]  " + " at " + mCurrentTime
                          + " " + element);
              }
              element.pauseElement();
          }
      }
  
      private synchronized void resumeActiveElements() {
          int size = mActiveElements.size();
          for (int i = 0; i < size; i++) {
              ElementTime element = mActiveElements.get(i);
              if (LOCAL_LOGV) {
                  Log.v(TAG, "[RESUME]  " + " at " + mCurrentTime
                          + " " + element);
              }
              element.resumeElement();
          }
      }
  
      private synchronized void waitForWakeUp() {
          try {
              while ( !(isStartAction() || isStopAction() || isReloadAction() ||
                      isNextAction() || isPrevAction()) ) {
                  wait(TIMESLICE);
              }
              if (isStartAction()) {
                  mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
                  mState = SmilPlayerState.PLAYING;
              }
          } catch (InterruptedException e) {
              Log.e(TAG, "Unexpected InterruptedException.", e);
          }
      }
  
      private synchronized void actionEntry(TimelineEntry entry) {
          switch (entry.getAction()) {
              case TimelineEntry.ACTION_BEGIN:
                  if (LOCAL_LOGV) {
                      Log.v(TAG, "[START] " + " at " + mCurrentTime + " "
                              + entry.getElement());
                  }
                  entry.getElement().beginElement();
                  mActiveElements.add(entry.getElement());
                  break;
              case TimelineEntry.ACTION_END:
                  if (LOCAL_LOGV) {
                      Log.v(TAG, "[STOP]  " + " at " + mCurrentTime + " "
                              + entry.getElement());
                  }
                  entry.getElement().endElement();
                  mActiveElements.remove(entry.getElement());
                  break;
              default:
                  break;
          }
      }
  
      private synchronized TimelineEntry reloadCurrentEntry() {
          // Check if the position is less than size of all entries
          if (mCurrentElement < mAllEntries.size()) {
              return mAllEntries.get(mCurrentElement);
          } else {
              return null;
          }
      }
  
      private void stopCurrentSlide() {
          HashSet<TimelineEntry> skippedEntries = new HashSet<TimelineEntry>();
          int totalEntries = mAllEntries.size();
          for (int i = mCurrentElement; i < totalEntries; i++) {
              // Stop any started entries, and skip the not started entries until
              // meeting the end of slide
              TimelineEntry entry = mAllEntries.get(i);
              int action = entry.getAction();
              if (entry.getElement() instanceof SmilParElementImpl &&
                      action == TimelineEntry.ACTION_END) {
                  actionEntry(entry);
                  mCurrentElement = i;
                  break;
              } else if (action == TimelineEntry.ACTION_END && !skippedEntries.contains(entry)) {
                      actionEntry(entry);
              } else if (action == TimelineEntry.ACTION_BEGIN) {
                  skippedEntries.add(entry);
              }
          }
      }
  
      private TimelineEntry loadNextSlide() {
        TimelineEntry entry;
        int totalEntries = mAllEntries.size();
        for (int i = mCurrentElement; i < totalEntries; i++) {
            entry = mAllEntries.get(i);
            if (isBeginOfSlide(entry)) {
                mCurrentElement = i;
                mCurrentSlide = i;
                mCurrentTime = (long)(entry.getOffsetTime() * 1000);
                return entry;
            }
        }
        // No slide, finish play back
        mCurrentElement++;
        entry = null;
        if (mCurrentElement < totalEntries) {
            entry = mAllEntries.get(mCurrentElement);
            mCurrentTime = (long)(entry.getOffsetTime() * 1000);
        }
        return entry;
      }
  
      private TimelineEntry loadPrevSlide() {
        int skippedSlides = 1;
        int latestBeginEntryIndex = -1;
        for (int i = mCurrentSlide; i >= 0; i--) {
          TimelineEntry entry = mAllEntries.get(i);
          if (isBeginOfSlide(entry)) {
              latestBeginEntryIndex = i;
            if (0 == skippedSlides-- ) {
              mCurrentElement = i;
              mCurrentSlide = i;
              mCurrentTime = (long)(entry.getOffsetTime() * 1000);
              return entry;
            }
          }
        }
        if (latestBeginEntryIndex != -1) {
            mCurrentElement = latestBeginEntryIndex;
            mCurrentSlide = latestBeginEntryIndex;
            return mAllEntries.get(mCurrentElement);
        }
        return null;
      }
  
      private synchronized TimelineEntry actionNext() {
          stopCurrentSlide();
          return loadNextSlide();
     }
  
      private synchronized TimelineEntry actionPrev() {
          stopCurrentSlide();
          return loadPrevSlide();
      }
  
      private synchronized void actionPause() {
          pauseActiveElements();
          mState = SmilPlayerState.PAUSED;
          mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
      }
  
      private synchronized void actionStop() {
          endActiveElements();
          mCurrentTime = 0;
          mCurrentElement = 0;
          mCurrentSlide = 0;
          mState = SmilPlayerState.STOPPED;
          mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
      }
  
      private synchronized void actionReload() {
          reloadActiveSlide();
          mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
      }
  
      public void run() {
          if (isStoppedState()) {
              return;
          }
          if (LOCAL_LOGV) {
              dumpAllEntries();
          }
          // Play the Element by following the timeline
          int size = mAllEntries.size();
          for (mCurrentElement = 0; mCurrentElement < size; mCurrentElement++) {
              TimelineEntry entry = mAllEntries.get(mCurrentElement);
              if (isBeginOfSlide(entry)) {
                  mCurrentSlide = mCurrentElement;
              }
              long offset = (long) (entry.getOffsetTime() * 1000); // in ms.
              while (offset > mCurrentTime) {
                  try {
                      waitForEntry(offset - mCurrentTime);
                  } catch (InterruptedException e) {
                      Log.e(TAG, "Unexpected InterruptedException.", e);
                  }
  
                  while (isPauseAction() || isStopAction() || isReloadAction() || isNextAction() ||
                      isPrevAction()) {
                      if (isPauseAction()) {
                          actionPause();
                          waitForWakeUp();
                      }
  
                      if (isStopAction()) {
                          actionStop();
                          return;
                      }
  
                      if (isReloadAction()) {
                          actionReload();
                          entry = reloadCurrentEntry();
                          if (entry == null)
                              return;
                          if (isPausedState()) {
                              mAction = SmilPlayerAction.PAUSE;
                          }
                      }
  
                      if (isNextAction()) {
                          TimelineEntry nextEntry = actionNext();
                          if (nextEntry != null) {
                              entry = nextEntry;
                          }
                          if (mState == SmilPlayerState.PAUSED) {
                              mAction = SmilPlayerAction.PAUSE;
                              actionEntry(entry);
                          } else {
                              mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
                          }
                          offset = mCurrentTime;
                      }
  
                      if (isPrevAction()) {
                          TimelineEntry prevEntry = actionPrev();
                          if (prevEntry != null) {
                              entry = prevEntry;
                          }
                          if (mState == SmilPlayerState.PAUSED) {
                              mAction = SmilPlayerAction.PAUSE;
                              actionEntry(entry);
                          } else {
                              mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
                          }
                          offset = mCurrentTime;
                      }
                  }
              }
              mCurrentTime = offset;
              actionEntry(entry);
          }
  
          mState = SmilPlayerState.PLAYED;
      }
  
      private static final class TimelineEntry {
          final static int ACTION_BEGIN = 0;
          final static int ACTION_END   = 1;
  
          private final double mOffsetTime;
          private final ElementTime mElement;
          private final int mAction;
  
          public TimelineEntry(double offsetTime, ElementTime element, int action) {
              mOffsetTime = offsetTime;
              mElement = element;
              mAction  = action;
          }
  
          public double getOffsetTime() {
              return mOffsetTime;
          }
  
          public ElementTime getElement() {
              return mElement;
          }
  
          public int getAction() {
              return mAction;
          }
  
          public String toString() {
              return "Type = " + mElement + " offset = " + getOffsetTime() + " action = " + getAction();
          }
      }
  
      private void dumpAllEntries() {
          if (LOCAL_LOGV) {
              for (TimelineEntry entry : mAllEntries) {
                  Log.v(TAG, "[Entry] "+ entry);
              }
          }
      }
  }
  