/*
 This file is part of GNUnet.
 (C) 2011, 2012 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 3, 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;

/**
 * Positive amount of time with no point of reference.
 * 
 * @author Florian Dold
 */
public final class RelativeTime implements Comparable<RelativeTime> {
    private static final Logger logger = LoggerFactory
            .getLogger(RelativeTime.class);

    public static final RelativeTime MICROSECOND = new RelativeTime(1);
    public static final RelativeTime MILLISECOND = MICROSECOND.multiply(1000);
    public static final RelativeTime SECOND = MILLISECOND.multiply(1000);
    public static final RelativeTime MINUTE = SECOND.multiply(60);
    public static final RelativeTime HOUR = MINUTE.multiply(60);
    public static final RelativeTime DAY = HOUR.multiply(24);
    public static final RelativeTime WEEK = DAY.multiply(7);
    public static final RelativeTime MONTH = DAY.multiply(30);
    public static final RelativeTime YEAR = DAY.multiply(365);

    public static final RelativeTime ZERO = new RelativeTime(0);
    public static final RelativeTime FOREVER = new RelativeTime(Long.MAX_VALUE);
    public static final RelativeTime STD_BACKOFF = MILLISECOND;
    public static final RelativeTime STD_BACKOFF_MAX = MINUTE.multiply(15);

    /**
     * Time offset in microseconds.
     */
    private final long rel_value_us;

    /**
     * Create a new RelativeTime value, with a given time in milliseconds.
     *
     * @param abs_value time in milliseconds
     */
    public RelativeTime(final long abs_value) {
        this.rel_value_us = abs_value;
    }

    public static RelativeTime fromMilliseconds(final long ms) {
        return new RelativeTime(ms * 1000);
    }

    public static RelativeTime fromMicroseconds(final long us) {
        return new RelativeTime(us);
    }

    /**
     * Add relative times together.
     * 
     * @param other
     *            the other timestamp
     * 
     * @return this + other
     */
    public RelativeTime add(final RelativeTime other) {
        if (this.rel_value_us == Long.MAX_VALUE
                || other.rel_value_us == Long.MAX_VALUE) {
            return RelativeTime.FOREVER;
        }
        final long new_rel_value = this.rel_value_us + other.rel_value_us;
        // check for numeric overflow
        if (new_rel_value < this.rel_value_us) {
            logger.warn("time overflow");
            return RelativeTime.FOREVER;
        }
        return new RelativeTime(new_rel_value);
    }

    /**
     * Divide relative time by a given factor.
     *
     * @param factor
     *            integer to divide by
     * @return FOREVER if this=FOREVER or factor=0; otherwise this/factor
     */
    public RelativeTime divide(final int factor) {
        if (factor == 0 || this.rel_value_us == Long.MAX_VALUE) {
            return RelativeTime.FOREVER;
        }
        return new RelativeTime(this.rel_value_us / factor);
    }

    /**
     * Returns the amount of time in milliseconds.
     * 
     * @return the amount of time in milliseconds
     */
    public long getMicroseconds() {
        return rel_value_us;
    }

    /**
     * Return the maximum of two relative time values.
     * 
     * @return max(t1, t2)
     */
    public static RelativeTime max(RelativeTime t1, RelativeTime t2) {
        return t1.rel_value_us >= t2.rel_value_us ? t1 : t2;
    }

    /**
     * Return the minimum of two relative time values.
     * 
     * @return min(this, other)
     */
    public static RelativeTime min(RelativeTime t1, RelativeTime t2) {
        return t1.rel_value_us <= t2.rel_value_us ? t1 : t2;
    }

    /**
     * Multiply relative time by a given factor.
     * 
     * @return FOREVER if this=FOREVER or on overflow; otherwise this*factor
     */
    public RelativeTime multiply(final int factor) {
        if (factor == 0) {
            return RelativeTime.ZERO;
        }
        final long ret = this.rel_value_us * factor;
        // check for numeric overflow
        if (ret / factor != rel_value_us) {
            logger.warn("time overflow");
            return RelativeTime.FOREVER;
        }
        return new RelativeTime(ret);
    }

    /**
     * Subtract relative timestamp from the other.
     * 
     * @param other
     *            second timestamp
     * @return ZERO if other>=this (including both FOREVER), FOREVER if
     *         this=FOREVER, this-other otherwise
     */
    public RelativeTime subtract(final RelativeTime other) {
        if (this.rel_value_us >= other.rel_value_us) {
            return RelativeTime.ZERO;
        } else if (this.rel_value_us == Long.MAX_VALUE) {
            return this;
        } else {
            return new RelativeTime(this.rel_value_us - other.rel_value_us);
        }
    }

    /**
     * Converts relative time to an absolute time in the future.
     * 
     * @return timestamp that is in the future, or FOREVER if this=FOREVER (or
     *         if we would overflow)
     */
    public AbsoluteTime toAbsolute() {
        return AbsoluteTime.now().add(this);
    }

    public boolean isForever() {
        return rel_value_us == FOREVER.rel_value_us;
    }

    public boolean equals(Object o) {
        return (o instanceof RelativeTime) && ((RelativeTime) o).rel_value_us == rel_value_us;
    }

    @Override
    public int hashCode() {
        return (int) this.rel_value_us;
    }

    @Override
    public int compareTo(RelativeTime other) {
        if (this.rel_value_us < other.rel_value_us) {
            return -1;
        }
        if (this.rel_value_us > other.rel_value_us) {
            return 1;
        }
        return 0;
    }

    @Override
    public String toString() {
        if (this.isForever()) {
            return "RelativeTime(FOREVER)";
        }
        return "RelativeTime("+this.rel_value_us +")";
    }



    public RelativeTimeMessage toNetwork() {
        long rval = this.rel_value_us;
        assert rval >= 0;
        if (rval == FOREVER.rel_value_us) {
            rval = -1L; /* 0xFFFFFFFFFFFFFFFF for network format! */
        }
        return new RelativeTimeMessage(rval);
    }

    public static RelativeTime fromNetwork(RelativeTimeMessage m) {
        if (m.value__ < 0) {
            return RelativeTime.FOREVER;
        } else {
            return new RelativeTime(m.value__);
        }
    }

    public RelativeTime backoff() {
        return RelativeTime.min(STD_BACKOFF_MAX, this.multiply(2));
    }

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

    public static RelativeTime fromSeconds(long i) {
        RelativeTime relativeTime = new RelativeTime(i * 1000 * 1000);
        return relativeTime;
    }
}
