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.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.media2.session.MediaSession;

import com.castlabs.android.PlayerSDK;
import com.castlabs.android.player.AbstractPlayerListener;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.PlayerController;
import com.castlabs.android.player.PlayerListener;
import com.castlabs.android.player.PlayerService;
import com.castlabs.android.player.PlayerView;
import com.castlabs.sdk.mediasession.MediaSessionBuilder;
import com.castlabs.sdk.mediasession.MediaSessionPlugin;
import com.castlabs.sdk.playerui.PlayerControllerProgressBar;
import com.castlabs.sdk.playerui.PlayerControllerView;
import com.castlabs.logutils.Log;
import com.google.android.material.snackbar.Snackbar;

public class SimplePlayServiceDemo extends Activity {
	private static final String TAG = "SimplePlayServiceDemo";

	private static final String INTENT_PLAYER_CATEGORY = "com.castlabs.sdk.demos.intent.player";
	private static final String ACTION_PLAYER_PLAY = "com.castlabs.sdk.demos.action.play";
	private static final String ACTION_PLAYER_PAUSE = "com.castlabs.sdk.demos.action.pause";
	private static final String ACTION_PLAYER_RELEASE = "com.castlabs.sdk.demos.action.release";

	// ID that we use to identify the background notifications
	private static final int NOTIFICATION_ID = 1;
	private static final String NOTIFICATION_CHANNEL_ID = "1";
	private static final String NOTIFICATION_CHANNEL_NAME = "Playback notification";
	// We use this main handler to post delayed messaged to hide the system
	// controls after 3 seconds
	@NonNull
	private final Handler handler = new Handler();
	// This is the player view that we use to start playback
	@Nullable
	private PlayerView playerView;
	// This is the service binder that we get once we connected to the service
	// Initialize it during first call to onResume() and keep it until the activity is destroyed:
	// onStop() && isFinishing() OR onDestroy() or when the service is disconnected
	@Nullable
	private PlayerService.Binder playerServiceBinder;
	// We store the bundle that contains the playback information here
	// so the service can use it once connected
	@Nullable
	private PlayerConfig playbackConfig;
	@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();
	private boolean notificationCreated;

	// The service connection
	//
	// The connection is responsible for either starting a new playback or recovering from background playback
	@NonNull
	private final ServiceConnection playerServiceConnection = new ServiceConnection() {

		public void onServiceConnected(ComponentName name, IBinder service) {
			// Get the service binder and initialize the service
			playerServiceBinder = (PlayerService.Binder) service;
			// Initialize the service binder. This will return true if
			// playback was recovered from background.
			boolean backgrounded = playerServiceBinder.init(playerView);

			// Now that PlayerView is initialized and we can get PlayerController instance from it
			MediaSessionPlugin.enableMediaSession(playerView.getPlayerController(), /* Optional */ mediaSessionBuilder);

			// From now on you can use playerView.getPlayerController() to interact with the controller
			if (!backgrounded) {
				if (playbackConfig != null) {
					// The player was not restored from background and we have a bundle to open.
					// This indicates that we came from an onCreate() call instead of resuming
					// an existing activity
					try {
						playerView.getPlayerController().open(playbackConfig);
					} catch (Exception e) {
						Snackbar.make(playerView,
								"Error while opening playback bundle: " + e.getMessage(),
								Snackbar.LENGTH_INDEFINITE).show();
					}
				} else {
					// The player was not restored from background and no bundle was passed.
					// In this case we delegate to the views lifecycle delegate to eventually
					// restore a session
					playerView.getLifecycleDelegate().resume();
				}
			}

			// Bind the controller view and its listener
			playerControllerView.bind(playerView);

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

			playbackConfig = null;
		}

		public void onServiceDisconnected(ComponentName name) {
			playerServiceBinder.release(playerView, true);
			playerServiceBinder = null;
		}
	};
	private PlayerControllerView playerControllerView;

	private final MediaSessionBuilder mediaSessionBuilder = new MediaSessionBuilder()
			.setFastForwardIncrementMs(10000)
			.setRewindIncrementMs(10000);

	@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);

		if (getIntent().hasExtra(NOTIFICATION_CHANNEL_NAME)) {
			notificationCreated = false;
		}

		// Hide the system toolbars
		//
		// We use the system UI flags to put this activity into a stable layout state
		// and hide the system toolbars. This ensures that we can use the full real estate
		// to show the video playback but avoid going into immersive fullscreen mode.
		//
		// With this setting, the system toolbars (the status bar and the navigation controls)
		// will become visible if the user interacts with the activity. In this demo, we
		// simply hide the controls again again after 3 seconds.
		hideSystemControls();
		getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(
				new View.OnSystemUiVisibilityChangeListener() {
					@Override
					public void onSystemUiVisibilityChange(int visibility) {
						if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
							// The navigation bar is no longer hidden, so we hide it
							// again in a few seconds
							delayedHideSystemControls();
						}
					}
				});

		// In this demo, we are using a background service so we can not start interacting
		// with the player controller before the service is connected and initialised.
		// We connect the service in onResume() and store the playback bundle here
		// so the service can use it to start playback later.
		if (getIntent() != null && getIntent().getExtras() != null) {
			playbackConfig = new PlayerConfig.Builder(getIntent().getExtras()).get();
		} else {
			playbackConfig = openConfig;
		}
	}

	// 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);
	}

	// Because we are using a service, we are NOT delegating directly to the player views lifecycle
	// event. Instead, we make sure that a connection to the player service is established. The
	// binder implementation will then take care of initializing playback or to recover from
	// a background playback session (see the implementation of playerServiceConnection).
	@Override
	protected void onResume() {
		super.onResume();

		if (playerServiceBinder == null) {
			// The first call to onResume() where we bind to the service to get playerServiceBinder instance
			Intent serviceIntent = new Intent(this, PlayerService.class);
			bindService(serviceIntent, playerServiceConnection, Context.BIND_AUTO_CREATE);
		} else {
			// We have the playerServiceBinder instance already, proceed with bindings
			playerServiceBinder.init(playerView);
			// Bind the controller view and its listener
			playerControllerView.bind(playerView);

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

			playbackConfig = null;
		}
	}

	// With the player service, we have to cover a few use cases when the activity is stopped.
	//
	// 1) If we have no service connection, we delegate to the lifecycle delegate of the
	//    view to release the player and any locks acquired when the activity started.
	//
	// 2) If isFinishing() returns true, we assume the user wants to actually leave the
	//    activity and not start background playback. In that case we release the player
	//    through the binder implementation
	// 3) If the activity is not finishing, we send the player to the background using the
	//    service binder implementation
	//
	// In both cases, we unbind from the service and reset the binder.
	@Override
	protected void onStop() {
		super.onStop();

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

		if (playerServiceBinder == null) {
			// We have no service connection, so we release the player without background playback
			playerView.getLifecycleDelegate().releasePlayer(false);
		} else {
			if (!isFinishing()) {
				Bundle openContentBundle = new Bundle();
				playerView.getPlayerController().saveState(openContentBundle);
				// The activity is not finishing but left through other means, i.e. Home button.
				// In this case, we send the player to background and display a notification
				playerServiceBinder.releaseToBackground(
						playerView,
						NOTIFICATION_ID,
						createBackgroundNotification(playerView.getPlayerController(), openContentBundle),
						true,
						new NotificationHandler(playerView.getPlayerController(), openContentBundle));
			} else {
				// We are not sending the player to background since the activity is finishing
				// so we fully release the player
				playerServiceBinder.release(playerView, true);
				unbindService(playerServiceConnection);
				playerServiceBinder = null;
			}
		}
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		if (!notificationCreated) {
			// Activity is finishing, we choose to stop the background playback
			if (playerServiceBinder != null) {
				playerServiceBinder.killBackgroundPlayer(true);
				unbindService(playerServiceConnection);
				playerServiceBinder = null;
			}
		}
	}

	@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);
	}

	// Utility function that we use to create a dummy notification that is displayed when the
	// player is going to background
	private Notification createBackgroundNotification(@NonNull PlayerController playerController, @NonNull Bundle openContentBundle) {
		NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), NOTIFICATION_CHANNEL_ID);
		builder.setSmallIcon(R.drawable.notifications_icon);

		// This is the Intent to come back to Activity when clicked on Notification
		Intent targetIntent = new Intent(getApplicationContext(), getClass());
		targetIntent.putExtras(openContentBundle);
		targetIntent.putExtra(NOTIFICATION_CHANNEL_NAME, true);

		TaskStackBuilder stackBuilder = TaskStackBuilder.create(getApplicationContext());
		stackBuilder.addParentStack(getClass());
		stackBuilder.addNextIntent(targetIntent);

		builder.setContentIntent(stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT));

		// IMPORTANT:
		// Instead of
		// >> builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle());
		// we link our notification with media session, which will place the notification in the dedicated position in UI as per Android 11,
		// see details https://developer.android.com/guide/topics/media/media-controls
		// Also, the system will not require a special user permission to show notifications
		MediaSession mediaSession = MediaSessionPlugin.getMediaSession(playerController);
		if (mediaSession != null) {
			builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(mediaSession.getSessionCompatToken()));
		}

		if (playerController.getPlayerState() == PlayerController.State.Playing) {
			builder.addAction(
				new NotificationCompat.Action.Builder(
					R.drawable.cl_ic_pause,
					"Pause",
					PendingIntent.getService(
						getApplicationContext(),
						0,
						new Intent(getApplicationContext(), PlayerService.class)
							.setAction(ACTION_PLAYER_PAUSE)
							.addCategory(INTENT_PLAYER_CATEGORY),
						0))
					.build());
		} else if (playerController.getPlayerState() == PlayerController.State.Pausing) {
			builder.addAction(
				new NotificationCompat.Action.Builder(
					R.drawable.cl_ic_play_arrow,
					"Play",
					PendingIntent.getService(
						getApplicationContext(),
						0,
						new Intent(getApplicationContext(), PlayerService.class)
							.setAction(ACTION_PLAYER_PLAY)
							.addCategory(INTENT_PLAYER_CATEGORY),
						0))
					.build());
		}

		builder.addAction(
			new NotificationCompat.Action.Builder(
				R.drawable.cl_ic_close,
				"Stop",
				PendingIntent.getService(
					getApplicationContext(),
					0,
					new Intent(getApplicationContext(), PlayerService.class)
						.setAction(ACTION_PLAYER_RELEASE)
						.addCategory(INTENT_PLAYER_CATEGORY),
					0))
				.build());

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
			NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
			notificationManager.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT));
		}

		notificationCreated = true;

		return builder.build();
	}

	// Utility method that hides the system controls and makes sure
	// the player view can use all available screen real estate.
	private void hideSystemControls() {
		final View decorView = getWindow().getDecorView();
		decorView.setSystemUiVisibility(
				View.SYSTEM_UI_FLAG_FULLSCREEN |
						View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
						View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
						View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
						View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
	}

	private void delayedHideSystemControls() {
		handler.postDelayed(new Runnable() {
			@Override
			public void run() {
				hideSystemControls();
			}
		}, 3000);
	}

	private final class NotificationHandler implements PlayerService.NotificationHandler {
		@Nullable
		private PlayerController playerController;
		@NonNull
		private Bundle openContentBundle;
		@NonNull
		private PlayerListener playerListener = new AbstractPlayerListener() {
			@Override
			public void onStateChanged(@NonNull PlayerController.State state) {
				if (playerController != null) {
					NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
					notificationManager.notify(NOTIFICATION_ID, createBackgroundNotification(playerController, openContentBundle));
				}
			}
		};

		NotificationHandler(@NonNull PlayerController playerController, @NonNull Bundle openContentBundle) {
			this.playerController = playerController;
			this.openContentBundle = openContentBundle;
			this.playerController.addPlayerListener(playerListener);
		}

		@Override
		public void release() {
			if (this.playerController != null) {
				this.playerController.removePlayerListener(playerListener);
				this.playerController = null;
			}
		}

		@Override
		public boolean onIntent(@NonNull Intent intent) {
			boolean release = false;
			Log.d(TAG, "Player action: " + intent.getAction());
			if (intent.hasCategory(INTENT_PLAYER_CATEGORY)) {
				if (ACTION_PLAYER_RELEASE.equals(intent.getAction())) {
					// Make sure to come back to the launching activity (list of play assets) when foregrounding the app
					SimplePlayServiceDemo.this.finish();
					release = true;
				} else if (ACTION_PLAYER_PLAY.equals(intent.getAction())) {
					if (playerController != null) {
						playerController.play();
					}
				} else if (ACTION_PLAYER_PAUSE.equals(intent.getAction())) {
					if (playerController != null) {
						playerController.pause();
					}
				}
			}
			return release;
		}
	}
}
