/*
 * 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.maven.surefire.junitcore.pc;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

import org.apache.maven.surefire.api.testset.TestSetFailedException;
import org.apache.maven.surefire.junitcore.JUnitCoreParameters;
import org.junit.runner.Description;

/**
 * An algorithm which configures {@link ParallelComputer} with allocated thread resources by given
 * {@link org.apache.maven.surefire.junitcore.JUnitCoreParameters}.
 * The {@code AbstractSurefireMojo} has to provide correct combinations of thread-counts and
 * configuration parameter {@code parallel}.
 *
 * @author Tibor Digana (tibor17)
 * @see org.apache.maven.surefire.junitcore.pc.ParallelComputerBuilder
 * @since 2.16
 */
final class ParallelComputerUtil {
    private static final Collection<Description> UNUSED_DESCRIPTIONS = Arrays.asList(
            null, Description.createSuiteDescription("null"), Description.TEST_MECHANISM, Description.EMPTY);

    private static int availableProcessors = Runtime.getRuntime().availableProcessors();

    private ParallelComputerUtil() {
        throw new IllegalStateException("Suppresses calling constructor, ensuring non-instantiability.");
    }

    /*
     * For testing purposes.
     */
    static void overrideAvailableProcessors(int availableProcessors) {
        ParallelComputerUtil.availableProcessors = availableProcessors;
    }

    /*
     * For testing purposes.
     */
    static void setDefaultAvailableProcessors() {
        ParallelComputerUtil.availableProcessors = Runtime.getRuntime().availableProcessors();
    }

    static Concurrency resolveConcurrency(JUnitCoreParameters params, RunnerCounter counts)
            throws TestSetFailedException {
        if (!params.isParallelismSelected()) {
            throw new TestSetFailedException("Unspecified parameter '" + JUnitCoreParameters.PARALLEL_KEY + "'.");
        }

        if (!params.isUseUnlimitedThreads() && !hasThreadCount(params) && !hasThreadCounts(params)) {
            throw new TestSetFailedException("Unspecified thread-count(s). "
                    + "See the parameters "
                    + JUnitCoreParameters.USEUNLIMITEDTHREADS_KEY + ", "
                    + JUnitCoreParameters.THREADCOUNT_KEY + ", "
                    + JUnitCoreParameters.THREADCOUNTSUITES_KEY + ", "
                    + JUnitCoreParameters.THREADCOUNTCLASSES_KEY + ", "
                    + JUnitCoreParameters.THREADCOUNTMETHODS_KEY + ".");
        }

        if (params.isUseUnlimitedThreads()) {
            return concurrencyForUnlimitedThreads(params);
        } else if (hasThreadCount(params)) {
            if (hasThreadCounts(params)) {
                return isLeafUnspecified(params)
                        ? concurrencyFromAllThreadCountsButUnspecifiedLeafCount(params, counts)
                        : concurrencyFromAllThreadCounts(params);
            } else {
                return estimateConcurrency(params, counts);
            }
        } else {
            return concurrencyFromThreadCounts(params);
        }
    }

    static boolean isUnusedDescription(Description examined) {
        if (UNUSED_DESCRIPTIONS.contains(examined)) {
            return true;
        } else {
            // UNUSED_DESCRIPTIONS ensures that "examined" cannot be null
            for (Description unused : UNUSED_DESCRIPTIONS) {
                if (unused != null && unused.getDisplayName().equals(examined.getDisplayName())) {
                    return true;
                }
            }
            return false;
        }
    }

    static void removeUnusedDescriptions(Collection<Description> examined) {
        for (Iterator<Description> it = examined.iterator(); it.hasNext(); ) {
            if (isUnusedDescription(it.next())) {
                it.remove();
            }
        }
    }

    private static Concurrency concurrencyForUnlimitedThreads(JUnitCoreParameters params) {
        Concurrency concurrency = new Concurrency();
        concurrency.suites = params.isParallelSuites() ? threadCountSuites(params) : 0;
        concurrency.classes = params.isParallelClasses() ? threadCountClasses(params) : 0;
        concurrency.methods = params.isParallelMethods() ? threadCountMethods(params) : 0;
        concurrency.capacity = Integer.MAX_VALUE;
        return concurrency;
    }

    private static Concurrency estimateConcurrency(JUnitCoreParameters params, RunnerCounter counts) {
        final Concurrency concurrency = new Concurrency();
        final int parallelEntities = countParallelEntities(params);
        concurrency.capacity = multiplyByCoreCount(params, params.getThreadCount());
        if (parallelEntities == 1 || counts == null || counts.classes == 0) {
            // Estimate parallel thread counts.
            double ratio = 1d / parallelEntities;
            int threads = multiplyByCoreCount(params, ratio * params.getThreadCount());
            concurrency.suites = params.isParallelSuites() ? minSuites(threads, counts) : 0;
            concurrency.classes = params.isParallelClasses() ? minClasses(threads, counts) : 0;
            concurrency.methods = params.isParallelMethods() ? minMethods(threads, counts) : 0;
            if (parallelEntities == 1) {
                concurrency.capacity = 0;
            } else {
                adjustLeaf(params, concurrency);
            }
        } else {
            // Try to allocate suites+classes+methods within threadCount,
            concurrency.suites = params.isParallelSuites() ? toNonNegative(counts.suites) : 0;
            concurrency.classes = params.isParallelClasses() ? toNonNegative(counts.classes) : 0;
            concurrency.methods =
                    params.isParallelMethods() ? toNonNegative(Math.ceil(counts.methods / (double) counts.classes)) : 0;
            double sum = toNonNegative(concurrency.suites + concurrency.classes + concurrency.methods);
            if (concurrency.capacity < sum && sum != 0) {
                // otherwise allocate them using the weighting factor < 1.
                double weight = concurrency.capacity / sum;
                concurrency.suites *= weight;
                concurrency.classes *= weight;
                concurrency.methods *= weight;
            }
            adjustLeaf(params, concurrency);
        }
        return concurrency;
    }

    private static Concurrency concurrencyFromAllThreadCountsButUnspecifiedLeafCount(
            JUnitCoreParameters params, RunnerCounter counts) {
        Concurrency concurrency = new Concurrency();
        concurrency.suites = params.isParallelSuites() ? params.getThreadCountSuites() : 0;
        concurrency.suites = params.isParallelSuites() ? multiplyByCoreCount(params, concurrency.suites) : 0;
        concurrency.classes = params.isParallelClasses() ? params.getThreadCountClasses() : 0;
        concurrency.classes = params.isParallelClasses() ? multiplyByCoreCount(params, concurrency.classes) : 0;
        concurrency.methods = params.isParallelMethods() ? params.getThreadCountMethods() : 0;
        concurrency.methods = params.isParallelMethods() ? multiplyByCoreCount(params, concurrency.methods) : 0;
        concurrency.capacity = multiplyByCoreCount(params, params.getThreadCount());

        if (counts != null) {
            concurrency.suites = toNonNegative(Math.min(concurrency.suites, counts.suites));
            concurrency.classes = toNonNegative(Math.min(concurrency.classes, counts.classes));
        }

        setLeafInfinite(params, concurrency);

        return concurrency;
    }

    private static Concurrency concurrencyFromAllThreadCounts(JUnitCoreParameters params) {
        Concurrency concurrency = new Concurrency();
        concurrency.suites = params.isParallelSuites() ? params.getThreadCountSuites() : 0;
        concurrency.classes = params.isParallelClasses() ? params.getThreadCountClasses() : 0;
        concurrency.methods = params.isParallelMethods() ? params.getThreadCountMethods() : 0;
        concurrency.capacity = params.getThreadCount();
        double all = sumThreadCounts(concurrency);

        concurrency.suites = params.isParallelSuites()
                ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.suites / all))
                : 0;

        concurrency.classes = params.isParallelClasses()
                ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.classes / all))
                : 0;

        concurrency.methods = params.isParallelMethods()
                ? multiplyByCoreCount(params, concurrency.capacity * (concurrency.methods / all))
                : 0;

        concurrency.capacity = multiplyByCoreCount(params, concurrency.capacity);
        adjustPrecisionInLeaf(params, concurrency);
        return concurrency;
    }

    private static Concurrency concurrencyFromThreadCounts(JUnitCoreParameters params) {
        Concurrency concurrency = new Concurrency();
        concurrency.suites = params.isParallelSuites() ? threadCountSuites(params) : 0;
        concurrency.classes = params.isParallelClasses() ? threadCountClasses(params) : 0;
        concurrency.methods = params.isParallelMethods() ? threadCountMethods(params) : 0;
        concurrency.capacity = toNonNegative(sumThreadCounts(concurrency));
        return concurrency;
    }

    private static int countParallelEntities(JUnitCoreParameters params) {
        int count = 0;
        if (params.isParallelSuites()) {
            count++;
        }

        if (params.isParallelClasses()) {
            count++;
        }

        if (params.isParallelMethods()) {
            count++;
        }
        return count;
    }

    private static void adjustPrecisionInLeaf(JUnitCoreParameters params, Concurrency concurrency) {
        if (params.isParallelMethods()) {
            concurrency.methods = concurrency.capacity - concurrency.suites - concurrency.classes;
        } else if (params.isParallelClasses()) {
            concurrency.classes = concurrency.capacity - concurrency.suites;
        }
    }

    private static void adjustLeaf(JUnitCoreParameters params, Concurrency concurrency) {
        if (params.isParallelMethods()) {
            concurrency.methods = Integer.MAX_VALUE;
        } else if (params.isParallelClasses()) {
            concurrency.classes = Integer.MAX_VALUE;
        }
    }

    private static void setLeafInfinite(JUnitCoreParameters params, Concurrency concurrency) {
        if (params.isParallelMethods()) {
            concurrency.methods = Integer.MAX_VALUE;
        } else if (params.isParallelClasses()) {
            concurrency.classes = Integer.MAX_VALUE;
        } else if (params.isParallelSuites()) {
            concurrency.suites = Integer.MAX_VALUE;
        }
    }

    private static boolean isLeafUnspecified(JUnitCoreParameters params) {
        int maskOfParallel = params.isParallelSuites() ? 4 : 0;
        maskOfParallel |= params.isParallelClasses() ? 2 : 0;
        maskOfParallel |= params.isParallelMethods() ? 1 : 0;

        int maskOfConcurrency = params.getThreadCountSuites() > 0 ? 4 : 0;
        maskOfConcurrency |= params.getThreadCountClasses() > 0 ? 2 : 0;
        maskOfConcurrency |= params.getThreadCountMethods() > 0 ? 1 : 0;

        maskOfConcurrency &= maskOfParallel;

        int leaf = Integer.lowestOneBit(maskOfParallel);
        return maskOfConcurrency == maskOfParallel - leaf;
    }

    private static double sumThreadCounts(Concurrency concurrency) {
        double sum = concurrency.suites;
        sum += concurrency.classes;
        sum += concurrency.methods;
        return sum;
    }

    private static boolean hasThreadCounts(JUnitCoreParameters jUnitCoreParameters) {
        return (jUnitCoreParameters.isParallelSuites() && jUnitCoreParameters.getThreadCountSuites() > 0)
                || (jUnitCoreParameters.isParallelClasses() && jUnitCoreParameters.getThreadCountClasses() > 0)
                || (jUnitCoreParameters.isParallelMethods() && jUnitCoreParameters.getThreadCountMethods() > 0);
    }

    private static boolean hasThreadCount(JUnitCoreParameters jUnitCoreParameters) {
        return jUnitCoreParameters.getThreadCount() > 0;
    }

    private static int threadCountMethods(JUnitCoreParameters jUnitCoreParameters) {
        return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountMethods());
    }

    private static int threadCountClasses(JUnitCoreParameters jUnitCoreParameters) {
        return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountClasses());
    }

    private static int threadCountSuites(JUnitCoreParameters jUnitCoreParameters) {
        return multiplyByCoreCount(jUnitCoreParameters, jUnitCoreParameters.getThreadCountSuites());
    }

    private static int multiplyByCoreCount(JUnitCoreParameters jUnitCoreParameters, double threadsPerCore) {
        double numberOfThreads = jUnitCoreParameters.isPerCoreThreadCount()
                ? threadsPerCore * (double) availableProcessors
                : threadsPerCore;

        return numberOfThreads > 0 ? toNonNegative(numberOfThreads) : Integer.MAX_VALUE;
    }

    private static int minSuites(int threads, RunnerCounter counts) {
        long count = counts == null ? Integer.MAX_VALUE : counts.suites;
        return Math.min(threads, toNonNegative(count));
    }

    private static int minClasses(int threads, RunnerCounter counts) {
        long count = counts == null ? Integer.MAX_VALUE : counts.classes;
        return Math.min(threads, toNonNegative(count));
    }

    private static int minMethods(int threads, RunnerCounter counts) {
        long count = counts == null ? Integer.MAX_VALUE : counts.methods;
        return Math.min(threads, toNonNegative(count));
    }

    private static int toNonNegative(long num) {
        return (int) Math.min(num > 0 ? num : 0, Integer.MAX_VALUE);
    }

    private static int toNonNegative(double num) {
        return (int) Math.min(num > 0 ? num : 0, Integer.MAX_VALUE);
    }
}
