public class MainActivity extends AppCompatActivity
		implements NavDrawerFragment.NavigationDrawerCallbacks, FileChooser.OnFileChosenListener {

	private static final String STATE_ITEMS = "items_list";
	private final String TAG = getClass().getName();
	private NavDrawerFragment mNavDrawerFragment;
	private EditorFragment mEditorFragment;
	private Toolbar mToolbar;
	private ViewPager mViewPager;
	private View mProgressOverlay;
	private MyPagerAdapter mPagerAdapter;
	/**
	 * Flag to indicate the need to switch the pager to the newly added item
	 */
	private boolean mAddedItemViaToolbar;
	/**
	 * A list that contains all the added items, shared throughout the app. It's used by
	 * {@link NavDrawerFragment}, {@link com.simas.vc.nav_drawer.NavAdapter},
	 * {@link MyPagerAdapter} and individual {@link NavItem}s.
	 */
	public static ObservableList sItems = new ObservableList();
	public static int sPlayerContainerSize;
	public static int sPreviewSize;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		// Reset player container and preview sizes when (re-)created
		sPlayerContainerSize = 0;
		sPreviewSize = 0;
		// Always reset observers when (re-)creating the activity
		sItems.unregisterAllObservers();
		// Restore items if available, otherwise make sure the list is empty
		if (savedInstanceState != null) {
			ArrayList<NavItem> items = savedInstanceState.getParcelableArrayList(STATE_ITEMS);
			if (items != null) {
				sItems = new ObservableList();
				sItems.addAll(items);
			} else {
				sItems.clear();
			}
		} else {
			sItems.clear();
		}

		// Init a ProgressBar (to be used by PlayerFragments)
		mProgressOverlay = getLayoutInflater().inflate(R.layout.progress_bar_overlay, null);

		setContentView(R.layout.activity_main);

		/* Toolbar */
		mToolbar = (Toolbar) findViewById(R.id.toolbar);
		setSupportActionBar(mToolbar);
//		addTooltips();

		/* Pager */
		mViewPager = (ViewPager) findViewById(R.id.view_pager);
		mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
		mViewPager.addOnPageChangeListener(new PagerScrollListener());
		mViewPager.setAdapter(mPagerAdapter);
		// This is to avoid a black screen flash when the SurfaceView is setting up another window
		// https://code.google.com/p/gmaps-api-issues/issues/detail?id=4639#c2
		mViewPager.requestTransparentRegion(mViewPager);
		// Observer
		final String PAGER_OBSERVER = "pager_observer";
		MainActivity.sItems.registerDataSetObserver(new ObservableList.Observer() {
			@Override
			public void onRemoved(int position) {
				Log.e(TAG, "removed: " + position);
				// Remove fragment and its state from adapter
				mPagerAdapter.onItemRemoved(position + 1);

				// Update count and notify
				mPagerAdapter.setCount(MainActivity.sItems.size());
			}

			@Override
			public void onModified() {
				super.onModified();
				// Update count and notify
				mPagerAdapter.setCount(MainActivity.sItems.size());
			}
		}, PAGER_OBSERVER);

		/* Drawer */
		mNavDrawerFragment = (NavDrawerFragment) getSupportFragmentManager()
				.findFragmentById(R.id.navigation_drawer);
		mNavDrawerFragment.setUp(R.id.navigation_drawer,
				(DrawerLayout) findViewById(R.id.drawer_layout));

		// Make sure editor item is == to the LV's current selection (e.g. on adapter data deletion)
		final String EDITOR_AND_DRAWER_ITEM_MATCHER = "match_items_between_editor_and_drawer";
		sItems.registerDataSetObserver(new ObservableList.Observer() {
			private ListView lv = mNavDrawerFragment.getListView();
			private Runnable mCheckItems = new Runnable() {
				@Override
				public void run() {
					// Make sure we're not in CAB mode (multiple selections)
					Object checkedItem;
					if (lv.getChoiceMode() == ListView.CHOICE_MODE_SINGLE) {
						try {
							checkedItem = lv.getItemAtPosition(lv.getCheckedItemPosition());
						} catch (IndexOutOfBoundsException ignored) {
							return;
						}

						// Make sure the editor's item is the same as the currently checked one
						if (getEditorFragment() != null &&
								getEditorFragment().getItem() != checkedItem) {
							mNavDrawerFragment.selectItem(ListView.INVALID_POSITION);
						}
					}
				}
			};
			@Override
			public void onModified() {
				super.onModified();
				mCheckItems.run();
			}

			@Override
			public void onRemoved(int position) {
				super.onRemoved(position);
				mCheckItems.run();
			}
		}, EDITOR_AND_DRAWER_ITEM_MATCHER);

		// ToDo default item test
		if (sItems.size() == 0) {
			new Handler().postDelayed(new Runnable() {
				@Override
				public void run() {
					onFileChosen(new File("/sdcard/Movies/Serenity.mp4"));
//					onFileChosen(new File("/sdcard/Movies/iwatch.mp4"));
//					onFileChosen(new File("/sdcard/Movies/macbook.mp4"));
//					onFileChosen(new File("/sdcard/Movies/1.mp4"));
//					onFileChosen(new File("/sdcard/Movies/2.mp4"));
//					onFileChosen(new File("/sdcard/Movies/3.mp4"));
				}
			}, 1000);
//			new Handler().postDelayed(new Runnable() {
//				@Override
//				public void run() {
////					sItems.remove(0);
//					mViewPager.setCurrentItem(2);
//					new Handler().postDelayed(new Runnable() {
//						@Override
//						public void run() {
//							sItems.remove(1);
//						}
//					}, 2000);
//				}
//			}, 2000);
		}
	}

	private class PagerScrollListener extends ViewPager.SimpleOnPageChangeListener {

		private class IntegerPair {
			Integer current, previous;
		}

		private final String TAG = getClass().getName();
		private static final float MIN_SCROLL_OFFSET = 0.01f;
		private IntegerPair mVisiblePositions = new IntegerPair();
		private float mPositionOffset;

		@Override
		public void onPageSelected(int position) {
			super.onPageSelected(position);
			Log.e(TAG, "got pos: " + position);
			// Update the EditorFragment and invalidate the PlayerFragment
			if (position < 1) {
				// Empty item selected
				setTitle(Utils.getString(R.string.app_name));
				mEditorFragment = null;
				// Deselect ListView items
				mNavDrawerFragment.selectItem(ListView.INVALID_POSITION);
			} else if (position-1 < sItems.size()) {
				// Normal item selected
				setTitle(sItems.get(position - 1).getFile().getName());
				Log.e(TAG, "title: " + sItems.get(position - 1).getFile().getName());
				mEditorFragment = (EditorFragment) mPagerAdapter.getItem(position);
				if (getEditorFragment() != null) {
					getEditorFragment().getPlayerFragment().setInitialized(false);
				}
				// Select ListView item
				mNavDrawerFragment.selectItem(position);
			}

			// Make sure the player is paused
			Player player = Player.getInstance();
			if (player.getState() == Player.State.STARTED) {
				player.pause();
			}
		}

		@Override
		public void onPageScrolled(int position, float positionOffset, int positionOffsetPxs) {
			final float previousOffset = mPositionOffset;
			mPositionOffset = positionOffset;

			// There can only be 2 pages visible at a time
			if (mVisiblePositions.current == null || mVisiblePositions.current != position) {
				mVisiblePositions.previous = mVisiblePositions.current;
				mVisiblePositions.current = position;
			}

			// Re-invoke onPageScrollStateChanged when switched to a valid scroll offset
			if (Math.abs(mPositionOffset) >= MIN_SCROLL_OFFSET &&
					Math.abs(previousOffset) < MIN_SCROLL_OFFSET) {
				onPageScrollStateChanged(ViewPager.SCROLL_STATE_DRAGGING);
			} else if (Math.abs(mPositionOffset) < MIN_SCROLL_OFFSET) {
				// Hide previews for the visible positions
				showPreviewsTemporarily(false);
			}
		}

		@Override
		public void onPageScrollStateChanged(int state) {
			switch (state) {
				case ViewPager.SCROLL_STATE_DRAGGING:
					// Ignore low offsets, to avoid invalid drags
					if (Math.abs(mPositionOffset) < MIN_SCROLL_OFFSET) {
						return;
					}

					// Pause player
					Player player = Player.getInstance();
					if (player.getState() == Player.State.STARTED) {
						player.pause();
					}

					// Show previews on the 2 visible editor fragments
					showPreviewsTemporarily(true);
					break;
			}
		}

		private void showPreviewsTemporarily(boolean show) {
			if (mVisiblePositions.current != null &&
					mVisiblePositions.current >= 1) {
				EditorFragment editor = (EditorFragment) mPagerAdapter
						.getItem(mVisiblePositions.current);
				if (editor != null) {
					editor.getPlayerFragment().showPreviewTemporarily(show);
				}
				if (mVisiblePositions.previous != null &&
						mVisiblePositions.previous >= 1) {
					editor = (EditorFragment) mPagerAdapter
							.getItem(mVisiblePositions.previous);
					if (editor != null) {
						editor.getPlayerFragment().showPreviewTemporarily(show);
					}
				}
			}
		}
	}

	public EditorFragment getEditorFragment() {
		return mEditorFragment;
	}

	public View getProgressOverlay() {
		return mProgressOverlay;
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
		outState.putParcelableArrayList(STATE_ITEMS, sItems);
	}

	/**
	 * Adds helper tooltips if they haven't yet been closed. Must be called after the toolbar is
	 * set.
	 */
	private void addTooltips() {
		getToolbar().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
			@Override
			public void onLayoutChange(View v, int left, int top, int right, int bottom,
			                           int oldLeft, int oldTop, int oldRight, int oldBottom) {
				View concat = mToolbar.findViewById(R.id.action_concat);
				View add = mToolbar.findViewById(R.id.action_add_item);
				if (concat != null && add != null) {
					add.setOnClickListener(new View.OnClickListener() {
						private boolean mRotated;
						@Override
						public void onClick(View v) {
							ObjectAnimator animator = ObjectAnimator.ofFloat(v, "rotation",
									(mRotated = !mRotated) ? 360 : 0);
							animator.setDuration(300);
							animator.start();
						}
					});
					new Tooltip(MainActivity.this, concat, Utils
							.getString(R.string.help_concatenate));
					new Tooltip(MainActivity.this, add, Utils.getString(R.string.help_add_item));
					mToolbar.removeOnLayoutChangeListener(this);
				}
			}
		});
		// Force re-draw
		getToolbar().requestLayout();
	}

	public boolean isConcatenatable() {
		if (sItems == null) {
			// Adapter not available yet
			return false;
		} else if (sItems.size() < 2) {
			// There must be at least 2 videos to concatenate
			return false;
		} else {
			// Loop and look for invalid items
			for (final NavItem item : sItems) {
				if (item.getState() == NavItem.State.INPROGRESS) {
					// If a progressing item is found, wait for it to load and try again
					item.registerUpdateListener(new NavItem.OnUpdatedListener() {
						@Override
						public void onUpdated(NavItem.ItemAttribute attr, Object old, Object newV) {
							// The reason we also allow invalid state, is because if a 3rd item
							// becomes invalid, the first two can still be concatenated
							if (newV == NavItem.State.VALID || newV == NavItem.State.INVALID)  {
								item.unregisterUpdateListener(this);
								// At this moment, the state is disabled, so only notify if changed
								if (isConcatenatable()) {
									Utils.runOnUiThread(new Runnable() {
										@Override
										public void run() {
											getToolbar().getMenu()
													.findItem(R.id.action_concat).setEnabled(true);
										}
									});
								}
							}
						}
					});
					return false;
				}
			}
		}
		return true;
	}

	@Override
	public void onNavigationDrawerItemSelected(int position) {
		ListView lv = mNavDrawerFragment.getListView();

		if (position == ListView.INVALID_POSITION ||
				position < lv.getHeaderViewsCount() ||
				position >= lv.getCount() - lv.getFooterViewsCount()) {
			// Skip headers and footers
			//noinspection UnnecessaryReturnStatement
			return;
		} else {
			// Check the item in the drawer
			lv.setItemChecked(position, true);

			Object checkedItem = lv.getItemAtPosition(lv.getCheckedItemPosition());
			// Make sure the item is not already selected in the current EditorFragment
			if (lv.getChoiceMode() == ListView.CHOICE_MODE_SINGLE &&
					getEditorFragment() != null && getEditorFragment().getItem() != checkedItem) {
				// Select item in pager
				int itemPosInPager = position - lv.getHeaderViewsCount() + 1; // +1 for empty item
				mViewPager.setCurrentItem(itemPosInPager);

				// Close drawer
				mNavDrawerFragment.setDrawerOpen(false);
			}
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		boolean result = super.onCreateOptionsMenu(menu);
		getMenuInflater().inflate(R.menu.menu_main, menu);

		final String CONCAT_OBSERVER_TAG = "concatenation_observer";
		sItems.registerDataSetObserver(new ObservableList.Observer() {
			@Override
			public void onModified() {
				getToolbar().getMenu().findItem(R.id.action_concat).setEnabled(isConcatenatable());
			}

			@Override
			public void onRemoved(int position) {
				super.onRemoved(position);
				getToolbar().getMenu().findItem(R.id.action_concat).setEnabled(isConcatenatable());
			}
		}, CONCAT_OBSERVER_TAG);

		return result;
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		sItems.unregisterAllObservers();
		if (isFinishing()) {
			Player.getInstance().release(); // ToDo need to release earlier
		}
	}

	@Override
	public boolean onPrepareOptionsMenu(Menu menu) {
		if (mNavDrawerFragment != null) {
			menu.findItem(R.id.action_concat).setEnabled(isConcatenatable());
		}
		return super.onPrepareOptionsMenu(menu);
	}

	private static int sNum = 0; // ToDo remove after destination is available

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
			case R.id.action_concat:
				// ToDo ask user for a destination
				String destination = Environment
						.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getPath();
				File output = new File(destination + File.separator +
						"output" + (++sNum) + ".mp4");
				if (output.exists()) {
					//noinspection ResultOfMethodCallIgnored
					output.delete();
				}
				try {
					// Concat videos
					FFmpeg.concat(output, sItems);
				} catch (IOException e) {
					Log.e(TAG, "Error!", e);
					new AlertDialog.Builder(this)
							.setTitle(Utils.getString(R.string.error))
							.setMessage("Unrecoverable error! Please try again.")
							.setPositiveButton("OK...", null)
							.show();
				} catch (VCException e) {
					Log.e(TAG, "Concatenation error:", e);
					new AlertDialog.Builder(this)
							.setTitle(Utils.getString(R.string.error))
							.setMessage(e.getMessage())
							.setPositiveButton("OK", null)
							.show();
				}
				return true;
			case R.id.action_add_item:
				// When adding a new item
				showFileChooser(true);
				return true;
			case R.id.action_settings:
				return true;
		}

		return super.onOptionsItemSelected(item);
	}

	public void showFileChooser(boolean fromToolbar) {
		FileChooser fileChooser = (FileChooser) getSupportFragmentManager()
				.findFragmentByTag(FileChooser.TAG);
		if (fileChooser == null) {
			fileChooser = FileChooser.getInstance();
			fileChooser.setOnFileChosenListener(this);
			fileChooser.show(getSupportFragmentManager(), FileChooser.TAG);
		}

		mAddedItemViaToolbar = fromToolbar;
	}

	@Override
	public void onFileChosen(File file) {
		final NavItem item = new NavItem(file);
		item.registerUpdateListener(new NavItem.OnUpdatedListener() {
			@Override
			public void onUpdated(NavItem.ItemAttribute attribute, Object old, Object newValue) {
				if (newValue == NavItem.State.INVALID) {
					Utils.runOnUiThread(new Runnable() {
						@Override
						public void run() {
							// If an exception was throw we can fetch the message via the NavItem
							if (item.getError() != null) {
								try {
									new AlertDialog.Builder(MainActivity.this)
											.setTitle(Utils.getString(R.string.error))
											.setMessage(item.getError())
											.setPositiveButton("OK", null)
											.show();
								} catch (Exception ignored) {
									// If an error occurs while showing a dialog, the context may
									// be dead, instead show a toast (The truth will be revealed!)
									Toast.makeText(VC.getAppContext(),
											item.getError(), Toast.LENGTH_LONG).show();
								}
							} else {
								// If an error was not registered, we just display a toast warning
								Toast.makeText(VC.getAppContext(),
										Utils.getString(R.string.format_parse_failed,
												item.getFile().getName()), Toast.LENGTH_LONG)
										.show();
							}

							// If an invalid state was reached, remove this item from the drawer
							MainActivity.sItems.remove(item);
						}
					});
				} else if (newValue == NavItem.State.VALID) {
					// Upon reaching the VALID state, remove this listener
					item.unregisterUpdateListener(this);
				}
			}
		});
		MainActivity.sItems.add(item);

		// Wait until item is added to the pager
		final boolean addedViaToolbar = mAddedItemViaToolbar;
		mPagerAdapter.registerDataSetObserver(new DataSetObserver() {
			@Override
			public void onChanged() {
				super.onChanged();
				// Only 1 change is needed, remove observer
				mPagerAdapter.unregisterDataSetObserver(this);
				// Let the viewPager receive the information about the changes
				mViewPager.post(new Runnable() {
					@Override
					public void run() {
						// If item added via the ToolBar, and the drawer is closed,
						// switch pager to the item
						if (addedViaToolbar && (mNavDrawerFragment.getDrawerState() ==
								NavDrawerFragment.DrawerState.CLOSING ||
								mNavDrawerFragment.getDrawerState() ==
										NavDrawerFragment.DrawerState.CLOSED)) {
							mViewPager.setCurrentItem(mPagerAdapter.getCount());
						}
					}
				});
			}
		});
	}

	@Override
	public void onBackPressed() {
		// Close drawer on back click
		if (mNavDrawerFragment.isDrawerOpen()) {
			mNavDrawerFragment.setDrawerOpen(false);
		} else {
			super.onBackPressed();
		}
	}

	public Toolbar getToolbar() {
		return mToolbar;
	}

} public class VCException extends Exception {

	private String mMessage;

	/**
	 * Default VCException
	 * @param message    message that will be shown to the user
	 */
	public VCException(String message) {
		mMessage = message;
	}

	@Override
	public String getMessage() {
		return mMessage;
	}

} public class AudioStream extends Stream {

	public static final String ATTRIBUTE_CHANNEL_COUNT = "Channel count";
	public static final String ATTRIBUTE_SAMPLE_RATE = "Sample rate";

	/**
	 * Values are displayed to the user in a specific order. This order is saved in an ArrayList.
	 * Index 0 will be displayed first.
	 */
	public static final List<String> ATTRIBUTE_PRIORITIES = new ArrayList<String>() {{
		addAll(Stream.ATTRIBUTE_PRIORITIES);
		add(ATTRIBUTE_DURATION);
		add(ATTRIBUTE_CHANNEL_COUNT);
		add(ATTRIBUTE_SAMPLE_RATE);
	}};

	public AudioStream(String codecName) throws VCException {
		super(codecName);
	}

	public Integer getChannelCount() {
		return (Integer) getValue(ATTRIBUTE_CHANNEL_COUNT);
	}
	public Integer getSampleRate() {
		return (Integer) getValue(ATTRIBUTE_SAMPLE_RATE);
	}

	public AudioStream setChannelCount(Integer channelCount) {
		setValue(ATTRIBUTE_CHANNEL_COUNT, channelCount);
		return this;
	}
	public AudioStream setSampleRate(Integer sampleRate) {
		setValue(ATTRIBUTE_CHANNEL_COUNT, sampleRate);
		return this;
	}

	@Override
	public List<String> getAttributePriorities() {
		return ATTRIBUTE_PRIORITIES;
	}

	@Override
	public String toString() {
		return String.format("%s\nAudioStream ChannelCount: %d, SampleRate: %d",
				super.toString(), getChannelCount(), getSampleRate());
	}

} public class FileAttributes implements Parcelable {

	private String mFileName, mLongName, mName;
	private Long mSize = 0l;
	private List<AudioStream> mAudioStreams = new ArrayList<>();
	private List<VideoStream> mVideoStreams = new ArrayList<>();
	private List<Stream> mStreams = new ArrayList<>();
	private Double mDuration;

	public FileAttributes(String filename, Long size, Double duration) throws VCException {
		setFileName(filename);
		setSize(size);
		setDuration(duration);
	}

	public String getFileName() {
		return mFileName;
	}

	public String getLongName() {
		return mLongName;
	}

	public String getName() {
		return mName;
	}

	public Long getSize() {
		return mSize;
	}

	/**
	 * Duration in seconds
	 */
	public Double getDuration() {
		return mDuration;
	}

	public List<AudioStream> getAudioStreams() {
		return mAudioStreams;
	}

	public List<VideoStream> getVideoStreams() {
		return mVideoStreams;
	}

	public List<Stream> getStreams() {
		return mStreams;
	}


	public FileAttributes setName(String name) {
		mName = name;
		return this;
	}

	public FileAttributes setLongName(String longName) {
		mLongName = longName;
		return this;
	}

	public FileAttributes setFileName(String fileName) throws VCException {
		if (TextUtils.isEmpty(fileName)) {
			throw new VCException(Utils.getString(R.string.file_name_invalid));
		}
		mFileName = fileName;
		return this;
	}

	public FileAttributes setSize(Long size) throws VCException {
		if (size == null) throw new VCException(Utils.getString(R.string.file_size_invalid));
		mSize = size;
		return this;
	}

	public FileAttributes addStream(Stream stream) {
		// Combined streams to be able too look up by index
		mStreams.add(stream);
		if (stream instanceof AudioStream) {
			mAudioStreams.add((AudioStream) stream);
		} else {
			mVideoStreams.add((VideoStream) stream);
		}
		return this;
	}

	public FileAttributes setDuration(Double duration) throws VCException {
		if (duration == null) {
			throw new VCException(Utils.getString(R.string.file_duration_invalid));
		}
		mDuration = duration;
		return this;
	}

	@Override
	public String toString() {
		return String.format("FileAttributes FileName: %s, LongName: %s, Name: %s, Size: %d",
				getFileName(), getLongName(), getName(), getSize());
	}

	/* Parcelable */
	public void writeToParcel(Parcel out, int flags) {
		out.writeValue(mFileName);
		out.writeValue(mLongName);
		out.writeValue(mName);
		out.writeValue(mSize);
		out.writeList(mAudioStreams);
		out.writeList(mVideoStreams);
		out.writeList(mStreams);
	}

	public static final Parcelable.Creator<FileAttributes> CREATOR
			= new Parcelable.Creator<FileAttributes>() {
		public FileAttributes createFromParcel(Parcel in) {
			return new FileAttributes(in);
		}

		public FileAttributes[] newArray(int size) {
			return new FileAttributes[size];
		}
	};

	private FileAttributes(Parcel in) {
		mFileName = (String) in.readValue(String.class.getClassLoader());
		mLongName = (String) in.readValue(String.class.getClassLoader());
		mName = (String) in.readValue(String.class.getClassLoader());
		mSize = (Long) in.readValue(Long.class.getClassLoader());
		// Audio
		mAudioStreams = new ArrayList<>();
		in.readList(mAudioStreams, AudioStream.class.getClassLoader());
		// Video
		mVideoStreams = new ArrayList<>();
		in.readList(mVideoStreams, VideoStream.class.getClassLoader());
		// Combined, indexed
		mStreams = new ArrayList<>();
		in.readList(mStreams, Stream.class.getClassLoader());
	}

	public int describeContents() {
		return 0;
	}

} public class VideoStream extends Stream {

	public static final String ATTRIBUTE_WIDTH = "Width";
	public static final String ATTRIBUTE_HEIGHT = "Height";
	public static final String ATTRIBUTE_ASPECT_RATIO = "Aspect ratio";
	private static final String ATTRIBUTE_TBN = "TBN";
	private static final String ATTRIBUTE_TBR = "TBR";
	private static final String ATTRIBUTE_TBC = "TBC";
	private static final String INVALID_ASPECT_RATIO = "0:1";

	/**
	 * Values are displayed to the user in a specific order. This order is saved in an ArrayList.
	 * Index 0 will be displayed first.
	 */
	public static final List<String> ATTRIBUTE_PRIORITIES = new ArrayList<String>() {{
		addAll(Stream.ATTRIBUTE_PRIORITIES);
		add(ATTRIBUTE_WIDTH);
		add(ATTRIBUTE_HEIGHT);
		add(ATTRIBUTE_ASPECT_RATIO);
	}};

	public VideoStream(Integer width, Integer height, String codecName) throws VCException {
		super(codecName);
		// Add values
		setWidth(width);
		setHeight(height);
	}

	public Integer getWidth() {
		return (Integer) getValue(ATTRIBUTE_WIDTH);
	}

	public Integer getHeight() {
		return (Integer) getValue(ATTRIBUTE_HEIGHT);
	}

	public String getAspectRatio() {
		return (String) getValue(ATTRIBUTE_ASPECT_RATIO);
	}

	public String getTBN() {
		return (String) getValue(ATTRIBUTE_TBN);
	}

	public String getTBR() {
		return (String) getValue(ATTRIBUTE_TBR);
	}

	public String getTBC() {
		return (String) getValue(ATTRIBUTE_TBC);
	}

	public VideoStream setWidth(Integer width) throws VCException {
		if (width == null) {
			throw new VCException(Utils.getString(R.string.video_stream_width_invalid));
		}
		setValue(ATTRIBUTE_WIDTH, width);
		return this;
	}

	public VideoStream setHeight(Integer height) throws VCException {
		if (height == null) {
			throw new VCException(Utils.getString(R.string.video_stream_height_invalid));
		}
		setValue(ATTRIBUTE_HEIGHT, height);
		return this;
	}

	public VideoStream setAspectRatio(String aspectRatio) {
		// Ignore invalid aspect ration
		if (!INVALID_ASPECT_RATIO.equals(aspectRatio)) {
			setValue(ATTRIBUTE_ASPECT_RATIO, aspectRatio);
		}
		return this;
	}

	public VideoStream setTBN(String tbn) {
		setValue(ATTRIBUTE_TBN, tbn);
		return this;
	}

	public VideoStream setTBC(String tbc) {
		setValue(ATTRIBUTE_TBC, tbc);
		return this;
	}

	public VideoStream setTBR(String tbr) {
		setValue(ATTRIBUTE_TBR, tbr);
		return this;
	}

	@Override
	public List<String> getAttributePriorities() {
		return ATTRIBUTE_PRIORITIES;
	}

	@Override
	public String toString() {
		return String.format("%s\nVideoStream Width: %d, Height: %d, AspectRation: %s",
				super.toString(), getWidth(), getHeight(), getAspectRatio());
	}

} public class Utils {

	private static final String TAG = "Utils";
	public static final String VIDEO_MIME = "video";
	private static final int IO_BUFFER_SIZE = 32768;

	public static class FlagContainer {

		private int mFlags;

		public int getFlags() {
			return mFlags;
		}

		/**
		 * @return true if flags have been modified
		 */
		public boolean addFlag(int flags) {
			return setFlags(flags, flags);
		}

		/**
		 * @return true if flags have been modified
		 */
		public boolean removeFlag(int flags) {
			return setFlags(0, flags);
		}

		/**
		 * @return true if flags have been modified
		 */
		private boolean setFlags(int flags, int mask) {
			int oldFlags = mFlags;
			mFlags = (mFlags & ~mask) | (flags & mask);
			return mFlags != oldFlags;
		}

	}

	/**
	 * Pipe bytes. Will close the streams after it's done.
	 * @param is    InputStream, will be closed
	 * @param os    OutputStream, will be closed
	 * @throws IOException
	 */
	public static void copyBytes(InputStream is, OutputStream os) throws IOException {
		byte[] buffer = new byte[IO_BUFFER_SIZE];
		int count;
		while ((count = is.read(buffer)) != -1) {
			os.write(buffer, 0, count);
		}
		is.close();
		os.close();
	}

	/**
	 *
	 * @param is
	 * @param destinationFile    must already exist
	 * @throws IOException
	 */
	public static void copyBytes(InputStream is, File destinationFile) throws IOException {
		OutputStream os = new FileOutputStream(destinationFile);
		copyBytes(is, os);
	}

	/**
	 * Will trim the file if it already exists
	 * @param bytes                  Byte array to be written to given file
	 * @param destionationFile    absolute path to destination file
	 */
	public static void copyBytes(byte[] bytes, File destionationFile) throws IOException {
		InputStream is = new ByteArrayInputStream(bytes);
		OutputStream os = new FileOutputStream(destionationFile);
		copyBytes(is, os);
	}

	/**
	 * Will use {@code destinationPath/assetFileName} as the output file
	 * @param assetName          Asset name
	 * @param destinationDir     Absolute path to the output directory
	 */
	public static void copyAsset(String assetName, String destinationDir)
			throws IOException {
		AssetManager assetManager = VC.getAppContext().getAssets();
		InputStream is = assetManager.open(assetName);
		File destinationFile = new File(destinationDir + File.separator + assetName);
		if (!destinationFile.exists()) {
			// If destinationDir creation failed or it already exists
			// AND
			// Failed to create a new file or it already exists =>
			// Throw because previous condition said it doesn't exist
			if (!new File(destinationDir).mkdirs() && !destinationFile.createNewFile()) {
				throw new IOException("The destination file doesn't exist and couldn't be" +
						"created! " + destinationFile.getPath());
			}
		}
		copyBytes(is, destinationFile);
	}

	/**
	 * Blocking method that will read InputStream to a string and return it
	 * @param is    InputStream from which data will be read
	 * @return null or a String containing the data that was read
	 * @throws IOException
	 */
	public static String readStream(InputStream is) throws IOException {
		BufferedReader reader;
		String output = "", line;
		reader = new BufferedReader(new InputStreamReader(is));
		while ((line = reader.readLine()) != null) {
			output += line + '\n';
		}

		is.close();
		reader.close();

		return TextUtils.isEmpty(output) ? null : output;
	}

	/**
	 * Converts density-independent pixels to pixels
	 * @param dp         density-independent pixels
	 * @return pixels representing the given dp value
	 */
	public static float dpToPx(float dp){
		DisplayMetrics metrics = VC.getAppResources().getDisplayMetrics();
		return dp * (metrics.densityDpi / 160f);
	}

	/**
	 * Converts pixels to density-independent pixels
	 * @param px         pixels
	 * @return dp representing the given pixel value
	 */
	public static float pxToDp(float px){
		DisplayMetrics metrics = VC.getAppResources().getDisplayMetrics();
		return px / (metrics.densityDpi / 160f);
	}

	public static float pxToSp(float px) {
		DisplayMetrics metrics = VC.getAppResources().getDisplayMetrics();
		return px / metrics.scaledDensity;
	}

	public static float spToPx(float sp) {
		DisplayMetrics metrics = VC.getAppResources().getDisplayMetrics();
		return sp * metrics.scaledDensity;
	}

	/**
	 * Fetches the given drawable from resources, converts it to a Bitmap and returns.
	 * @param resId      resource id representing the drawable, i.e. R.drawable.my_drawable
	 * @return a bitmap converted from a resource drawable
	 */
	public static Bitmap bitmapFromRes(int resId) {
		return BitmapFactory.decodeResource(VC.getAppResources(), resId);
	}

	/**
	 * Convenience method to fetch a string from a {@code JSONObject} or return null if it's not
	 * found.
	 * @param obj    JSONObject to look in
	 * @param key    String representing the key to look for
	 * @return String for the given key or null if it wasn't found
	 */
	public static String getJSONString(JSONObject obj, String key) {
		try {
			return obj.getString(key);
		} catch (JSONException e) {
			Log.w(TAG, "Error fetching a String!", e);
			return null;
		}
	}

	/**
	 * Convenience method to fetch a int from a {@code JSONObject} or return null if it's not
	 * found.
	 * @param obj    JSONObject to look in
	 * @param key    String representing the key to look for
	 * @return int for the given key or null if it wasn't found
	 */
	public static Integer getJSONInteger(JSONObject obj, String key) {
		try {
			return obj.getInt(key);
		} catch (JSONException e) {
			Log.w(TAG, "Error fetching an int!", e);
			return null;
		}
	}

	/**
	 * Convenience method to fetch a double from a {@code JSONObject} or return null if it's not
	 * found.
	 * @param obj    JSONObject to look in
	 * @param key    String representing the key to look for
	 * @return double for the given key or null if it wasn't found
	 */
	public static Double getJSONDouble(JSONObject obj, String key) {
		try {
			return obj.getDouble(key);
		} catch (JSONException e) {
			Log.w(TAG, "Error fetching double!", e);
			return null;
		}
	}

	/**
	 * Convenience method to fetch a long from a {@code JSONObject} or return null if it's not
	 * found.
	 * @param obj    JSONObject to look in
	 * @param key    String representing the key to look for
	 * @return long for the given key or null if it wasn't found
	 */
	public static Long getLong(JSONObject obj, String key) {
		try {
			return obj.getLong(key);
		} catch (JSONException e) {
			Log.w(TAG, "Error fetching long!", e);
			return null;
		}
	}

	public static void runOnUiThread(Runnable runnable) {
		new Handler(Looper.getMainLooper()).post(runnable);
	}

	public static int getNavigationBarHeight() {
		Resources resources = VC.getAppContext().getResources();
		int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
		if (resourceId > 0) {
			return resources.getDimensionPixelSize(resourceId);
		}
		return 0;
	}

	/**
	 * Converts seconds to a time string. Format hh:mm:ss
	 * @return string in the format of hh:mm:ss, 00:00:00 if given seconds are negative
	 */
	public static String secsToTime(int secs) {
		if (secs < 0) return "00:00:00";
		int hours = secs / 3600;
		int minutes = (secs % 3600) / 60;
		int seconds = secs % 60;

		return String.format("%02d:%02d:%02d", hours, minutes, seconds);
	}


	/**
	 * Converts seconds to a time full time string. Format h hour(s) XX minute(s) XX second
	 * @return string in the format of hh:mm:ss, 00:00:00 if given seconds are negative
	 */
	public static String secsToFullTime(int secs) {
		if (secs < 0) return "0" + Utils.getString(R.string.second_short);
		int hours = secs / 3600;
		int minutes = (secs % 3600) / 60;
		int seconds = secs % 60;

		String output = "";
		if (hours > 0) {
			// If hours present, print minutes and seconds too
			output = String.format("%d%s %d%s ",
					hours, Utils.getString(R.string.hour_short),
					minutes, Utils.getString(R.string.minute_short));
		} else if (minutes > 0) {
			// If hours aren't present but minutes are, print them
			output = String.format("%d%s ", minutes, Utils.getString(R.string.minute_short));
		}
		// When not 0, seconds are always printed
		if (seconds > 0) {
			output += String.format("%d%s", seconds, Utils.getString(R.string.second_short));
		}

		return output;
	}

	public static Size getScreenSize() {
		DisplayMetrics metrics = VC.getAppResources().getDisplayMetrics();
		return new Size(metrics.widthPixels, metrics.heightPixels);
	}

	/**
	 * Convert bytes to megabytes with 2 digits after the decimal point and output as a string
	 */
	public static String bytesToMb(Long bytes) {
		if (bytes == null) bytes = 0L;
		double mb = bytes / 1024.0 / 1024.0;
		return String.format("%.2f %s", mb, Utils.getString(R.string.megabyte));
	}

	public static class Size {

		private int mWidth, mHeight;

		public Size(int width, int height) {
			mWidth = width;
			mHeight = height;
		}

		public int getWidth() {
			return mWidth;
		}

		public int getHeight() {
			return mHeight;
		}

	}

	/**
	 * Compares 2 {@code Object} references.
	 * @return false if the objects differ or either of them is {@code null}, otherwise true
	 */
	public static boolean equals(@Nullable Object obj1, @Nullable Object obj2) {
		if (obj1 == null || obj2 == null) return false;
		return obj1.equals(obj2);
	}

	public static Bitmap screenshot(View v) {
		v.setDrawingCacheEnabled(true);
		Bitmap b = Bitmap.createBitmap(v.getDrawingCache());
		v.setDrawingCacheEnabled(false);

		return b;
	}

	public static Bitmap screenshot2(View v) {
		Bitmap b = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(b);
		v.draw(canvas);

		return b;
	}

	/**
	 * Convenience method that uses the application context.
	 */
	public static String getString(int resId) {
		return VC.getAppContext().getString(resId);
	}

	public static String getString(int resId, Object... formatArgs) {
		return VC.getAppContext().getString(resId, formatArgs);
	}
} public class VC extends Application {

	private static Context mContext;

	static {
		System.loadLibrary("vc");
	}

	@Override
	public void onCreate() {
		super.onCreate();
		mContext = getApplicationContext();
	}

	public static Context getAppContext() {
		return mContext;
	}

	public static Resources getAppResources() {
		return getAppContext().getResources();
	}

} public abstract class Stream implements Parcelable {

	public static final String ATTRIBUTE_CODEC_NAME = "Cocec name";
	public static final String ATTRIBUTE_CODEC_LONG_NAME = "Codec long name";
	public static final String ATTRIBUTE_DURATION = "Duration";
	private static final String ATTRIBUTE_CODEC_TAG = "Codec tag";

	/**
	 * Values are displayed to the user in a specific order. This order is saved in an ArrayList.
	 * Index 0 will be displayed first.
	 */
	public static final List<String> ATTRIBUTE_PRIORITIES = new ArrayList<String>() {{
		add(ATTRIBUTE_CODEC_NAME);
		add(ATTRIBUTE_CODEC_LONG_NAME);
		add(ATTRIBUTE_DURATION);
	}};

	/**
	 * Attributes that are displayable to the user.
	 */
	public SerializableSparseArray<Object> attributes = new SerializableSparseArray<>();
	/**
	 * Attributes that are for programming purposes only.
	 */
	private HashMap<String, Object> mAttributes = new HashMap<>();

	protected Stream(String codecName) throws VCException {


		// Set the values
		setCodecName(codecName);
		setCodecTag(0);
	}

	public String getCodecName() {
		return (String) getValue(ATTRIBUTE_CODEC_NAME);
	}

	public String getCodecLongName() {
		return (String) getValue(ATTRIBUTE_CODEC_LONG_NAME);
	}

	public Double getDuration() {
		return (Double) getValue(ATTRIBUTE_DURATION);
	}

	public Integer getCodecTag() {
		return (Integer) mAttributes.get(ATTRIBUTE_CODEC_TAG);
	}

	public Stream setCodecName(String codecName) throws VCException {
		if (TextUtils.isEmpty(codecName)) {
			throw new VCException(Utils.getString(R.string.stream_no_codec));
		}
		setValue(ATTRIBUTE_CODEC_NAME, codecName);
		return this;
	}

	public Stream setCodecLongName(String codecLongName) {
		setValue(ATTRIBUTE_CODEC_LONG_NAME, codecLongName);
		return this;
	}

	public Stream setDuration(Double duration) {
		setValue(ATTRIBUTE_DURATION, duration);
		return this;
	}


	public Stream setCodecTag(Integer codecTag) {
		mAttributes.put(ATTRIBUTE_CODEC_TAG, codecTag);
		return this;
	}

	public List<String> getAttributePriorities() {
		return ATTRIBUTE_PRIORITIES;
	}

	/**
	 * Convenience method that decides whether the value should be put into a public {@code
	 * attributes} sparse array or the private {@code mAttributes} HashMap.
	 */
	protected void setValue(String key, Object value) {
		int keyIndex = getAttributePriorities().indexOf(key);
		if (keyIndex == -1) {
			mAttributes.put(key, value);
		} else {
			attributes.put(keyIndex, value);
		}
	}

	/**
	 * Determines whether the request item is located in the public {@code attributes} sparse array
	 * or the private {@code mAttributes} HashMap and returns it.
	 */
	protected Object getValue(String key) {
		int keyIndex = getAttributePriorities().indexOf(key);
		if (keyIndex == -1) {
			return mAttributes.get(key);
		} else {
			return attributes.get(keyIndex);
		}
	}

	@Override
	public String toString() {
		return String.format("CodecName: %s, CodecLongName: %s, Duration: %f",
				getCodecName(), getCodecLongName(), getDuration());
	}

	/* Parcelable */
	public void writeToParcel(Parcel out, int flags) {
		out.writeSerializable(attributes);
		out.writeSerializable(mAttributes);
	}

	public static final Parcelable.Creator<Stream> CREATOR = new Parcelable
			.Creator<Stream>() {
		public Stream createFromParcel(Parcel in) {
			try {
				Class<?> parceledClass = Class.forName(in.readString());
				return (Stream) parceledClass.newInstance();
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}

		public Stream[] newArray(int size) {
			return new Stream[size];
		}
	};

	protected Stream(Parcel in) {
		//noinspection unchecked
		attributes = (SerializableSparseArray<Object>) in.readSerializable();
		//noinspection unchecked
		mAttributes = (HashMap<String, Object>) in.readSerializable();
	}

	public int describeContents() {
		return 0;
	}

	public class SerializableSparseArray<E> extends SparseArray<E> implements Serializable {

		public SerializableSparseArray() {
			super();
		}

		private void writeObject(ObjectOutputStream oos) throws IOException {
			Object[] data = new  Object[size()];

			for (int i=data.length-1;i>=0;i--){
				Object[] pair = {keyAt(i),valueAt(i)};
				data[i] = pair;
			}
			oos.writeObject(data);
		}

		private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
			Object[] data = (Object[]) ois.readObject();
			for (Object aData : data) {
				Object[] pair = (Object[]) aData;
				//noinspection unchecked
				this.append((Integer) pair[0], (E) pair[1]);
			}
		}

	}

} public class FFmpeg {

	private static final String TAG = "ffmpeg";

	// C method prototypes
	public static native Bitmap createPreview(String videoPath);
	static native int cFFmpeg(String[] args);

	/**
	 *
	 * @param outputFile       output file (must already exist)
	 * @param items            items which will be concatenated
	 * @throws IOException An un-recoverable, internal error
	 * @throws VCException An error message to be printed out for the user
	 */
	public static void concat(@NonNull File outputFile, @NonNull List<NavItem> items)
			throws IOException, VCException {
		// Check source count
		if (items.size() < 2) {
			throw new VCException(Utils.getString(R.string.concat_at_least_2_videos));
		}

		// Fetch item validity and calculate total duration
		int duration = 0;
		for (NavItem item : items) {
			if (item.getState() == NavItem.State.VALID) {
				duration += item.getAttributes().getDuration();
			} else {
				throw new VCException(Utils.getString(R.string.concat_some_not_valid));
			}
		}

		// Prepare a tmp file for progress output
		File progressFile = File.createTempFile("vc-pg", null);

		boolean needsResizing = false, needsFiltering = false;
		VideoStream stream = null;
		// Loop items
		for (NavItem item : items) {
			VideoStream vs = item.getSelectedVideoStream();
			if (stream == null) {
				stream = vs;
			} else {
				if (streamsNeedResizing(stream, vs)) {
					needsResizing = true;
				} else if (!streamsConcatenateableByDemuxing(stream, vs)) {
					needsFiltering = true;
				}
			}
		}

		// Call the method based on the priorities. Resizing is the most important factor as it
		// will use a filter, other use a filter without resizing. The most basic concatenation
		// involves uses the concat demuxer.
		if (needsResizing) {
			concatFilterWithScaleAndPadding(outputFile, progressFile, items, duration);
		} else if (needsFiltering) {
			concatFilter(outputFile, progressFile, items, duration);
		} else {
			concatDemuxer(outputFile, progressFile, items, duration);
		}
	}

	private static void concatDemuxer(@NonNull File outputFile, @NonNull File progressFile,
	                                  @NonNull List<NavItem> items, int duration)
			throws IOException {
		/* Command
			./ffmpeg -y \
			-progress progress \
			-f concat \
			-auto_convert 1 \
			-i inputs \
			-codec copy \
			'output.mp4'
		 */

		// Prepare a tmp file with all video file names
		File tmpFile = File.createTempFile("vc-ls", null);
		String sourceList = "";
		for (NavItem item : items) {
			sourceList += String.format("file '%s'\n", item.getFile().getPath());
		}
		Utils.copyBytes(sourceList.getBytes(), tmpFile);

		// Prepare arguments
		String[] args = new ArgumentBuilder(TAG)
				.add("-y")                                  // Overwrite output file if it exists
				.add("-progress")                           // Output progress to tmp file
				.addSpaced("%s", progressFile.getPath())
				.add("-f concat")                           // Format concat
				.add("-auto_convert 1")                     // Convert packets to make streams concatenable
				.add("-i")                                  // Input files listed in tmpFile
				.addSpaced("%s", tmpFile.getPath())
				.add("-codec copy")                         // Copy source codecs
				.addSpaced("%s", outputFile.getPath())      // Output file
				.build();

		// Call service
		Context context = VC.getAppContext();
		Intent intent = new Intent(context, FFmpegService.class);
		intent.putExtra(FFmpegService.ARG_EXEC_ARGS, args);
		intent.putExtra(FFmpegService.ARG_OUTPUT_FILE, outputFile);
		intent.putExtra(FFmpegService.ARG_PROGRESS_FILE, progressFile);
		intent.putExtra(FFmpegService.ARG_OUTPUT_DURATION, duration);
		context.startService(intent);
	}

	private static void concatFilterWithScaleAndPadding(@NonNull File outputFile,
	                                                    @NonNull File progressFile,
	                                                    @NonNull List<NavItem> items, int duration)
			throws IOException {
		/* Command
			./ffmpeg -y \
			-progress progress \
			-i 'nature/goose.mp4' \
			-i 'nature/bee.mp4' \
			-filter_complex '\
			[0:0]scale=320:200[v1],[1:0]scale=320:200[v2],\
			[v1]pad=320:200:0:0[v1],[v2]pad=320:200:0:0[v2],\
			[v1][0:1][v2][1:1]concat=n=2:v=1:a=1[v][a]' \
			-map '[v]' -map '[a]' \
			'output.mp4'
		 */
		int itemCount = items.size();

		/* Output dimensions */
		int ow = 0;
		int oh = 0;
		// Use the biggest width and height from all items as output dimensions
		for (int i=0; i<itemCount; ++i) {
			NavItem item = items.get(i);
			VideoStream vs = item.getSelectedVideoStream();
			if (vs.getWidth() > ow) ow = vs.getWidth();
			if (vs.getHeight() > oh) oh = vs.getHeight();
		}

		String[] inputs = new String[itemCount*2];
		String scaleFilters = "", padFilters = "", streams = "";
		int audioStreamCount = 0, videoStreamCount = 0;
		for (int i=0; i<itemCount; ++i) {
			NavItem item = items.get(i);
			VideoStream vs = item.getSelectedVideoStream();
			/* Input dimensions */
			int iw = vs.getWidth();
			int ih = vs.getHeight();

			// Inputs
			inputs[i*2] = "-i";
			inputs[i*2 + 1] = item.getFile().getPath();

			// If width and height already match the output's, then only set the SAR and save stream
			if (iw == ow && ih == oh) {
				int selectedVideoStreamIndex = item.getSelectedVideoStreamIndex();
				if (selectedVideoStreamIndex >= 0) {
					streams += String.format("[%d:%d]", i, selectedVideoStreamIndex);
					++videoStreamCount;
				}
				int selectedAudioStreamIndex = item.getSelectedAudioStreamIndex();
				if (selectedAudioStreamIndex >= 0) {
					streams += String.format("[%d:%d]", i, selectedAudioStreamIndex);
					++audioStreamCount;
				}
			} else {
				/* Scale */
				// Use the bigger dimension and preserve the other
				int scaleW = -1, scaleH = -1;
				double modifier = ow / iw;
				if (ih * modifier > oh) {
					scaleH = oh;
				} else {
					scaleW = ow;
				}

				// Apply scale and padding filters to the selected video stream
				int selectedVideoStreamIndex = item.getSelectedVideoStreamIndex();
				if (selectedVideoStreamIndex >= 0) {
					// (,)[i:streamID]scale=w:h[vi]
					scaleFilters += String.format("%s[%d:%d]scale=%d:%d[v%d]",
							(scaleFilters.isEmpty()) ? "" : ',', i, selectedVideoStreamIndex,
							scaleW, scaleH, i);

					// Pad // (,)[vi]pad=w:h:topx:topy[vi]
					padFilters += String.format("%s[v%d]pad=%d:%d:(%d-iw)/2:(%d-ih)/2[v%d]",
							(padFilters.isEmpty()) ? "" : ',', i, ow, oh, ow, oh, i);

					// Video output identifier
					streams += String.format("[v%d]", i);
					++videoStreamCount;
				}

				// Specify the audio stream
				int selectedAudioStreamIndex = item.getSelectedAudioStreamIndex();
				if (selectedAudioStreamIndex >= 0) {
					streams += String.format("[%d:%d]", i, selectedAudioStreamIndex);
					++audioStreamCount;
				}
			}
		}

		// Add a separator after each filter
		if (!scaleFilters.isEmpty()) scaleFilters += ",";
		if (!padFilters.isEmpty()) padFilters += ",";

		// Prepare arguments
		String[] args = new ArgumentBuilder(TAG)
				.add("-y")                                  // Overwrite output file if it exists
				.add("-strict experimental")                // Use experimental decoders
				/* Output progress to tmp file */
				.add("-progress")
				.addSpaced("%s", progressFile.getPath())
				.addSpaced(inputs)                          // List of sources
				/* Filters (scale,pad,concat) */
				.add("-filter_complex")
				.add("%s%s%sconcat=n=%d:v=1:a=1[v][a]",
						scaleFilters, padFilters, streams, itemCount)
				.add("-map [v] -map [a]")
				.add("-strict experimental")                // Use experimental encoders
				.addSpaced("%s", outputFile.getPath())      // Output file
				.build();

		// Call service
		Context context = VC.getAppContext();
		Intent intent = new Intent(context, FFmpegService.class);
		intent.putExtra(FFmpegService.ARG_EXEC_ARGS, args);
		intent.putExtra(FFmpegService.ARG_OUTPUT_FILE, outputFile);
		intent.putExtra(FFmpegService.ARG_PROGRESS_FILE, progressFile);
		intent.putExtra(FFmpegService.ARG_OUTPUT_DURATION, duration);
		context.startService(intent);
	}

	/**
	 * This method expects the first stream is the video and the second one is audio. Ignores
	 * others.
	 */
	private static void concatFilter(@NonNull File outputFile, @NonNull File progressFile,
	                                  @NonNull List<NavItem> items, int duration)
			throws IOException {
		/* Command
			./ffmpeg -y \
			-progress progress \
			-i 'nature/goose.mp4' \
			-i 'nature/bee.mp4' \
			-filter_complex '[0:0] [0:1] [1:0] [1:1]  concat=n=2:v=1:a=1 [v] [a]' \
			-map '[v]' -map '[a]' \
			'output.mp4'
		 */

		int itemCount = items.size();
		String[] inputs = new String[itemCount*2];
		String streams = "";
		for (int i=0; i<itemCount; ++i) {
			NavItem item = items.get(i);
			inputs[i*2] = "-i";
			inputs[i*2 + 1] = item.getFile().getPath();

			int selectedVideoStreamIndex = item.getSelectedVideoStreamIndex();
			if (selectedVideoStreamIndex >= 0) {
				streams += String.format("[%d:%d]", i, selectedVideoStreamIndex);
			}
			int selectedAudioStreamIndex = item.getSelectedAudioStreamIndex();
			if (selectedAudioStreamIndex >= 0) {
				streams += String.format("[%d:%d]", i, selectedAudioStreamIndex);
			}
		}

		// Prepare arguments
		String[] args = new ArgumentBuilder(TAG)
				.add("-y")                                  // Overwrite output file if it exists
				.add("-strict experimental")                // Use experimental decoders
				.add("-progress")                           // Output progress to tmp file
				.addSpaced("%s", progressFile.getPath())
				.addSpaced(inputs)                          // List of sources
				/* Concat filter */
				.add("-filter_complex")
				.add("%sconcat=n=%d:v=1:a=1[v][a]", streams, itemCount)
				.add("-map [v] -map [a]")
				.add("-strict experimental")                // Use experimental encoders
				.addSpaced("%s", outputFile.getPath())      // Output file
				.build();

		// Call service
		Context context = VC.getAppContext();
		Intent intent = new Intent(context, FFmpegService.class);
		intent.putExtra(FFmpegService.ARG_EXEC_ARGS, args);
		intent.putExtra(FFmpegService.ARG_OUTPUT_FILE, outputFile);
		intent.putExtra(FFmpegService.ARG_PROGRESS_FILE, progressFile);
		intent.putExtra(FFmpegService.ARG_OUTPUT_DURATION, duration);
		context.startService(intent);
	}

	/**
	 * Assumes that {@code streamsNeedResizing} returns false for these streams.
	 * Check if the required attributes match for both {@code VideoAttributes}. The required attributes are:
	 * <ul>
	 *     <li>
	 *         Width
	 *     </li>
	 *     <li>
	 *         Height
	 *     </li>
	 *     <li>
	 *         Codec tag
	 *     </li>
	 *     <li>
	 *         TBN
	 *     </li>
	 *     <li>
	 *         TBC
	 *     </li>
	 *     <li>
	 *         TBR
	 *     </li>
	 * </ul>
	 * @return true if the required field set matches
	 */
	public static boolean streamsConcatenateableByDemuxing(VideoStream va1, VideoStream va2) {
		return Utils.equals(va1.getCodecTag(), va2.getCodecTag()) &&
				Utils.equals(va1.getTBN(), va2.getTBN()) &&
				Utils.equals(va1.getTBC(), va2.getTBC()) &&
				Utils.equals(va1.getTBR(), va2.getTBR());
	}

	/**
	 * Checks whether 2 {@code VideoStream}s need resizing.
	 * @return true if the 2 streams have different widths and/or heights
	 */
	public static boolean streamsNeedResizing(VideoStream va1, VideoStream va2) {
		return !Utils.equals(va1.getWidth(), va2.getWidth()) ||
				!Utils.equals(va1.getHeight(), va2.getHeight());
	}

} public class FFprobe {

	private static final String TAG = "ffprobe";
	private static final List<String> INVALID_CODEC_NAMES = new ArrayList<String>() {
		{
			add("mjpeg");
		}
	};

	private static native int cFFprobe(String[] args, String outputPath);

	/**
	 * This operation is synchronous and cannot be run on the UI thread.
	 */
	@SuppressWarnings("ResultOfMethodCallIgnored")
	public synchronized static FileAttributes parseAttributes(File inputFile) throws VCException {
		if (Looper.myLooper() == Looper.getMainLooper()) {
			throw new IllegalStateException("parseAttributes cannot be run on the UI thread!");
		}

		/* Executable call used
			./ffprobe -i 'nature/bee.mp4' \
			-v quiet -print_format json \
			-show_format -show_entries format=duration,size,format_name,format_long_name,filename,nb_streams \
			-show_streams -show_entries stream=codec_name,codec_long_name,codec_type,sample_rate,channels,duration,display_aspect_ratio,width,height,time_base,codec_time_base,r_frame_rate
		*/

		// Check if input exists
		if (!inputFile.exists()) {
			throw new VCException(Utils.getString(R.string.input_not_found));
		}

		// Create a temporary file to hold the stdout output
		File tmpFile;
		try {
			tmpFile = File.createTempFile("vc-out", null);
			tmpFile.delete();
			tmpFile.createNewFile();
		} catch (IOException e) {
			e.printStackTrace();
			throw new VCException(Utils.getString(R.string.tmp_not_created));
		}

		// Create arguments for ffprobe
		final String[] args = new ArgumentBuilder(TAG)
				.add("-i")
				.addSpaced("%s", inputFile.getPath())   // Spaced input file path
				.add("-v quiet -print_format json")     // Output quietly in JSON
						// Format entries to show
				.add("-show_format -show_entries format=%s,%s,%s,%s,%s,%s",
						Utils.getString(R.string.format_duration),
						Utils.getString(R.string.format_size),
						Utils.getString(R.string.format_name),
						Utils.getString(R.string.format_long_name),
						Utils.getString(R.string.format_filename),
						Utils.getString(R.string.format_stream_count))
						// Stream entries to show
				.add("-show_streams -show_entries stream=%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
						Utils.getString(R.string.stream_name),
						Utils.getString(R.string.stream_long_name),
						Utils.getString(R.string.stream_type),
						Utils.getString(R.string.stream_sample_rate),
						Utils.getString(R.string.stream_channels),
						Utils.getString(R.string.stream_duration),
						Utils.getString(R.string.stream_aspect_ratio),
						Utils.getString(R.string.stream_width),
						Utils.getString(R.string.stream_height),
						Utils.getString(R.string.stream_tbn),
						Utils.getString(R.string.stream_tbc),
						Utils.getString(R.string.stream_tbr),
						Utils.getString(R.string.stream_codec_tag))
				.build();

		if (cFFprobe(args, tmpFile.getPath()) != 0) {
			throw new VCException(Utils.getString(R.string.ffprobe_fail));
		}

		BufferedReader reader = null;
		FileAttributes fa = null;
		try {
			// Parse file
			reader = new BufferedReader(new FileReader(tmpFile));
			StringBuilder sb = new StringBuilder();
			String line = reader.readLine();

			while (line != null) {
				sb.append(line);
				sb.append('\n');
				line = reader.readLine();
			}
			String content = sb.toString();

			int firstOpeningBrace = content.indexOf('{');
			int lastClosingBrace = content.lastIndexOf('}');
			if (firstOpeningBrace == -1 || lastClosingBrace == -1) {
				return null;
			}
			String json = content.substring(firstOpeningBrace, lastClosingBrace+1);

			// Parse JSON
			fa = parseJsonAttributes(json);
			Log.i(TAG, "Parsed attributes: " + fa);
		} catch (IOException e) {
			e.printStackTrace();
			throw new VCException(Utils.getString(R.string.ffprobe_fail));
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		// Delete the tmp file
		tmpFile.delete();

		// No support for files with no audio/video streams (for now?)
		if (fa != null) {
			if (fa.getAudioStreams().size() == 0) {
				throw new VCException(Utils.getString(R.string.audioless_unsupported));
			} else if (fa.getVideoStreams().size() == 0) {
				throw new VCException(Utils.getString(R.string.videoless_unsupported));
			}
		}

		return fa;
	}

	private static FileAttributes parseJsonAttributes(@NonNull String json)
			throws IOException, VCException {
		Log.d(TAG, json);
		// Parse JSON
		List<JSONObject> jStream = new ArrayList<>();
		JSONObject format;
		try {
			JSONObject obj = new JSONObject(json);
			// Fetch streams
			JSONArray streamArr = obj.getJSONArray("streams");
			int streamCount = streamArr.length();
			for (int i=0; i<streamCount; ++i) {
				jStream.add(streamArr.getJSONObject(i));
			}
			// Fetch format
			format = obj.getJSONObject("format");
		} catch (JSONException e) {
			e.printStackTrace();
			return null;
		}

		// Format
		FileAttributes fa = new FileAttributes(
				getJSONString(format, Utils.getString(R.string.format_filename)),
				getLong(format, Utils.getString(R.string.format_size)),
				getJSONDouble(format, Utils.getString(R.string.format_duration)));
		fa.setName(getJSONString(format, Utils.getString(R.string.format_name)))
				.setLongName(getJSONString(format, Utils.getString(R.string.format_long_name)));

		// Streams
		for (JSONObject jsonObj : jStream) {
			StreamType streamType = getStreamType(jsonObj);
			if (streamType == null) continue;

			// Stream
			Stream stream;

			// Codec name
			String codecName = getJSONString(jsonObj, Utils.getString(R.string.stream_name));
			if (codecName == null || INVALID_CODEC_NAMES.contains(codecName.toLowerCase())) {
				Log.w(TAG, "Skipped invalid codec: " + codecName);
				continue;
			}

			switch (streamType) {
				case AUDIO:
					stream = new AudioStream(codecName)
							.setChannelCount(getJSONInteger(jsonObj,
									Utils.getString(R.string.stream_channels)))
							.setSampleRate(getJSONInteger(jsonObj,
									Utils.getString(R.string.stream_sample_rate)));
					break;
				case VIDEO:
					// Create VideoStream
					Integer w = getJSONInteger(jsonObj, Utils.getString(R.string.stream_width));
					Integer h = getJSONInteger(jsonObj, Utils.getString(R.string.stream_height));

					stream = new VideoStream(w, h, codecName)
							.setAspectRatio(getJSONString(jsonObj,
									Utils.getString(R.string.stream_aspect_ratio)))
							.setTBN(getJSONString(jsonObj, Utils.getString(R.string.stream_tbn)))
							.setTBC(getJSONString(jsonObj, Utils.getString(R.string.stream_tbc)))
							.setTBR(getJSONString(jsonObj, Utils.getString(R.string.stream_tbr)));
					break;
				default:
					continue;
			}

			/* Shared stream attributes */
			// Codec name
			stream.setCodecName(codecName);

			// Stream duration
			Double duration = getJSONDouble(jsonObj, Utils.getString(R.string.stream_duration));

			// Codec tag (default is 0)
			String tag = getJSONString(jsonObj, Utils.getString(R.string.stream_codec_tag));
			try {
				stream.setCodecTag(Integer.decode(tag));
			} catch (NumberFormatException e) {
				e.printStackTrace();
//				throw new VCException("Unrecognized codec found!");
			}

			// Codec long name
			stream.setCodecLongName(getJSONString(jsonObj,
					Utils.getString(R.string.stream_long_name)));

			// Append to FileAttributes
			fa.addStream(stream);
		}

		return fa;
	}

	private static StreamType getStreamType(JSONObject obj) {
		String attributeType = getJSONString(obj, Utils.getString(R.string.stream_type));
		if (attributeType != null) {
			StreamType streamType;
			try {
				return StreamType.valueOf(attributeType.toUpperCase());
			} catch (Exception e) {
				Log.w(TAG, "Unrecognized attribute type: " + attributeType);
			}
		}
		return null;
	}

	enum StreamType {
		AUDIO, VIDEO
	}

}
import android.app.AlertDialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.Log;
import com.simas.vc.MainActivity;
import com.simas.vc.VCException;
import com.simas.vc.attributes.AudioStream;
import com.simas.vc.attributes.FileAttributes;
import com.simas.vc.R;
import com.simas.vc.attributes.VideoStream;
import com.simas.vc.helpers.Utils;
import com.simas.vc.VC;
import com.simas.vc.attributes.Stream;
import com.simas.vc.background_tasks.FFmpeg;
import com.simas.vc.background_tasks.FFprobe;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
					final VideoStream oldStream = mSelectedVideoStream;
					mSelectedVideoStream = stream;

					// Announce to listeners
					for (OnUpdatedListener listener : mUpdateListeners) {
						listener.onUpdated(ItemAttribute.VIDEO_STREAM, oldStream, stream);
					}
				}
				return;
			}
			// If wasn't found throw
			throw new IllegalStateException("No streams have been selected! Are you sure you're " +
					"passing a stream that belongs to the attributes of this item?");
		}
	}

	public AudioStream getSelectedAudioStream() {
		return mSelectedAudioStream;
	}

	public VideoStream getSelectedVideoStream() {
		return mSelectedVideoStream;
	}

	/**
	 * @return -1 if there is no selected audio stream.
	 * @throws IllegalStateException if the selected stream cannot be found in file
	 */
	public int getSelectedAudioStreamIndex() {
		if (getSelectedAudioStream() != null) {
			int index = getAttributes().getStreams().indexOf(getSelectedAudioStream());
			if (index < 0) {
				throw new IllegalStateException("The selected audio stream wasn't found!");
			}

			return index;
		} else {
			return -1;
		}
	}

	/**
	 * @return -1 if there is no selected video stream.
	 * @throws IllegalStateException if the selected stream cannot be found in file
	 */
	public int getSelectedVideoStreamIndex() {
		if (getSelectedVideoStream() != null) {
			int index = getAttributes().getStreams().indexOf(getSelectedVideoStream());
			if (index < 0) {
				throw new IllegalStateException("The selected video stream wasn't found!");
			}

			return index;
		} else {
			return -1;
		}
	}

	/**
	 * Fetches the error message that occurred and made the item enter an {@link State#INVALID}
	 * state. Only a single call is permitted, subsequent calls will return null.
	 * @return null if no such exception had occurred or if the exception was already fetched
	 */
	public String getError() {
		return mError;
	}

	/**
	 * {@code mFile} must be set before this is called.
	 */
	private void parseItem() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				// Attributes
				FileAttributes attributes = null;
				try {
					attributes = FFprobe.parseAttributes(getFile());
				} catch (final VCException e) {
					Log.e(TAG, "Attribute parse error:", e);
					mError = e.getMessage();
				}
				if (attributes == null) {
					setState(State.INVALID);
					return;
				}
				setAttributes(attributes);
				// By default, select first audio and first video streams (if they exist)
				if (attributes.getAudioStreams().size() > 0) {
					setSelectedAudioStream(attributes.getAudioStreams().get(0));
				}
				if (attributes.getVideoStreams().size() > 0) {
					setSelectedVideoStream(attributes.getVideoStreams().get(0));
				}

				// Preview
				final Bitmap preview = parsePreview();
				if (preview == null) {
					setState(State.INVALID);
				} else {
					setState(State.VALID);
					// Update the preview on the UI thread
					Utils.runOnUiThread(new Runnable() {
						@Override
						public void run() {
							setPreview(preview);
						}
					});
				}
			}
		}).start();
	}

	/**
	 * Blocking method that fetches and scales the preview image.<br/>
	 * <b>Note:</b> if this item is a duplicate of another one (file paths match), then existing
	 * preview is used.
	 */
	private Bitmap parsePreview() {
		// Loop existing items
		for (NavItem item : MainActivity.sItems) {