/*
 *  Copyright 2001-2013 Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.joda.time.base;

import java.io.Serializable;

import org.joda.time.Chronology;
import org.joda.time.DateTimeUtils;
import org.joda.time.DurationField;
import org.joda.time.DurationFieldType;
import org.joda.time.MutablePeriod;
import org.joda.time.Period;
import org.joda.time.PeriodType;
import org.joda.time.ReadableInstant;
import org.joda.time.ReadablePartial;
import org.joda.time.ReadablePeriod;
import org.joda.time.chrono.ISOChronology;
import org.joda.time.field.FieldUtils;

/**
 * BaseSingleFieldPeriod is an abstract implementation of ReadablePeriod that
 * manages a single duration field, such as days or minutes.
 * <p>
 * This class should generally not be used directly by API users.
 * The {@link ReadablePeriod} interface should be used when different 
 * kinds of period objects are to be referenced.
 * <p>
 * BaseSingleFieldPeriod subclasses may be mutable and not thread-safe.
 *
 * @author Stephen Colebourne
 * @since 1.4
 */
public abstract class BaseSingleFieldPeriod
        implements ReadablePeriod, Comparable<BaseSingleFieldPeriod>, Serializable {

    /** Serialization version. */
    private static final long serialVersionUID = 9386874258972L;
    /** The start of 1972. */
    private static final long START_1972 = 2L * 365L * 86400L * 1000L;

    /** The period in the units of this period. */
    private volatile int iPeriod;

    //-----------------------------------------------------------------------
    /**
     * Calculates the number of whole units between the two specified datetimes.
     *
     * @param start  the start instant, validated to not be null
     * @param end  the end instant, validated to not be null
     * @param field  the field type to use, must not be null
     * @return the period
     * @throws IllegalArgumentException if the instants are null or invalid
     */
    protected static int between(ReadableInstant start, ReadableInstant end, DurationFieldType field) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("ReadableInstant objects must not be null");
        }
        Chronology chrono = DateTimeUtils.getInstantChronology(start);
        int amount = field.getField(chrono).getDifference(end.getMillis(), start.getMillis());
        return amount;
    }

    //-----------------------------------------------------------------------
    /**
     * Calculates the number of whole units between the two specified partial datetimes.
     * <p>
     * The two partials must contain the same fields, for example you can specify
     * two <code>LocalDate</code> objects.
     *
     * @param start  the start partial date, validated to not be null
     * @param end  the end partial date, validated to not be null
     * @param zeroInstance  the zero instance constant, must not be null
     * @return the period
     * @throws IllegalArgumentException if the partials are null or invalid
     */
    protected static int between(ReadablePartial start, ReadablePartial end, ReadablePeriod zeroInstance) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("ReadablePartial objects must not be null");
        }
        if (start.size() != end.size()) {
            throw new IllegalArgumentException("ReadablePartial objects must have the same set of fields");
        }
        for (int i = 0, isize = start.size(); i < isize; i++) {
            if (start.getFieldType(i) != end.getFieldType(i)) {
                throw new IllegalArgumentException("ReadablePartial objects must have the same set of fields");
            }
        }
        if (DateTimeUtils.isContiguous(start) == false) {
            throw new IllegalArgumentException("ReadablePartial objects must be contiguous");
        }
        Chronology chrono = DateTimeUtils.getChronology(start.getChronology()).withUTC();
        int[] values = chrono.get(zeroInstance, chrono.set(start, START_1972), chrono.set(end, START_1972));
        return values[0];
    }

    /**
     * Creates a new instance representing the number of complete standard length units
     * in the specified period.
     * <p>
     * This factory method converts all fields from the period to hours using standardised
     * durations for each field. Only those fields which have a precise duration in
     * the ISO UTC chronology can be converted.
     * <ul>
     * <li>One week consists of 7 days.
     * <li>One day consists of 24 hours.
     * <li>One hour consists of 60 minutes.
     * <li>One minute consists of 60 seconds.
     * <li>One second consists of 1000 milliseconds.
     * </ul>
     * Months and Years are imprecise and periods containing these values cannot be converted.
     *
     * @param period  the period to get the number of hours from, must not be null
     * @param millisPerUnit  the number of milliseconds in one standard unit of this period
     * @return the int value
     * @throws IllegalArgumentException if the period contains imprecise duration values
     */
    protected static int standardPeriodIn(ReadablePeriod period, long millisPerUnit) {
        if (period == null) {
            return 0;
        }
        Chronology iso = ISOChronology.getInstanceUTC();
        long duration = 0L;
        for (int i = 0; i < period.size(); i++) {
            int value = period.getValue(i);
            if (value != 0) {
                DurationField field = period.getFieldType(i).getField(iso);
                if (field.isPrecise() == false) {
                    throw new IllegalArgumentException(
                            "Cannot convert period to duration as " + field.getName() +
                            " is not precise in the period " + period);
                }
                duration = FieldUtils.safeAdd(duration, FieldUtils.safeMultiply(field.getUnitMillis(), value));
            }
        }
        return FieldUtils.safeToInt(duration / millisPerUnit);
    }

    //-----------------------------------------------------------------------
    /**
     * Creates a new instance representing the specified period.
     *
     * @param period  the period to represent
     */
    protected BaseSingleFieldPeriod(int period) {
        super();
        iPeriod = period;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the amount of this period.
     *
     * @return the period value
     */
    protected int getValue() {
        return iPeriod;
    }

    /**
     * Sets the amount of this period.
     * To make a subclass immutable you must declare it final, or block this method.
     *
     * @param value  the period value
     */
    protected void setValue(int value) {
        iPeriod = value;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the single duration field type.
     *
     * @return the duration field type, not null
     */
    public abstract DurationFieldType getFieldType();

    /**
     * Gets the period type which matches the duration field type.
     *
     * @return the period type, not null
     */
    public abstract PeriodType getPeriodType();

    //-----------------------------------------------------------------------
    /**
     * Gets the number of fields that this period supports, which is one.
     *
     * @return the number of fields supported, which is one
     */
    public int size() {
        return 1;
    }

    /**
     * Gets the field type at the specified index.
     * <p>
     * The only index supported by this period is zero which returns the
     * field type of this class.
     *
     * @param index  the index to retrieve, which must be zero
     * @return the field at the specified index
     * @throws IndexOutOfBoundsException if the index is invalid
     */
    public DurationFieldType getFieldType(int index) {
        if (index != 0) {
            throw new IndexOutOfBoundsException(String.valueOf(index));
        }
        return getFieldType();
    }

    /**
     * Gets the value at the specified index.
     * <p>
     * The only index supported by this period is zero.
     *
     * @param index  the index to retrieve, which must be zero
     * @return the value of the field at the specified index
     * @throws IndexOutOfBoundsException if the index is invalid
     */
    public int getValue(int index) {
        if (index != 0) {
            throw new IndexOutOfBoundsException(String.valueOf(index));
        }
        return getValue();
    }

    /**
     * Gets the value of a duration field represented by this period.
     * <p>
     * If the field type specified does not match the type used by this class
     * then zero is returned.
     *
     * @param type  the field type to query, null returns zero
     * @return the value of that field, zero if field not supported
     */
    public int get(DurationFieldType type) {
        if (type == getFieldType()) {
            return getValue();
        }
        return 0;
    }

    /**
     * Checks whether the duration field specified is supported by this period.
     *
     * @param type  the type to check, may be null which returns false
     * @return true if the field is supported
     */
    public boolean isSupported(DurationFieldType type) {
        return (type == getFieldType());
    }

    //-----------------------------------------------------------------------
    /**
     * Get this period as an immutable <code>Period</code> object.
     * The period will use <code>PeriodType.standard()</code>.
     *
     * @return a <code>Period</code> representing the same number of days
     */
    public Period toPeriod() {
        return Period.ZERO.withFields(this);
    }

    /**
     * Get this object as a <code>MutablePeriod</code>.
     * <p>
     * This will always return a new <code>MutablePeriod</code> with the same fields.
     * The period will use <code>PeriodType.standard()</code>.
     * 
     * @return a MutablePeriod using the same field set and values
     */
    public MutablePeriod toMutablePeriod() {
        MutablePeriod period = new MutablePeriod();
        period.add(this);
        return period;
    }

    //-----------------------------------------------------------------------
    /**
     * Compares this object with the specified object for equality based on the
     * value of each field. All ReadablePeriod instances are accepted, but only
     * those with a matching <code>PeriodType</code> can return true.
     *
     * @param period  a readable period to check against
     * @return true if all the field values are equal, false if
     *  not or the period is null or of an incorrect type
     */
    @Override
    public boolean equals(Object period) {
        if (this == period) {
            return true;
        }
        if (period instanceof ReadablePeriod == false) {
            return false;
        }
        ReadablePeriod other = (ReadablePeriod) period;
        return (other.getPeriodType() == getPeriodType() && other.getValue(0) == getValue());
    }

    /**
     * Gets a hash code for the period as defined by ReadablePeriod.
     *
     * @return a hash code
     */
    @Override
    public int hashCode() {
        int total = 17;
        total = 27 * total + getValue();
        total = 27 * total + getFieldType().hashCode();
        return total;
    }

    /**
     * Compares this period to another object of the same class.
     *
     * @param other  the other period, must not be null
     * @return zero if equal, positive if greater, negative if less
     * @throws NullPointerException if the other period is null
     * @throws ClassCastException if the other period is of a different type
     */
    public int compareTo(BaseSingleFieldPeriod other) {
        if (other.getClass() != getClass()) {
            throw new ClassCastException(getClass() + " cannot be compared to " + other.getClass());
        }
        int otherValue = other.getValue();
        int thisValue = getValue();
        if (thisValue > otherValue) {
            return 1;
        }
        if (thisValue < otherValue) {
            return -1;
        }
        return 0;
    }

}
