package anastore.util;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Represents a bounded or unbounded range of times.  An unbounded
 * time range can be bounded, but not vice-versa.  The values of the
 * upper and lower bounds are <i>inclusive</i>.
 */
public class TimeRange implements Comparable<TimeRange>, Serializable
{
    private final long _lb;
    private final AtomicLong _ub;

    /**
     * Construct a time range with the specified lower bound and no
     * upper bound.
     *
     * @throws IllegalArgumentException if lowerBound is negative.
     */
    public TimeRange(long lowerBound) throws IllegalArgumentException
    {
        if (lowerBound < 0)
            throw new IllegalArgumentException
                ("Lower bound must be non-negative");

        _lb = lowerBound;
        _ub = new AtomicLong(-1);
    }

    /**
     * Construct a time range with the specified lower and upper
     * bounds.
     *
     * @throws IllegalArgumentException if upperBound is less than or
     * equal to lowerBound or if either bound is negative.
     */
    public TimeRange(long lowerBound, long upperBound)
        throws IllegalArgumentException
    {
        if (lowerBound < 0)
            throw new IllegalArgumentException
                ("Lower bound must be non-negative");
        if (upperBound < lowerBound)
            throw new IllegalArgumentException
                ("Upper bound must be >= lower bound");

        _lb = lowerBound;
        _ub = new AtomicLong(upperBound);
    }

    /**
     * Construct a time range that is a copy of another time range.
     */
    public TimeRange(TimeRange o)
    {
        _lb = o._lb;
        _ub = new AtomicLong(o._ub.get());
    }

    /**
     * Get the lower bound of this range.  Any timestamp <i>strictly
     * less than</i> this is outside of the range.
     */
    public long getLowerBound()
    {
        return _lb;
    }

    /**
     * Determine if this is a bounded or unbounded time range.
     */
    public boolean hasUpperBound()
    {
        return _ub.get() != -1;
    }

    /**
     * Get the upper bound of this range.  Any timestamp <i>strictly
     * greater than</i> this is outside of the range.
     */
    public long getUpperBound() throws IllegalStateException
    {
        long ub = _ub.get();
        if (ub == -1)
            throw new IllegalStateException("Range is unbounded");
        return ub;
    }

    /**
     * Sets the upper bound to ub.
     *
     * @throws IllegalArgumentException if ub is less than the lower bound.
     * @throws IllegalStateException if this time range has an upper bound.
     */
    public void setUpperBound(long ub)
        throws IllegalArgumentException, IllegalStateException
    {
        if (ub < _lb)
            throw new IllegalArgumentException("Upper bound must be >= lower bound");
        if (!_ub.compareAndSet(-1, ub))
            throw new IllegalStateException("Time range is already bounded");
    }

    /**
     * Returns true if and only if timestamp is contained in this time
     * range.  For unbounded timestamps, a lower bound on the upper
     * bound must be specified.
     *
     * @throws IllegalArgumentException if it cannot be determined if
     * the range contains timestamp (that is, if this is an unbounded
     * range and timestamp is greater than leastUpperBound)
     */
    public boolean contains(long timestamp, long leastUpperBound)
        throws IllegalArgumentException
    {
        if (timestamp < getLowerBound())
            return false;
        if (hasUpperBound())
            return timestamp <= getUpperBound();
        else if (timestamp <= leastUpperBound)
            return true;
        else
            throw new IllegalArgumentException
                ("Cannot determine if " + this + " contains " + timestamp +
                 " given a LUB of " + leastUpperBound);
    }

    /**
     * Compares this time range against another.  The time ranges must
     * not overlap (unless they exactly overlap and will always
     * exactly overlap in the future) or have the possibility of
     * overlapping in the future.
     *
     * @throws IllegalArgumentException if this range cannot be
     * compared against o (which includes if o is null).
     */
    public int compareTo(TimeRange o)
        throws IllegalArgumentException
    {
        if (o == null)
            throw new IllegalArgumentException
                ("Cannot compare time range against null");

        // If we're comparing against ourselves, it doesn't matter
        // even if the range is unbounded.
        if (o == this)
            return 0;

        long myUb = _ub.get();
        long oUb = o._ub.get();

        if (_lb < o._lb) {
            if (myUb != -1 && myUb < o._lb)
                return -1;
        } else if (_lb == o._lb) {
            // If either range is unbounded, then they may get
            // different upper bounds in the future, so we consider
            // them uncomparable.
            if (myUb != -1 && myUb == oUb)
                return 0;
        } else if (_lb > o._lb) {
            if (oUb != -1 && oUb < _lb)
                return 1;
        }

        if (myUb == -1 || oUb == -1)
            throw new IllegalArgumentException
                ("Time ranges " + this + " and " + o + " may overlap");
        else
            throw new IllegalArgumentException
                ("Time ranges " + this + " and " + o + " overlap");
    }

    /**
     * Test whether two time ranges represent the same range of time.
     * Note that two unbounded ranges are not considered equal unless
     * they are the same object.  This ensures that two ranges that
     * are considered equal <i>cannot</i>, at a later time, be
     * considered not equal; however, two ranges that are considered
     * not equal <i>can</i>, at a later time, be considered equal.
     */
    @Override
    public boolean equals(Object o)
    {
        if (o == null || !(o instanceof TimeRange))
            return false;

        TimeRange t = (TimeRange)o;

        if (t == this)
            return true;

        long myUb = _ub.get();
        long oUb = t._ub.get();
        if (_lb == t._lb && myUb != -1 && myUb == oUb)
            return true;
        return false;
    }

    /**
     * Compute the hash code of this time range.  This is guaranteed
     * to not change over time, so unbounded time ranges can be safely
     * used in hash tables.
     */
    @Override
    public int hashCode()
    {
        return (int)(_lb ^ (_lb >>> 32));
    }

    /**
     * Convert this time range to a string indicating the bounds of
     * the range.
     */
    @Override
    public String toString()
    {
        if (hasUpperBound())
            return "[" + getLowerBound() + ".." + getUpperBound() + "]";
        else
            return "[" + getLowerBound() + "..?]";
    }
}
