/* 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/. */

package org.mozilla.gecko.media;

import android.media.MediaCrypto;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import java.util.ArrayList;

final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub
    implements IBinder.DeathRecipient {
  private static final String LOGTAG = "RemoteDrmBridgeStub";
  private static final boolean DEBUG = false;
  private volatile IMediaDrmBridgeCallbacks mCallbacks = null;

  // Underlying bridge implmenetaion, i.e. GeckoMediaDrmBrdigeV21.
  private GeckoMediaDrm mBridge = null;

  // mStubId is initialized during stub construction. It should be a unique
  // string which is generated in MediaDrmProxy in Fennec App process and is
  // used for Codec to obtain corresponding MediaCrypto as input to achieve
  // decryption.
  // The generated stubId will be delivered to Codec via a code path starting
  // from MediaDrmProxy -> MediaDrmCDMProxy -> RemoteDataDecoder => IPC => Codec.
  private String mStubId = "";

  public static final ArrayList<RemoteMediaDrmBridgeStub> mBridgeStubs =
      new ArrayList<RemoteMediaDrmBridgeStub>();

  private String getId() {
    return mStubId;
  }

  private MediaCrypto getMediaCryptoFromBridge() {
    return mBridge != null ? mBridge.getMediaCrypto() : null;
  }

  public static synchronized MediaCrypto getMediaCrypto(final String stubId) {
    if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");

    for (int i = 0; i < mBridgeStubs.size(); i++) {
      if (mBridgeStubs.get(i) != null && mBridgeStubs.get(i).getId().equals(stubId)) {
        return mBridgeStubs.get(i).getMediaCryptoFromBridge();
      }
    }
    return null;
  }

  // Callback to RemoteMediaDrmBridge.
  private final class Callbacks implements GeckoMediaDrm.Callbacks {
    private IMediaDrmBridgeCallbacks mRemoteCallbacks;

    public Callbacks(final IMediaDrmBridgeCallbacks remote) {
      mRemoteCallbacks = remote;
    }

    @Override
    public void onSessionCreated(
        final int createSessionToken,
        final int promiseId,
        final byte[] sessionId,
        final byte[] request) {
      if (DEBUG) Log.d(LOGTAG, "onSessionCreated()");
      try {
        mRemoteCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onSessionUpdated(final int promiseId, final byte[] sessionId) {
      if (DEBUG) Log.d(LOGTAG, "onSessionUpdated()");
      try {
        mRemoteCallbacks.onSessionUpdated(promiseId, sessionId);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onSessionClosed(final int promiseId, final byte[] sessionId) {
      if (DEBUG) Log.d(LOGTAG, "onSessionClosed()");
      try {
        mRemoteCallbacks.onSessionClosed(promiseId, sessionId);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onSessionMessage(
        final byte[] sessionId, final int sessionMessageType, final byte[] request) {
      if (DEBUG) Log.d(LOGTAG, "onSessionMessage()");
      try {
        mRemoteCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onSessionError(final byte[] sessionId, final String message) {
      if (DEBUG) Log.d(LOGTAG, "onSessionError()");
      try {
        mRemoteCallbacks.onSessionError(sessionId, message);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onSessionBatchedKeyChanged(
        final byte[] sessionId, final SessionKeyInfo[] keyInfos) {
      if (DEBUG) Log.d(LOGTAG, "onSessionBatchedKeyChanged()");
      try {
        mRemoteCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }

    @Override
    public void onRejectPromise(final int promiseId, final String message) {
      if (DEBUG) Log.d(LOGTAG, "onRejectPromise()");
      try {
        mRemoteCallbacks.onRejectPromise(promiseId, message);
      } catch (final RemoteException e) {
        Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
      }
    }
  }

  /* package-private */ void assertTrue(final boolean condition) {
    if (DEBUG && !condition) {
      throw new AssertionError("Expected condition to be true");
    }
  }

  RemoteMediaDrmBridgeStub(final String keySystem, final String stubId) throws RemoteException {
    if (Build.VERSION.SDK_INT < 21) {
      Log.e(LOGTAG, "Pre-Lollipop should never enter here!!");
      throw new RemoteException("Error, unsupported version!");
    }
    try {
      if (Build.VERSION.SDK_INT < 23) {
        mBridge = new GeckoMediaDrmBridgeV21(keySystem);
      } else {
        mBridge = new GeckoMediaDrmBridgeV23(keySystem);
      }
      mStubId = stubId;
      mBridgeStubs.add(this);
    } catch (final Exception e) {
      throw new RemoteException("RemoteMediaDrmBridgeStub cannot create bridge implementation.");
    }
  }

  @Override
  public synchronized void setCallbacks(final IMediaDrmBridgeCallbacks callbacks)
      throws RemoteException {
    if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
    assertTrue(mBridge != null);
    assertTrue(callbacks != null);
    mCallbacks = callbacks;
    callbacks.asBinder().linkToDeath(this, 0);
    mBridge.setCallbacks(new Callbacks(mCallbacks));
  }

  @Override
  public synchronized void createSession(
      final int createSessionToken,
      final int promiseId,
      final String initDataType,
      final byte[] initData)
      throws RemoteException {
    if (DEBUG) Log.d(LOGTAG, "createSession()");
    try {
      assertTrue(mCallbacks != null);
      assertTrue(mBridge != null);
      mBridge.createSession(createSessionToken, promiseId, initDataType, initData);
    } catch (final Exception e) {
      Log.e(LOGTAG, "Failed to createSession.", e);
      mCallbacks.onRejectPromise(promiseId, "Failed to createSession.");
    }
  }

  @Override
  public synchronized void updateSession(
      final int promiseId, final String sessionId, final byte[] response) throws RemoteException {
    if (DEBUG) Log.d(LOGTAG, "updateSession()");
    try {
      assertTrue(mCallbacks != null);
      assertTrue(mBridge != null);
      mBridge.updateSession(promiseId, sessionId, response);
    } catch (final Exception e) {
      Log.e(LOGTAG, "Failed to updateSession.", e);
      mCallbacks.onRejectPromise(promiseId, "Failed to updateSession.");
    }
  }

  @Override
  public synchronized void closeSession(final int promiseId, final String sessionId)
      throws RemoteException {
    if (DEBUG) Log.d(LOGTAG, "closeSession()");
    try {
      assertTrue(mCallbacks != null);
      assertTrue(mBridge != null);
      mBridge.closeSession(promiseId, sessionId);
    } catch (final Exception e) {
      Log.e(LOGTAG, "Failed to closeSession.", e);
      mCallbacks.onRejectPromise(promiseId, "Failed to closeSession.");
    }
  }

  // IBinder.DeathRecipient
  @Override
  public synchronized void binderDied() {
    Log.e(LOGTAG, "Binder died !!");
    try {
      release();
    } catch (final Exception e) {
      Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
    }
  }

  @Override
  public synchronized void release() {
    if (DEBUG) Log.d(LOGTAG, "release()");
    mBridgeStubs.remove(this);
    if (mBridge != null) {
      mBridge.release();
      mBridge = null;
    }
    mCallbacks.asBinder().unlinkToDeath(this, 0);
    mCallbacks = null;
    mStubId = "";
  }

  @Override
  public synchronized void setServerCertificate(final byte[] cert) {
    try {
      mBridge.setServerCertificate(cert);
    } catch (final IllegalStateException e) {
      Log.e(LOGTAG, "Failed to setServerCertificate.", e);
      throw e;
    }
  }
}
