// Copyright © 2016-2018 Shawn Baker using the MIT License.
package ca.frozen.rpicameraviewer.activities;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.hardware.display.DisplayManager;
import android.media.MediaActionSound;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

import ca.frozen.library.classes.Log;
import ca.frozen.library.views.ZoomPanTextureView;
import ca.frozen.rpicameraviewer.App;
import ca.frozen.rpicameraviewer.R;
import ca.frozen.rpicameraviewer.classes.Camera;
import ca.frozen.rpicameraviewer.classes.SpsParser;
import ca.frozen.rpicameraviewer.classes.TcpIpReader;
import ca.frozen.rpicameraviewer.classes.Utils;

public class VideoFragment extends Fragment implements TextureView.SurfaceTextureListener
{
	// public interfaces
	public interface OnFadeListener
	{
		void onStartFadeIn();
		void onStartFadeOut();
	}

	// public constants
	public final static String CAMERA = "camera";
	public final static String FULL_SCREEN = "full_screen";

	// local constants
	private final static float MIN_ZOOM = 1;
	private final static float MAX_ZOOM = 10;
	private final static int FADEOUT_TIMEOUT = 8000;
	private final static int FADEOUT_ANIMATION_TIME = 500;
	private final static int FADEIN_ANIMATION_TIME = 400;
	private final static int REQUEST_WRITE_EXTERNAL_STORAGE = 73;

	// instance variables
	private Camera camera;
	private boolean fullScreen;
	private DecoderThread decoder;
	private ZoomPanTextureView textureView;
	private TextView nameView, messageView;
	private Button closeButton, snapshotButton;
	private Runnable fadeInRunner, fadeOutRunner, finishRunner, startVideoRunner;
	private Handler fadeInHandler, fadeOutHandler, finishHandler, startVideoHandler;
	private OnFadeListener fadeListener;

	//******************************************************************************
	// newInstance
	//******************************************************************************
	public static VideoFragment newInstance(Camera camera, boolean fullScreen)
	{
		VideoFragment fragment = new VideoFragment();

		Bundle args = new Bundle();
		args.putParcelable(CAMERA, camera);
		args.putBoolean(FULL_SCREEN, fullScreen);
		fragment.setArguments(args);

		return fragment;
	}

	//******************************************************************************
	// onCreate
	//******************************************************************************
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		// configure the activity
		super.onCreate(savedInstanceState);

		// initialize the logger
		Utils.initLogFile(getClass().getSimpleName());

		// load the settings and cameras
		Utils.loadData();

		// get the parameters
		camera = getArguments().getParcelable(CAMERA);
		fullScreen = getArguments().getBoolean(FULL_SCREEN);
		Log.info("camera: " + camera.toString());

		// create the fade in handler and runnable
		fadeInHandler = new Handler();
		fadeInRunner = new Runnable()
		{
			@Override
			public void run()
			{
				Animation fadeInName = new AlphaAnimation(0, 1);
				fadeInName.setDuration(FADEIN_ANIMATION_TIME);
				fadeInName.setFillAfter(true);
				Animation fadeInSnapshot = new AlphaAnimation(0, 1);
				fadeInSnapshot.setDuration(FADEIN_ANIMATION_TIME);
				fadeInSnapshot.setFillAfter(true);
				nameView.startAnimation(fadeInName);
				closeButton.startAnimation(fadeInSnapshot);
				snapshotButton.startAnimation(fadeInSnapshot);
				fadeListener.onStartFadeIn();
			}
		};

		// create the fade out handler and runnable
		fadeOutHandler = new Handler();
		fadeOutRunner = new Runnable()
		{
			@Override
			public void run()
			{
				Animation fadeOutName = new AlphaAnimation(1, 0);
				fadeOutName.setDuration(FADEOUT_ANIMATION_TIME);
				fadeOutName.setFillAfter(true);
				Animation fadeOutSnapshot = new AlphaAnimation(1, 0);
				fadeOutSnapshot.setDuration(FADEOUT_ANIMATION_TIME);
				fadeOutSnapshot.setFillAfter(true);
				nameView.startAnimation(fadeOutName);
				closeButton.startAnimation(fadeOutSnapshot);
				snapshotButton.startAnimation(fadeOutSnapshot);
				fadeListener.onStartFadeOut();
			}
		};

		// create the finish handler and runnable
		finishHandler = new Handler();
		finishRunner = new Runnable()
		{
			@Override
			public void run()
			{
				getActivity().finish();
			}
		};

		// create the start video handler and runnable
		startVideoHandler = new Handler();
		startVideoRunner = new Runnable()
		{
			@Override
			public void run()
			{
				MediaFormat format = decoder.getMediaFormat();
				int videoWidth = format.getInteger(MediaFormat.KEY_WIDTH);
				int videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
				textureView.setVideoSize(videoWidth, videoHeight);
			}
		};

		if (fullScreen)
		{
			DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener()
			{
				@Override
				public void onDisplayAdded(int displayId)
				{
					Log.info("Display #" + displayId + " added.");
				}

				@Override
				public void onDisplayChanged(int displayId)
				{
					Log.info("Display #" + displayId + " changed.");
					setControlMargins();
				}

				@Override
				public void onDisplayRemoved(int displayId)
				{
					Log.info("Display #" + displayId + " removed.");
				}
			};
			DisplayManager displayManager = (DisplayManager)getActivity().getSystemService(Context.DISPLAY_SERVICE);
			displayManager.registerDisplayListener(displayListener, null);
		}
	}

	//******************************************************************************
	// onCreateView
	//******************************************************************************
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		View view = inflater.inflate(R.layout.fragment_video, container, false);

		// configure the name
		nameView = view.findViewById(R.id.video_name);
		nameView.setText(camera.name);

		// initialize the message
		messageView = view.findViewById(R.id.video_message);
		messageView.setTextColor(App.getClr(R.color.good_text));
		messageView.setText(R.string.initializing_video);

		// set the texture listener
		textureView = view.findViewById(R.id.video_surface);
		textureView.setSurfaceTextureListener(this);
		textureView.setZoomRange(MIN_ZOOM, MAX_ZOOM);
		textureView.setOnTouchListener(new View.OnTouchListener()
		{
			@Override
			public boolean onTouch(View v, MotionEvent e)
			{
				switch (e.getAction())
				{
					case MotionEvent.ACTION_DOWN:
						stopFadeOutTimer();
						break;
					case MotionEvent.ACTION_UP:
						if (e.getPointerCount() == 1)
						{
							startFadeOutTimer(false);
						}
						break;
				}
				return false;
			}
		});

		// create the close button listener
		closeButton = view.findViewById(R.id.video_close);
		closeButton.setOnClickListener(new View.OnClickListener()
		{
			@Override
			public void onClick(View view)
			{
				stop();
				getActivity().finish();
			}
		});

		// create the snapshot button listener
		snapshotButton = view.findViewById(R.id.video_snapshot);
		snapshotButton.setOnClickListener(new View.OnClickListener()
		{
			@Override
			public void onClick(View view)
			{
				int check = ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
				if (check != PackageManager.PERMISSION_GRANTED)
				{
					Log.info("ask for external storage permission");
					requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
										REQUEST_WRITE_EXTERNAL_STORAGE);
				}
				else
				{
					takeSnapshot();
				}
			}
		});

		// adjust the controls to account for the navigation and status bars
		if (fullScreen)
		{
			setControlMargins();
		}

		return view;
	}

	//******************************************************************************
	// onRequestPermissionsResult
	//******************************************************************************
	@Override
	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)
	{
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
		if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0 &&
			grantResults[0] == PackageManager.PERMISSION_GRANTED)
		{
			Log.info("external storage permission granted");
			takeSnapshot();
		}
	}

	//******************************************************************************
	// onAttach
	//******************************************************************************
	@Override
	public void onAttach(Context context)
	{
		super.onAttach(context);
		try
		{
			Activity activity = (Activity) context;
			fadeListener = (OnFadeListener) activity;
		}
		catch (ClassCastException e)
		{
			throw new ClassCastException(context.toString() + " must implement OnFadeListener");
		}
	}

	//******************************************************************************
	// onDestroy
	//******************************************************************************
	@Override
	public void onDestroy()
	{
		super.onDestroy();
		finishHandler.removeCallbacks(finishRunner);
	}

	//******************************************************************************
	// onStart
	//******************************************************************************
	@Override
	public void onStart()
	{
		super.onStart();

		// create the decoder thread
		decoder = new DecoderThread();
		decoder.start();
	}

	//******************************************************************************
	// onStop
	//******************************************************************************
	@Override
	public void onStop()
	{
		super.onStop();

		if (decoder != null)
		{
			decoder.interrupt();
			decoder = null;
		}
	}

	//******************************************************************************
	// onPause
	//******************************************************************************
	@Override
	public void onPause()
	{
		super.onPause();
		stopFadeOutTimer();
	}

	//******************************************************************************
	// onResume
	//******************************************************************************
	@Override
	public void onResume()
	{
		super.onResume();
		if (snapshotButton.getVisibility() == View.VISIBLE)
		{
			startFadeOutTimer(false);
		}
	}

	//******************************************************************************
	// onSurfaceTextureAvailable
	//******************************************************************************
	@Override
	public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height)
	{
		if (decoder != null)
		{
			decoder.setSurface(new Surface(surfaceTexture), startVideoHandler, startVideoRunner);
		}
	}

	//******************************************************************************
	// onSurfaceTextureSizeChanged
	//******************************************************************************
	@Override
	public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height)
	{
	}

	//******************************************************************************
	// onSurfaceTextureDestroyed
	//******************************************************************************
	@Override
	public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture)
	{
		if (decoder != null)
		{
			decoder.setSurface(null, null, null);
		}
		return true;
	}

	//******************************************************************************
	// onSurfaceTextureUpdated
	//******************************************************************************
	@Override
	public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture)
	{
	}

	//******************************************************************************
	// setControlMargins
	//******************************************************************************
	public void setControlMargins()
	{
		Activity activity = getActivity();
		if (activity != null)
		{
			// get the margins accounting for the navigation and status bars
			Display display = activity.getWindowManager().getDefaultDisplay();
			float scale = getContext().getResources().getDisplayMetrics().density;
			int margin = (int)(5 * scale + 0.5f);
			int extra = Utils.getNavigationBarWidth(getContext());
			int rotation = display.getRotation();
			int leftMargin = margin;
			int rightMargin = margin;
			if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270)
			{
				leftMargin += extra;
			}
			else
			{
				rightMargin += extra;
			}
			int topMargin = margin + Utils.getStatusBarHeight(getContext());

			// set the control margins
			ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)closeButton.getLayoutParams();
			lp.setMargins(leftMargin, topMargin, rightMargin, margin);
			lp = (ViewGroup.MarginLayoutParams)snapshotButton.getLayoutParams();
			lp.setMargins(leftMargin, margin, rightMargin, margin);
			lp = (ViewGroup.MarginLayoutParams)nameView.getLayoutParams();
			lp.setMargins(leftMargin, margin, rightMargin, margin);
		}
	}

	//******************************************************************************
	// startFadeIn
	//******************************************************************************
	public void startFadeIn()
	{
		stopFadeOutTimer();
		fadeInHandler.removeCallbacks(fadeInRunner);
		fadeInHandler.post(fadeInRunner);
		startFadeOutTimer(true);
	}

	//******************************************************************************
	// startFadeOutTimer
	//******************************************************************************
	private void startFadeOutTimer(boolean addFadeInTime)
	{
		fadeOutHandler.removeCallbacks(fadeOutRunner);
		fadeOutHandler.postDelayed(fadeOutRunner, FADEOUT_TIMEOUT + (addFadeInTime ? FADEIN_ANIMATION_TIME : 0));
	}

	//******************************************************************************
	// stopFadeOutTimer
	//******************************************************************************
	private void stopFadeOutTimer()
	{
		fadeOutHandler.removeCallbacks(fadeOutRunner);
	}

	//******************************************************************************
	// takeSnapshot
	//******************************************************************************
	private void takeSnapshot()
	{
		// get the snapshot image
		Bitmap image = textureView.getBitmap();

		// save the image
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss");
		String name = camera.network + "_" + camera.name.replaceAll("\\s+", "") + "_" + sdf.format(new Date()) + ".jpg";
		Utils.saveImage(getActivity().getContentResolver(), image, name, null);
		Log.info("takeSnapshot: " + name);

		// play the shutter sound
		MediaActionSound sound = new MediaActionSound();
		sound.play(MediaActionSound.SHUTTER_CLICK);

		// display a message
		String msg = String.format(getString(R.string.image_saved), getString(R.string.app_name));
		Toast toast = Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT);
		toast.show();
	}

	//******************************************************************************
	// stop
	//******************************************************************************
	public void stop()
	{
		if (decoder != null)
		{
			messageView.setText(R.string.closing_video);
			messageView.setTextColor(App.getClr(R.color.good_text));
			messageView.setVisibility(View.VISIBLE);
			decoder.interrupt();
			try
			{
				decoder.join(TcpIpReader.IO_TIMEOUT * 2);
			}
			catch (Exception ex) {}
			decoder = null;
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// DecoderThread
	////////////////////////////////////////////////////////////////////////////////
	private class DecoderThread extends Thread
	{
		// local constants
		private final static int FINISH_TIMEOUT = 5000;
		private final static int BUFFER_SIZE = 16384;
		private final static int NAL_SIZE_INC = 4096;
		private final static int MAX_READ_ERRORS = 300;

		// instance variables
		private MediaCodec decoder = null;
		private MediaFormat format;
		private boolean decoding = false;
		private Surface surface;
		private byte[] buffer = null;
		private ByteBuffer[] inputBuffers = null;
		private long presentationTime;
		private long presentationTimeInc = 66666;
		private TcpIpReader reader = null;
		private Handler startVideoHandler;
		private Runnable startVideoRunner;

		//******************************************************************************
		// setSurface
		//******************************************************************************
		void setSurface(Surface surface, Handler handler, Runnable runner)
		{
			this.surface = surface;
			this.startVideoHandler = handler;
			this.startVideoRunner = runner;
			if (decoder != null)
			{
				if (surface != null)
				{
					boolean newDecoding = decoding;
					if (decoding)
					{
						setDecodingState(false);
					}
					if (format != null)
					{
						try
						{
							decoder.configure(format, surface, null, 0);
						}
						catch (Exception ex) {}
						if (!newDecoding)
						{
							newDecoding = true;
						}
					}
					if (newDecoding)
					{
						setDecodingState(newDecoding);
					}
				}
				else if (decoding)
				{
					setDecodingState(false);
				}
			}
		}

		//******************************************************************************
		// getMediaFormat
		//******************************************************************************
		MediaFormat getMediaFormat()
		{
			return format;
		}

		//******************************************************************************
		// setDecodingState
		//******************************************************************************
		private synchronized void setDecodingState(boolean newDecoding)
		{
			try
			{
				if (newDecoding != decoding && decoder != null)
				{
					if (newDecoding)
					{
						decoder.start();
					}
					else
					{
						decoder.stop();
					}
					decoding = newDecoding;
				}
			} catch (Exception ex) {}
		}

		//******************************************************************************
		// run
		//******************************************************************************
		@Override
		public void run()
		{
			byte[] nal = new byte[NAL_SIZE_INC];
			int nalLen = 0;
			int numZeroes = 0;
			int numReadErrors = 0;

			try
			{
				// create the decoder
				decoder = MediaCodec.createDecoderByType("video/avc");

				// create the reader
				buffer = new byte[BUFFER_SIZE];
				reader = new TcpIpReader(camera);
				if (!reader.isConnected())
				{
					throw new Exception();
				}

				// read until we're interrupted
				while (!isInterrupted())
				{
					// read from the stream
					int len = reader.read(buffer);
					if (isInterrupted()) break;

					// process the input buffer
					if (len > 0)
					{
						numReadErrors = 0;
						for (int i = 0; i < len && !isInterrupted(); i++)
						{
							// add the byte to the NAL
							if (nalLen == nal.length)
							{
								nal = Arrays.copyOf(nal, nal.length + NAL_SIZE_INC);
							}
							nal[nalLen++] = buffer[i];

							// look for a header
							if (buffer[i] == 0)
							{
								numZeroes++;
							}
							else
							{
								if (buffer[i] == 1 && numZeroes == 3)
								{
									if (nalLen > 4)
									{
										int nalType = processNal(nal, nalLen - 4);
										if (isInterrupted()) break;
										if (nalType == -1)
										{
											nal[0] = nal[1] = nal[2] = 0;
											nal[3] = 1;
										}
									}
									nalLen = 4;
								}
								numZeroes = 0;
							}
						}
					}
					else
					{
						numReadErrors++;
						if (numReadErrors >= MAX_READ_ERRORS)
						{
							setMessage(R.string.error_lost_connection);
							break;
						}
					}

					// send an output buffer to the surface
					if (format != null && decoding)
					{
						if (isInterrupted()) break;
						MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
						int index;
						do
						{
							index = decoder.dequeueOutputBuffer(info, 0);
							if (isInterrupted()) break;
							if (index >= 0)
							{
								decoder.releaseOutputBuffer(index, true);
							}
							//Log.info(String.format("dequeueOutputBuffer index = %d", index));
						} while (index >= 0);
					}
				}
			}
			catch (Exception ex)
			{
				Log.error(ex.toString());
				if (reader == null || !reader.isConnected())
				{
					setMessage(R.string.error_couldnt_connect);
					finishHandler.postDelayed(finishRunner, FINISH_TIMEOUT);
				}
				else
				{
					setMessage(R.string.error_lost_connection);
				}
				ex.printStackTrace();
			}

			// close the reader
			if (reader != null)
			{
				try
				{
					reader.close();
				}
				catch (Exception ex) {}
				reader = null;
			}

			// stop the decoder
			if (decoder != null)
			{
				try
				{
					setDecodingState(false);
					decoder.release();
				}
				catch (Exception ex) {}
				decoder = null;
			}
		}

		//******************************************************************************
		// processNal
		//******************************************************************************
		private int processNal(byte[] nal, int nalLen)
		{
			// get the NAL type
			int nalType = (nalLen > 4 && nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 1) ? (nal[4] & 0x1F) : -1;
			//Log.info(String.format("NAL: type = %d, len = %d", nalType, nalLen));

			// process the first SPS record we encounter
			if (nalType == 7 && !decoding)
			{
				SpsParser parser = new SpsParser(nal, nalLen);
				format = MediaFormat.createVideoFormat("video/avc", parser.width, parser.height);
				presentationTimeInc = 66666;
				presentationTime = System.nanoTime() / 1000;
				Log.info(String.format("SPS: %02X, %d x %d, %d", nal[4], parser.width, parser.height, presentationTimeInc));
				decoder.configure(format, surface, null, 0);
				setDecodingState(true);
				inputBuffers = decoder.getInputBuffers();
				hideMessage();
				startVideoHandler.post(startVideoRunner);
			}

			// queue the frame
			if (nalType > 0 && decoding)
			{
				int index = decoder.dequeueInputBuffer(0);
				if (index >= 0)
				{
					ByteBuffer inputBuffer = inputBuffers[index];
					//ByteBuffer inputBuffer = decoder.getInputBuffer(index);
					inputBuffer.put(nal, 0, nalLen);
					decoder.queueInputBuffer(index, 0, nalLen, presentationTime, 0);
					presentationTime += presentationTimeInc;
				}
				//Log.info(String.format("dequeueInputBuffer index = %d", index));
			}
			return nalType;
		}

		//******************************************************************************
		// hideMessage
		//******************************************************************************
		private void hideMessage()
		{
			getActivity().runOnUiThread(new Runnable()
			{
				public void run()
				{
					messageView.setVisibility(View.GONE);
				}
			});
		}

		//******************************************************************************
		// setMessage
		//******************************************************************************
		private void setMessage(final int id)
		{
			getActivity().runOnUiThread(new Runnable()
			{
				public void run()
				{
					messageView.setText(id);
					messageView.setTextColor(App.getClr(R.color.bad_text));
					messageView.setVisibility(View.VISIBLE);
				}
			});
		}
	}
}