/*
 * 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.brooklyn.util.core.internal;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.brooklyn.util.JavaGroovyEquivalents;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.FlagUtils;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Callables;

/**
 * Simple DSL to repeat a fragment of code periodically until a condition is satisfied.
 *
 * In its simplest case, it is passed two {@link groovy.lang.Closure}s / {@link Callable} - 
 * the first is executed, then the second. If the second closure returns false, the loop
 * is repeated; if true, it finishes. Further customization can be applied to set the period 
 * between loops and place a maximum limit on how long the loop should run for.
 * <p>
 * It is configured in a <em>fluent</em> manner. For example, in Groovy:
 * <pre>
 * {@code
 * Repeater.create("Wait until the Frobnitzer is ready")
 *     .repeat {
 *         status = frobnitzer.getStatus()
 *     }
 *     .until {
 *         status == "Ready" || status == "Failed"
 *     }
 *     .limitIterationsTo(30)
 *     .run()
 * }
 * </pre>
 * 
 * Or in Java:
 * <pre>
 * {@code
 * Repeater.create("Wait until the Frobnitzer is ready")
 *     .until(new Callable<Boolean>() {
 *              public Boolean call() {
 *                  String status = frobnitzer.getStatus()
 *                  return "Ready".equals(status) || "Failed".equals(status);
 *              }})
 *     .limitIterationsTo(30)
 *     .run()
 * }
 * </pre>
 * 
 * @deprecated since 0.7.0, use {@link org.apache.brooklyn.util.repeat.Repeater} instead
 */
@Deprecated
public class Repeater {
    
    // TODO Was converted to Java, from groovy. Needs thorough review and improvements
    // to use idiomatic java
    
    private static final Logger log = LoggerFactory.getLogger(Repeater.class);

    @SetFromFlag
    private String description;
    private Callable<?> body = Callables.returning(null);
    private Callable<Boolean> exitCondition;
    @SetFromFlag
    private Long period = null;
    @SetFromFlag("timeout")
    private Long durationLimit = null;
    private int iterationLimit = 0;
    private boolean rethrowException = false;
    private boolean rethrowExceptionImmediately = false;
    private boolean warnOnUnRethrownException = true;

    public Repeater() {
        this(MutableMap.of(), null);
    }

    public Repeater(Map<?,?> flags) {
        this(flags, null);
    }

    public Repeater(String description) {
        this(MutableMap.of(), description);
    }
    
    /**
     * Construct a new instance of Repeater.
     *
     * @param flags       can include period, timeout, description
     * @param description a description of the operation that will appear in debug logs.
     */
    public Repeater(Map<?,?> flags, String description) {
        setFromFlags(flags);
        this.description = JavaGroovyEquivalents.elvis(description, this.description, "Repeater");
    }

    public void setFromFlags(Map<?,?> flags) {
        FlagUtils.setFieldsFromFlags(flags, this);
    }
    
    public static Repeater create() {
        return create(MutableMap.of());
    }
    public static Repeater create(Map<?,?> flags) {
        return create(flags, null);
    }
    public static Repeater create(String description) {
        return create(MutableMap.of(), description);
    }
    public static Repeater create(Map<?,?> flags, String description) {
        return new Repeater(flags, description);
    }

    /**
     * Sets the main body of the loop to be a no-op.
     * 
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater repeat() {
        return repeat(Callables.returning(null));
    }
    
    /**
     * Sets the main body of the loop.
     *
     * @param body a closure or other Runnable that is executed in the main body of the loop.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater repeat(Runnable body) {
        checkNotNull(body, "body must not be null");
        this.body = (body instanceof Callable) ? (Callable<?>)body : Executors.callable(body);
        return this;
    }
    
    /**
     * Sets the main body of the loop.
     *
     * @param body a closure or other Callable that is executed in the main body of the loop.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater repeat(Callable<?> body) {
        checkNotNull(body, "body must not be null");
        this.body = body;
        return this;
    }

    /**
     * Set how long to wait between loop iterations.
     *
     * @param period how long to wait between loop iterations.
     * @param unit the unit of measurement of the period.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater every(long period, TimeUnit unit) {
        Preconditions.checkArgument(period > 0, "period must be positive: %s", period);
        checkNotNull(unit, "unit must not be null");
        this.period = unit.toMillis(period);
        return this;
    }

    /**
     * @see #every(long, TimeUnit)
     */
    public Repeater every(Duration duration) {
        Preconditions.checkNotNull(duration, "duration must not be null");
        Preconditions.checkArgument(duration.toMilliseconds()>0, "period must be positive: %s", duration);
        this.period = duration.toMilliseconds();
        return this;
    }
    
    public Repeater every(groovy.time.Duration duration) {
        return every(Duration.of(duration));
    }

    /**
     * @see #every(long, TimeUnit)
     * @deprecated specify unit
     */
    @Deprecated
    public Repeater every(long duration) {
        return every(duration, TimeUnit.MILLISECONDS);
    }

    /**
     * Set code fragment that tests if the loop has completed.
     *
     * @param exitCondition a closure or other Callable that returns a boolean. If this code returns {@literal true} then the
     * loop will stop executing.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater until(Callable<Boolean> exitCondition) {
        Preconditions.checkNotNull(exitCondition, "exitCondition must not be null");
        this.exitCondition = exitCondition;
        return this;
    }

    /**
     * If the exit condition check throws an exception, it will be recorded and the last exception will be thrown on failure.
     *
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater rethrowException() {
        this.rethrowException = true;
        return this;
    }

    /**
     * If the repeated body or the exit condition check throws an exception, then propagate that exception immediately.
     *
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater rethrowExceptionImmediately() {
        this.rethrowExceptionImmediately = true;
        return this;
    }

    public Repeater suppressWarnings() {
        this.warnOnUnRethrownException = false;
        return this;
    }

    /**
     * Set the maximum number of iterations.
     *
     * The loop will exit if the condition has not been satisfied after this number of iterations.
     *
     * @param iterationLimit the maximum number of iterations.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater limitIterationsTo(int iterationLimit) {
        Preconditions.checkArgument(iterationLimit > 0, "iterationLimit must be positive: %s", iterationLimit);
        this.iterationLimit = iterationLimit;
        return this;
    }

    /**
     * Set the amount of time to wait for the condition.
     * The repeater will wait at least this long for the condition to be true,
     * and will exit soon after even if the condition is false.
     *
     * @param deadline the time that the loop should wait.
     * @param unit the unit of measurement of the period.
     * @return {@literal this} to aid coding in a fluent style.
     */
    public Repeater limitTimeTo(long deadline, TimeUnit unit) {
        Preconditions.checkArgument(deadline > 0, "deadline must be positive: %s", deadline);
        Preconditions.checkNotNull(unit, "unit must not be null");
        this.durationLimit = unit.toMillis(deadline);
        return this;
    }

    /**
     * @see #limitTimeTo(long, TimeUnit)
     */
    public Repeater limitTimeTo(Duration duration) {
        Preconditions.checkNotNull(duration, "duration must not be null");
        Preconditions.checkArgument(duration.toMilliseconds() > 0, "deadline must be positive: %s", duration);
        this.durationLimit = duration.toMilliseconds();
        return this;
    }

    /**
     * Run the loop.
     *
     * @return true if the exit condition was satisfied; false if the loop terminated for any other reason.
     */
    public boolean run() {
        Preconditions.checkState(body != null, "repeat() method has not been called to set the body");
        Preconditions.checkState(exitCondition != null, "until() method has not been called to set the exit condition");
        Preconditions.checkState(period != null, "every() method has not been called to set the loop period time units");

        Throwable lastError = null;
        int iterations = 0;
        long endTime = -1;
        if (durationLimit != null) {
            endTime = System.currentTimeMillis() + durationLimit;
        }

        while (true) {
            iterations++;

            try {
                body.call();
            } catch (Exception e) {
                log.warn(description, e);
                if (rethrowExceptionImmediately) throw Exceptions.propagate(e);
            }

            boolean done = false;
            try {
                lastError = null;
                done = exitCondition.call();
            } catch (Exception e) {
                if (log.isDebugEnabled()) log.debug(description, e);
                lastError = e;
                if (rethrowExceptionImmediately) throw Exceptions.propagate(e);
            }
            if (done) {
                if (log.isDebugEnabled()) log.debug("{}: condition satisfied", description);
                return true;
            } else {
                if (log.isDebugEnabled()) {
                    String msg = String.format("%s: unsatisfied during iteration %s %s", description, iterations,
                            (iterationLimit > 0 ? "(max "+iterationLimit+" attempts)" : "") + 
                            (endTime > 0 ? "("+Time.makeTimeStringRounded(endTime - System.currentTimeMillis())+" remaining)" : ""));
                    if (iterations == 1) {
                        log.debug(msg);
                    } else {
                        log.trace(msg);
                    }
                }
            }

            if (iterationLimit > 0 && iterations == iterationLimit) {
                if (log.isDebugEnabled()) log.debug("{}: condition not satisfied and exceeded iteration limit", description);
                if (rethrowException && lastError != null) {
                    log.warn("{}: error caught checking condition (rethrowing): {}", description, lastError.getMessage());
                    throw Exceptions.propagate(lastError);
                }
                if (warnOnUnRethrownException && lastError != null)
                    log.warn("{}: error caught checking condition: {}", description, lastError.getMessage());
                return false;
            }

            if (endTime > 0) {
                if (System.currentTimeMillis() > endTime) {
                    if (log.isDebugEnabled()) log.debug("{}: condition not satisfied and deadline {} passed", 
                            description, Time.makeTimeStringRounded(endTime - System.currentTimeMillis()));
                    if (rethrowException && lastError != null) {
                        log.error("{}: error caught checking condition: {}", description, lastError.getMessage());
                        throw Exceptions.propagate(lastError);
                    }
                    return false;
                }
            }

            Time.sleep(period);
        }
    }
}
