/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.commons.numbers.combinatorics;

import org.apache.commons.numbers.gamma.LogGamma;

/**
 * Class for computing the natural logarithm of the factorial of a number.
 * It allows to allocate a cache of precomputed values.
 * In case of cache miss, computation is performed by a call to
 * {@link LogGamma#value(double)}.
 */
public final class LogFactorial {
    /**
     * Size of precomputed factorials.
     * @see Factorial
     */
    private static final int FACTORIALS_CACHE_SIZE = 21;
    /**
     * Precomputed values of the function: {@code logFactorials[i] = Math.log(i!)}.
     */
    private final double[] logFactorials;

    /**
     * Creates an instance, reusing the already computed values if available.
     *
     * @param numValues Number of values of the function to compute.
     * @param cache Cached values.
     * @throws IllegalArgumentException if {@code n < 0}.
     */
    private LogFactorial(int numValues,
                         double[] cache) {
        if (numValues < 0) {
            throw new CombinatoricsException(CombinatoricsException.NEGATIVE, numValues);
        }

        logFactorials = new double[numValues];

        final int beginCopy = 2;
        final int endCopy;
        if (cache == null || cache.length <= beginCopy) {
            endCopy = beginCopy;
        } else {
            endCopy = Math.min(cache.length, numValues);
        }


        // Copy available values.
        if (endCopy - beginCopy > 0) {
            System.arraycopy(cache, beginCopy, logFactorials, beginCopy, endCopy - beginCopy);
        }

        // Precompute.
        for (int i = endCopy; i < numValues; i++) {
            logFactorials[i] = logFactorials[i - 1] + Math.log(i);
        }
    }

    /**
     * Creates an instance with no precomputed values.
     * @return instance with no precomputed values
     */
    public static LogFactorial create() {
        return new LogFactorial(0, null);
    }

    /**
     * Creates an instance with the specified cache size.
     *
     * @param cacheSize Number of precomputed values of the function.
     * @return a new instance where {@code cacheSize} values have been
     * precomputed.
     * @throws IllegalArgumentException if {@code cacheSize < 0}.
     */
    public LogFactorial withCache(final int cacheSize) {
        return new LogFactorial(cacheSize, logFactorials);
    }

    /**
     * Computes \( log_e(n!) \).
     *
     * @param n Argument.
     * @return {@code log(n!)}.
     * @throws IllegalArgumentException if {@code n < 0}.
     */
    public double value(int n) {
        if (n < 0) {
            throw new CombinatoricsException(CombinatoricsException.NEGATIVE, n);
        }

        // Use cache of precomputed values.
        if (n < logFactorials.length) {
            return logFactorials[n];
        }

        // Use cache of precomputed factorial values.
        if (n < FACTORIALS_CACHE_SIZE) {
            return Math.log(Factorial.value(n));
        }

        // Delegate.
        return LogGamma.value(n + 1.0);
    }
}
