/*
 * The MIT License
 * Copyright (c) 2012 Microsoft Corporation
 *
 * 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.
 */

package microsoft.exchange.webservices.data.misc;

import microsoft.exchange.webservices.data.core.exception.misc.FormatException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Class TimeSpan.
 */
public class TimeSpan implements Comparable<TimeSpan>, java.io.Serializable, Cloneable {

  private static final Log LOG = LogFactory.getLog(TimeSpan.class);

  /**
   * Constant serialized ID used for compatibility.
   */
  private static final long serialVersionUID = 1L;

  /**
   * The time.
   */
  private long time = 0;

  /**
   * Constant for milliseconds unit and conversion.
   */
  public static final int MILLISECONDS = 1;

  /**
   * Constant for seconds unit and conversion.
   */
  public static final int SECONDS = MILLISECONDS * 1000;

  /**
   * Constant for minutes unit and conversion.
   */
  public static final int MINUTES = SECONDS * 60;

  /**
   * Constant for hours unit and conversion.
   */
  public static final int HOURS = MINUTES * 60;

  /**
   * Constant for days unit and conversion.
   */
  public static final int DAYS = HOURS * 24;

  /**
   * Represents the Maximum TimeSpan value.
   */
  public static final TimeSpan MAX_VALUE = new TimeSpan(Long.MAX_VALUE);

  /**
   * Represents the Minimum TimeSpan value.
   */
  public static final TimeSpan MIN_VALUE = new TimeSpan(Long.MIN_VALUE);

  /**
   * Represents the TimeSpan with a value of zero.
   */
  public static final TimeSpan ZERO = new TimeSpan(0L);

  /**
   * Creates a new instance of TimeSpan based on the number of milliseconds
   * entered.
   *
   * @param time the number of milliseconds for this TimeSpan.
   */
  public TimeSpan(long time) {
    this.time = time;
  }

  /**
   * Creates a new TimeSpan object based on the unit and value entered.
   *
   * @param units the type of unit to use to create a TimeSpan instance.
   * @param value the number of units to use to create a TimeSpan instance.
   */
  public TimeSpan(int units, long value) {
    this.time = TimeSpan.toMilliseconds(units, value);
  }

	/*
         * public static TimeSpan fromMinutes(int value) { int l = value*60*100;
	 * return l; }
	 */

  /**
   * Subtracts two Date objects creating a new TimeSpan object.
   *
   * @param date1 Date to use as the base value.
   * @param date2 Date to subtract from the base value.
   * @return a TimeSpan object representing the difference bewteen the two
   * Date objects.
   */
  public static TimeSpan subtract(java.util.Date date1,
      java.util.Date date2) {
    return new TimeSpan(date1.getTime() - date2.getTime());
  }

  /**
   * Compares this object with the specified object for order. Returns a
   * negative integer, zero, or a positive integer as this object is less
   * than, equal to, or greater than the specified object. Comparison is based
   * on the number of milliseconds in this TimeSpan.
   *
   * @param o the Object to be compared.
   * @return a negative integer, zero, or a positive integer as this object is
   * less than, equal to, or greater than the specified object.
   */
  public int compareTo(TimeSpan o) {
    TimeSpan compare = (TimeSpan) o;
    if (this.time == compare.time) {
      return 0;
    }
    if (this.time > compare.time) {
      return +1;
    }
    return -1;
  }

  /**
   * Indicates whether some other object is "equal to" this one. Comparison is
   * based on the number of milliseconds in this TimeSpan.
   *
   * @param obj the reference object with which to compare.
   * @return if the obj argument is a TimeSpan object with the exact same
   * number of milliseconds. otherwise.
   */
  public boolean equals(Object obj) {
    if (obj instanceof TimeSpan) {
      TimeSpan compare = (TimeSpan) obj;
      if (this.time == compare.time) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns a hash code value for the object. This method is supported for
   * the benefit of hashtables such as those provided by
   * <code>java.util.Hashtable</code>. The method uses the same algorithm as
   * found in the Long class.
   *
   * @return a hash code value for this object.
   * @see Object#equals(Object)
   * @see java.util.Hashtable
   */
  public int hashCode() {
    return Long.valueOf(this.time).hashCode();
  }

  /**
   * Returns a string representation of the object in the format.
   * "[-]d.hh:mm:ss.ff" where "-" is an optional sign for negative TimeSpan
   * values, the "d" component is days, "hh" is hours, "mm" is minutes, "ss"
   * is seconds, and "ff" is milliseconds
   *
   * @return a string containing the number of milliseconds.
   */
  public String toString() {
    StringBuffer sb = new StringBuffer();
    long millis = this.time;
    if (millis < 0) {
      sb.append("-");
      millis = -millis;
    }

    long day = millis / TimeSpan.DAYS;

    if (day != 0) {
      sb.append(day);
      sb.append("d.");
      millis = millis % TimeSpan.DAYS;
    }

    sb.append(millis / TimeSpan.HOURS);
    millis = millis % TimeSpan.HOURS;
    sb.append("h:");
    sb.append(millis / TimeSpan.MINUTES);
    millis = millis % TimeSpan.MINUTES;
    sb.append("m:");
    sb.append(millis / TimeSpan.SECONDS);
    sb.append("s");
    millis = millis % TimeSpan.SECONDS;
    if (millis != 0) {
      sb.append(".");
      sb.append(millis);
      sb.append("ms");
    }
    return sb.toString();
  }

  /**
   * Returns a clone of this TimeSpan.
   *
   * @return a clone of this TimeSpan.
   */
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      LOG.error(e);
      throw new InternalError();
    }
  }

  /**
   * Indicates whether the value of the TimeSpan is positive.
   *
   * @return if the value of the TimeSpan is greater than
   * zero.  otherwise.
   */
  public boolean isPositive() {
    return this.compareTo(TimeSpan.ZERO) > 0 ? true : false;
  }

  /**
   * Indicates whether the value of the TimeSpan is negative.
   *
   * @return if the value of the TimeSpan is less than zero.
   * otherwise.
   */
  public boolean isNegative() {
    return this.compareTo(TimeSpan.ZERO) < 0 ? true : false;
  }

  /**
   * Indicates whether the value of the TimeSpan is zero.
   *
   * @return if the value of the TimeSpan is equal to zero.
   * otherwise.
   */
  public boolean isZero() {
    return this.equals(TimeSpan.ZERO);
  }

  /**
   * Gets the number of milliseconds.
   *
   * @return the number of milliseconds.
   */
  public long getMilliseconds() {
    return (((this.time % TimeSpan.HOURS) % TimeSpan.MINUTES) % TimeSpan.SECONDS)
        / TimeSpan.MILLISECONDS;
  }

  /**
   * Gets the number of milliseconds.
   *
   * @return the number of milliseconds.
   */
  public long getTotalMilliseconds() {
    return this.time;
  }

  /**
   * Gets the number of seconds (truncated).
   *
   * @return the number of seconds.
   */
  public long getSeconds() {
    return ((this.time % TimeSpan.HOURS) % TimeSpan.MINUTES) / TimeSpan.SECONDS;
  }

  /**
   * Gets the number of seconds including fractional seconds.
   *
   * @return the number of seconds.
   */
  public double getTotalSeconds() {
    return this.time / 1000.0d;
  }

  /**
   * Gets the number of minutes (truncated).
   *
   * @return the number of minutes.
   */
  public long getMinutes() {
    return (this.time % TimeSpan.HOURS) / TimeSpan.MINUTES;// (this.time/1000)/60;
  }

  /**
   * Gets the number of minutes including fractional minutes.
   *
   * @return the number of minutes.
   */
  public double getTotalMinutes() {
    return (this.time / 1000.0d) / 60.0d;
  }

  /**
   * Gets the number of hours (truncated).
   *
   * @return the number of hours.
   */
  public long getHours() {
    return ((this.time / 1000) / 60) / 60;
  }

  /**
   * Gets the number of hours including fractional hours.
   *
   * @return the number of hours.
   */
  public double getTotalHours() {
    return ((this.time / 1000.0d) / 60.0d) / 60.0d;
  }

  /**
   * Gets the number of days (truncated).
   *
   * @return the number of days.
   */
  public long getDays() {
    return (((this.time / 1000) / 60) / 60) / 24;
  }

  /**
   * Gets the number of days including fractional days.
   *
   * @return the number of days.
   */
  public double getTotalDays() {
    return (((this.time / 1000.0d) / 60.0d) / 60.0d) / 24.0d;
  }

  /**
   * Adds a TimeSpan to this TimeSpan.
   *
   * @param timespan the TimeSpan to add to this TimeSpan.
   */
  public void add(TimeSpan timespan) {
    add(TimeSpan.MILLISECONDS, timespan.time);
  }

  /**
   * Adds a number of units to this TimeSpan.
   *
   * @param units the type of unit to add to this TimeSpan.
   * @param value the number of units to add to this TimeSpan.
   */
  public void add(int units, long value) {
    this.time += TimeSpan.toMilliseconds(units, value);
  }

  /**
   * Compares two TimeSpan objects.
   *
   * @param first  first TimeSpan to use in the compare.
   * @param second second TimeSpan to use in the compare.
   * @return a negative integer, zero, or a positive integer as the first
   * TimeSpan is less than, equal to, or greater than the second
   * TimeSpan.
   */
  public static int compare(TimeSpan first, TimeSpan second) {
    if (first.time == second.time) {
      return 0;
    }
    if (first.time > second.time) {
      return +1;
    }
    return -1;
  }

  /**
   * Returns a TimeSpan whose value is the absolute value of this TimeSpan.
   *
   * @return a TimeSpan whose value is the absolute value of this TimeSpan.
   */
  public TimeSpan duration() {
    return new TimeSpan(Math.abs(this.time));
  }

  /**
   * Returns a TimeSpan whose value is the negated value of this TimeSpan.
   *
   * @return a TimeSpan whose value is the negated value of this TimeSpan.
   */
  public TimeSpan negate() {
    return new TimeSpan(-this.time);
  }

  /**
   * Subtracts a TimeSpan from this TimeSpan.
   *
   * @param timespan the TimeSpan to subtract from this TimeSpan.
   */
  public void subtract(TimeSpan timespan) {
    subtract(TimeSpan.MILLISECONDS, timespan.time);
  }

  /**
   * Subtracts a number of units from this TimeSpan.
   *
   * @param units the type of unit to subtract from this TimeSpan.
   * @param value the number of units to subtract from this TimeSpan.
   */
  public void subtract(int units, long value) {
    add(units, -value);
  }

  /**
   * To milliseconds.
   *
   * @param units the units
   * @param value the value
   * @return the long
   */
  private static long toMilliseconds(int units, long value) {
    long millis;
    switch (units) {
      case TimeSpan.MILLISECONDS:
      case TimeSpan.SECONDS:
      case TimeSpan.MINUTES:
      case TimeSpan.HOURS:
      case TimeSpan.DAYS:
        millis = value * units;
        break;
      default:
        throw new IllegalArgumentException("Unrecognized units: " + units);
    }
    return millis;
  }

  public static TimeSpan parse(String s) throws Exception {
    String str = s.trim();
    String[] st1 = str.split("\\.");
    int days = 0, millsec = 0, totMillSec = 0;
    String data = str;
    switch (st1.length) {
      case 1:
        data = str;
        break;
      case 2:
        if (st1[0].split(":").length > 1) {
          millsec = Integer.parseInt(st1[1]);
          data = st1[0];
        } else {
          days = Integer.parseInt(st1[0]);
          data = st1[1];
        }
        break;
      case 3:
        days = Integer.parseInt(st1[0]);
        data = st1[1];
        millsec = Integer.parseInt(st1[2]);
        break;
      default:
        throw new FormatException("Bad Format");

    }
    String[] st = data.split(":");
    switch (st.length) {
      case 1:
        totMillSec = Integer.parseInt(str) * 24 * 60 * 60 * 1000;
        break;
      case 2:
        totMillSec = (Integer.parseInt(st[0]) * 60 * 60 * 1000) + (Integer.parseInt(st[1]) * 60 * 1000);
        break;
      case 3:
        totMillSec = (Integer.parseInt(st[0]) * 60 * 60 * 1000) + (Integer.parseInt(st[1]) * 60 * 1000) + (
            Integer.parseInt(st[2]) * 1000);
        break;
      case 4:
        totMillSec =
            (Integer.parseInt(st[0]) * 24 * 60 * 60 * 1000) + (Integer.parseInt(st[1]) * 60 * 60 * 1000) + (
                Integer.parseInt(st[2]) * 60 * 1000) + (Integer.parseInt(st[3]) * 1000);
        break;
      default:
        throw new FormatException("Bad Format/Overflow");
    }
    totMillSec += (days * 24 * 60 * 60 * 1000) + millsec;
    return new TimeSpan(totMillSec);
  }

}
