package com.glass.cuxtomcam;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.media.MediaRecorder.OnErrorListener;
import android.media.MediaRecorder.OnInfoListener;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;

import com.coremedia.iso.boxes.Container;
import com.glass.cuxtomcam.CameraOverlay.Mode;
import com.glass.cuxtomcam.VideoMerger.OnVideoListener;
import com.glass.cuxtomcam.constants.CuxtomIntent;
import com.glass.cuxtomcam.constants.CuxtomIntent.CAMERA_MODE;
import com.glass.cuxtomcam.constants.CuxtomIntent.FILE_TYPE;
import com.google.android.glass.touchpad.Gesture;
import com.google.android.glass.touchpad.GestureDetector;
import com.google.android.glass.touchpad.GestureDetector.BaseListener;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.AppendTrack;

public class CuxtomCamActivity extends Activity implements BaseListener,
		CameraListener, MediaScannerConnection.OnScanCompletedListener {

	private final String TAG = "CuXtom Cam ACTIVITY";
	private final int KEY_SWIPE_DOWN = 4;
	private Camera mCamera;
	private CameraPreview mPreview;
	private RelativeLayout previewCameraLayout;
	private GestureDetector mGestureDetector;
	private MediaRecorder recorder;
	private TextView tv_recordingDuration;
	private int totalVideoDuration;
	private ScheduledExecutorService mExecutorService;
	// For multiple Video Files
	private ArrayList<File> videoFiles;

	/*
	 * This is the Maximum Allowed video duration in Seconds. For now its 21
	 * minutes as that amounts to a video having size < 2GB
	 */
	private int maxAllowedVideoDuration = 21 * 60;
	// *****************************
	// these values are set by the calling activity
	// *****************************
	private final String DEFAULT_DIRECTORY = "CuXtom Cam";
	private final String VIDEO_DIRECTORY = "Videos";
	private final String PHOTO_DIRECTORY = "Photos";

	private int cameraMode;
	private String fileName;
	private String folderPath;
	private int video_duration;
	private boolean enablezoom;
	private File videofile;
	private CameraOverlay mOverlay;
	private SoundEffectPlayer mSoundEffects;
	private Runnable recordingTimer = new Runnable() {

		@Override
		public synchronized void run() {
			totalVideoDuration++;
			final int seconds = totalVideoDuration % 60;
			final int minutes = totalVideoDuration / 60;

			if (seconds < 10) {
				if (minutes < 10) {
					tv_recordingDuration.post(new Runnable() {

						@Override
						public void run() {
							tv_recordingDuration.setText("0" + minutes + ":0"
									+ seconds);

						}
					});
				} else {
					tv_recordingDuration.post(new Runnable() {

						@Override
						public void run() {
							tv_recordingDuration.setText(minutes + ":0"
									+ seconds);
						}
					});
				}

			} else {
				if (minutes < 10) {
					tv_recordingDuration.post(new Runnable() {

						@Override
						public void run() {
							tv_recordingDuration.setText("0" + minutes + ":"
									+ seconds);
						}
					});
				} else {
					tv_recordingDuration.post(new Runnable() {

						@Override
						public void run() {
							tv_recordingDuration.setText(minutes + ":"
									+ seconds);
						}
					});
				}
			}

		}
	};

	private Camera getCameraInstance() {
		Camera c = null;
		try {
			c = Camera.open(); // attempt to get a Camera instance
		} catch (Exception e) {
			// Camera is not available (in use or does not exist)
			Log.e(TAG, "Error in getCameraInstance--> " + e.getMessage());
		}
		return c; // returns null if camera is unavailable
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.e(TAG, "Oncreate()");
		mSoundEffects = new SoundEffectPlayer();
		mSoundEffects.setup(this);
		loadExtras(getIntent());
		Log.e(TAG, "Loading extras completed");
		loadUI();
		Log.e(TAG, "Loading UI completed");

	}

	@Override
	protected void onDestroy() {
		mSoundEffects.deconstruct();
		System.gc();
		super.onDestroy();
	}

	/**
	 * Load all the extra values that have been sent by the calling activity
	 * 
	 * @param intent
	 *            containing extras
	 */
	private void loadExtras(Intent intent) {
		// Check for CameraMode
		if (intent.hasExtra(CuxtomIntent.CAMERA_MODE)) {
			cameraMode = intent.getIntExtra(CuxtomIntent.CAMERA_MODE,
					CAMERA_MODE.PHOTO_MODE);
		} else {
			cameraMode = CAMERA_MODE.PHOTO_MODE;
		}

		// check for folder path where pictures will be saved
		if (intent.hasExtra(CuxtomIntent.FOLDER_PATH)) {
			folderPath = intent.getStringExtra(CuxtomIntent.FOLDER_PATH);
		} else {
			folderPath = Environment.getExternalStorageDirectory()
					+ File.separator + Environment.DIRECTORY_PICTURES
					+ File.separator + DEFAULT_DIRECTORY;
			createSubDirectory();
		}

		// Check for FileName
		if (intent.hasExtra(CuxtomIntent.FILE_NAME)) {
			fileName = intent.getStringExtra(CuxtomIntent.FILE_NAME);
		} else {
			String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss")
					.format(new Date());
			if (cameraMode == CAMERA_MODE.PHOTO_MODE) {
				fileName = "pic_" + timeStamp;
			} else {
				fileName = "vid_" + timeStamp;
			}
		}

		// Check for video duration
		if (cameraMode == CAMERA_MODE.VIDEO_MODE
				&& intent.hasExtra(CuxtomIntent.VIDEO_DURATION)) {
			video_duration = intent.getIntExtra(CuxtomIntent.VIDEO_DURATION,
					3600);
		} else {
			video_duration = 3600;
		}

		// check whether zoom functionailty should be enabled
		if (intent.hasExtra(CuxtomIntent.ENABLE_ZOOM)) {
			enablezoom = intent.getBooleanExtra(CuxtomIntent.ENABLE_ZOOM, true);
		} else {
			enablezoom = true;
		}

	}

	/**
	 * Create a sub directory to save photos and videos
	 */
	private void createSubDirectory() {

		if (cameraMode == CAMERA_MODE.VIDEO_MODE) {
			folderPath = folderPath + File.separator + VIDEO_DIRECTORY;
		} else {
			folderPath = folderPath + File.separator + PHOTO_DIRECTORY;
		}
	}

	/**
	 * Load UI according to the settings provided by calling activity
	 */
	private void loadUI() {
		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		previewCameraLayout = new RelativeLayout(this);
		previewCameraLayout.setLayoutParams(new LayoutParams(
				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
				android.view.ViewGroup.LayoutParams.MATCH_PARENT));
		// Create an instance of Camera
		mCamera = getCameraInstance();
		// Create our Preview view and set it as the content of our activity.
		mPreview = new CameraPreview(this, mCamera, cameraMode, new Handler());
		mPreview.setCameraListener(this);
		mOverlay = new CameraOverlay(this);
		previewCameraLayout.addView(mPreview);
		previewCameraLayout.addView(mOverlay);
		setContentView(previewCameraLayout);
		tv_recordingDuration = new TextView(this);
		mGestureDetector = new GestureDetector(this);
		mGestureDetector.setBaseListener(this);

	}

	/**
	 * initialize video recording UI with timer
	 */
	private void initVideoRecordingUI(String initializeTime) {
		LayoutParams rl_param = new LayoutParams(
				android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
				android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
		rl_param.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
		rl_param.addRule(RelativeLayout.CENTER_HORIZONTAL);
		rl_param.addRule(RelativeLayout.ALIGN_BOTTOM, mPreview.getId());
		rl_param.setMargins(0, 0, 0, 30);
		tv_recordingDuration.setText(initializeTime);
		tv_recordingDuration.setTextSize(28);
		tv_recordingDuration.setLayoutParams(rl_param);
		previewCameraLayout.addView(tv_recordingDuration);
		mExecutorService = Executors.newSingleThreadScheduledExecutor();
		totalVideoDuration = 0;
		mExecutorService.scheduleAtFixedRate(recordingTimer, 1, 1,
				TimeUnit.SECONDS);
	}

	/**
	 * Events occurred by performing gestures on activity will be received here
	 */
	@Override
	public boolean onGenericMotionEvent(MotionEvent event) {
		if (mGestureDetector != null) {
			return mGestureDetector.onMotionEvent(event);
		}
		return false;
	}

	/**
	 * Handle Glass Swipe and Tap Gestures
	 */

	@Override
	public synchronized boolean onGesture(Gesture g) {
		switch (g) {
		case TAP:
			if (cameraMode == CAMERA_MODE.PHOTO_MODE) {
				mOverlay.setMode(CameraOverlay.Mode.FOCUS);
				mCamera.takePicture(null, null, mPictureCallback);
				mSoundEffects.shutter();
			} else
				endVideoRecording();

			return true;
		case SWIPE_RIGHT:
			if (enablezoom)
				mPreview.zoomIn();
			return true;
		case TWO_SWIPE_RIGHT:
			if (enablezoom)
				mPreview.zoomIn();
			return true;
		case SWIPE_LEFT:
			if (enablezoom)
				mPreview.zoomOut();
			return true;
		case TWO_SWIPE_LEFT:
			if (enablezoom)
				mPreview.zoomOut();
			return true;
		case SWIPE_DOWN:
			releaseMediaRecorder();
			mSoundEffects.camcorderStop();
			mExecutorService.shutdown();
			Toast.makeText(CuxtomCamActivity.this, "Cancelled", Toast.LENGTH_LONG)
					.show();
			for (int i = 0; i < videoFiles.size(); i++) {
				if(videoFiles.get(0).exists())
				videoFiles.get(i).delete();
			}
			onBackPressed();
			return true;
		default:
			return false;
		}
	}

	/**
	 * Ignore any key that is pressed. Just handle camera key
	 */
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_CAMERA) {
			if (cameraMode == CAMERA_MODE.PHOTO_MODE) {
				mOverlay.setMode(CameraOverlay.Mode.FOCUS);
				mCamera.takePicture(null, null, mPictureCallback);
				mSoundEffects.shutter();
			}
			return true;
		} else
			return false;
	}

	/**
	 * Ignore swipe down event
	 */
	@Override
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		if (keyCode == KEY_SWIPE_DOWN) {
			if (videofile != null) {
				// if you are using MediaRecorder, release it
				// first
				releaseMediaRecorder();
				mExecutorService.shutdown();
				mOverlay.setMode(Mode.PLAIN);
				if (videofile.exists())
					videofile.delete();
			}
			setResult(RESULT_CANCELED);
			onScanCompleted(null, null);
			super.onKeyUp(keyCode, event);
		}
		return false;
	}

	/**
	 * Save picture once its taken and send result to the calling activity
	 */
	private PictureCallback mPictureCallback = new PictureCallback() {

		@Override
		public void onPictureTaken(byte[] data, Camera camera) {
			try {
				// Directories can be created but they cannot be seen when you
				// connect to computer unless you access them from
				// ddms in eclipse. There is some sort of special viewing
				// permission on the glass directores for now
				File dir = new File(folderPath);
				if (!dir.exists()) {
					dir.mkdirs();
				}
				File f = new File(dir, fileName + ".jpg");
				FileOutputStream fos = new FileOutputStream(f);
				fos.write(data);
				fos.flush();
				fos.close();

				mOverlay.setMode(CameraOverlay.Mode.PLAIN);
				Intent intent = new Intent();
				intent.putExtra(CuxtomIntent.FILE_PATH, f.getPath());
				intent.putExtra(CuxtomIntent.FILE_TYPE, FILE_TYPE.PHOTO);
				setResult(RESULT_OK, intent);
				// initiate media scan and put the new things into the path
				// array to
				// make the scanner aware of the location and the files you want
				// to
				// see
				MediaScannerConnection.scanFile(getApplicationContext(),
						new String[] { f.getPath() }, null,
						CuxtomCamActivity.this);
			} catch (FileNotFoundException e) {
				Log.e(TAG, "File not found: " + e.getMessage());
				setResult(RESULT_CANCELED);
			} catch (IOException e) {
				Log.e(TAG, "Error accessing file: " + e.getMessage());
				setResult(RESULT_CANCELED);
			}
		}
	};

	/**
	 * Initialize Video Recorder and start configuring recorder for single or
	 * Multiple files
	 */
	private void initVideoRecorder() {
		recorder = new MediaRecorder();
		// Let's initRecorder
		if (video_duration > maxAllowedVideoDuration) {
			// Configure Multiple Video Files Recorder
			videoFiles = new ArrayList<File>();
			int remainingVideoDuration = video_duration
					- maxAllowedVideoDuration;
			// Video Duration that is left to be recorded
			video_duration = remainingVideoDuration;
			startMultipleVideoRecorder(maxAllowedVideoDuration);
		} else
			// Configure Single Video Files Recorder
			startSingleVideoRecorder();
	}

	/**
	 * Configure video recorder to record a single video file. There will be no
	 * need to merge this file.
	 */
	private void startSingleVideoRecorder() {

		File dir = new File(folderPath);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		videofile = new File(dir, fileName + ".mp4");
		recorder.setCamera(mCamera);

		// Step 2: Set sources
		recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
		recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

		// Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
		recorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
		// Step 4: Set output file
		recorder.setOutputFile(videofile.getAbsolutePath());
		// Step 5: Set the preview output
		recorder.setPreviewDisplay(mPreview.getHolder().getSurface());
		// Step 6: Prepare configured MediaRecorder
		recorder.setMaxDuration(0);
		recorder.setMaxFileSize(0);
		recorder.setOnErrorListener(new OnErrorListener() {

			@Override
			public void onError(MediaRecorder mr, int what, int extra) {
				Log.e("Error Recording", what + " Extra " + extra);

			}
		});
		recorder.setOnInfoListener(new OnInfoListener() {

			@Override
			public void onInfo(MediaRecorder mr, int what, int extra) {
				if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
					endVideoRecording();
				}

			}
		});
		runOnUiThread(new Runnable() {

			@Override
			public void run() {

				try {
					mSoundEffects.camcorder();
					recorder.prepare();
					recorder.start();
					mOverlay.setMode(Mode.RECORDING);

				} catch (Exception e) {
					if (e != null && e.getMessage() != null)
						Log.e("Error Starting CuXtom Camera for video recording",
								e.getMessage());
					endVideoRecording();
				}

			}
		});

	}

	/**
	 * Configure video recorder Start recording videos that will be merged later
	 * onwards, once all videos have been recorded
	 */
	private void startMultipleVideoRecorder(int currentRecordingDuration) {

		File dir = new File(folderPath);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		File vFile = new File(dir, System.currentTimeMillis() + ".mp4");
		videoFiles.add(vFile);
		recorder.setCamera(mCamera);

		// Step 2: Set sources
		recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
		recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

		// Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
		recorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
		// Step 4: Set output file
		recorder.setOutputFile(vFile.getAbsolutePath());
		// Step 5: Set the preview output
		recorder.setPreviewDisplay(mPreview.getHolder().getSurface());
		// Step 6: Prepare configured MediaRecorder
		recorder.setMaxDuration(currentRecordingDuration * 1000);
		recorder.setMaxFileSize(-1);
		recorder.setOnErrorListener(new OnErrorListener() {

			@Override
			public void onError(MediaRecorder mr, int what, int extra) {
				Log.e("Error Recording", what + " Extra " + extra);

			}
		});
		recorder.setOnInfoListener(new OnInfoListener() {

			@Override
			public void onInfo(MediaRecorder mr, int what, int extra) {
				if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
					resumeOrEndVideoRecording();
				}

			}
		});
		runOnUiThread(new Runnable() {

			@Override
			public void run() {

				try {
					mSoundEffects.camcorder();
					recorder.prepare();
					recorder.start();
					mOverlay.setMode(Mode.RECORDING);
					totalVideoDuration = (totalVideoDuration / 60) * 60;
				} catch (Exception e) {
					if (e != null && e.getMessage() != null)
						Log.e("Error Starting CuXtom Camera for video recording",
								e.getMessage());
					endVideoRecording();
				}

			}
		});

	}

	/**
	 * End Video Recording, Refresh the Disk and End the activity
	 */
	private void endVideoRecording() {
		runOnUiThread(new Runnable() {

			@Override
			public void run() {
				// if there are More than 1 Video Files
				if (videoFiles != null && videoFiles.size() > 1) {
					releaseMediaRecorder();
					mSoundEffects.camcorderStop();
					mExecutorService.shutdown();
					mOverlay.setMode(Mode.PLAIN);
					// Merge video files into 1 file
					mergeVideoIntoOneFile();
				} else {
					// If only 1 file was recorded in Multiple Video Recording
					// Mode. Then there is no Need to merge the video file. Just
					// rename it to FileName that was sent by calling activity
					if (videoFiles != null && videoFiles.size() == 1) {
						File tempFile = videoFiles.get(0);
						// Make a directory with the path that was sent by
						// Calling Activity
						File dir = new File(folderPath);
						if (!dir.exists())
							dir.mkdirs();
						// Rename File to the File Name that was sent by
						// calling activity
						videofile = new File(dir, fileName + ".mp4");
						tempFile.renameTo(videofile);

					}

					releaseMediaRecorder();
					mSoundEffects.camcorderStop();
					mExecutorService.shutdown();
					mOverlay.setMode(Mode.PLAIN);
					Intent intent = new Intent();
					intent.putExtra(CuxtomIntent.FILE_PATH, videofile.getPath());
					intent.putExtra(CuxtomIntent.FILE_TYPE, FILE_TYPE.VIDEO);
					setResult(RESULT_OK, intent);
					/*
					 * initiate media scan and put the new things into the path
					 * array to make the scanner aware of the location and the
					 * files you want to see
					 */MediaScannerConnection.scanFile(getApplicationContext(),
							new String[] { videofile.getPath() }, null,
							CuxtomCamActivity.this);
				}

			}
		});
	}

	/**
	 * Resume or End video recording depending upon whether the video has been
	 * recorded for the whole duration or not
	 */
	private void resumeOrEndVideoRecording() {
		runOnUiThread(new Runnable() {

			@Override
			public void run() {
				// The Whole video has been recorded successfully
				if (video_duration <= 0)
					endVideoRecording();
				else {
					if (recorder != null) {
						try {
							recorder.stop();
							recorder.reset();
						} catch (Exception e) {
							Log.e("error stopping", e.getMessage());
						}

					}
					mCamera.stopPreview();
					mCamera.unlock();
					if (video_duration < maxAllowedVideoDuration)
						startMultipleVideoRecorder(video_duration);
					else {
						int remainingVideoDuration = video_duration
								- maxAllowedVideoDuration;
						// Video Duration that is left to be recorded
						video_duration = remainingVideoDuration;
						// Start recording the another video file
						startMultipleVideoRecorder(maxAllowedVideoDuration);
					}
				}
			}
		});

	}

	/**
	 * Start merging the video files
	 */
	private void mergeVideoIntoOneFile() {
		previewCameraLayout.removeAllViews();
		File dir = new File(folderPath);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		videofile = new File(dir, fileName + ".mp4");
		VideoMerger videoMerger = new VideoMerger(CuxtomCamActivity.this,
				videoFiles, videofile);
		videoMerger.setVideoMergeListener(new OnVideoListener() {

			@Override
			public void onVideoMerged() {
				Intent intent = new Intent();
				intent.putExtra(CuxtomIntent.FILE_PATH, videofile.getPath());
				intent.putExtra(CuxtomIntent.FILE_TYPE, FILE_TYPE.VIDEO);
				setResult(RESULT_OK, intent);
				/*
				 * initiate media scan and put the new things into the path
				 * array to make the scanner aware of the location and the files
				 * you want to see
				 */MediaScannerConnection.scanFile(getApplicationContext(),
						new String[] { videofile.getPath() }, null,
						CuxtomCamActivity.this);
			}

			@Override
			public void onVideoMergeFailed(Exception e) {
				Log.e("Error", e.getMessage());

			}
		});
		videoMerger.execute();
	}

	private void releaseMediaRecorder() {
		if (recorder != null) {
			try {
				recorder.stop();

			} catch (Exception e) {
				Log.e("error stopping", e.getMessage());
			}
			recorder.reset(); // clear recorder configuration
			recorder.release(); // release the recorder object
			recorder = null;
		}

		if (mCamera != null)
			mCamera.stopPreview();
		mCamera = null;
		mPreview.surfaceDestroyed(null);

	}

	@Override
	public synchronized void onCameraInit() {
		Log.e(TAG, "onCameraInit");
		if (cameraMode == CAMERA_MODE.VIDEO_MODE) {
			Log.e(TAG, "As VIDEO_MODE");
			new Thread(new Runnable() {

				@Override
				public void run() {
					Log.e(TAG, "Running runOnUiThread");
					mCamera.stopPreview();
					mCamera.unlock();
					initVideoRecorder();
					Log.e(TAG, "Start recorder");
					runOnUiThread(new Runnable() {

						@Override
						public void run() {
							initVideoRecordingUI("00:00");
							Log.e(TAG, "Recorder Started");
						}
					});

				}
			}).start();
		}

	}

	@Override
	public void onScanCompleted(String path, Uri uri) {
		tv_recordingDuration.removeCallbacks(recordingTimer);
		// previewCameraLayout.removeAllViewsInLayout();
		CuxtomCamActivity.this.finish();
		Log.e(TAG, "Activity Ended");
	}

}