/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * 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.optaplanner.core.impl.domain.valuerange.buildin.temporal;

import java.time.DateTimeException;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;

import org.optaplanner.core.impl.domain.valuerange.AbstractCountableValueRange;
import org.optaplanner.core.impl.domain.valuerange.util.ValueRangeIterator;
import org.optaplanner.core.impl.solver.random.RandomUtils;

public class TemporalValueRange<Temporal_ extends Temporal & Comparable<? super Temporal_>>
        extends AbstractCountableValueRange<Temporal_> {

    private final Temporal_ from;
    private final Temporal_ to;
    /** We could not use a {@link TemporalAmount} as {@code incrementUnit} due to lack of calculus functions. */
    private final long incrementUnitAmount;
    private final TemporalUnit incrementUnitType;

    private final long size;

    /**
     * @param from never null, inclusive minimum
     * @param to never null, exclusive maximum, {@code >= from}
     * @param incrementUnitAmount {@code > 0}
     * @param incrementUnitType never null, must be {@link Temporal#isSupported(TemporalUnit) supported} by {@code from}
     *        and {@code to}
     */
    public TemporalValueRange(Temporal_ from, Temporal_ to, long incrementUnitAmount, TemporalUnit incrementUnitType) {
        this.from = from;
        this.to = to;
        this.incrementUnitAmount = incrementUnitAmount;
        this.incrementUnitType = incrementUnitType;

        if (from == null || to == null || incrementUnitType == null) {
            throw new IllegalArgumentException("The " + getClass().getSimpleName()
                    + " must have a from (" + from + "), to (" + to + ") and incrementUnitType (" + incrementUnitType
                    + ") that are not null.");
        }
        if (incrementUnitAmount <= 0) {
            throw new IllegalArgumentException("The " + getClass().getSimpleName()
                    + " must have strictly positive incrementUnitAmount (" + incrementUnitAmount + ").");
        }
        if (!from.isSupported(incrementUnitType) || !to.isSupported(incrementUnitType)) {
            throw new IllegalArgumentException("The " + getClass().getSimpleName()
                    + " must have an incrementUnitType (" + incrementUnitType
                    + ") that is supported by its from (" + from + ") class (" + from.getClass().getSimpleName()
                    + ") and to (" + to + ") class (" + to.getClass().getSimpleName() + ").");
        }
        // We cannot use Temporal.until() to check bounds due to rounding errors
        if (from.compareTo(to) > 0) {
            throw new IllegalArgumentException("The " + getClass().getSimpleName()
                    + " cannot have a from (" + from + ") which is strictly higher than its to (" + to + ").");
        }
        long space = from.until(to, incrementUnitType);
        Temporal expectedTo = from.plus(space, incrementUnitType);
        if (!to.equals(expectedTo)) {
            // Temporal.until() rounds down, but it needs to round up, to be consistent with Temporal.plus()
            space++;
            Temporal roundedExpectedTo;
            try {
                roundedExpectedTo = from.plus(space, incrementUnitType);
            } catch (DateTimeException e) {
                throw new IllegalArgumentException("The " + getClass().getSimpleName()
                        + "'s incrementUnitType (" + incrementUnitType
                        + ") must fit an integer number of times in the space (" + space
                        + ") between from (" + from + ") and to (" + to + ").\n"
                        + "The to (" + to + ") is not the expectedTo (" + expectedTo + ").", e);
            }
            // Fail fast if there's a remainder on type (to be consistent with other value ranges)
            if (!to.equals(roundedExpectedTo)) {
                throw new IllegalArgumentException("The " + getClass().getSimpleName()
                        + "'s incrementUnitType (" + incrementUnitType
                        + ") must fit an integer number of times in the space (" + space
                        + ") between from (" + from + ") and to (" + to + ").\n"
                        + "The to (" + to + ") is not the expectedTo (" + expectedTo
                        + ") nor the roundedExpectedTo (" + roundedExpectedTo + ").");
            }
        }

        // Fail fast if there's a remainder on amount (to be consistent with other value ranges)
        if (space % incrementUnitAmount > 0) {
            throw new IllegalArgumentException("The " + getClass().getSimpleName()
                    + "'s incrementUnitAmount (" + incrementUnitAmount
                    + ") must fit an integer number of times in the space (" + space
                    + ") between from (" + from + ") and to (" + to + ").");
        }
        size = space / incrementUnitAmount;
    }

    @Override
    public long getSize() {
        return size;
    }

    @Override
    public Temporal_ get(long index) {
        if (index >= size || index < 0) {
            throw new IndexOutOfBoundsException();
        }
        return (Temporal_) from.plus(index * incrementUnitAmount, incrementUnitType);
    }

    @Override
    public boolean contains(Temporal_ value) {
        if (value == null || !value.isSupported(incrementUnitType)) {
            return false;
        }
        // We cannot use Temporal.until() to check bounds due to rounding errors
        if (value.compareTo(from) < 0 || value.compareTo(to) >= 0) {
            return false;
        }
        long fromSpace = from.until(value, incrementUnitType);
        if (value.equals(from.plus(fromSpace + 1, incrementUnitType))) {
            // Temporal.until() rounds down, but it needs to round up, to be consistent with Temporal.plus()
            fromSpace++;
        }

        // Only checking the modulus is not enough: 1-MAR + 1 month doesn't include 7-MAR but the modulus is 0 anyway
        return fromSpace % incrementUnitAmount == 0
                && value.equals(from.plus(fromSpace, incrementUnitType));
    }

    @Override
    public Iterator<Temporal_> createOriginalIterator() {
        return new OriginalTemporalValueRangeIterator();
    }

    private class OriginalTemporalValueRangeIterator extends ValueRangeIterator<Temporal_> {

        private long index = 0L;

        @Override
        public boolean hasNext() {
            return index < size;
        }

        @Override
        public Temporal_ next() {
            if (index >= size) {
                throw new NoSuchElementException();
            }

            // Do not use upcoming += incrementUnitAmount because 31-JAN + 1 month + 1 month returns 28-MAR
            Temporal_ next = get(index);
            index++;
            return next;
        }

    }

    @Override
    public Iterator<Temporal_> createRandomIterator(Random workingRandom) {
        return new RandomTemporalValueRangeIterator(workingRandom);
    }

    private class RandomTemporalValueRangeIterator extends ValueRangeIterator<Temporal_> {

        private final Random workingRandom;

        public RandomTemporalValueRangeIterator(Random workingRandom) {
            this.workingRandom = workingRandom;
        }

        @Override
        public boolean hasNext() {
            return size > 0L;
        }

        @Override
        public Temporal_ next() {
            long index = RandomUtils.nextLong(workingRandom, size);
            return get(index);
        }

    }

    @Override
    public String toString() {
        return "[" + from + "-" + to + ")"; // Formatting: interval (mathematics) ISO 31-11
    }

}
