package com.castlabs.sdk.mediasession_demo;

import static androidx.media2.common.MediaMetadata.METADATA_KEY_ALBUM_ART;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE;

import android.app.Activity;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaDrm;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.castlabs.android.PlayerSDK;
import com.castlabs.android.drm.MemoryKeyStore;
import com.castlabs.android.player.ConfigurationProvider;
import com.castlabs.android.player.DrmLicenseLoader;
import com.castlabs.android.player.ExternalSourceSelector;
import com.castlabs.android.player.models.VideoTrackQuality;
import com.castlabs.sdk.ima.ImaAdRequest;
import com.castlabs.sdk.mediasession.MediaSessionBuilder;
import com.castlabs.sdk.mediasession.MediaSessionPlugin;
import com.castlabs.utils.TimeUtils;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;

import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import com.castlabs.android.SdkConsts;
import com.castlabs.android.adverts.Ad;
import com.castlabs.android.adverts.AdInterface;
import com.castlabs.android.drm.DrmConfiguration;
import com.castlabs.android.drm.KeyStore;
import com.castlabs.android.player.AbstractPlayerListener;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.PlayerController;
import com.castlabs.android.player.PlayerView;
import com.castlabs.android.player.exceptions.CastlabsPlayerException;
import com.castlabs.android.views.SubtitlesViewComponent;
import com.castlabs.sdk.playerui.PlayerControllerProgressBar;
import com.castlabs.sdk.playerui.PlayerControllerView;
import com.castlabs.sdk.thumbs.DefaultThumbnailView;
import com.castlabs.sdk.thumbs.ThumbsPlugin;

public class SimplePlaybackDemo
			extends Activity
			implements PlayerControllerView.SeekBarListener, DefaultThumbnailView.Callback, AdInterface.Listener {
	private static final String TAG = "SimplePlaybackDemo";
	private static final String SAVED_PLAYBACK_STATE_BUNDLE_KEY = "SAVED_PLAYBACK_STATE_BUNDLE_KEY";
	// This is the player view that we use to start playback
	private PlayerView playerView;

	// This is an example implementation that demonstrates how you can
	// listen to changes on the player controller and how you can access the
	// SubtitleViewComponent to apply adjustments.
	//
	// In this example we add some padding to move the subtitles view up a bit when
	// we are showing the controller.
	private PlayerControllerView.Listener playerControllerViewListener = new PlayerControllerView.Listener() {
		@Override
		public void onVisibilityChanged(int visibility) {
			// This is how you can get a component, in this case the SubtitlesViewComponent
			// from the player view.
			SubtitlesViewComponent svc = playerView.getComponent(SubtitlesViewComponent.class);
			if (svc != null && svc.getView() != null) {
				int height = visibility == View.VISIBLE ? playerControllerView.getHeight() : 0;
				svc.getView().setPadding(0, 0, 0, height);
			}
		}
	};
	private final MediaSessionBuilder mediaSessionBuilder = new MediaSessionBuilder()
			.setFastForwardIncrementMs(10000)
			.setRewindIncrementMs(10000);
	private PlayerControllerView playerControllerView;

	private Button buttonPreroll;
	private Button buttonPostroll;
	private Button buttonSkippable;
	private ViewGroup thumbnailTimeContainer;
	private TextView thumbnailTime;
	private TextView requestedTime;
	@NonNull
	private final PlayerConfig openConfig = new PlayerConfig.Builder("http://demo.cf.castlabs.com/media/TOS/abr/Manifest_clean_sizes.mpd")
		.metaData(new BundleBuilder()
			.put(METADATA_KEY_TITLE, "MediaSession2 Single Item Playback")
			.put(METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(PlayerSDK.getContext().getResources(), R.drawable.castlabs))
			.get())
		.get();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_simple_playback_demo);

		// Get the view components from the layout
		playerView = (PlayerView) findViewById(R.id.player_view);
		// Get the controller view
		playerControllerView = (PlayerControllerView) findViewById(R.id.player_controls);
		playerControllerView.showExtendedTrackInfo(true);
		thumbnailTimeContainer = findViewById(R.id.thumbnailTimeLayout);
		thumbnailTime = findViewById(R.id.thumbnailTime);
		requestedTime = findViewById(R.id.requestedTime);

		PlayerConfig playbackConfig = null;

		if (savedInstanceState == null) {
			Log.d(TAG, "Opening playback from config");
			// This demo assumes that you send an intent to this Activity that contains the
			// playback information.
			playbackConfig = openConfig;
		} else {
			Log.d(TAG, "Opening playback from saved state bundle");
			playbackConfig = new PlayerConfig.Builder(savedInstanceState.getBundle(SAVED_PLAYBACK_STATE_BUNDLE_KEY)).get();
		}

		MediaSessionPlugin.enableMediaSession(playerView.getPlayerController(), /* Optional */ mediaSessionBuilder);

		if (playbackConfig != null) {
			try {
				// Subscribe for the playback errors
				final DrmConfiguration drmConfiguration = (DrmConfiguration) playbackConfig.drmConfiguration;

				final PlayerConfig finalPlaybackConfig = playbackConfig;
				playerView.getPlayerController().addPlayerListener(new AbstractPlayerListener() {
					@Nullable Snackbar snackbar;

					@Override
					public void onStateChanged(@NonNull PlayerController.State state) {
						if (state == PlayerController.State.Playing
								|| state == PlayerController.State.Pausing
								|| state == PlayerController.State.Preparing) {
							if (snackbar != null) {
								snackbar.dismiss();
							}
						}
					}

					@Override
					public void onFatalErrorOccurred(@NonNull CastlabsPlayerException error) {
						// This callback is only invoked for fatal errors, before the controller
						// is released. This yields a chance to query it for status.
						//
						final PlayerController pc = playerView.getPlayerController();
						VideoTrackQuality videoQuality = pc.getVideoQuality();
						final long position = pc.getPosition();
						final long bufferTime = pc.getPreBufferTime();
						Log.e(TAG, "Fatal exception, position: " + TimeUtils.us2s(position)
									+ " seconds, buffer ahead: " + TimeUtils.us2s(bufferTime - position)
									+ " seconds, video quality: " + (videoQuality == null ? "None" : videoQuality.getBitrate()));
					}

					@Override
					public void onError(@NonNull CastlabsPlayerException error) {
						// Handle expired offline licenses.
						// Check if the error is either TYPE_KEY_EXPIRED or TYPE_VIDEO_DECRYPTION_ERROR with
						// the cause being MediaCodec.CryptoException.ERROR_NO_KEY. In the first
						// case the key is expired and we can safely decided to re-load and see if
						// we get an new key from the DRM system. The second case unfortunately has
						// more than one meaning. It could imply that no key was loaded for the
						// content, i.e. the _wrong_ key was used for instance due to a misalignment
						// with the offlineId in the keystore.
						// It could however _also_ mean that the user fiddled around with his device
						// time and the CDM loaded the license but refuses to use it. We can still
						// remove the key and load again, since the DRM response should
						// give us a license (or not) and we would have a definitive answer.
						if (error.getType() == CastlabsPlayerException.TYPE_KEY_EXPIRED ||
								((error.getType() == CastlabsPlayerException.TYPE_VIDEO_DECRYPTION_ERROR ||
									error.getType() == CastlabsPlayerException.TYPE_AUDIO_DECRYPTION_ERROR) &&
								(error.getCause() instanceof MediaCodec.CryptoException) &&
								((MediaCodec.CryptoException) error.getCause()).getErrorCode() == MediaDrm.ErrorCodes.ERROR_NO_KEY)) {

							KeyStore keyStore = PlayerController.getKeyStore();
							if (keyStore != null && drmConfiguration != null && drmConfiguration.offlineId != null) {
								if (keyStore.delete(drmConfiguration.offlineId)){
									Log.e(TAG, "DRM license key expired or not usable. Cleared key and reload");
									playerView.getPlayerController().open(finalPlaybackConfig);
									return;
								}
							}
						}

						if (error.getSeverity() == CastlabsPlayerException.SEVERITY_ERROR) {
							snackbar = Snackbar.make(findViewById(R.id.snack_bar), "Error while opening player: " + error.getMessage(),
									BaseTransientBottomBar.LENGTH_INDEFINITE);
							snackbar.show();
						} else {
							Log.e(TAG, "Error while opening player: " + error.getMessage());
						}
					}
				});
				// (Optional) Install the CDN fallback implementation
				playerView.getPlayerController().setExternalSourceSelector(new ExternalSourceSelector() {
					@Nullable
					@Override
					public SourceData onError(@NonNull String manifestUrl, @NonNull Exception error) {
						// Have the fallback CDN only for one particular URL
						if (manifestUrl.equals("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_4/Manifest.mpd")) {
							return new SourceData("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_2/Manifest.mpd");
						}
						// No fallback CDN for other URLs
						return null;
					}

					@Nullable
					@Override
					public PlayerConfig onRestart(@NonNull String manifestUrl, @NonNull PlayerConfig playerConfig) {
						// Do the 'hard' restart for the selected URL when the 'soft' one fails
						if (manifestUrl.equals("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_4/Manifest.mpd")) {
							// Simplify here and just update the currently failed player config with the fallback CDN URL
							// Usually, ensure that the DRM and other player config parameters are valid for the
							// returned player config or create a completely new player config
							return new PlayerConfig.Builder(playerConfig)
									.contentUrl("https://demo.cf.castlabs.com/media/QA/QA_BBB_single_2/Manifest.mpd")
								.get();
						}
						// No fallback CDN for other URLs
						return null;
					}
				});

				// Optional, it is only needed for Widevine automatic renewal
				if (DrmLicenseLoader.fetchLicense(playbackConfig, new MemoryKeyStore())) {
					playerView.getPlayerController().setConfigurationProvider(new ConfigurationProvider() {
						@Nullable
						@Override
						public DrmConfiguration getDrmConfiguration(@NonNull DrmConfiguration drmConfiguration) {
							// We just supply here the same DRM configuration to show how the DRM configuration
							// can be provided for the license renewal. If needed any other configuration can be created and returned here.
							// In case the DRM configuration stays the same over license renewals then this callback is not needed to be
							// installed and the same DRM configuration is used by default
							return drmConfiguration;
						}

						@Nullable
						public PlayerConfig getPlayerConfig(@NonNull PlayerConfig current) {
							return current;
						}
					});
				}

				// (Optional) Add ad events listener
				playerView.getPlayerController().getAdInterface().addAdListener(this);


				// Need to pass the bundle on to the PlayerController
				// to start playback. The open() method might throw an Exception in case the bundle
				// contains not all mandatory parameters or the parameters are malformed.
				playerView.getPlayerController().open(playbackConfig);
			} catch (Exception e) {
				Log.e(TAG, "Error while opening player: " + e.getMessage(), e);
				Snackbar.make(playerView, "Error while opening player: " + e.getMessage(),
						Snackbar.LENGTH_INDEFINITE).show();
			}
			// Show manual ad insertion layout if ad schedule is present
			if (SdkConsts.AD_SCHEDULE_MANUAL.equals(playbackConfig.adSchedule)) {
				findViewById(R.id.manualAdsLayout).setVisibility(View.VISIBLE);
				buttonPreroll = findViewById(R.id.buttonRequestAdPreroll);
				buttonPostroll = findViewById(R.id.buttonRequestAdPostroll);
				buttonSkippable = findViewById(R.id.buttonRequestAdSkippable);
				View.OnClickListener adsClickListener = new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						String adTag = null;
						if (v == buttonPreroll) {
							adTag = "https://pubads.g.doubleclick.net/gampad/ads?slotname=/124319096/external/ad_rule_samples&sz=640x480&ciu_szs=300x250&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&url=&unviewed_position_start=1&output=xml_vast3&impl=s&env=vp&gdfp_req=1&ad_rule=0&vad_type=linear&vpos=preroll&pod=1&ppos=1&lip=true&min_ad_duration=0&max_ad_duration=30000&vrid=6256&video_doc_id=short_onecue&cmsid=496&kfa=0&tfcd=0";
						} else if (v == buttonPostroll) {
							adTag = "https://pubads.g.doubleclick.net/gampad/ads?slotname=/124319096/external/ad_rule_samples&sz=640x480&ciu_szs=300x250&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&url=&unviewed_position_start=1&output=xml_vast3&impl=s&env=vp&gdfp_req=1&ad_rule=0&vad_type=linear&vpos=postroll&pod=3&ppos=1&lip=true&min_ad_duration=0&max_ad_duration=30000&vrid=6256&video_doc_id=short_onecue&cmsid=496&kfa=0&tfcd=0";
						} else if (v == buttonSkippable) {
							adTag = "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=";
						}
						// Manually request ad
						final PlayerController pc = playerView.getPlayerController();
						pc.getAdInterface().scheduleAd(new ImaAdRequest.Builder()
								.tagUrl(adTag)
								//.scheduleDelayMs(4000)
							.get()
							.toAdRequest());
					}
				};
				buttonPreroll.setOnClickListener(adsClickListener);
				buttonPostroll.setOnClickListener(adsClickListener);
				buttonSkippable.setOnClickListener(adsClickListener);
			}
		} else {
			Snackbar.make(playerView, "Can not start playback: no bundle specified", Snackbar.LENGTH_INDEFINITE).show();
		}
	}


	@Override
	public boolean dispatchKeyEvent(KeyEvent event) {
		boolean handled = false;
		if (playerControllerView != null && playerControllerView.getVisibility() == View.VISIBLE) {
			handled = playerControllerView.dispatchKeyEvent(event);
		} else { // Not visible
			if (event.getAction() == KeyEvent.ACTION_DOWN){
				handled = playerControllerView.dispatchKeyEvent(event);
			}
		}
		return handled || super.dispatchKeyEvent(event);
	}

	// Delegate the onStart event to the player views lifecycle delegate.
	// The delegate will make sure that the screen safer will be disabled and
	// the display will not go to sleep
	@Override
	protected void onStart() {
		super.onStart();
		playerView.getLifecycleDelegate().start(this);
	}

	// Delegate the onResume event to the player views lifecycle delegate.
	// The delegate ensures that the player recovers from a saved state. This needs to
	// be implemented to ensure the the user can for example go to the home screen and
	// come back to this activity.
	@Override
	protected void onResume() {
		super.onResume();
		// Bind the controller view and its listener
		playerControllerView.bind(playerView);
		playerControllerView.addListener(playerControllerViewListener);
		playerControllerView.setSeekBarListener(this); // This is for Thumbnails

		PlayerControllerProgressBar progressBar = (PlayerControllerProgressBar) findViewById(R.id.progress_bar);
		progressBar.bind(playerView.getPlayerController());

		// (Optional) Add ad events listener
		playerView.getPlayerController().getAdInterface().addAdListener(this);

		MediaSessionPlugin.enableMediaSession(playerView.getPlayerController(), /* Optional */ mediaSessionBuilder);

		playerView.getLifecycleDelegate().resume();
	}

	// Delegate the onStop event to the player views lifecycle delegate.
	// We release the player when the activity is stopped. This will release all the player
	// resources and save the current playback state. Saving the state is required so the
	// onResume callback can recover properly.
	@Override
	protected void onStop() {
		super.onStop();

		// Unbind the player controller view and remove the listener
		playerControllerView.unbind();
		playerControllerView.removeListener(playerControllerViewListener);
		playerControllerView.setSeekBarListener(null);

		playerView.getLifecycleDelegate().releasePlayer(false);
	}

	// Save the playback state when the activity is destroyed in order to correctly
	// resume after the activity is re-created again i.e. onCreate is called
	@Override
	public void onSaveInstanceState(Bundle outState) {
		Bundle savedStateBundle = new Bundle();
		PlayerConfig playerConfig = playerView.getPlayerController().getPlayerConfig();
		if (playerConfig != null) {
			playerView.getPlayerController().getPlayerConfig().save(savedStateBundle);
			outState.putBundle(SAVED_PLAYBACK_STATE_BUNDLE_KEY, savedStateBundle);
		}
		super.onSaveInstanceState(outState);
	}

	// Required for Thumbnails
	@Override
	public void onSeekbarScrubbed(long positionUs, double seekBarProgressPercent) {
		// User interaction on the seekbar. Request Thumbnail rendering to the Thumbnail View
		ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
				ThumbsPlugin.ThumbnailViewComponent.class);
		if (thumbsView != null){
			thumbsView.show(positionUs, this, ThumbsPlugin.THUMBNAIL_INDEX_CURRENT);
		}
	}

	@Override
	public void getThumbnailRect(Rect output, DefaultThumbnailView.ThumbnailInfo info, boolean isSmallScreen) {
		// Fill the output Rect with the coordinates where we want the Thumbnail to be displayed
		ViewGroup videoView = playerView.getVideoView();
		output.set(
				videoView.getLeft(),
				videoView.getTop(),
				videoView.getRight(),
				videoView.getBottom());
		thumbnailTimeContainer.setVisibility(View.VISIBLE);
		//noinspection SetTextI18n
		requestedTime.setText(info.getRequestedTimeUs() / 1e6f + "s");
		//noinspection SetTextI18n
		thumbnailTime.setText(info.getActualTimeUs() / 1e6f + "s");
	}

	@Override
	public void onSeekbarReleased() {
		// Seek bar has been released, we want to stop showing Thumbnails. For this we get the
		// Thumbnails View and call .hide() on it.
		final ThumbsPlugin.ThumbnailViewComponent thumbsView = playerView.getComponent(
				ThumbsPlugin.ThumbnailViewComponent.class);
		if (thumbsView != null){
			thumbsView.hide();
		}
		thumbnailTimeContainer.setVisibility(View.GONE);
	}

	// AdInterface.Listener implementations
	@Override
	public void onAdWillStart(@NonNull Ad ad) {

	}

	@Override
	public void onAdStarted(@NonNull Ad ad) {
		Log.d(TAG, "Ad started : " + ad.id + ", position = " + ad.position);
		// May hide controls and disable touch event triggers
	}

	@Override
	public void onAdCompleted() {
		Log.d(TAG, "Ad completed");
		// May show the controls again if we keep them on screen
	}

	@Override
	public void onAdError(Ad ad, CastlabsPlayerException exception) {

	}

	@Override
	public void onAdSkipped() {

	}

	@Override
	public void onAdPlaybackPositionChanged(long playbackPositionMs) {
		// Log.d(TAG, "Ad progress " + playbackPositionMs);
	}

	@Override
	public void onAdPaused() {

	}

	@Override
	public void onAdResumed(Ad ad) {

	}

	@Override
	public void onAdClicked(String clickthroughUrl) {

	}
}
