package tim.prune.load;

import java.io.File;
import java.util.ArrayList;

import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import tim.prune.App;
import tim.prune.I18nManager;
import tim.prune.cmd.LoadPhotosWithPointsCmd;
import tim.prune.config.Config;
import tim.prune.data.Altitude;
import tim.prune.data.Coordinate;
import tim.prune.data.DataPoint;
import tim.prune.data.LatLonRectangle;
import tim.prune.data.Latitude;
import tim.prune.data.Longitude;
import tim.prune.data.Photo;
import tim.prune.data.Timestamp;
import tim.prune.data.TimestampLocal;
import tim.prune.data.TimestampUtc;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.Describer;
import tim.prune.jpeg.InternalExifLibrary;
import tim.prune.jpeg.JpegData;
import tim.prune.tips.TipManager;

/**
 * Class to manage the loading of Jpegs and dealing with the GPS data from them
 */
public class JpegLoader
{
	private final App _app;
	private final JFrame _parentFrame;
	private JFileChooser _fileChooser = null;
	private final GenericFileFilter _fileFilter;
	private JCheckBox _subdirCheckbox = null;
	private JCheckBox _noExifCheckbox = null;
	private JCheckBox _outsideAreaCheckbox = null;
	private MediaLoadProgressDialog _progressDialog = null;
	private LoadCounts _counters = null;
	private boolean _cancelled = false;
	private LatLonRectangle _trackRectangle = null;
	private ArrayList<Photo> _photos = null;
	private boolean _foundDuplicatePhoto = false;


	/**
	 * Constructor
	 * @param inApp Application object to inform of photo load
	 * @param inParentFrame parent frame to reference for dialogs
	 */
	public JpegLoader(App inApp, JFrame inParentFrame)
	{
		_app = inApp;
		_parentFrame = inParentFrame;
		_fileFilter = new JpegFileFilter();
	}

	/** @return function name for dialog title */
	private String getName() {
		return I18nManager.getText("menu.file.addphotos");
	}

	/**
	 * Open the GUI to select options and start the load
	 * @param inRectangle track rectangle
	 */
	public void openDialog(LatLonRectangle inRectangle)
	{
		// Create file chooser if necessary
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setMultiSelectionEnabled(true);
			_fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
			_fileChooser.setFileFilter(_fileFilter);
			_fileChooser.setDialogTitle(getName());
			_subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.open.includesubdirectories"));
			_subdirCheckbox.setSelected(true);
			_noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
			_noExifCheckbox.setSelected(true);
			_outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
			_outsideAreaCheckbox.setSelected(true);
			JPanel panel = new JPanel();
			panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
			panel.add(_subdirCheckbox);
			panel.add(_noExifCheckbox);
			panel.add(_outsideAreaCheckbox);
			_fileChooser.setAccessory(panel);
			// start from directory in config if already set by other operations
			String configDir = _app.getConfig().getConfigString(Config.KEY_PHOTO_DIR);
			if (configDir == null) {
				configDir = _app.getConfig().getConfigString(Config.KEY_TRACK_DIR);
			}
			if (configDir != null) {
				_fileChooser.setCurrentDirectory(new File(configDir));
			}
		}
		// enable/disable track checkbox
		_trackRectangle = inRectangle;
		_outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
		// Show file dialog to choose file / directory(ies)
		if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
		{
			// Bring up dialog before starting
			_progressDialog = new MediaLoadProgressDialog(_parentFrame, () -> _cancelled = true);
			_progressDialog.show();
			// start thread for processing
			new Thread(this::run).start();
		}
	}

	/**
	 * Run method for performing tasks in separate thread
	 */
	public void run()
	{
		// Initialise arrays, errors, summaries
		_counters = new LoadCounts();
		_photos = new ArrayList<>();
		File[] files = _fileChooser.getSelectedFiles();
		_foundDuplicatePhoto = false;
		// Loop recursively over selected files/directories to count files
		int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
		// Set up the progress bar for this number of files
		_progressDialog.showProgress(0, numFiles);
		_cancelled = false;

		// Process the files recursively and build lists of photos
		processFileList(files, true, _subdirCheckbox.isSelected());
		SwingUtilities.invokeLater(() -> _progressDialog.close());
		if (_cancelled) {
			return;
		}
		if (_counters.getNumFiles() == 0) {
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
		}
		else if (_counters.getNumPhotos() == 0) {
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
		}
		else if (!_noExifCheckbox.isSelected() && _counters.getNumPhotos() == 0)
		{
			// Need coordinates but no gps information found
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
		}
		else
		{
			// Found some photos to load
			if (_foundDuplicatePhoto) {
				_app.showTip(TipManager.Tip_DuplicatePhotoLoaded);
			}
			// pass information back to app
			_photos.sort(new MediaSorter());
			LoadPhotosWithPointsCmd command = new LoadPhotosWithPointsCmd(_photos);
			final int numPhotos = _photos.size();
			Describer confirmDescriber = new Describer("confirm.jpegload.single", "confirm.jpegload.multi");
			command.setConfirmText(confirmDescriber.getDescriptionWithCount(numPhotos));
			Describer undoDescriber = new Describer("undo.loadphoto", "undo.loadphotos");
			String firstPhotoName = _photos.get(0).getName();
			command.setDescription(undoDescriber.getDescriptionWithNameOrCount(firstPhotoName, numPhotos));
			// TODO: If app execute says false, then we need to show a message or something?
			_app.execute(command);
		}
	}


	/**
	 * Process a list of files and/or directories
	 * @param inFiles array of file/directories
	 * @param inFirstDir true if first directory
	 * @param inDescend true to descend to subdirectories
	 */
	private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
	{
		if (inFiles == null) {
			return;
		}
		// Loop over elements in array
		for (File file : inFiles)
		{
			if (!_cancelled && file.exists() && file.canRead())
			{
				// Check whether it's a file or a directory
				if (file.isFile()) {
					processFile(file);
				}
				else if (file.isDirectory() && (inFirstDir || inDescend))
				{
					// Always process first directory,
					// only process subdirectories if checkbox selected
					File[] files = file.listFiles();
					processFileList(files, false, inDescend);
				}
			}
			// if file doesn't exist or isn't readable - ignore
		}
	}


	/**
	 * Process the given file, by attempting to extract its tags
	 * @param inFile file object to read
	 */
	private void processFile(File inFile)
	{
		_counters.foundFile();
		_progressDialog.setValue(_counters.getNumFiles());

		// Check whether filename corresponds with accepted filenames
		if (!_fileFilter.acceptFilename(inFile.getName())) {
			return;
		}

		if (inFile.exists() && inFile.canRead()) {
			_counters.foundPhoto();
		}
		Photo photo = createPhoto(inFile);
		if (photo.getDataPoint() != null) {
			_counters.foundPhotoWithCoords();
		}
		// Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
		if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
			&& (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
				|| _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
		{
			_photos.add(photo);
			if (_app.getTrackInfo().getPhotoList().hasDuplicate(photo)) {
				_foundDuplicatePhoto = true;
			}
		}
	}

	/**
	 * Create a Photo object for the given file, including reading exif information
	 * @param inFile file object
	 * @return Photo object
	 */
	public static Photo createPhoto(File inFile)
	{
		// Create Photo object
		Photo photo = new Photo(inFile);
		// Try to get information out of exif
		JpegData jpegData = new InternalExifLibrary().getJpegData(inFile);
		Timestamp timestamp = null;
		if (jpegData != null)
		{
			if (jpegData.isGpsValid())
			{
				timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
				// Make DataPoint and attach to Photo
				DataPoint point = createDataPoint(jpegData);
				point.setPhoto(photo);
				point.setSegmentStart(true);
				photo.setDataPoint(point);
				photo.setOriginalStatus(Photo.Status.TAGGED);
			}
			// Use exif timestamp if gps timestamp not available
			if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
				timestamp = createTimestamp(jpegData.getOriginalTimestamp());
			}
			if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
				timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
			}
			photo.setExifThumbnail(jpegData.getThumbnailImage());
			// Also extract orientation tag for setting rotation state of photo
			photo.setRotation(jpegData.getRequiredRotation());
			// Set bearing, if any
			photo.setBearing(jpegData.getBearing());
		}
		// Use file timestamp if exif timestamp isn't available
		if (timestamp == null) {
			timestamp = new TimestampUtc(inFile.lastModified());
		}
		// Apply timestamp to photo (but not its point)
		photo.setTimestamp(timestamp);

		return photo;
	}


	/**
	 * Recursively count the selected Files so we can draw a progress bar
	 * @param inFiles file list
	 * @param inFirstDir true if first directory
	 * @param inDescend true to descend to subdirectories
	 * @return count of the files selected
	 */
	private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
	{
		int fileCount = 0;
		if (inFiles != null)
		{
			// Loop over elements in array
			for (int i=0; i<inFiles.length; i++)
			{
				File file = inFiles[i];
				if (file.exists() && file.canRead())
				{
					// Store first directory in config for later
					if (i == 0 && inFirstDir)
					{
						File workingDir = file.isDirectory() ? file : file.getParentFile();
						_app.getConfig().setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
					}
					// Check whether it's a file or a directory
					if (file.isFile()) {
						fileCount++;
					}
					else if (file.isDirectory() && (inFirstDir || inDescend)) {
						fileCount += countFileList(file.listFiles(), false, inDescend);
					}
				}
			}
		}
		return fileCount;
	}


	/**
	 * Create a DataPoint object from the given jpeg data
	 * @param inData Jpeg data including coordinates
	 * @return DataPoint object for Track
	 */
	private static DataPoint createDataPoint(JpegData inData)
	{
		// Create model objects from jpeg data
		double latval = getCoordinateDoubleValue(inData.getLatitude(),
			inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
		Coordinate latitude = Latitude.make(latval);
		double lonval = getCoordinateDoubleValue(inData.getLongitude(),
			inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
		Coordinate longitude = Longitude.make(lonval);
		Altitude altitude = null;
		if (inData.hasAltitude()) {
			altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
		}
		return new DataPoint(latitude, longitude, altitude);
	}


	/**
	 * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
	 * @param inValues array of three doubles for deg-min-sec
	 * @param isPositive true for positive hemisphere, for positive double value
	 * @return double value of coordinate, either positive or negative
	 */
	private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
	{
		if (inValues == null || inValues.length != 3) {
			return 0.0;
		}
		double value = inValues[0]        // degrees
			+ inValues[1] / 60.0          // minutes
			+ inValues[2] / 60.0 / 60.0;  // seconds
		// make sure it's the correct sign
		value = Math.abs(value);
		return isPositive ? value : -value;
	}


	/**
	 * Use the given int values to create a timestamp
	 * @param inDate ints describing date
	 * @param inTime ints describing time
	 * @return Timestamp object corresponding to inputs
	 */
	private static Timestamp createTimestamp(int[] inDate, int[] inTime)
	{
		if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
			return null;
		}
		return new TimestampLocal(inDate[0], inDate[1], inDate[2],
			inTime[0], inTime[1], inTime[2]);
	}


	/**
	 * Use the given String value to create a timestamp
	 * @param inStamp timestamp from exif
	 * @return Timestamp object corresponding to input
	 */
	private static Timestamp createTimestamp(String inStamp)
	{
		Timestamp stamp = null;
		try
		{
			stamp = new TimestampLocal(Integer.parseInt(inStamp.substring(0, 4)),
				Integer.parseInt(inStamp.substring(5, 7)),
				Integer.parseInt(inStamp.substring(8, 10)),
				Integer.parseInt(inStamp.substring(11, 13)),
				Integer.parseInt(inStamp.substring(14, 16)),
				Integer.parseInt(inStamp.substring(17)));
		}
		catch (NumberFormatException | StringIndexOutOfBoundsException e) {}
		return stamp;
	}
}
