/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.cdi.tck.util;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.jboss.cdi.tck.api.Configuration;
import org.jboss.cdi.tck.impl.ConfigurationFactory;

/**
 * Delays thread execution for specified time or unless stop conditions are satisfied according to the actual {@link ResolutionLogic}. This class is not
 * thread-safe.
 * <p>
 * Setting the sleep interval to the value less than 15 ms is questionable since some operating systems do not provide such precision. Moreover such values may
 * impact test performance.
 * </p>
 * <p>
 * In case of no stop conditions are specified (not recommended), the timer logic corresponds to the regular {@link Thread#sleep(long)} execution.
 * </p>
 */
public class Timer {

    private static final long DEFAULT_SLEEP_INTERVAL = 50l;

    private static final ResolutionLogic DEFAULT_RESOLUTION_LOGIC = ResolutionLogic.DISJUNCTION;

    /**
     * Delay in ms
     */
    private long delay;

    /**
     * Thread sleep interval
     */
    private long sleepInterval;

    /**
     * Stop conditions
     */
    private List<StopCondition> stopConditions = null;

    /**
     * Stop conditions resolution logic
     */
    private ResolutionLogic resolutionLogic;

    private boolean stopConditionsSatisfiedBeforeTimeout;

    /**
     * Create new timer with default delay, sleep interval and stop conditions resolution logic.
     */
    public Timer() {
        reset();
    }

    /**
     * Set the delay value. The value is automatically adjusted according to the {@link Configuration#getTestTimeoutFactor()} so that it's possible to configure
     * timeouts according to the testing runtime performance and throughput.
     *
     * @param delay The delay in milliseconds
     * @return self
     */
    public Timer setDelay(long delay) {
        return setDelay(delay, TimeUnit.MILLISECONDS);
    }

    /**
     * Set the delay value. The value is automatically adjusted according to the {@link Configuration#getTestTimeoutFactor()} so that it's possible to configure
     * timeouts according to the testing runtime performance and throughput.
     *
     * @param delay
     * @param timeUnit
     * @return self
     */
    public Timer setDelay(long delay, TimeUnit timeUnit) {
        if (delay <= 0) {
            throw new IllegalArgumentException("Delay must be greater than zero");
        }
        this.delay = (timeUnit.toMillis(delay) * ConfigurationFactory.get().getTestTimeoutFactor() -1) / 100 + 1;
        return this;
    }

    /**
     * Set new sleep interval value.
     *
     * @param sleepInterval
     * @return self
     */
    public Timer setSleepInterval(long sleepInterval) {
        this.sleepInterval = sleepInterval;
        return this;
    }

    /**
     * Set new resolution logic.
     *
     * @param resolutionLogic
     * @return self
     */
    public Timer setResolutionLogic(ResolutionLogic resolutionLogic) {
        this.resolutionLogic = resolutionLogic;
        return this;
    }

    /**
     * Add new stop condition.
     *
     * @param condition
     * @return self
     */
    public Timer addStopCondition(StopCondition condition) {
        return addStopCondition(condition, false);
    }

    /**
     * Add new stop condition.
     *
     * @param condition
     * @param clear Clear stop conditions and reset {@link #stopConditionsSatisfiedBeforeTimeout}
     * @return self
     */
    public Timer addStopCondition(StopCondition condition, boolean clear) {

        if (condition != null) {

            if (stopConditions == null) {
                stopConditions = new ArrayList<Timer.StopCondition>(5);
            } else if (clear) {
                clearStopConditions();
            }
            stopConditions.add(condition);
        }
        return this;
    }

    /**
     * Start the timer.
     *
     * @throws InterruptedException
     */
    public Timer start() throws InterruptedException {

        checkConfiguration();

        if (stopConditions == null || stopConditions.isEmpty()) {
            Thread.sleep(delay);
        } else {

            long start = System.currentTimeMillis();

            while (resolveSleepConditions(start)) {
                Thread.sleep(sleepInterval);
            }
        }
        return this;
    }

    /**
     * Reset to default values.
     */
    public void reset() {
        this.delay = -1;
        this.sleepInterval = DEFAULT_SLEEP_INTERVAL;
        this.resolutionLogic = DEFAULT_RESOLUTION_LOGIC;
        clearStopConditions();
    }

    /**
     * Clear stop conditions and reset {@link #stopConditionsSatisfiedBeforeTimeout}.
     */
    public void clearStopConditions() {
        if (stopConditions != null) {
            stopConditions.clear();
        }
        this.stopConditionsSatisfiedBeforeTimeout = false;
    }

    /**
     * @return <code>true</code> if stop conditions are satisfied according to actual {@link #resolutionLogic} before timeout occurs, <code>false</code>
     *         otherwise
     */
    public boolean isStopConditionsSatisfiedBeforeTimeout() {
        return stopConditionsSatisfiedBeforeTimeout;
    }

    /**
     * @return the current delay in ms
     */
    public long getDelay() {
        return delay;
    }

    /**
     * @return the current sleep interval in ms
     */
    public long getSleepInterval() {
        return sleepInterval;
    }

    /**
     * Start a new timer with specified delay.
     *
     * @param delay
     * @return finished timer
     * @throws InterruptedException
     */
    public static Timer startNew(long delay) throws InterruptedException {
        return new Timer().setDelay(delay).start();
    }

    /**
     * Start a new timer with specified delay and sleep interval.
     *
     * @param delay
     * @return finished timer
     * @throws InterruptedException
     */
    public static Timer startNew(long delay, long sleepInterval) throws InterruptedException {
        return new Timer().setDelay(delay).setSleepInterval(sleepInterval).start();
    }

    private boolean resolveSleepConditions(long start) {

        if (hasConditionsSatisfied()) {
            stopConditionsSatisfiedBeforeTimeout = true;
            return false;
        }
        if (isTimeoutExpired(start)) {
            return false;
        }
        return true;
    }

    private void checkConfiguration() {
        if (delay < 0 || sleepInterval < 0 || delay < sleepInterval)
            throw new IllegalStateException("Invalid timer configuration");
    }

    private boolean hasConditionsSatisfied() {
        switch (resolutionLogic) {
            case DISJUNCTION:
                return hasAtLeastOneConditionsSatisfied();
            case CONJUNCTION:
                return hasAllConditionsSatisfied();
            default:
                throw new IllegalStateException("Unsupported condition resolution logic");
        }
    }

    private boolean hasAtLeastOneConditionsSatisfied() {

        for (StopCondition condition : stopConditions) {
            if (condition.isSatisfied())
                return true;
        }
        return false;
    }

    private boolean hasAllConditionsSatisfied() {

        for (StopCondition condition : stopConditions) {
            if (!condition.isSatisfied())
                return false;
        }
        return true;
    }

    private boolean isTimeoutExpired(long start) {
        return (System.currentTimeMillis() - start) >= delay;
    }

    /**
     *
     */
    public static interface StopCondition {

        /**
         * @return <code>true</code> if stop condition satisfied, <code>false</code> otherwise
         */
        public boolean isSatisfied();

    }

    public static enum ResolutionLogic {

        /**
         * At least one condition must be satisfied
         */
        DISJUNCTION,
        /**
         * All conditions must be satisfied
         */
        CONJUNCTION, ;

    }

}