package com.castlabs.sdk.demos;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.slider.Slider;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.tabs.TabLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.TextView;

import com.castlabs.android.PlayerSDK;
import com.castlabs.android.player.AbrConfiguration;
import com.castlabs.android.player.AbstractPlayerListener;
import com.castlabs.android.player.BufferConfiguration;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.PlayerView;
import com.castlabs.android.player.exceptions.CastlabsPlayerException;
import com.castlabs.sdk.debug.DebugPlugin;
import com.castlabs.sdk.debug.RateLimiter;
import com.castlabs.sdk.debug.view.AbrPlotsFragment;
import com.castlabs.sdk.debug.view.DownloadsFragment;
import com.castlabs.sdk.debug.view.ExtendedPlayerViewProvider;
import com.castlabs.sdk.debug.view.PlayerSettingsFragment;
import com.castlabs.sdk.debug.view.PlayerDataFragment;
import com.castlabs.sdk.debug.view.PlayerStatsFragment;
import com.castlabs.sdk.debug.view.RateLimitTraces;
import com.castlabs.sdk.debug.view.RenditionsFragment;
import com.castlabs.sdk.debug.view.TrackSelectionsFragment;
import com.castlabs.sdk.playerui.PlayerControllerProgressBar;
import com.castlabs.sdk.playerui.PlayerControllerView;
import com.github.mikephil.charting.charts.LineChart;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.MAX_RATE_LIMIT_BPS;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.MIN_RATE_LIMIT_BPS;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_BACK_BUFFER_DURATION;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_CLIP_END;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_CLIP_START;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_DEGRADATION_PENALTY;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_DEGRADATION_RECOVERY;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_DEGRADATION_SAMPLES;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_DRAIN_WHILE_CHARGING;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_ALGO;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_BANDWIDTH_FRACTION;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_BUFFER_SIZE_BYTES;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_BUFFER_SIZE_MAX_TIME;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_BUFFER_SIZE_MIN_TIME;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_DOWNLOAD_TIME_FACTOR;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_INITIAL_BANDWIDTH;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_MAX_DECREASE;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_MIN_INCREASE;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_RATELIMIT;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_RETAIN;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_KEY_SAFE_BUFFER;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_MIN_RESTART_BUFFER;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_MIN_START_BUFFER;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_PERCENTILE_WEIGHT;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_STORE_NAME;
import static com.castlabs.sdk.debug.view.PlayerSettingsFragment.PREFS_TIME_OVER_MEMORY;
import static com.castlabs.utils.StringUtils.stringForBitrate;
import static com.google.android.material.slider.LabelFormatter.LABEL_GONE;

public class AbrPlaybackDemo extends AppCompatActivity implements ExtendedPlayerViewProvider {

	private static final String TAG = "AbrPlaybackDemo";
	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;
	private PlayerControllerView playerControllerView;
	private PlayerConfig currentConfig;
	private TextView rateLimitLabel;
	private Slider rateLimitBar;
	private SharedPreferences store;
	private TabLayout tabLayout;
	private final List<Listener> listeners = new CopyOnWriteArrayList<>();
	private boolean smallView;
	private RateLimitTraces tracesController;
	private RateLimiter limiter;
	@Nullable
	private SwitchMaterial enableTracesSwitch;
	@Nullable
	private LineChart currentTraceChart;
	private boolean started = false;
	private Handler handler;

	@NonNull
	@Override
	public PlayerView getPlayerView() {
		return playerView;
	}

	@Override
	public void addPlayerViewActivityListener(Listener listener) {
		listeners.add(listener);
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		handler = new Handler();
		setContentView(R.layout.activity_abr_playbackdemo);
		playerView = findViewById(R.id.player_view);
		playerControllerView = findViewById(R.id.player_controls);
		smallView = findViewById(R.id.plotsFragment) == null;
		enableTracesSwitch = findViewById(R.id.enableTracesSwitch);
		currentTraceChart = findViewById(R.id.currentTraceChart);
		if (currentTraceChart != null) {
			currentTraceChart.setViewPortOffsets(0, 0, 0, 0);
		}

		if (enableTracesSwitch != null) {
			enableTracesSwitch.setOnCheckedChangeListener(
					new CompoundButton.OnCheckedChangeListener() {
						@Override
						public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
							enableTraces(isChecked);
						}
					});
		}
		// Subscribe for the playback errors
		playerView.getPlayerController().addPlayerListener(new AbstractPlayerListener() {
			@Override
			public void onError(@NonNull CastlabsPlayerException error) {
				Snackbar.make(playerView, "Error while opening player: " + error.getMessage(),
						Snackbar.LENGTH_SHORT).show();
			}
		});

		store = getSharedPreferences(PREFS_STORE_NAME, Context.MODE_PRIVATE);

		// hook up UI controls
		final PlayerControllerProgressBar progressBar = findViewById(R.id.progress_bar);
		progressBar.setVisible(false);
		final Button reloadBtn = findViewById(R.id.reloadBtn);
		if (reloadBtn != null) {
			if (reloadBtn instanceof Button) {
				((Button) reloadBtn).setText("Start");
			}
			reloadBtn.setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						progressBar.setVisible(true);
						if (reloadBtn instanceof Button) {
							((Button) reloadBtn).setText("Restart");
						}
						open();
					}
				});
		}
		final View showTracesButton = findViewById(R.id.showTracesBtn);
		if (showTracesButton != null) {
			showTracesButton.setOnClickListener(
				new View.OnClickListener () {
					@Override
					public void onClick(View v) {
						showTraces();
					}
				});
		}
		rateLimitLabel = findViewById(R.id.rateLimitLabel);
		rateLimitBar = findViewById(R.id.rateLimitBar);
		rateLimitBar.setValueFrom(MIN_RATE_LIMIT_BPS);
		rateLimitBar.setValueTo(MAX_RATE_LIMIT_BPS);
		rateLimitBar.setValue(MAX_RATE_LIMIT_BPS);
		rateLimitBar.setLabelBehavior(LABEL_GONE);
		rateLimitBar.addOnChangeListener(new Slider.OnChangeListener() {
			@Override
			public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
				applyRateLimitProgress((int)value);
			}
		});

		DebugPlugin debugPlugin = PlayerSDK.getPlugin(DebugPlugin.class);
		if (debugPlugin != null) {
			limiter = debugPlugin.getRateLimiter();
		}

		final View tracesSheet = findViewById(R.id.tracesSheet);
		if (tracesSheet != null) {
			this.tracesController = new RateLimitTraces(tracesSheet, limiter);
			// load from store
			int storedRateProgress = store.getInt(PREFS_KEY_RATELIMIT, 100);
			if (storedRateProgress != (int)rateLimitBar.getValue()) {
				rateLimitBar.setValue(storedRateProgress);
			} else {
				applyRateLimitProgress(storedRateProgress);
			}
		}

		tabLayout = findViewById(R.id.tabLayout);
		ViewPager viewPager = findViewById(R.id.viewPager);
		FragmentPagerAdapter tabAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
			@Override
			public Fragment getItem(int i) {
				switch (i) {
					case 0:
						return new PlayerSettingsFragment();
					case 1:
						if (smallView) {
							return new PlayerStatsFragment();
						} else {
							return new PlayerDataFragment();
						}
					case 2:
						return new RenditionsFragment();
					case 3:
						return new TrackSelectionsFragment();
					case 4:
						return new DownloadsFragment();
					case 5:
						return new AbrPlotsFragment();
				}
				return null;
			}

			@Override
			public int getCount() {
				return smallView ? 6 : 2;
			}

			@Nullable
			@Override
			public CharSequence getPageTitle(int position) {
				switch (position) {
					case 0:
						return "Configuration";
					case 1:
						return smallView ? "Stats" : "Data";
					case 2:
						return "Renditions";
					case 3:
						return "Selections";
					case 4:
						return "Downloads";
					case 5:
						return "Plots";
				}
				return null;
			}
		};
		viewPager.setAdapter(tabAdapter);
		tabLayout.setupWithViewPager(viewPager);

		ActionBar supportActionBar = getSupportActionBar();
		if (supportActionBar != null) {
			supportActionBar.hide();
		}

		Bundle playbackBundle = null;
		if (savedInstanceState == null) {
			Log.d(TAG, "Opening playback from intent bundle");
			// This demo assumes that you send an intent to this Activity that contains the
			// playback information.
			if (getIntent() != null) {
				playbackBundle = getIntent().getExtras();
			} else {
				Snackbar.make(playerView, "No intent specified", Snackbar.LENGTH_INDEFINITE).show();
			}
		} else {
			Log.d(TAG, "Opening playback from saved state bundle");
			playbackBundle = savedInstanceState.getBundle(SAVED_PLAYBACK_STATE_BUNDLE_KEY);
		}

		if (playbackBundle != null) {
			currentConfig = new PlayerConfig.Builder(playbackBundle).get();
		}
	}

	private void enableTraces(boolean enabled) {
		if (enabled) {
			tracesController.start(currentTraceChart);
			currentTraceChart.setVisibility(View.VISIBLE);
			rateLimitLabel.setVisibility(View.GONE);
			rateLimitBar.setVisibility(View.GONE);
		} else {
			tracesController.stop();
			currentTraceChart.setVisibility(View.GONE);
			rateLimitLabel.setVisibility(View.VISIBLE);
			rateLimitBar.setVisibility(View.VISIBLE);
		}
	}

	private void showTraces() {
		ViewGroup tracesSheet = findViewById(R.id.tracesSheet);
		BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(tracesSheet);
		bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
	}


	private void applyRateLimitProgress(int progress) {
		if (progress == MAX_RATE_LIMIT_BPS) {
			if (setRateLimit(-1)) {
				Log.i(TAG, "Disable rate limit");
				rateLimitLabel.setText("Rate Limit: disabled");
			}
		} else {
			if (setRateLimit(progress)) {
				Log.i(TAG, "Rate limit to " + stringForBitrate(progress));
				rateLimitLabel.setText("Rate Limit: " + stringForBitrate(progress));
			}
		}
		store.edit().putInt(PREFS_KEY_RATELIMIT, progress).apply();
	}

	private boolean setRateLimit(long rateLimit) {
		if (limiter != null) {
			limiter.setLimit(rateLimit);
			return true;
		}
		return false;
	}

	void open() {
		if (currentConfig == null) {
			Snackbar.make(playerView, "Can not start playback: no bundle specified", Snackbar.LENGTH_LONG).show();
			return;
		}

		if (playerView.getPlayerController().getPlayerConfig() != null) {
			playerView.getPlayerController().release();
		}

		AbrConfiguration.Builder abrConfigBuilder = currentConfig.abrConfiguration != null
			? new AbrConfiguration.Builder(currentConfig.abrConfiguration)
			: new AbrConfiguration.Builder();
		final PlayerConfig.Builder builder = new PlayerConfig.Builder(currentConfig);

		String algorithm = store.getString(PREFS_KEY_ALGO, "Exo");
		switch (algorithm) {
			case "NBA":
				Log.i(TAG, "Selected ABR Algorithm: NBA");
				abrConfigBuilder.method(AbrConfiguration.METHOD_COMMON_NBA);
				break;
			case "Iterate":
				Log.i(TAG, "Selected ABR Algorithm: Iterate");
				abrConfigBuilder.method(AbrConfiguration.METHOD_ITERATE);
				break;
			case "Flip":
				Log.i(TAG, "Selected ABR Algorithm: FLIP");
				abrConfigBuilder.method(AbrConfiguration.METHOD_FLIP);
				break;
			default:
				Log.i(TAG, "Selected ABR Algorithm: Exo");
				abrConfigBuilder.method(AbrConfiguration.METHOD_EXO);
		}

		builder.abrConfiguration(
			abrConfigBuilder
				.maxInitialBitrate(store.getLong(PREFS_KEY_INITIAL_BANDWIDTH, AbrConfiguration.DEFAULT_MAX_INITIAL_BITRATE))
				.minDurationForQualityIncrease(store.getLong(PREFS_KEY_MIN_INCREASE, AbrConfiguration.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_US / 1000), TimeUnit.MILLISECONDS)
				.maxDurationForQualityDecrease(store.getLong(PREFS_KEY_MAX_DECREASE, AbrConfiguration.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_US / 1000), TimeUnit.MILLISECONDS)
				.bandwidthFraction(store.getFloat(PREFS_KEY_BANDWIDTH_FRACTION, AbrConfiguration.DEFAULT_BANDWIDTH_FRACTION))
				.minDurationToRetainAfterDiscard(store.getLong(PREFS_KEY_RETAIN, AbrConfiguration.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_US / 1000), TimeUnit.MILLISECONDS)
				.downloadTimeFactor(store.getFloat(PREFS_KEY_DOWNLOAD_TIME_FACTOR, AbrConfiguration.DEFAULT_DOWNLOAD_TIME_FACTOR))
				.degradationPenalty(store.getFloat(PREFS_DEGRADATION_PENALTY, AbrConfiguration.DEFAULT_DEGRADATION_PENALTY))
				.degradationRecovery(store.getFloat(PREFS_DEGRADATION_RECOVERY, AbrConfiguration.DEFAULT_DEGRADATION_RECOVERY))
				.minDegradationSamples(store.getInt(PREFS_DEGRADATION_SAMPLES, AbrConfiguration.DEFAULT_DEGRADATION_SAMPLES))
				.safeBufferSize(store.getLong(PREFS_KEY_SAFE_BUFFER, AbrConfiguration.DEFAULT_SAFE_BUFFER_SIZE_US / 1000), TimeUnit.MILLISECONDS)
				.percentileWeight(store.getInt(PREFS_PERCENTILE_WEIGHT, AbrConfiguration.DEFAULT_PERCENTILE_WEIGHT))
				.get());

		BufferConfiguration.Builder bufferConfigBuilder = currentConfig.bufferConfiguration != null
			? new BufferConfiguration.Builder(currentConfig.bufferConfiguration)
			: new BufferConfiguration.Builder();

		bufferConfigBuilder
				.bufferSizeBytes((int) store.getLong(PREFS_KEY_BUFFER_SIZE_BYTES, BufferConfiguration.DEFAULT_BUFFER_SIZE))
				.lowMediaTime((int) store.getLong(PREFS_KEY_BUFFER_SIZE_MIN_TIME, BufferConfiguration.DEFAULT_LOW_MEDIA_TIME), TimeUnit.MILLISECONDS)
				.highMediaTime((int) store.getLong(PREFS_KEY_BUFFER_SIZE_MAX_TIME, BufferConfiguration.DEFAULT_HIGH_MEDIA_TIME), TimeUnit.MILLISECONDS)
				.drainWhileCharging(store.getBoolean(PREFS_DRAIN_WHILE_CHARGING, BufferConfiguration.DEFAULT_DRAIN_WHILE_CHARGING))
				.prioritizeTimeOverSizeThresholds(store.getBoolean(PREFS_TIME_OVER_MEMORY, BufferConfiguration.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS))
				.minPlaybackStart(store.getInt(PREFS_MIN_START_BUFFER, BufferConfiguration.DEFAULT_MIN_PLAYBACK_START_MS), TimeUnit.MILLISECONDS)
				.minRebufferStart(store.getInt(PREFS_MIN_RESTART_BUFFER, BufferConfiguration.DEFAULT_MIN_REBUFFER_START_MS), TimeUnit.MILLISECONDS)
				.backBufferDuration(store.getLong(PREFS_BACK_BUFFER_DURATION, BufferConfiguration.DEFAULT_BACK_BUFFER_LENGTH_MS), TimeUnit.MILLISECONDS);


		long clipStartMs = store.getLong(PREFS_CLIP_START, 0);
		if (clipStartMs > 0) {
			builder.clippingStartUs(clipStartMs * 1000);
		}

		long clipEndMs = store.getLong(PREFS_CLIP_END, 0);
		if (clipEndMs > 0 && clipEndMs < (10 * 60000) - 1) {
			builder.clippingEndUs(clipEndMs * 1000);
		}

		builder.bufferConfiguration(bufferConfigBuilder.get());

		playerView.getPlayerController().release();

		tabLayout.getTabAt(1).select();
		// We release now and than open 500 ms later so that UI can cleanup and
		// does not receive any stale events.

		handler.postDelayed(new Runnable() {
			@Override
			public void run() {

				for (Listener listener : listeners) {
					listener.onReload();
				}
				if (tracesController != null && tracesController.isRunning()) {
					tracesController.restart();
				}

				playerView.getPlayerController().open(builder.get());
			}
		}, 500);

	}


	@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);
		PlayerControllerProgressBar progressBar = findViewById(R.id.progress_bar);
		progressBar.bind(playerView.getPlayerController());
		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();
		playerView.getLifecycleDelegate().releasePlayer(false);
		if (tracesController != null) {
			tracesController.stop();
		}
	}

	// 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
	@SuppressLint("MissingSuperCall")
	@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);
		}
		// We don't call this on purpose cause it break rotation change for the view page adapter
		//super.onSaveInstanceState(outState);
	}
}
