package tim.prune.correlate;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.cmd.PointAndMedia;
import tim.prune.config.TimezoneHelper;
import tim.prune.data.DataPoint;
import tim.prune.data.Distance;
import tim.prune.data.Field;
import tim.prune.data.MediaList;
import tim.prune.data.MediaObject;
import tim.prune.data.TimeDifference;
import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.Unit;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.Describer;
import tim.prune.tips.TipManager;

/**
 * Abstract superclass of the two correlator functions
 */
public abstract class Correlator extends GenericFunction
{
	protected JDialog _dialog;
	private CardStack _cards = null;
	private JTable _selectionTable = null;
	protected JTable _previewTable = null;
	private boolean _previewEnabled = false; // flag required to enable preview function on final panel
	private boolean[] _cardEnabled = null; // flag for each card
	private TimeZone _timezone = null;
	private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
	private JRadioButton _mediaLaterOption = null, _pointLaterOption = null;
	private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
	private JTextField _limitMinBox = null, _limitSecBox = null;
	private JTextField _limitDistBox = null;
	private JComboBox<String> _distUnitsDropdown = null;
	private JButton _nextButton = null, _backButton = null;
	protected JButton _okButton = null;


	/**
	 * Constructor
	 * @param inApp App object to report actions to
	 */
	public Correlator(App inApp) {
		super(inApp);
	}

	/**
	 * @return type key eg photo, audio
	 */
	protected abstract String getMediaTypeKey();

	/**
	 * @return media list
	 */
	protected abstract MediaList<?> getMediaList();

	/**
	 * Begin the function by initialising and showing the dialog
	 */
	public void begin()
	{
		// Check whether track has timestamps, exit if not
		if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
				getName(), JOptionPane.INFORMATION_MESSAGE);
			return;
		}
		// Show warning if no uncorrelated audios
		if (!getMediaList().hasUncorrelatedMedia())
		{
			Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
			if (JOptionPane.showOptionDialog(_parentFrame,
					I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"),
					getName(), JOptionPane.YES_NO_OPTION,
					JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
				== JOptionPane.NO_OPTION)
			{
				return;
			}
		}
		// Create dialog if necessary
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, getName(), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.getContentPane().add(makeDialogContents());
			_dialog.pack();
		}
		_okButton.setEnabled(false);
		// Init timezone to the currently selected one
		_timezone = TimezoneHelper.getSelectedTimezone(getConfig());
		// Go to first available card
		int card = 0;
		_cardEnabled = null;
		while (!isCardEnabled(card)) {
			card++;
		}
		_cards.showCard(card);
		showCard(0); // does set up and next/prev enabling
		if (!isCardEnabled(1)) {
			_app.showTip(TipManager.Tip_ManuallyCorrelateOne);
		}
		_dialog.setVisible(true);
	}

	/**
	 * Make contents of correlate dialog
	 * @return JPanel containing gui elements
	 */
	private JPanel makeDialogContents()
	{
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BorderLayout());

		// Card panel in the middle
		_cards = new CardStack();

		// First panel (not required by photo correlator)
		JPanel card1 = makeFirstPanel();
		if (card1 == null) {card1 = new JPanel();}
		_cards.addCard(card1);

		// Second panel for selection of linked media
		_cards.addCard(makeSecondPanel());

		// Third panel for options and preview
		_cards.addCard(makeThirdPanel());
		mainPanel.add(_cards, BorderLayout.CENTER);

		// Button panel at the bottom
		JPanel buttonPanel = new JPanel();
		_backButton = new JButton(I18nManager.getText("button.back"));
		_backButton.addActionListener(e -> showCard(-1));
		_backButton.setEnabled(false);
		buttonPanel.add(_backButton);
		_nextButton = new JButton(I18nManager.getText("button.next"));
		_nextButton.addActionListener(e -> showCard(1));
		buttonPanel.add(_nextButton);
		_okButton = new JButton(I18nManager.getText("button.ok"));
		_okButton.addActionListener(e -> {
			finishCorrelation();
			_dialog.dispose();
		});
		_okButton.setEnabled(false);
		buttonPanel.add(_okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(e -> _dialog.dispose());
		buttonPanel.add(cancelButton);
		mainPanel.add(buttonPanel, BorderLayout.SOUTH);
		return mainPanel;
	}

	/**
	 * Construct a table model for the photo / audio selection table
	 * @return table model
	 */
	protected MediaSelectionTableModel makeSelectionTableModel()
	{
		MediaList<?> mediaList = getMediaList();
		MediaSelectionTableModel model = new MediaSelectionTableModel(
			"dialog.correlate.select." + getMediaTypeKey() + "name",
			"dialog.correlate.select." + getMediaTypeKey() + "later",
			getTimezone());
		int numMedia = mediaList.getCount();
		for (int i=0; i<numMedia; i++)
		{
			MediaObject media = mediaList.get(i);
			// For working out time differences, can't use media which already had point information
			if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp()
				&& media.getOriginalStatus() == MediaObject.Status.NOT_CONNECTED)
			{
				// Calculate time difference, add to table model
				long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp(), _timezone);
				model.addMedia(media, timeDiff);
			}
		}
		return model;
	}

	/**
	 * Group the two radio buttons together with a ButtonGroup
	 * @param inButton1 first radio button
	 * @param inButton2 second radio button
	 */
	protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
	{
		ButtonGroup buttonGroup = new ButtonGroup();
		buttonGroup.add(inButton1);
		buttonGroup.add(inButton2);
		inButton1.setSelected(true);
	}


	/**
	 * Try to parse the given string
	 * @param inText String to parse
	 * @return value if parseable, 0 otherwise
	 */
	protected static int getValue(String inText)
	{
		int value = 0;
		try {
			value = Integer.parseInt(inText);
		}
		catch (NumberFormatException nfe) {}
		return value;
	}


	/**
	 * Calculate the median index to select from the table
	 * @param inModel table model
	 * @return index of entry to select from table
	 */
	protected static int getMedianIndex(MediaSelectionTableModel inModel)
	{
		// make sortable list
		TreeSet<TimeIndexPair> set = new TreeSet<>();
		// loop through rows of table adding to list
		int numRows = inModel.getRowCount();
		for (int i=0; i<numRows; i++)
		{
			MediaSelectionTableRow row = inModel.getRow(i);
			set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
		}
		// pull out middle entry and return index
		TimeIndexPair pair = null;
		Iterator<TimeIndexPair> iterator = set.iterator();
		for (int i=0; i<(numRows+1)/2; i++) {
			pair = iterator.next();
		}
		return pair.getIndex();
	}


	/**
	 * Disable the ok button
	 */
	public void disableOkButton()
	{
		if (_okButton != null) {
			_okButton.setEnabled(false);
		}
	}

	/**
	 * @return gui components for first panel, or null if empty
	 */
	protected JPanel makeFirstPanel() {
		return null;
	}

	/**
	 * Make the second panel for the selection screen
	 * @return JPanel object containing gui elements
	 */
	private JPanel makeSecondPanel()
	{
		JPanel card = new JPanel();
		card.setLayout(new BorderLayout(10, 10));
		JLabel introLabel = new JLabel(I18nManager.getText(
			"dialog.correlate." + getMediaTypeKey() + "select.intro"));
		introLabel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
		card.add(introLabel, BorderLayout.NORTH);
		// table doesn't have model yet - that will be attached later
		_selectionTable = new JTable();
		JScrollPane photoScrollPane = new JScrollPane(_selectionTable);
		photoScrollPane.setPreferredSize(new Dimension(400, 100));
		card.add(photoScrollPane, BorderLayout.CENTER);
		return card;
	}


	/**
	 * Make contents of third panel including options and preview
	 * @return JPanel containing gui elements
	 */
	private JPanel makeThirdPanel()
	{
		OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
		// Second panel for options
		JPanel card2 = new JPanel();
		card2.setLayout(new BorderLayout());
		JPanel card2Top = new JPanel();
		card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
		JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
		introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
		card2Top.add(introLabel);
		// time offset section
		JPanel offsetPanel = new JPanel();
		offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
		offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
		JPanel offsetPanelTop = new JPanel();
		offsetPanelTop.setLayout(new FlowLayout());
		offsetPanelTop.setBorder(null);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
		_offsetHourBox = new JTextField(3);
		_offsetHourBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetHourBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
		_offsetMinBox = new JTextField(3);
		_offsetMinBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetMinBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
		_offsetSecBox = new JTextField(3);
		_offsetSecBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetSecBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
		offsetPanel.add(offsetPanelTop);

		// radio buttons for photo / point later
		JPanel offsetPanelBot = new JPanel();
		offsetPanelBot.setLayout(new FlowLayout());
		offsetPanelBot.setBorder(null);
		_mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later"));
		_pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater" + getMediaTypeKey()));
		_mediaLaterOption.addItemListener(optionsChangedListener);
		_pointLaterOption.addItemListener(optionsChangedListener);
		ButtonGroup laterGroup = new ButtonGroup();
		laterGroup.add(_mediaLaterOption);
		laterGroup.add(_pointLaterOption);
		offsetPanelBot.add(_mediaLaterOption);
		offsetPanelBot.add(_pointLaterOption);
		offsetPanel.add(offsetPanelBot);
		offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		card2Top.add(offsetPanel);

		// listener for radio buttons
		ActionListener radioListener = e -> enableEditBoxes();
		// time limits section
		JPanel limitsPanel = new JPanel();
		limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
		limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
		JPanel timeLimitPanel = new JPanel();
		timeLimitPanel.setLayout(new FlowLayout());
		JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
		noTimeLimitRadio.addItemListener(optionsChangedListener);
		noTimeLimitRadio.addActionListener(radioListener);
		timeLimitPanel.add(noTimeLimitRadio);
		_timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + ": ");
		_timeLimitRadio.addItemListener(optionsChangedListener);
		_timeLimitRadio.addActionListener(radioListener);
		timeLimitPanel.add(_timeLimitRadio);
		groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
		_limitMinBox = new JTextField(3);
		_limitMinBox.addKeyListener(optionsChangedListener);
		timeLimitPanel.add(_limitMinBox);
		timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
		_limitSecBox = new JTextField(3);
		_limitSecBox.addKeyListener(optionsChangedListener);
		timeLimitPanel.add(_limitSecBox);
		timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
		limitsPanel.add(timeLimitPanel);
		// distance limits
		JPanel distLimitPanel = new JPanel();
		distLimitPanel.setLayout(new FlowLayout());
		JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
		noDistLimitRadio.addItemListener(optionsChangedListener);
		noDistLimitRadio.addActionListener(radioListener);
		distLimitPanel.add(noDistLimitRadio);
		_distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit") + ": ");
		_distLimitRadio.addItemListener(optionsChangedListener);
		_distLimitRadio.addActionListener(radioListener);
		distLimitPanel.add(_distLimitRadio);
		groupRadioButtons(noDistLimitRadio, _distLimitRadio);
		_limitDistBox = new JTextField(4);
		_limitDistBox.addKeyListener(optionsChangedListener);
		distLimitPanel.add(_limitDistBox);
		String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
			I18nManager.getText("units.miles")};
		_distUnitsDropdown = new JComboBox<>(distUnitsOptions);
		_distUnitsDropdown.addItemListener(optionsChangedListener);
		distLimitPanel.add(_distUnitsDropdown);
		limitsPanel.add(distLimitPanel);
		limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		card2Top.add(limitsPanel);

		// preview button
		JButton previewButton = new JButton(I18nManager.getText("button.preview"));
		previewButton.addActionListener(e -> createPreview(true));
		card2Top.add(previewButton);
		card2.add(card2Top, BorderLayout.NORTH);
		// preview
		_previewTable = new JTable(new MediaPreviewTableModel("dialog.correlate.select." + getMediaTypeKey() + "name", getTimezone()));
		JScrollPane previewScrollPane = new JScrollPane(_previewTable);
		previewScrollPane.setPreferredSize(new Dimension(300, 100));
		card2.add(previewScrollPane, BorderLayout.CENTER);
		return card2;
	}

	/** @return the selected timezone */
	protected TimeZone getTimezone() {
		return TimezoneHelper.getSelectedTimezone(getConfig());
	}

	/**
	 * Go to the next or previous card in the stack
	 * @param increment 1 for next, -1 for previous card
	 */
	private void showCard(int increment)
	{
		int currCard = _cards.getCurrentCardIndex();
		int next = currCard + increment;
		if (!isCardEnabled(next)) {
			next += increment;
		}
		setupCard(next);
		_backButton.setEnabled(next > 0 && (isCardEnabled(next-1) || isCardEnabled(next-2)));
		_nextButton.setEnabled(next < (_cards.getNumCards()-1));
		_cards.showCard(next);
	}

	/**
	 * @param inCardNum index of card
	 * @return true if specified card is enabled
	 */
	private boolean isCardEnabled(int inCardNum)
	{
		if (_cardEnabled == null) {
			_cardEnabled = getCardEnabledFlags();
		}
		return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]);
	}

	/**
	 * @return array of boolean flags denoting availability of cards
	 */
	protected boolean[] getCardEnabledFlags()
	{
		// by default first is off and third is always on; second depends on selection table
		return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true};
	}

	/**
	 * Set up the specified card
	 * @param inCardNum index of card
	 */
	protected void setupCard(int inCardNum)
	{
		_previewEnabled = false;
		if (inCardNum == 1)
		{
			// set up photo selection card
			MediaSelectionTableModel model = makeSelectionTableModel();
			_selectionTable.setModel(model);
			for (int i=0; i<model.getColumnCount(); i++) {
				_selectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
			}
			// Calculate median time difference, select corresponding row of table
			int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
			_selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
			_nextButton.requestFocus();
		}
		else if (inCardNum == 2)
		{
			// set up the options/preview card - first check for given time difference
			TimeDifference timeDiff = null;
			if (isCardEnabled(1))
			{
				int rowNum = _selectionTable.getSelectedRow();
				if (rowNum < 0) {rowNum = 0;}
				MediaSelectionTableRow selectedRow =
					((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum);
				timeDiff = selectedRow.getTimeDiff();
			}
			setupPreviewCard(timeDiff, getMediaList().get(0));
		}
		// enable ok button if any photos have been selected
		_okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected());
	}

	/**
	 * Enable or disable the edit boxes according to the radio button selections
	 */
	private void enableEditBoxes()
	{
		// enable/disable text field for distance input
		_limitDistBox.setEnabled(_distLimitRadio.isSelected());
		// and for time limits
		_limitMinBox.setEnabled(_timeLimitRadio.isSelected());
		_limitSecBox.setEnabled(_timeLimitRadio.isSelected());
	}

	/**
	 * Parse the time limit values entered and validate them
	 * @return TimeDifference object describing limit
	 */
	protected TimeDifference parseTimeLimit()
	{
		if (!_timeLimitRadio.isSelected()) {return null;}
		int mins = getValue(_limitMinBox.getText());
		_limitMinBox.setText("" + mins);
		int secs = getValue(_limitSecBox.getText());
		_limitSecBox.setText("" + secs);
		if (mins <= 0 && secs <= 0) {return null;}
		return new TimeDifference(0, mins, secs, true);
	}

	/**
	 * Parse the distance limit value entered and validate
	 * @return angular distance in radians
	 */
	protected double parseDistanceLimit()
	{
		double value = -1.0;
		if (_distLimitRadio.isSelected())
		{
			try {
				value = Double.parseDouble(_limitDistBox.getText());
			}
			catch (NumberFormatException nfe) {}
		}
		if (value <= 0.0) {
			_limitDistBox.setText("0");
			return -1.0;
		}
		_limitDistBox.setText("" + value);
		return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
	}


	/**
	 * @return the selected distance units from the dropdown
	 */
	protected Unit getSelectedDistanceUnits()
	{
		final Unit[] distUnits = {UnitSetLibrary.UNITS_KILOMETRES, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_MILES};
		return distUnits[_distUnitsDropdown.getSelectedIndex()];
	}

	/**
	 * Create a preview of the correlate action using the selected time difference
	 * @param inFromButton true if triggered from button press, false if automatic
	 */
	public void createPreview(boolean inFromButton)
	{
		// Exit if still on first panel
		if (!_previewEnabled) {
			return;
		}
		// Create a TimeDifference based on the edit boxes
		int numHours = getValue(_offsetHourBox.getText());
		int numMins = getValue(_offsetMinBox.getText());
		int numSecs = getValue(_offsetSecBox.getText());
		boolean isPos = _mediaLaterOption.isSelected();
		createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
	}

	/**
	 * Set up the final card using the given time difference and show it
	 * @param inTimeDiff time difference to use for time offsets
	 * @param inFirstMedia first media object to use for calculating timezone
	 */
	protected void setupPreviewCard(TimeDifference inTimeDiff, MediaObject inFirstMedia)
	{
		_previewEnabled = false;
		TimeDifference timeDiff = inTimeDiff;
		if (timeDiff == null)
		{
			// No time difference available, so try with zero
			timeDiff = new TimeDifference(0L);
		}
		// Use time difference to set edit boxes
		_offsetHourBox.setText("" + timeDiff.getNumHours());
		_offsetMinBox.setText("" + timeDiff.getNumMinutes());
		_offsetSecBox.setText("" + timeDiff.getNumSeconds());
		_mediaLaterOption.setSelected(timeDiff.getIsPositive());
		_pointLaterOption.setSelected(!timeDiff.getIsPositive());
		_previewEnabled = true;
		enableEditBoxes();
		createPreview(timeDiff, true);
	}

	/**
	 * Create a preview of the correlate action using the selected time difference
	 * @param inTimeDiff TimeDifference to use for preview
	 * @param inShowWarning true to show warning if all points out of range
	 */
	protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning);


	/**
	 * Get the timestamp of the given media
	 * @param inMedia media object
	 * @return normally just returns the media timestamp, overridden by audio correlator
	 */
	protected Timestamp getMediaTimestamp(MediaObject inMedia)
	{
		return inMedia.getTimestamp();
	}

	/**
	 * Get the point pair surrounding the given media item
	 * @param inTrack track object
	 * @param inMedia media object
	 * @param inOffset time offset to apply
	 * @return point pair resulting from correlation
	 */
	protected PointMediaPair getPointPairForMedia(Track inTrack, MediaObject inMedia, TimeDifference inOffset)
	{
		PointMediaPair pair = new PointMediaPair(inMedia);
		if (inMedia.hasTimestamp())
		{
			// Add/subtract offset to media timestamp
			Timestamp mediaStamp = getMediaTimestamp(inMedia);
			int numPoints = inTrack.getNumPoints();
			for (int i=0; i<numPoints; i++)
			{
				DataPoint point = inTrack.getPoint(i);
				if (point.getPhoto() == null && point.getAudio() == null)
				{
					Timestamp pointStamp = point.getTimestamp();
					if (pointStamp != null && pointStamp.isValid())
					{
						long numSeconds = pointStamp.getSecondsSince(mediaStamp, _timezone)
							+ inOffset.getTotalSeconds();
						pair.addPoint(point, numSeconds);
					}
				}
			}
		}
		return pair;
	}


	/**
	 * Finish the correlation
	 */
	protected abstract void finishCorrelation();

	/**
	 * Construct an array of the point pairs to use for correlation
	 * @return array of PointMediaPair objects
	 */
	protected PointMediaPair[] getPointPairs()
	{
		MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel();
		int numMedia = model.getRowCount();
		PointMediaPair[] pairs = new PointMediaPair[numMedia];
		// Loop over items in preview table model
		for (int i=0; i<numMedia; i++)
		{
			MediaPreviewTableRow row = model.getRow(i);
			// add all selected pairs to array (other elements remain null)
			if (row.getCorrelateFlag()) {
				pairs[i] = row.getPointPair();
			}
		}
		return pairs;
	}

	/**
	 * Make the undo text for the given number of correlated items
	 */
	protected String makeUndoText(int inNumMedia)
	{
		Describer undoDescriber = new Describer("undo.correlate" + getMediaTypeKey(),
				"undo.correlate" + getMediaTypeKey() + "s");
		return undoDescriber.getDescriptionWithCount(inNumMedia);
	}

	/**
	 * Make the confirm text for the given number of correlated items
	 */
	protected String makeConfirmText(int inNumMedia)
	{
		Describer confirmDescriber = new Describer("confirm.correlate" + getMediaTypeKey() + "s.single",
				"confirm.correlate" + getMediaTypeKey() + "s.multi");
		return confirmDescriber.getDescriptionWithCount(inNumMedia);
	}

	/** @return true if the given point is present in the list already */
	protected static boolean pointAlreadyBeingConnected(DataPoint inPoint, List<PointAndMedia> inPairs)
	{
		for (PointAndMedia pair : inPairs)
		{
			if (pair.getPoint() == inPoint) {
				// Point is already in the list
				return true;
			}
		}
		return false;
	}
}
