/*
 * libjingle
 * Copyright 2014 Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.appspot.apprtc.test;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
import org.appspot.apprtc.util.LooperExecutor;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;
import org.webrtc.VideoRenderer;

import android.test.InstrumentationTestCase;
import android.util.Log;

public class PeerConnectionClientTest extends InstrumentationTestCase
    implements PeerConnectionEvents {
  private static final String TAG = "RTCClientTest";
  private static final String STUN_SERVER = "stun:stun.l.google.com:19302";
  private static final int WAIT_TIMEOUT = 5000;
  private static final int EXPECTED_VIDEO_FRAMES = 15;

  // The peer connection client is assumed to be thread safe in itself; the
  // reference is written by the test thread and read by worker threads.
  private volatile PeerConnectionClient pcClient;
  private volatile boolean loopback;

  // These are protected by their respective event objects.
  private boolean isClosed;
  private boolean isIceConnected;
  private SessionDescription localSdp;
  private List<IceCandidate> iceCandidates = new LinkedList<IceCandidate>();
  private final Object localSdpEvent = new Object();
  private final Object iceCandidateEvent = new Object();
  private final Object iceConnectedEvent = new Object();
  private final Object closeEvent = new Object();

  // Mock renderer implementation.
  private static class MockRenderer implements VideoRenderer.Callbacks {
    // These are protected by 'this' since we gets called from worker threads.
    private int width = -1;
    private int height = -1;
    private boolean renderFrameCalled = false;
    private boolean setSizeCalledBeforeRenderFrame = false;

    // Thread-safe in itself.
    private final CountDownLatch doneRendering;

    public MockRenderer(int expectedFrames) {
      doneRendering = new CountDownLatch(expectedFrames);
    }

    @Override
    public synchronized void setSize(int width, int height) {
      Log.d(TAG, "Set size: " + width + " x " + height);
      this.width = width;
      this.height = height;
      if (!renderFrameCalled) {
        setSizeCalledBeforeRenderFrame = true;
      }
    }

    @Override
    public synchronized void renderFrame(VideoRenderer.I420Frame frame) {
      renderFrameCalled = true;
      doneRendering.countDown();
    }

    public synchronized int getWidth() { return width; }
    public synchronized int getHeight() { return height; }
    public synchronized boolean setSizeCalledBeforeRenderFrame() {
      return setSizeCalledBeforeRenderFrame;
    }

    // This method shouldn't hold any locks or touch member variables since it
    // blocks.
    public boolean waitForFramesRendered(int timeoutMs)
        throws InterruptedException {
      doneRendering.await(timeoutMs, TimeUnit.MILLISECONDS);
      return (doneRendering.getCount() <= 0);
    }
  }

  // Peer connection events implementation.
  @Override
  public void onLocalDescription(SessionDescription sdp) {
    Log.d(TAG, "LocalSDP type: " + sdp.type);
    synchronized (localSdpEvent) {
      localSdp = sdp;
      localSdpEvent.notifyAll();
    }
  }

  @Override
  public void onIceCandidate(IceCandidate candidate) {
    Log.d(TAG, "IceCandidate: " + candidate.sdp);
    synchronized(iceCandidateEvent) {
      if (loopback) {
        pcClient.addRemoteIceCandidate(candidate);
      }
      iceCandidates.add(candidate);
      iceCandidateEvent.notifyAll();
    }
  }

  @Override
  public void onIceConnected() {
    Log.d(TAG, "ICE Connected");
    synchronized(iceConnectedEvent) {
      isIceConnected = true;
      iceConnectedEvent.notifyAll();
    }
  }

  @Override
  public void onIceDisconnected() {
    Log.d(TAG, "ICE Disconnected");
    synchronized(iceConnectedEvent) {
      isIceConnected = false;
      iceConnectedEvent.notifyAll();
    }
  }

  @Override
  public void onPeerConnectionClosed() {
    Log.d(TAG, "PeerConnection closed");
    synchronized(closeEvent) {
      isClosed = true;
      closeEvent.notifyAll();
    }
  }

  @Override
  public void onPeerConnectionError(String description) {
    fail("PC Error: " + description);
  }

  // Helper wait functions.
  private boolean waitForLocalSDP(int timeoutMs)
      throws InterruptedException {
    synchronized(localSdpEvent) {
      if (localSdp == null) {
        localSdpEvent.wait(timeoutMs);
      }
      return (localSdp != null);
    }
  }

  private boolean waitForIceCandidates(int timeoutMs)
      throws InterruptedException {
    synchronized(iceCandidateEvent) {
      if (iceCandidates.size() == 0) {
        iceCandidateEvent.wait(timeoutMs);
      }
      return (iceCandidates.size() > 0);
    }
  }

  private boolean waitForIceConnected(int timeoutMs)
      throws InterruptedException {
    synchronized(iceConnectedEvent) {
      if (!isIceConnected) {
        iceConnectedEvent.wait(timeoutMs);
      }
      return isIceConnected;
    }
  }

  private boolean waitForPeerConnectionClosed(int timeoutMs)
      throws InterruptedException {
    synchronized(closeEvent) {
      if (!isClosed) {
        closeEvent.wait(timeoutMs);
      }
      return isClosed;
    }
  }

  private SignalingParameters getTestSignalingParameters() {
    List<PeerConnection.IceServer> iceServers =
        new LinkedList<PeerConnection.IceServer>();
    PeerConnection.IceServer iceServer = new
        PeerConnection.IceServer(STUN_SERVER, "", "");
    iceServers.add(iceServer);
    MediaConstraints pcConstraints = new MediaConstraints();
    MediaConstraints videoConstraints = new MediaConstraints();
    MediaConstraints audioConstraints = new MediaConstraints();
    SignalingParameters signalingParameters = new SignalingParameters(
        iceServers, true,
        pcConstraints, videoConstraints, audioConstraints,
        null, null,
        null, null,
        null, null);
    return signalingParameters;
  }

  PeerConnectionClient createPeerConnectionClient(MockRenderer localRenderer,
                                                  MockRenderer remoteRenderer) {
    SignalingParameters signalingParameters = getTestSignalingParameters();

    PeerConnectionClient client = new PeerConnectionClient();
    client.createPeerConnectionFactory(
        getInstrumentation().getContext(), "VP8", true, null, this);
    client.createPeerConnection(
        localRenderer, remoteRenderer, signalingParameters, 1000);
    client.createOffer();
    return client;
  }

  public void testSetLocalOfferMakesVideoFlowLocally()
      throws InterruptedException {
    Log.d(TAG, "testSetLocalOfferMakesVideoFlowLocally");
    MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES);
    pcClient = createPeerConnectionClient(localRenderer, new MockRenderer(0));

    // Wait for local SDP and ice candidates set events.
    assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    assertTrue("ICE candidates were not generated.",
        waitForIceCandidates(WAIT_TIMEOUT));

    // Check that local video frames were rendered.
    assertTrue("Local video frames were not rendered.",
        localRenderer.waitForFramesRendered(WAIT_TIMEOUT));

    pcClient.close();
    assertTrue("PeerConnection close event was not received.",
        waitForPeerConnectionClosed(WAIT_TIMEOUT));
    Log.d(TAG, "testSetLocalOfferMakesVideoFlowLocally Done.");
  }

  public void testSizeIsSetBeforeStartingToRender()
      throws InterruptedException {
    Log.d(TAG, "testSizeIsSetBeforeStartingToRender");
    MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES);
    pcClient = createPeerConnectionClient(localRenderer, new MockRenderer(0));

    waitForLocalSDP(WAIT_TIMEOUT);
    waitForIceCandidates(WAIT_TIMEOUT);

    // Check that local video frames were rendered.
    assertTrue("Local video frames were not rendered.",
        localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    assertTrue("Should have set size before rendering frames; size wasn't set",
        localRenderer.setSizeCalledBeforeRenderFrame());
    assertTrue(localRenderer.getWidth() > 0);
    assertTrue(localRenderer.getHeight() > 0);

    pcClient.close();
    waitForPeerConnectionClosed(WAIT_TIMEOUT);
    Log.d(TAG, "testSizeIsSetBeforeStartingToRender Done.");
  }

  public void testLoopback() throws InterruptedException {
    Log.d(TAG, "testLoopback");
    loopback = true;

    MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES);
    MockRenderer remoteRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES);

    pcClient = createPeerConnectionClient(localRenderer, remoteRenderer);

    // Wait for local SDP, rename it to answer and set as remote SDP.
    assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    SessionDescription remoteSdp = new SessionDescription(
        SessionDescription.Type.fromCanonicalForm("answer"),
        localSdp.description);
    pcClient.setRemoteDescription(remoteSdp);

    // Wait for ICE connection.
    assertTrue("ICE connection failure.", waitForIceConnected(WAIT_TIMEOUT));

    // Check that local video frames were rendered.
    assertTrue("Local video frames were not rendered.",
        localRenderer.waitForFramesRendered(WAIT_TIMEOUT));

    // Check that remote video frames were rendered.
    assertTrue("Remote video frames were not rendered.",
        remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));

    pcClient.close();
    assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
    Log.d(TAG, "testLoopback Done.");
  }
}
