/*
 * ************************************************************************
 *
 * ADOBE SYSTEMS INCORPORATED
 * Copyright 2013 Adobe Systems Incorporated
 * All Rights Reserved.
 * NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the 
 * terms of the Adobe license agreement accompanying it.  If you have received this file from a 
 * source other than Adobe, then your use, modification, or distribution of it requires the prior 
 * written permission of Adobe.
 *
 **************************************************************************
 */

package com.adobe.primetime.reference.manager;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;

import com.adobe.mediacore.MediaPlayerEvent;
import com.adobe.mediacore.drm.*;
import com.adobe.mediacore.DRMMetadataInfoEvent;
import com.adobe.mediacore.DRMMetadataInfoEventListener;
import com.adobe.mediacore.MediaPlayer;
import com.adobe.mediacore.MediaPlayerItemLoader;
import com.adobe.mediacore.MediaResource;
import com.adobe.mediacore.MediaPlayerItemLoader.LoaderListener;
import com.adobe.primetime.reference.PrimetimeReference;
import com.adobe.primetime.reference.drm.DRMStringsRes;

/**
 * Listens for events from the media player: Event.DRM and Event.PLAYBACK, and
 * filters for events that indicate a DRM error (native error codes 3300 to
 * 3399). These are processed in three ways depending on the error: re-acquire
 * the license from the license server, clear the local license cache, notify
 * the user that an error has occurred. The caller is notified by
 * DRMErrorHandlerListener callback if the error has been resolved and play can
 * be retried, or if an error has occurred which prevents play.
 * 
 */
public class DrmManager implements IManager {

	public final static boolean DEFAULT_DRM_PRECACHE = false;
	public final static String SETTINGS_DRM_PRE_CACHE = "settings_drm_precache";

	private final static String LOG_TAG = PrimetimeReference.LOG_APP_NAME + "DRMManager";

	private MediaPlayer mediaPlayer;
	private DRMManager mDrmManager;
	private DRMMetadata mDrmMetadata;
	private int hasRetried = 0;

	private List<DrmManagerEventListener> eventListeners = new ArrayList<DrmManagerEventListener>();

	public DrmManager(MediaPlayer mediaPlayer) {
		this.mediaPlayer = mediaPlayer;

		this.mediaPlayer.addEventListener(MediaPlayerEvent.DRM_METADATA, drmEventListener);

		// try to get DRMManager now, however if the player is not initialized the DRMManager is null
		// if the DRMMananger is null, it will get set once the DRMMetadata is available
		//this.mDrmManager = this.mediaPlayer.getDRMManager();
	}

	/**
	 * Destroy manager by removing PSDK listeners
	 */
	@Override
	public void destroy() {
		mediaPlayer.removeEventListener(MediaPlayerEvent.DRM_METADATA, drmEventListener);
	}

	public void addEventListener(DrmManagerEventListener eventListener) {
		eventListeners.add(eventListener);
	}

	public void removeEventListener(DrmManagerEventListener eventListener) {
		eventListeners.remove(eventListener);
	}

	public interface DrmManagerEventListener {
		/**
		 * Notify the user that a DRM error has occurred.
		 * 
		 * @param majorCode
		 *            the major error code of the DRM error
		 * @param minorCode
		 *            the minor error code of the DRM error
		 * @param error
		 *            a custom error message configured on the DRM service for
		 *            this particualar error
		 * 
		 */
		void onError(long majorCode, long minorCode, String error);

		/**
		 * The player should try to re-load the media following automatic
		 * resolution of the DRM error.
		 */
		void onRetry();

	}

	/**
	 * Perform operations necessary to start up the DRM service. This
	 * precomputes anything that can be precomputed so that future playback will
	 * have the shortest delay possible.
	 * 
	 */
	public void loadDRMServices() {
		if (mDrmManager == null)
		{
			PrimetimeReference.logger.w(LOG_TAG, "DRMManager is not set, failed to initialize DRM services.");
			return;
		}

		mDrmManager.initialize(new DRMOperationCompleteListener() {
			@Override
			public void onDRMOperationComplete() {
				PrimetimeReference.logger.i(LOG_TAG, "DRM Manager initialized.");
			}

			@Override
			public void onDRMError(int major, int minor, String erroString, String serverErrorURL) {
				PrimetimeReference.logger.w(LOG_TAG, "DRM Manager failed to initialize: '" +
                        erroString + "' from URL '" + serverErrorURL + "'" +
						"(" + major + "." + minor + ")");
			}
		});
	}

	/**
	 * Pre-load the DRM licenses. If the licenses are not already cached
	 * locally, they will be retrieved from the license server identified by the
	 * DRMMetadata associated with the resource. This function is provided to
	 * enable early completion of the license workflow, which can result in
	 * faster playback start. Only metadata is loaded, not the actual media.
	 * 
	 * @param url
	 *            the manifest URL
	 * @param listener
	 *            An instance of MediaPlayerItemLoader.LoaderListener to receive
	 *            the completion notifications.
	 * @param context
	 *            the application context where the preload occurs from
	 */
	public static void preLoadDrmLicenses(String url, final LoaderListener listener, final Context context) {

		final MediaResource resource = new MediaResource(url, MediaResource.Type.HLS, null);

		new Handler().post(new Runnable() {
			@Override
			public void run() {
				MediaPlayerItemLoader itemLoader = new MediaPlayerItemLoader(context, listener);
				itemLoader.load(resource);
			}
		});
	}

	/**
	 * @param majorCode
	 *            is the NATIVE_ERROR_CODE from the MediaPlayerNotification
	 * @return true if the majorCode is a DRM error.
	 */
	public static boolean isDrmError(long majorCode) {
		return majorCode >= 3300 && majorCode <= 3400;
	}

	private final DRMMetadataInfoEventListener drmEventListener = new DRMMetadataInfoEventListener() {
		
		@Override
		public void onDRMMetadataInfo(DRMMetadataInfoEvent event) {
			
			DRMMetadataInfo drmMetadataInfo = event.getDRMMetadataInfo();
			PrimetimeReference.logger.i(LOG_TAG + "::onDRMMetadata",
					"DRM metadata available: " + drmMetadataInfo + ".");

			mDrmMetadata = drmMetadataInfo.getDRMMetadata();

			// if not set previously, the DRMManager is available now
			mDrmManager = mediaPlayer.getDRMManager();
		}
	};

	/**
	 * Handle DRM error base on the major code
	 * 
	 * @param major major error for DRM
     * @param minor minor error for DRM
     * @param erroString DRM error string
     * @param serverErrorURL DRM server error URL
	 */
	private void handleDRMError(int major, int minor, String erroString, String serverErrorURL) {

		PrimetimeReference.logger.d(LOG_TAG, "DRM error, majorCode " + major
				+ " minorCode " + minor + " retried " + hasRetried);

		// Try two times. Fist try forceAcquireLicense
		// If that doesn't work out resetLocalDRMCache
		if (hasRetried < 2) {

			hasRetried++;

			if (hasRetried == 1 && requiresRetry(major, minor)) {
				forceAcquireLicense();
				return;
			} else if (requiresResetVouchers(major, minor)) {
				resetLocalDRMCache();
				return;
			}
		}

		sendDrmError(major, minor, erroString, serverErrorURL);
	}

	/**
	 * Reset local DRM cache and notify the listeners to retry playback
	 */
	private void resetLocalDRMCache() {
		if (mDrmManager == null)
		{
			PrimetimeReference.logger.w(LOG_TAG, "DRMManager is not set, failed to reset local DRM cache");
			return;
		}

		PrimetimeReference.logger.d(LOG_TAG, "Clearing all locally cached DRM licenses.");
		mDrmManager.resetDRM(new DRMOperationCompleteListener() {
			@Override
			public void onDRMOperationComplete() {
				PrimetimeReference.logger.d(LOG_TAG, "DRM license cache reset successfully, restarting player.");
				sendDrmRetry();
			}

			@Override
			public void onDRMError(int major, int minor, String erroString, String serverErrorURL) {
				PrimetimeReference.logger.w(LOG_TAG, "DRM license cache reset failed: " +
                        erroString + ", from URL " + serverErrorURL +
						" (" + major + "." + minor + ")");
				sendDrmError(major, minor, erroString, serverErrorURL);
			}
		});
	}

	/**
	 * Force to acquire the DRM license and notify the listeners to retry
	 * playback
	 */
	private void forceAcquireLicense() {

		if (mDrmManager == null)
		{
			PrimetimeReference.logger.w(LOG_TAG, "DRMManager is not set, failed to refresh DRM license.");
			return;
		}

		if (mDrmMetadata == null) {
			PrimetimeReference.logger.d(LOG_TAG, "DRMMetadata is not set, failed to refresh DRM license");
			return;
		}

		PrimetimeReference.logger.d(LOG_TAG,
				"Attempting to re-aquire DRM license from server.");

		mDrmManager.acquireLicense(mDrmMetadata, DRMAcquireLicenseSettings.FORCE_REFRESH, new DRMAcquireLicenseListener() {
			@Override
			public void onLicenseAcquired(DRMLicense drmLicense) {
				PrimetimeReference.logger.d(LOG_TAG, "DRM license acquisition successfully.");
				sendDrmRetry();
			}

			@Override
			public void onDRMError(int major, int minor, String erroString, String serverErrorURL) {
				PrimetimeReference.logger.w(LOG_TAG, "DRM license acquisition failed: " +
                        erroString + ", from URL " + serverErrorURL +
						" (" + major + "." + minor + ")");

				sendDrmError(major, minor, erroString, serverErrorURL);
			}
		});
	}

	// DRM major code that requires retry
	private static long[] sRequiresRetry = { 3300, // Invalid Voucher
			3303, // Content expired
			3305, // Server connection failed
			3308, // Wrong license key
			3312, // LicenseIntegrity
			3321, // Individualization Failed
			3322, // DeviceBindingFailed
			3325, // CorruptServerStateStore
			3326, // StoreTamperingDetected
			3327, // ClockTamperingDetected
			3328, // ServerErrorTryAgai
			3332 }; // CachedLicenseExpired

	// DRM major code that requires reset local cache
	private static long[] sRequiresResetVouchers = { 3322, // DeviceBindingFailed
			3323, // CorruptGlobalStateStore
			3326, // StoreTamperingDetected
			3346 }; // Migration failed

	/**
	 * Helpler function to check if the given major code belongs to a DRM code
	 * category
	 * 
	 * @param majorCode
	 *            the major code to be categorized
	 * @param category
	 *            the category to check whether the major code belongs to
	 * @return true if majorCode belongs to category, false otherwise
	 */
	private boolean requiredFor(long majorCode, long[] category) {
		for (long l : category)
			if (l == majorCode)
				return true;
		return false;
	}

	private boolean requiresRetry(long majorCode, long minorCode) {
		return requiredFor(majorCode, sRequiresRetry);
	}

	private boolean requiresResetVouchers(long majorCode, long minorCode) {
		return requiredFor(majorCode, sRequiresResetVouchers);
	}

	/**
	 * Notify the listeners to handle DRM error
	 *
     * @param major major error for DRM
     * @param minor minor error for DRM
     * @param erroString DRM error string
     * @param serverErrorURL DRM server error URL
	 */
	private void sendDrmError(int major, int minor, String erroString, String serverErrorURL) {
		for (DrmManagerEventListener listener : eventListeners) {
			listener.onError(major, minor, erroString);
		}
	}

	/**
	 * Notify the listeners to retry DRM playback
	 */
	private void sendDrmRetry() {
		for (DrmManagerEventListener listener : eventListeners) {
			listener.onRetry();
		}
	}

	/**
	 * Build a application default message for DRM errors, defined in the String
	 * resource
	 * 
	 * @param majorCode
	 *            the major code of the DRM error
	 * @param minorCode
	 *            the minor code of the DRM error
	 * @param packageName
	 *            the package name of the player application
	 * @param resources
	 *            the resource
	 * 
	 * @return an DRM error message defined from the application String resource
	 *         (res/values/strings)
	 */
	public static String buildMessage(long majorCode, long minorCode,
			String packageName, Resources resources) {
		return new DRMStringsRes(packageName, resources).buildMessage(
				majorCode, minorCode);
	}

}
