/*
 *  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.field;

import org.joda.time.DateTimeField;
import org.joda.time.DateTimeFieldType;
import org.joda.time.DurationField;

/**
 * Divides a DateTimeField such that the retrieved values are reduced by a
 * fixed divisor. The field's unit duration is scaled accordingly, but the
 * range duration is unchanged.
 * <p>
 * DividedDateTimeField is thread-safe and immutable.
 *
 * @see RemainderDateTimeField
 * 
 * @author Stephen Colebourne
 * @author Brian S O'Neill
 * @since 1.0
 */
public class DividedDateTimeField extends DecoratedDateTimeField {

    @SuppressWarnings("unused")
    private static final long serialVersionUID = 8318475124230605365L;

    // Shared with RemainderDateTimeField.
    final int iDivisor;
    final DurationField iDurationField;
    final DurationField iRangeDurationField;

    private final int iMin;
    private final int iMax;

    /**
     * Constructor.
     * 
     * @param field  the field to wrap, like "year()".
     * @param type  the field type this field will actually use
     * @param divisor  divisor, such as 100 years in a century
     * @throws IllegalArgumentException if divisor is less than two
     */
    public DividedDateTimeField(DateTimeField field,
                                DateTimeFieldType type, int divisor) {
        this(field, field.getRangeDurationField(), type, divisor);
    }

    /**
     * Constructor.
     * 
     * @param field  the field to wrap, like "year()".
     * @param rangeField  the range field, null to derive
     * @param type  the field type this field will actually use
     * @param divisor  divisor, such as 100 years in a century
     * @throws IllegalArgumentException if divisor is less than two
     */
    public DividedDateTimeField(DateTimeField field, DurationField rangeField,
                                DateTimeFieldType type, int divisor) {
        super(field, type);
        if (divisor < 2) {
            throw new IllegalArgumentException("The divisor must be at least 2");
        }
        DurationField unitField = field.getDurationField();
        if (unitField == null) {
            iDurationField = null;
        } else {
            iDurationField = new ScaledDurationField(
                unitField, type.getDurationType(), divisor);
        }
        iRangeDurationField = rangeField;
        iDivisor = divisor;
        int i = field.getMinimumValue();
        int min = (i >= 0) ? i / divisor : ((i + 1) / divisor - 1);
        int j = field.getMaximumValue();
        int max = (j >= 0) ? j / divisor : ((j + 1) / divisor - 1);
        iMin = min;
        iMax = max;
    }

    /**
     * Construct a DividedDateTimeField that compliments the given
     * RemainderDateTimeField.
     *
     * @param remainderField  complimentary remainder field, like "yearOfCentury()".
     * @param type  the field type this field will actually use
     */
    public DividedDateTimeField(RemainderDateTimeField remainderField, DateTimeFieldType type) {
        this(remainderField, null, type);
    }

    /**
     * Construct a DividedDateTimeField that compliments the given
     * RemainderDateTimeField.
     *
     * @param remainderField  complimentary remainder field, like "yearOfCentury()".
     * @param rangeField  the range field, null to derive
     * @param type  the field type this field will actually use
     */
    public DividedDateTimeField(RemainderDateTimeField remainderField, DurationField rangeField, DateTimeFieldType type) {
        super(remainderField.getWrappedField(), type);
        int divisor = iDivisor = remainderField.iDivisor;
        iDurationField = remainderField.iRangeField;
        iRangeDurationField = rangeField;
        DateTimeField field = getWrappedField();
        int i = field.getMinimumValue();
        int min = (i >= 0) ? i / divisor : ((i + 1) / divisor - 1);
        int j = field.getMaximumValue();
        int max = (j >= 0) ? j / divisor : ((j + 1) / divisor - 1);
        iMin = min;
        iMax = max;
    }

    @Override
    public DurationField getRangeDurationField() {
        if (iRangeDurationField != null) {
            return iRangeDurationField;
        }
        return super.getRangeDurationField();
    }

    /**
     * Get the amount of scaled units from the specified time instant.
     * 
     * @param instant  the time instant in millis to query.
     * @return the amount of scaled units extracted from the input.
     */
    public int get(long instant) {
        int value = getWrappedField().get(instant);
        if (value >= 0) {
            return value / iDivisor;
        } else {
            return ((value + 1) / iDivisor) - 1;
        }
    }

    /**
     * Add the specified amount of scaled units to the specified time
     * instant. The amount added may be negative.
     * 
     * @param instant  the time instant in millis to update.
     * @param amount  the amount of scaled units to add (can be negative).
     * @return the updated time instant.
     */
    public long add(long instant, int amount) {
        return getWrappedField().add(instant, amount * iDivisor);
    }

    /**
     * Add the specified amount of scaled units to the specified time
     * instant. The amount added may be negative.
     * 
     * @param instant  the time instant in millis to update.
     * @param amount  the amount of scaled units to add (can be negative).
     * @return the updated time instant.
     */
    public long add(long instant, long amount) {
        return getWrappedField().add(instant, amount * iDivisor);
    }

    /**
     * Add to the scaled component of the specified time instant,
     * wrapping around within that component if necessary.
     * 
     * @param instant  the time instant in millis to update.
     * @param amount  the amount of scaled units to add (can be negative).
     * @return the updated time instant.
     */
    public long addWrapField(long instant, int amount) {
        return set(instant, FieldUtils.getWrappedValue(get(instant), amount, iMin, iMax));
    }

    public int getDifference(long minuendInstant, long subtrahendInstant) {
        return getWrappedField().getDifference(minuendInstant, subtrahendInstant) / iDivisor;
    }

    public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
        return getWrappedField().getDifferenceAsLong(minuendInstant, subtrahendInstant) / iDivisor;
    }

    /**
     * Set the specified amount of scaled units to the specified time instant.
     * 
     * @param instant  the time instant in millis to update.
     * @param value  value of scaled units to set.
     * @return the updated time instant.
     * @throws IllegalArgumentException if value is too large or too small.
     */
    public long set(long instant, int value) {
        FieldUtils.verifyValueBounds(this, value, iMin, iMax);
        int remainder = getRemainder(getWrappedField().get(instant));
        return getWrappedField().set(instant, value * iDivisor + remainder);
    }

    /**
     * Returns a scaled version of the wrapped field's unit duration field.
     */
    public DurationField getDurationField() {
        return iDurationField;
    }

    /**
     * Get the minimum value for the field.
     * 
     * @return the minimum value
     */
    public int getMinimumValue() {
        return iMin;
    }

    /**
     * Get the maximum value for the field.
     * 
     * @return the maximum value
     */
    public int getMaximumValue() {
        return iMax;
    }

    public long roundFloor(long instant) {
        DateTimeField field = getWrappedField();
        return field.roundFloor(field.set(instant, get(instant) * iDivisor));
    }

    public long remainder(long instant) {
        return set(instant, get(getWrappedField().remainder(instant)));
    }

    /**
     * Returns the divisor applied, in the field's units.
     * 
     * @return the divisor
     */
    public int getDivisor() {
        return iDivisor;
    }

    private int getRemainder(int value) {
        if (value >= 0) {
            return value % iDivisor;
        } else {
            return (iDivisor - 1) + ((value + 1) % iDivisor);
        }
    }

}
