/*
 * @(#)Counter.java
 *
 * Copyright (c) 2002-2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.util;

import java.io.Serializable;
import java.util.Objects;

import static com.jscape.util.Parameters.assertGreaterOrEqual;
import static com.jscape.util.Parameters.assertStateValid;


/**
 * The integral objects counter.
 *
 * @author Alex Usun
 */
public class Counter
        implements Serializable {

    private long lowerBound;
    private long upperBound;
    private long value;
    private boolean wrapped;

    /**
     * Creates a new counter instance.
     *
     * @param lowerBound the counter lower bound
     * @param upperBound the counter upper bound
     * @param wrapped    wrapping flag
     */
    public Counter(long lowerBound, long upperBound, boolean wrapped) {
        assertGreaterOrEqual(upperBound, lowerBound, "Invalid bounds.");

        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
        this.value = this.lowerBound;
        this.wrapped = wrapped;
    }

    /**
     * Copy constructor.
     *
     * @param that counter to copy
     */
    public Counter(Counter that) {
        this(that.lowerBound, that.upperBound, that.wrapped);
    }

    /**
     * Returns the lower counter bound.
     *
     * @return the lower counter bound
     */
    public long getLowerBound() {
        return this.lowerBound;
    }

    /**
     * Returns the upper counter bound.
     *
     * @return the upper counter bound
     */
    public long getUpperBound() {
        return this.upperBound;
    }

    /**
     * Checks if wrapping is eanbled.
     *
     * @return <code>true</code> if wrapping is enabled;
     * <code>false</code> otherwise
     */
    public boolean isWrapped() {
        return this.wrapped;
    }

    /**
     * Returns current value.
     *
     * @return current value
     */
    public synchronized long getValue() {
        return this.value;
    }

    public synchronized boolean incrementAllowed() {
        return (this.wrapped || this.value != this.upperBound);
    }

    public synchronized boolean decrementAllowed() {
        return (this.wrapped || this.value != this.lowerBound);
    }

    /**
     * Moves the counter to the next value.
     *
     * @throws IllegalStateException if counter shouldn't wrap and upper bound is reached
     */
    public synchronized void increment() {
        assertStateValid(incrementAllowed(), "Upper bound reached.");

        ++this.value;

        if (this.wrapped && this.value == (this.upperBound + 1)) {
            this.value = this.lowerBound;
        }
    }

    /**
     * Moves the counter to the previous value.
     *
     * @throws IllegalStateException if counter shouldn't wrap and lower bound is reached
     */
    public synchronized void decrement() {
        assertStateValid(decrementAllowed(), "Lower bound reached.");

        --this.value;

        if (this.wrapped && this.value == (this.lowerBound - 1)) {
            this.value = this.upperBound;
        }
    }

    /**
     * Resets the counter.
     */
    public synchronized void reset() {
        this.value = this.lowerBound;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Counter that = (Counter) o;

        return this.lowerBound == that.lowerBound
               && this.upperBound == that.upperBound
               && this.wrapped == that.wrapped;
    }

    @Override
    public int hashCode() {
        return Objects.hash(lowerBound, upperBound, wrapped);
    }

    @Override
    public String toString() {
        return "Counter {"
               + "lowerBound=" + this.lowerBound
               + ", upperBound=" + this.upperBound
               + ", value=" + this.value
               + ", wrapped=" + this.wrapped
               + '}';
    }
}
