/*
     This file is part of GNUnet.
     (C) 2009 Christian Grothoff (and other contributing authors)

     GNUnet is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published
     by the Free Software Foundation; either version 2, or (at your
     option) any later version.

     GNUnet is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 59 Temple Place - Suite 330,
     Boston, MA 02111-1307, USA.
 */

package org.gnunet.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * A specific point in time.
 * 
 * @author Florian Dold
 */
public class AbsoluteTime implements Comparable<AbsoluteTime> {
    private static final Logger logger = LoggerFactory
            .getLogger(AbsoluteTime.class);

    /**
     * Constant for 'the beginning of time' in our frame.
     */
    public final static AbsoluteTime ZERO = new AbsoluteTime(0);
    public final static AbsoluteTime FOREVER = new AbsoluteTime(Long.MAX_VALUE);

    /**
     * Absolute time value in microseconds.
     */
    private final long absValueUs;

    /**
     * Gets the current time.
     * 
     * @return the current time
     */
    public static AbsoluteTime now() {
        return new AbsoluteTime(System.currentTimeMillis() * 1000);
    }

    public AbsoluteTime(final long absValueUs) {
        this.absValueUs = absValueUs;
    }

    /**
     * Adds a relative time value to an absolute time.
     * 
     * @param duration duration to add to {@literal this}
     * @return {@literal this + duration}
     */
    public AbsoluteTime add(RelativeTime duration) {
        if (absValueUs == Long.MAX_VALUE
                || duration.isForever()) {
            return AbsoluteTime.FOREVER;
        }
        if (absValueUs + duration.getMicroseconds() < absValueUs) {
            return AbsoluteTime.FOREVER;
        }
        return new AbsoluteTime(absValueUs + duration.getMicroseconds());
    }

    /**
     * Calculates the estimate time of arrival/completion for an operation.
     * 
     * @param start
     *            when did the operation start?
     * @param finished
     *            how much has been done?
     * @param total
     *            how much must be done overall (same unit as for "finished")
     * @return remaining duration for the operation, assuming it continues at
     *         the same speed
     */
    public static RelativeTime calculateETA(final AbsoluteTime start,
            final long finished, final long total) {
        if (finished >= total) {
            return RelativeTime.ZERO;
        }
        if (finished == 0) {
            return RelativeTime.FOREVER;
        }
        final RelativeTime dur = start.getDuration();
        final double exp = dur.getMicroseconds() * total
                / (double) finished;
        return new RelativeTime((long) exp);
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object other) {
        return other instanceof AbsoluteTime && compareTo((AbsoluteTime) other) == 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return (int) this.absValueUs;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int compareTo(AbsoluteTime other) {
        if (this.absValueUs < other.absValueUs) {
            return -1;
        }
        if (this.absValueUs > other.absValueUs) {
            return 1;
        }
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        if (this.isForever()) {
            return "AbsoluteTime(FOREVER)";
        }
        return "AbsoluteTime("+this.absValueUs +")";
    }


    /**
     * Check if a deadline is due.
     * @return true if NOW is greater than the given time, false otherwise
     */
    public boolean isDue() {
        return this.absValueUs < now().absValueUs;
    }

    /**
     * Does this AbsoluteTime value represent forever?
     *
     * @return this==FOREVER
     */
    public boolean isForever() {
        return this.absValueUs == Long.MAX_VALUE;
    }

    /**
     * Calculates the difference between two absolute times.
     * 
     * @param other ...
     * @return this - other
     */
    public RelativeTime getDifference(final AbsoluteTime other) {
        if (other.absValueUs == Long.MAX_VALUE) {
            return RelativeTime.FOREVER;
        }
        return new RelativeTime(absValueUs - other.absValueUs);
    }

    /**
     * Gets the duration of an operation as the difference of the current time
     * and {@literal this}.
     *
     * @return this - now
     */
    public RelativeTime getDuration() {
        assert absValueUs != Long.MAX_VALUE;
        return getDifference(AbsoluteTime.now());
    }

    /**
     * Returns the milliseconds since some fixed point of reference.
     * 
     * @return the absolute time in milliseconds
     */
    public long getMicroseconds() {
        return absValueUs;
    }

    /**
     * Calculates the remaining time relative to now.
     * 
     * @return this - now
     */
    public RelativeTime getRemaining() {
        if (absValueUs == Long.MAX_VALUE) {
            return RelativeTime.FOREVER;
        }
        return getDifference(AbsoluteTime.now());
    }

    /**
     * Returns the maximum of two time values.
     * 
     * @param other ...
     * @return max(this,other)
     */
    public AbsoluteTime max(final AbsoluteTime other) {
        return absValueUs >= other.absValueUs ? this : other;

    }

    /**
     * Returns the minimum of two time values.
     * 
     * @param other ...
     * @return min(this,other)
     */
    public AbsoluteTime min(final AbsoluteTime other) {
        return absValueUs <= other.absValueUs ? this : other;
    }

    /**
     * Subtracts a relative time value to an absolute time
     * 
     * @param duration ...
     * @return this - duration
     */
    public AbsoluteTime subtract(final RelativeTime duration) {
        if (absValueUs <= duration.getMicroseconds()) {
            return AbsoluteTime.ZERO;
        }
        if (absValueUs == Long.MAX_VALUE) {
            return this;
        }
        return new AbsoluteTime(absValueUs - duration.getMicroseconds());
    }

    /**
     * Get a serializable message corresponding to this AbsoluteTime.
     *
     * @return a serializable message corresponding to this AbsoluteTime
     */
    public AbsoluteTimeMessage asMessage() {
        return new AbsoluteTimeMessage(this);
    }

    /**
     * Get the AbsoluteTime from a AbsoluteTimeMessage.
     *
     * @param m serializable representation of an AbsoluteTime
     *
     * @return the real AbsoluteTime associated with m
     */
    public static AbsoluteTime fromNetwork(AbsoluteTimeMessage m) {
        return m.value < 0 ? AbsoluteTime.FOREVER : new AbsoluteTime(m.value);
    }

    public Date toDate() {
        return new Date(absValueUs / 1000);
    }

    public long getSeconds() {
        return absValueUs / (1000 * 1000);
    }

    public static AbsoluteTime fromSeconds(long stamp) {
        return new AbsoluteTime(stamp * 1000 * 1000);
    }

    public String toFancyString() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(new Date(getMilliseconds()));
    }

    private long getMilliseconds() {
        return absValueUs / 1000;
    }

    public static AbsoluteTime fromString(String s) {
        Date date = null;

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
            date = sdf.parse(s);
            return new AbsoluteTime(date.getTime() * 1000);
        } catch (ParseException e) {
            // try next format
        }

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm");
            date = sdf.parse(s);
            return new AbsoluteTime(date.getTime() * 1000);
        } catch (ParseException e) {
            // try next format
        }

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd");
            date = sdf.parse(s);
            return new AbsoluteTime(date.getTime() * 1000);
        } catch (ParseException e) {
            // try next format
        }
        return null;
    }
}
