/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.google.android.exoplayer2.video;

import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.TextureView;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions;

/**
 * Listener of video {@link Renderer} events.
 */
public interface VideoRendererEventListener {

  /**
   * Called when the renderer is enabled.
   *
   * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
   *     remains enabled.
   */
  void onVideoEnabled(DecoderCounters counters);

  /**
   * Called when a decoder is created.
   *
   * @param decoderName The decoder that was created.
   * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
   *     finished.
   * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
   */
  void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
      long initializationDurationMs);

  /**
   * Called when the format of the media being consumed by the renderer changes.
   *
   * @param format The new format.
   */
  void onVideoInputFormatChanged(Format format);

  /**
   * Called to report the number of frames dropped by the renderer. Dropped frames are reported
   * whenever the renderer is stopped having dropped frames, and optionally, whenever the count
   * reaches a specified threshold whilst the renderer is started.
   *
   * @param count The number of dropped frames.
   * @param elapsedMs The duration in milliseconds over which the frames were dropped. This
   *     duration is timed from when the renderer was started or from when dropped frames were
   *     last reported (whichever was more recent), and not from when the first of the reported
   *     drops occurred.
   */
  void onDroppedFrames(int count, long elapsedMs);

  /**
   * Called before a frame is rendered for the first time since setting the surface, and each time
   * there's a change in the size, rotation or pixel aspect ratio of the video being rendered.
   *
   * @param width The video width in pixels.
   * @param height The video height in pixels.
   * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
   *     rotation in degrees that the application should apply for the video for it to be rendered
   *     in the correct orientation. This value will always be zero on API levels 21 and above,
   *     since the renderer will apply all necessary rotations internally. On earlier API levels
   *     this is not possible. Applications that use {@link TextureView} can apply the rotation by
   *     calling {@link TextureView#setTransform}. Applications that do not expect to encounter
   *     rotated videos can safely ignore this parameter.
   * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
   *     of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
   *     content.
   */
  void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
      float pixelWidthHeightRatio);

  /**
   * Called when a frame is rendered for the first time since setting the surface, and when a frame
   * is rendered for the first time since the renderer was reset.
   *
   * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
   *     the renderer renders to something that isn't a {@link Surface}.
   */
  void onRenderedFirstFrame(Surface surface);

  /**
   * Called when the renderer is disabled.
   *
   * @param counters {@link DecoderCounters} that were updated by the renderer.
   */
  void onVideoDisabled(DecoderCounters counters);

  /**
   * Dispatches events to a {@link VideoRendererEventListener}.
   */
  final class EventDispatcher {

    @Nullable private final Handler handler;
    @Nullable private final VideoRendererEventListener listener;

    /**
     * @param handler A handler for dispatching events, or null if creating a dummy instance.
     * @param listener The listener to which events should be dispatched, or null if creating a
     *     dummy instance.
     */
    public EventDispatcher(@Nullable Handler handler,
        @Nullable VideoRendererEventListener listener) {
      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
      this.listener = listener;
    }

    /**
     * Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}.
     */
    public void enabled(final DecoderCounters decoderCounters) {
      if (listener != null) {
        handler.post(new Runnable() {
          @Override
          public void run() {
            listener.onVideoEnabled(decoderCounters);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}.
     */
    public void decoderInitialized(final String decoderName,
        final long initializedTimestampMs, final long initializationDurationMs) {
      if (listener != null) {
        handler.post(new Runnable() {
          @Override
          public void run() {
            listener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
                initializationDurationMs);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}.
     */
    public void inputFormatChanged(final Format format) {
      if (listener != null) {
        handler.post(new Runnable() {
          @Override
          public void run() {
            listener.onVideoInputFormatChanged(format);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
     */
    public void droppedFrames(final int droppedFrameCount, final long elapsedMs) {
      if (listener != null) {
        handler.post(new Runnable()  {
          @Override
          public void run() {
            listener.onDroppedFrames(droppedFrameCount, elapsedMs);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}.
     */
    public void videoSizeChanged(final int width, final int height,
        final int unappliedRotationDegrees, final float pixelWidthHeightRatio) {
      if (listener != null) {
        handler.post(new Runnable()  {
          @Override
          public void run() {
            listener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
                pixelWidthHeightRatio);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}.
     */
    public void renderedFirstFrame(final Surface surface) {
      if (listener != null) {
        handler.post(new Runnable()  {
          @Override
          public void run() {
            listener.onRenderedFirstFrame(surface);
          }
        });
      }
    }

    /**
     * Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}.
     */
    public void disabled(final DecoderCounters counters) {
      if (listener != null) {
        handler.post(new Runnable() {
          @Override
          public void run() {
            counters.ensureUpdated();
            listener.onVideoDisabled(counters);
          }
        });
      }
    }

  }

}
