package eu.dnetlib.miscutils.datetime;

/*
 * HumanTime.java
 * 
 * Created on 06.10.2008
 * 
 * Copyright (c) 2008 Johann Burkard (<mailto:jb@eaio.com>) <http://eaio.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Iterator;

/**
 * HumanTime parses and formats time deltas for easier reading by humans. It can format time information without losing
 * information but its main purpose is to generate more easily understood approximations. <h3>Using HumanTime</h3>
 * <p>
 * Use HumanTime by creating an instance that contains the time delta ({@link HumanTime#HumanTime(long)}), create an
 * empty instance through ({@link HumanTime#HumanTime()}) and set the delta using the {@link #y()}, {@link #d()},
 * {@link #h()}, {@link #s()} and {@link #ms()} methods or parse a {@link CharSequence} representation (
 * {@link #eval(CharSequence)}). Parsing ignores whitespace and is case insensitive.
 * </p>
 * <h3>HumanTime format</h3>
 * <p>
 * HumanTime will format time deltas in years ("y"), days ("d"), hours ("h"), minutes ("m"), seconds ("s") and
 * milliseconds ("ms"), separated by a blank character. For approximate representations, the time delta will be round up
 * or down if necessary.
 * </p>
 * <h3>HumanTime examples</h3>
 * <ul>
 * <li>HumanTime.eval("1 d 1d 2m 3m").getExactly() = "2 d 5 m"</li>
 * <li>HumanTime.eval("2m8d2h4m").getExactly() = "8 d 2 h 6 m"</li>
 * <li>HumanTime.approximately("2 d 8 h 20 m 50 s") = "2 d 8 h"</li>
 * <li>HumanTime.approximately("55m") = "1 h"</li>
 * </ul>
 * <h3>Implementation details</h3>
 * <ul>
 * <li>The time delta can only be increased.</li>
 * <li>Instances of this class are thread safe.</li>
 * <li>Getters using the Java Beans naming conventions are provided for use in environments like JSP or with expression
 * languages like OGNL. See {@link #getApproximately()} and {@link #getExactly()}.</li>
 * <li>To keep things simple, a year consists of 365 days.</li>
 * </ul>
 * 
 * @author <a href="mailto:jb@eaio.com">Johann Burkard</a>
 * @version $Id: HumanTime.java 323 2008-10-08 19:06:22Z Johann $
 * @see #eval(CharSequence)
 * @see #approximately(CharSequence)
 * @see <a href="http://johannburkard.de/blog/programming/java/date-formatting-parsing-humans-humantime.html">Date
 *      Formatting and Parsing for Humans in Java with HumanTime</a>
 */
public class HumanTime implements Externalizable, Comparable<HumanTime>, Cloneable {

	/**
	 * The serial version UID.
	 */
	private static final long serialVersionUID = 5179328390732826722L;

	/**
	 * One second.
	 */
	private static final long SECOND = 1000;

	/**
	 * One minute.
	 */
	private static final long MINUTE = SECOND * 60;

	/**
	 * One hour.
	 */
	private static final long HOUR = MINUTE * 60;

	/**
	 * One day.
	 */
	private static final long DAY = HOUR * 24;

	/**
	 * One year.
	 */
	private static final long YEAR = DAY * 365;

	/**
	 * Percentage of what is round up or down.
	 */
	private static final int CEILING_PERCENTAGE = 15;

	/**
	 * Parsing state.
	 */
	static enum State {

		NUMBER, IGNORED, UNIT

	}

	static State getState(char c) {
		State out;
		switch (c) {
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			out = State.NUMBER;
			break;
		case 's':
		case 'm':
		case 'h':
		case 'd':
		case 'y':
		case 'S':
		case 'M':
		case 'H':
		case 'D':
		case 'Y':
			out = State.UNIT;
			break;
		default:
			out = State.IGNORED;
		}
		return out;
	}

	/**
	 * Parses a {@link CharSequence} argument and returns a {@link HumanTime} instance.
	 * 
	 * @param s
	 *            the char sequence, may not be <code>null</code>
	 * @return an instance, never <code>null</code>
	 */
	public static HumanTime eval(final CharSequence s) {
		HumanTime out = new HumanTime(0L);

		int num = 0;

		int start = 0;
		int end = 0;

		State oldState = State.IGNORED;

		for (char c : new Iterable<Character>() {

			/**
			 * @see java.lang.Iterable#iterator()
			 */
			@Override
			public Iterator<Character> iterator() {
				return new Iterator<Character>() {

					private int p = 0;

					/**
					 * @see java.util.Iterator#hasNext()
					 */
					@Override
					public boolean hasNext() {
						return p < s.length();
					}

					/**
					 * @see java.util.Iterator#next()
					 */
					@Override
					public Character next() {
						return s.charAt(p++);
					}

					/**
					 * @see java.util.Iterator#remove()
					 */
					@Override
					public void remove() {
						throw new UnsupportedOperationException();
					}

				};
			}

		}) {
			State newState = getState(c);
			if (oldState != newState) {
				if (oldState == State.NUMBER && (newState == State.IGNORED || newState == State.UNIT)) {
					num = Integer.parseInt(s.subSequence(start, end).toString());
				} else if (oldState == State.UNIT && (newState == State.IGNORED || newState == State.NUMBER)) {
					out.nTimes(s.subSequence(start, end).toString(), num);
					num = 0;
				}
				start = end;
			}
			++end;
			oldState = newState;
		}
		if (oldState == State.UNIT) {
			out.nTimes(s.subSequence(start, end).toString(), num);
		}

		return out;
	}

	/**
	 * Parses and formats the given char sequence, preserving all data.
	 * <p>
	 * Equivalent to <code>eval(in).getExactly()</code>
	 * 
	 * @param in
	 *            the char sequence, may not be <code>null</code>
	 * @return a formatted String, never <code>null</code>
	 */
	public static String exactly(CharSequence in) {
		return eval(in).getExactly();
	}

	/**
	 * Formats the given time delta, preserving all data.
	 * <p>
	 * Equivalent to <code>new HumanTime(in).getExactly()</code>
	 * 
	 * @param l
	 *            the time delta
	 * @return a formatted String, never <code>null</code>
	 */
	public static String exactly(long l) {
		return new HumanTime(l).getExactly();
	}

	/**
	 * Parses and formats the given char sequence, potentially removing some data to make the output easier to
	 * understand.
	 * <p>
	 * Equivalent to <code>eval(in).getApproximately()</code>
	 * 
	 * @param in
	 *            the char sequence, may not be <code>null</code>
	 * @return a formatted String, never <code>null</code>
	 */
	public static String approximately(CharSequence in) {
		return eval(in).getApproximately();
	}

	/**
	 * Formats the given time delta, preserving all data.
	 * <p>
	 * Equivalent to <code>new HumanTime(l).getApproximately()</code>
	 * 
	 * @param l
	 *            the time delta
	 * @return a formatted String, never <code>null</code>
	 */
	public static String approximately(long l) {
		return new HumanTime(l).getApproximately();
	}

	/**
	 * The time delta.
	 */
	private long delta;

	/**
	 * No-argument Constructor for HumanTime.
	 * <p>
	 * Equivalent to calling <code>new HumanTime(0L)</code>.
	 */
	public HumanTime() {
		this(0L);
	}

	/**
	 * Constructor for HumanTime.
	 * 
	 * @param delta
	 *            the initial time delta, interpreted as a positive number
	 */
	public HumanTime(long delta) {
		super();
		this.delta = Math.abs(delta);
	}

	private void nTimes(String unit, int n) {
		if ("ms".equalsIgnoreCase(unit)) {
			ms(n);
		} else if ("s".equalsIgnoreCase(unit)) {
			s(n);
		} else if ("m".equalsIgnoreCase(unit)) {
			m(n);
		} else if ("h".equalsIgnoreCase(unit)) {
			h(n);
		} else if ("d".equalsIgnoreCase(unit)) {
			d(n);
		} else if ("y".equalsIgnoreCase(unit)) {
			y(n);
		}
	}

	private long upperCeiling(long x) {
		return (x / 100) * (100 - CEILING_PERCENTAGE);
	}

	private long lowerCeiling(long x) {
		return (x / 100) * CEILING_PERCENTAGE;
	}

	private String ceil(long d, long n) {
		return Integer.toString((int) Math.ceil((double) d / n));
	}

	private String floor(long d, long n) {
		return Integer.toString((int) Math.floor((double) d / n));
	}

	/**
	 * Adds one year to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime y() {
		return y(1);
	}

	/**
	 * Adds n years to the time delta.
	 * 
	 * @param n
	 *            n
	 * @return this HumanTime object
	 */
	public HumanTime y(int n) {
		delta += YEAR * Math.abs(n);
		return this;
	}

	/**
	 * Adds one day to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime d() {
		return d(1);
	}

	/**
	 * Adds n days to the time delta.
	 * 
	 * @param n
	 *            n
	 * @return this HumanTime object
	 */
	public HumanTime d(int n) {
		delta += DAY * Math.abs(n);
		return this;
	}

	/**
	 * Adds one hour to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime h() {
		return h(1);
	}

	/**
	 * Adds n hours to the time delta.
	 * 
	 * @param n
	 *            n
	 * @return this HumanTime object
	 */
	public HumanTime h(int n) {
		delta += HOUR * Math.abs(n);
		return this;
	}

	/**
	 * Adds one month to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime m() {
		return m(1);
	}

	/**
	 * Adds n months to the time delta.
	 * 
	 * @param n
	 *            n
	 * @return this HumanTime object
	 */
	public HumanTime m(int n) {
		delta += MINUTE * Math.abs(n);
		return this;
	}

	/**
	 * Adds one second to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime s() {
		return s(1);
	}

	/**
	 * Adds n seconds to the time delta.
	 * 
	 * @param n
	 *            seconds
	 * @return this HumanTime object
	 */
	public HumanTime s(int n) {
		delta += SECOND * Math.abs(n);
		return this;
	}

	/**
	 * Adds one millisecond to the time delta.
	 * 
	 * @return this HumanTime object
	 */
	public HumanTime ms() {
		return ms(1);
	}

	/**
	 * Adds n milliseconds to the time delta.
	 * 
	 * @param n
	 *            n
	 * @return this HumanTime object
	 */
	public HumanTime ms(int n) {
		delta += Math.abs(n);
		return this;
	}

	/**
	 * Returns a human-formatted representation of the time delta.
	 * 
	 * @return a formatted representation of the time delta, never <code>null</code>
	 */
	public String getExactly() {
		return getExactly(new StringBuilder()).toString();
	}

	/**
	 * Appends a human-formatted representation of the time delta to the given {@link Appendable} object.
	 * 
	 * @param <T>
	 *            the return type
	 * @param a
	 *            the Appendable object, may not be <code>null</code>
	 * @return the given Appendable object, never <code>null</code>
	 */
	public <T extends Appendable> T getExactly(T a) {
		try {
			boolean prependBlank = false;
			long d = delta;
			if (d >= YEAR) {
				a.append(floor(d, YEAR));
				a.append(' ');
				a.append('y');
				prependBlank = true;
			}
			d %= YEAR;
			if (d >= DAY) {
				if (prependBlank) {
					a.append(' ');
				}
				a.append(floor(d, DAY));
				a.append(' ');
				a.append('d');
				prependBlank = true;
			}
			d %= DAY;
			if (d >= HOUR) {
				if (prependBlank) {
					a.append(' ');
				}
				a.append(floor(d, HOUR));
				a.append(' ');
				a.append('h');
				prependBlank = true;
			}
			d %= HOUR;
			if (d >= MINUTE) {
				if (prependBlank) {
					a.append(' ');
				}
				a.append(floor(d, MINUTE));
				a.append(' ');
				a.append('m');
				prependBlank = true;
			}
			d %= MINUTE;
			if (d >= SECOND) {
				if (prependBlank) {
					a.append(' ');
				}
				a.append(floor(d, SECOND));
				a.append(' ');
				a.append('s');
				prependBlank = true;
			}
			d %= SECOND;
			if (d > 0) {
				if (prependBlank) {
					a.append(' ');
				}
				a.append(Integer.toString((int) d));
				a.append(' ');
				a.append('m');
				a.append('s');
			}
		} catch (IOException ex) {
			// What were they thinking...
		}
		return a;
	}

	/**
	 * Returns an approximate, human-formatted representation of the time delta.
	 * 
	 * @return a formatted representation of the time delta, never <code>null</code>
	 */
	public String getApproximately() {
		return getApproximately(new StringBuilder()).toString();
	}

	/**
	 * Appends an approximate, human-formatted representation of the time delta to the given {@link Appendable} object.
	 * 
	 * @param <T>
	 *            the return type
	 * @param a
	 *            the Appendable object, may not be <code>null</code>
	 * @return the given Appendable object, never <code>null</code>
	 */
	public <T extends Appendable> T getApproximately(T a) {

		try {
			int parts = 0;
			boolean rounded = false;
			boolean prependBlank = false;
			long d = delta;
			long mod = d % YEAR;

			if (mod >= upperCeiling(YEAR)) {
				a.append(ceil(d, YEAR));
				a.append(' ');
				a.append('y');
				++parts;
				rounded = true;
				prependBlank = true;
			} else if (d >= YEAR) {
				a.append(floor(d, YEAR));
				a.append(' ');
				a.append('y');
				++parts;
				rounded = mod <= lowerCeiling(YEAR);
				prependBlank = true;
			}

			if (!rounded) {
				d %= YEAR;
				mod = d % DAY;

				if (mod >= upperCeiling(DAY)) {
					if (prependBlank) {
						a.append(' ');
					}
					a.append(ceil(d, DAY));
					a.append(' ');
					a.append('d');
					++parts;
					rounded = true;
					prependBlank = true;
				} else if (d >= DAY) {
					if (prependBlank) {
						a.append(' ');
					}
					a.append(floor(d, DAY));
					a.append(' ');
					a.append('d');
					++parts;
					rounded = mod <= lowerCeiling(DAY);
					prependBlank = true;
				}

				if (parts < 2) {
					d %= DAY;
					mod = d % HOUR;

					if (mod >= upperCeiling(HOUR)) {
						if (prependBlank) {
							a.append(' ');
						}
						a.append(ceil(d, HOUR));
						a.append(' ');
						a.append('h');
						++parts;
						rounded = true;
						prependBlank = true;
					} else if (d >= HOUR && !rounded) {
						if (prependBlank) {
							a.append(' ');
						}
						a.append(floor(d, HOUR));
						a.append(' ');
						a.append('h');
						++parts;
						rounded = mod <= lowerCeiling(HOUR);
						prependBlank = true;
					}

					if (parts < 2) {
						d %= HOUR;
						mod = d % MINUTE;

						if (mod >= upperCeiling(MINUTE)) {
							if (prependBlank) {
								a.append(' ');
							}
							a.append(ceil(d, MINUTE));
							a.append(' ');
							a.append('m');
							++parts;
							rounded = true;
							prependBlank = true;
						} else if (d >= MINUTE && !rounded) {
							if (prependBlank) {
								a.append(' ');
							}
							a.append(floor(d, MINUTE));
							a.append(' ');
							a.append('m');
							++parts;
							rounded = mod <= lowerCeiling(MINUTE);
							prependBlank = true;
						}

						if (parts < 2) {
							d %= MINUTE;
							mod = d % SECOND;

							if (mod >= upperCeiling(SECOND)) {
								if (prependBlank) {
									a.append(' ');
								}
								a.append(ceil(d, SECOND));
								a.append(' ');
								a.append('s');
								++parts;
								rounded = true;
								prependBlank = true;
							} else if (d >= SECOND && !rounded) {
								if (prependBlank) {
									a.append(' ');
								}
								a.append(floor(d, SECOND));
								a.append(' ');
								a.append('s');
								++parts;
								rounded = mod <= lowerCeiling(SECOND);
								prependBlank = true;
							}

							if (parts < 2) {
								d %= SECOND;

								if (d > 0 && !rounded) {
									if (prependBlank) {
										a.append(' ');
									}
									a.append(Integer.toString((int) d));
									a.append(' ');
									a.append('m');
									a.append('s');
								}
							}

						}

					}

				}
			}
		} catch (IOException ex) {
			// What were they thinking...
		}

		return a;
	}

	/**
	 * Returns the time delta.
	 * 
	 * @return the time delta
	 */
	public long getDelta() {
		return delta;
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof HumanTime)) {
			return false;
		}
		return delta == ((HumanTime) obj).delta;
	}

	/**
	 * Returns a 32-bit representation of the time delta.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return (int) (delta ^ (delta >> 32));
	}

	/**
	 * Returns a String representation of this.
	 * <p>
	 * The output is identical to {@link #getExactly()}.
	 * 
	 * @see java.lang.Object#toString()
	 * @see #getExactly()
	 * @return a String, never <code>null</code>
	 */
	@Override
	public String toString() {
		return getExactly();
	}

	/**
	 * Compares this HumanTime to another HumanTime.
	 * 
	 * @param t
	 *            the other instance, may not be <code>null</code>
	 * @return which one is greater
	 */
	@Override
	public int compareTo(HumanTime t) {
		return delta == t.delta ? 0 : (delta < t.delta ? -1 : 1);
	}

	/**
	 * Deep-clones this object.
	 * 
	 * @see java.lang.Object#clone()
	 * @throws CloneNotSupportedException
	 */
	@Override
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	/**
	 * @see java.io.Externalizable#readExternal(java.io.ObjectInput)
	 */
	@Override
	public void readExternal(ObjectInput in) throws IOException {
		delta = in.readLong();
	}

	/**
	 * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
	 */
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeLong(delta);
	}

}
