// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.streetside;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;

/**
 * Abstract superclass for all image objects. At the moment there are just 2,
 * {@link StreetsideImportedImage} and {@link StreetsideImage}.
 *
 * @author nokutu
 *
 */
public abstract class StreetsideAbstractImage implements Comparable<StreetsideAbstractImage> {
	/**
	 * If two values for field cd differ by less than EPSILON both values are
	 * considered equal.
	 */
	private static final float EPSILON = 1e-5f;

	protected String id;

	/** The time the image was captured, in Epoch format. */
	protected long cd;
	/** Sequence of pictures containing this object. */
	private StreetsideSequence sequence;

	/** Position of the picture. */
	protected LatLon latLon;
	/** Direction of the picture. */
	protected double he;
	/** Temporal position of the picture until it is uploaded. */
	private LatLon tempLatLon;
	/**
	 * When the object is being dragged in the map, the temporal position is stored
	 * here.
	 */
	private LatLon movingLatLon;
	/** Temporal direction of the picture until it is uploaded */
	private double tempHe;
	/**
	 * When the object direction is being moved in the map, the temporal direction
	 * is stored here
	 */
	protected double movingHe;
	/** Whether the image must be drown in the map or not */
	private boolean visible;

	/**
	 * Creates a new object in the given position and with the given direction.
	 * {@link LatLon}
	 *
	 * @param id - the Streetside image id
	 *
	 * @param latLon
	 *            The latitude and longitude of the image.
	 * @param he
	 *            The direction of the picture (0 means north im Mapillary
	 *            camera direction is not yet supported in the Streetside plugin).
	 */
	protected StreetsideAbstractImage(final String id, final LatLon latLon, final double he) {
		this.id = id;
		this.latLon = latLon;
		tempLatLon = this.latLon;
		movingLatLon = this.latLon;
		this.he = he;
		tempHe = he;
		movingHe = he;
		visible = true;
	}

	/**
	 * Creates a new object with the given id.
	 *
	 * @param id - the image id (All images require ids in Streetside)
	 */
	protected StreetsideAbstractImage(final String id) {
		this.id = id;

		visible = true;
	}

	/**
	 * @return the id
	 */
	public String getId() {
		return id;
	}

	/**
	 * @param id
	 *            the id to set
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Returns the original direction towards the image has been taken.
	 *
	 * @return The direction of the image (0 means north and goes clockwise).
	 */
	public double getHe() {
		return he;
	}

	/**
	 * Returns the Epoch time when the image was captured.
	 *
	 * @return The long containing the Epoch time when the image was captured.
	 */
	public long getCd() {
		return cd;
	}

	/**
	 * Returns the date the picture was taken in DMY format.
	 *
	 * @return A String object containing the date when the picture was taken.
	 */
	public String getDate() {
		final StringBuilder format = new StringBuilder(26);
		format.append("m/d/YYYY");
		if (StreetsideProperties.DISPLAY_HOUR.get()) {
			if (StreetsideProperties.TIME_FORMAT_24.get()) {
				format.append(" - HH:mm:ss");
			} else {
				format.append(" - h:mm:ss a");
			}
		}
		return getDate(format.toString());
	}

	/**
	 * Returns the date the picture was taken in the given format.
	 *
	 * @param format
	 *            Format of the date. See {@link SimpleDateFormat}.
	 * @return A String containing the date the picture was taken using the given
	 *         format.
	 * @throws NullPointerException
	 *             if parameter format is <code>null</code>
	 */
	public String getDate(String format) {
		final Date date = new Date(getCd());
		final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US);
		formatter.setTimeZone(Calendar.getInstance().getTimeZone());
		return formatter.format(date);
	}

	/**
	 * Returns a LatLon object containing the original coordinates of the object.
	 *
	 * @return The LatLon object with the position of the object.
	 */
	public LatLon getLatLon() {
		return latLon;
	}

	/**
	 * Returns the direction towards the image has been taken.
	 *
	 * @return The direction of the image (0 means north and goes clockwise).
	 */
	public double getMovingHe() {
		return movingHe;
	}

	/**
	 * Returns a LatLon object containing the current coordinates of the object.
	 * When you are dragging the image this changes.
	 *
	 * @return The LatLon object with the position of the object.
	 */
	public LatLon getMovingLatLon() {
		return movingLatLon;
	}

	/**
	 * Returns the sequence which contains this image. Never null.
	 *
	 * @return The StreetsideSequence object that contains this StreetsideImage.
	 */

	public StreetsideSequence getSequence() {
		synchronized (this) {
			if (sequence == null) {
				sequence = new StreetsideSequence();
				sequence.add(this);
			}
			return sequence;
		}
	}

	/**
	 * Returns the last fixed direction of the object.
	 *
	 * @return The last fixed direction of the object. 0 means north.
	 */
	public double getTempHe() {
		return tempHe;
	}

	/**
	 * Returns the last fixed coordinates of the object.
	 *
	 * @return A LatLon object containing.
	 */
	public LatLon getTempLatLon() {
		return tempLatLon;
	}

	/**
	 * Returns whether the object has been modified or not.
	 *
	 * @return true if the object has been modified; false otherwise.
	 */
	public boolean isModified() {
		return !getMovingLatLon().equals(latLon) || Math.abs(getMovingHe() - cd) > EPSILON;
	}

	/**
	 * Returns whether the image is visible on the map or not.
	 *
	 * @return True if the image is visible; false otherwise.
	 */
	public boolean isVisible() {
		return visible;
	}

	/**
	 * Moves the image temporally to another position
	 *
	 * @param x
	 *            The movement of the image in longitude units.
	 * @param y
	 *            The movement of the image in latitude units.
	 */
	public void move(final double x, final double y) {
		movingLatLon = new LatLon(tempLatLon.getY() + y, tempLatLon.getX() + x);
	}

	/**
	 * If the StreetsideImage belongs to a StreetsideSequence, returns the next
	 * image in the sequence.
	 *
	 * @return The following StreetsideImage, or null if there is none.
	 */
	public StreetsideAbstractImage next() {
		synchronized (this) {
			return getSequence().next(this);
		}
	}

	/**
	 * If the StreetsideImage belongs to a StreetsideSequence, returns the previous
	 * image in the sequence.
	 *
	 * @return The previous StreetsideImage, or null if there is none.
	 */
	public StreetsideAbstractImage previous() {
		synchronized (this) {
			return getSequence().previous(this);
		}
	}

	public void setHe(final double he) {
		this.he = he;
	}

	/**
	 * Sets the Epoch time when the picture was captured.
	 *
	 * @param cd
	 *            Epoch time when the image was captured.
	 */
	public synchronized void setCd(final long cd) {
		this.cd = cd;
	}

	public void setLatLon(final LatLon latLon) {
		if (latLon != null) {
			this.latLon = latLon;
		}
	}

	/**
	 * Sets the StreetsideSequence object which contains the StreetsideImage.
	 *
	 * @param sequence
	 *            The StreetsideSequence that contains the StreetsideImage.
	 * @throws IllegalArgumentException
	 *             if the image is not already part of the
	 *             {@link StreetsideSequence}. Call
	 *             {@link StreetsideSequence#add(StreetsideAbstractImage)} first.
	 */
	public void setSequence(final StreetsideSequence sequence) {
		synchronized (this) {
			if (sequence != null && !sequence.getImages().contains(this)) {
				throw new IllegalArgumentException();
			}
			this.sequence = sequence;
		}
	}

	/**
	 * Set's whether the image should be visible on the map or not.
	 *
	 * @param visible
	 *            true if the image is set to be visible; false otherwise.
	 */
	public void setVisible(final boolean visible) {
		this.visible = visible;
	}

	/**
	 * Called when the mouse button is released, meaning that the picture has
	 * stopped being dragged, so the temporal values are saved.
	 */
	public void stopMoving() {
		tempLatLon = movingLatLon;
		tempHe = movingHe;
	}

	/**
	 * Turns the image direction.
	 *
	 * @param ca
	 *            The angle the image is moving.
	 */
	public void turn(final double ca) {
		movingHe = tempHe + ca;
	}
}