/*
 * 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.test.performance;

import static com.google.common.base.Preconditions.checkArgument;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

import java.io.File;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;

import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.time.Duration;
import org.apache.commons.io.FileUtils;

import com.google.common.annotations.Beta;
import com.google.common.base.MoreObjects;

/**
 * For building up a description of what to measure.
 * <p>
 * Users are strongly encouraged to call the setter methods, rather than accessing the fields 
 * directly. The fields may be made protected in a future release.
 * <p>
 * The following fields are compulsory:
 * <ul>
 *   <li>{@link #job(Runnable)}
 *   <li>Exactly one of {@link #duration(Duration)} or {@link #iterations(int)} 
 * </ul>
 * 
 * See {@link PerformanceTestUtils#run(PerformanceTestDescriptor)}.
 */
@Beta
public class PerformanceTestDescriptor {
    public String summary;
    public Duration warmup;
    public Integer warmupIterations;
    public Duration duration;
    public Integer iterations;
    public int numConcurrentJobs = 1;
    public Duration abortIfIterationLongerThan;
    public Runnable job;
    public Runnable preJob;
    public Runnable postJob;
    public Runnable postWarmup;
    public CountDownLatch completionLatch;
    public Duration completionLatchTimeout = Duration.FIVE_MINUTES;
    public Double minAcceptablePerSecond;
    public Duration sampleCpuInterval = Duration.ONE_SECOND;
    public Duration logInterval = Duration.FIVE_SECONDS;
    public boolean histogram = true;
    public MeasurementResultPersister persister = new FilePersister(new File(FileUtils.getUserDirectory(), "brooklyn-performance"));
    public boolean sealed;
    
    public static PerformanceTestDescriptor create() {
        return new PerformanceTestDescriptor();
    }
    
    public static PerformanceTestDescriptor create(String summary) {
        return create().summary(summary);
    }
    
    public PerformanceTestDescriptor summary(String val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.summary = val; return this;
    }
    
    /**
     * The length of time to repeatedly execute the job for, before doing the proper performance 
     * test. At most one of {@link #warmup(Duration)} or {@link #warmupIterations(int)} should be
     * set - if neither is set, the warmup defaults to one tenth of the test duration.
     */
    public PerformanceTestDescriptor warmup(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.warmup = val; return this;
    }
    
    /**
     * See {@link #warmup(Duration)}.
     */
    public PerformanceTestDescriptor warmupIterations(int val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.warmupIterations = val; return this;
    }
    
    /**
     * The length of time to repeatedly execute the job for, when measuring the performance.
     * Exactly one of {@link #duration(Duration)} or {@link #iterations(int)} should be
     * set.
     */
    public PerformanceTestDescriptor duration(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.duration = val; return this;
    }
    
    /**
     * See {@link #duration(Duration)}.
     */
    public PerformanceTestDescriptor iterations(int val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.iterations = val; return this;
    }

    /**
     * The number of concurrent jobs to execute. If used with {@link #preJob(Runnable)} or {@link #postJob(Runnable)},
     * then those pre/post hooks will be called before/after the concurrent jobs are all done.
     */
    public PerformanceTestDescriptor numConcurrentJobs(int val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        checkArgument(val >= 1, "val (%s) must be one or more", val);
        this.numConcurrentJobs = val; return this;
    }

    public PerformanceTestDescriptor abortIfIterationLongerThan(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.abortIfIterationLongerThan = val; return this;
    }
    
    /**
     * The job to be repeatedly executed.
     */
    public PerformanceTestDescriptor job(Runnable val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.job = val; return this;
    }
    
    /**
     * See {@link #job(Runnable)}
     */
    public PerformanceTestDescriptor job(Callable<?> val) {
        return job(new Runnable() {
            public void run() {
                try {
                    val.call();
                } catch (Exception e) {
                    throw Exceptions.propagate(e);
                }
            }});
    }
    
    /**
     * To be run each time before the job (pausing the timer while this is run).
     */
    public PerformanceTestDescriptor preJob(Runnable val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.preJob = val; return this;
    }
    
    /**
     * To be run each time after the job (pausing the timer while this is run).
     */
    public PerformanceTestDescriptor postJob(Runnable val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.postJob = val; return this;
    }
    
    /**
     * To be run once, after the warmup.
     */
    public PerformanceTestDescriptor postWarmup(Runnable val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.postWarmup = val; return this;
    }
    
    /**
     * If non-null, the performance test will wait for this latch before stopping the timer.
     * This is useful for asynchronous work. For example, 1000 iterations of the job might
     * be executed that each submits work asynchronously, and then the latch signals when all
     * of those 1000 tasks have completed.
     */
    public PerformanceTestDescriptor completionLatch(CountDownLatch val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.completionLatch = val; return this;
    }
    
    /**
     * The maximum length of time to wait for the {@link #completionLatch(CountDownLatch)}, after 
     * executing the designated number of jobs. If the latch has not completed within this time, 
     * then the test will fail.
     */
    public PerformanceTestDescriptor completionLatchTimeout(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.completionLatchTimeout = val; return this;
    }

    /**
     * If non-null, the measured jobs-per-second will be compared against this number. If the 
     * jobs-per-second is not high enough, then the test wil fail.
     */
    public PerformanceTestDescriptor minAcceptablePerSecond(Double val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.minAcceptablePerSecond = val; return this;
    }
    
    /**
     * Whether to collect a histogram of the individual job times. This histogram stores the count
     * in buckets (e.g. number of jobs that took 1-2ms, number that took 2-4ms, number that took
     * 4-8ms, etc).
     */
    public PerformanceTestDescriptor histogram(boolean val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.histogram = val; return this;
    }
    
    /**
     * How often to log progress (e.g. number of iterations completed so far). If null, then no 
     * progress will be logged.
     */
    public PerformanceTestDescriptor logInterval(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.logInterval = val; return this;
    }
    
    /**
     * How often to calculate + record the fraction of CPU being used. If null, then CPU usage 
     * will not be recorded. 
     */
    public PerformanceTestDescriptor sampleCpuInterval(Duration val) {
        if (sealed) throw new IllegalStateException("Should not modify after sealed (e.g. after use)");
        this.sampleCpuInterval = val; return this;
    }
    
    public void seal() {
        sealed = true;
        assertNotNull(job, "Job must be supplied: "+toString());
        assertTrue(duration != null ^ iterations != null, "Exactly one of duration or iterations must be set: "+toString());
        assertFalse(warmup != null && warmupIterations != null, "At most one of duration and iterations must be set: "+toString());
        if (warmup == null && warmupIterations == null) {
            if (duration != null) warmup = Duration.millis(duration.toMilliseconds() / 10);
            if (iterations != null) warmupIterations = iterations / 10;
        }
        if (summary == null) {
            summary = job.toString();
        }
    }
    
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .omitNullValues()
                .add("summary", summary)
                .add("duration", duration)
                .add("warmup", warmup)
                .add("iterations", iterations)
                .add("warmupIterations", warmupIterations)
                .add("job", job)
                .add("completionLatch", completionLatch)
                .add("minAcceptablePerSecond", minAcceptablePerSecond)
                .add("abortIfIterationLongerThan", abortIfIterationLongerThan)
                .add("numConcurrentJobs", numConcurrentJobs)
                .toString();
    }
}