/*
 * 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.jmeter.timers;

import java.io.Serializable;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.testelement.ThreadListener;
import org.apache.jmeter.threads.JMeterContextService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The purpose of the SyncTimer is to block threads until X number of threads
 * have been blocked, and then they are all released at once. A SyncTimer can
 * thus create large instant loads at various points of the test plan.
 *
 */
public class SyncTimer extends AbstractTestElement implements Timer, Serializable, TestBean, TestStateListener, ThreadListener {
    private static final Logger log = LoggerFactory.getLogger(SyncTimer.class);

    /**
     * Wrapper to {@link CyclicBarrier} to allow lazy init of CyclicBarrier when SyncTimer is configured with 0
     */
    private static class BarrierWrapper implements Cloneable {

        private CyclicBarrier barrier;

        /**
         *
         */
        public BarrierWrapper() {
            this.barrier = null;
        }

        /**
         * @param parties Number of parties
         */
        public BarrierWrapper(int parties) {
            this.barrier = new CyclicBarrier(parties);
        }

        /**
         * Synchronized is required to ensure CyclicBarrier is initialized only once per Thread Group
         * @param parties Number of parties
         */
        public synchronized void setup(int parties) {
            if(this.barrier== null) {
                this.barrier = new CyclicBarrier(parties);
            }
        }


        /**
         * Wait until all threads called await on this timer
         * 
         * @return The arrival index of the current thread
         * @throws InterruptedException
         *             when interrupted while waiting, or the interrupted status
         *             is set on entering this method
         * @throws BrokenBarrierException
         *             if the barrier is reset while waiting or broken on
         *             entering or while waiting
         * @see java.util.concurrent.CyclicBarrier#await()
         */
        public int await() throws InterruptedException, BrokenBarrierException{
            return barrier.await();
        }
        
        /**
         * Wait until all threads called await on this timer
         * 
         * @param timeout
         *            The timeout in <code>timeUnit</code> units
         * @param timeUnit
         *            The time unit for the <code>timeout</code>
         * @return The arrival index of the current thread
         * @throws InterruptedException
         *             when interrupted while waiting, or the interrupted status
         *             is set on entering this method
         * @throws BrokenBarrierException
         *             if the barrier is reset while waiting or broken on
         *             entering or while waiting
         * @throws TimeoutException
         *             if the specified time elapses
         * @see java.util.concurrent.CyclicBarrier#await()
         */
        public int await(long timeout, TimeUnit timeUnit) throws InterruptedException, BrokenBarrierException, TimeoutException {
            return barrier.await(timeout, timeUnit);
        }

        /**
         * @see java.util.concurrent.CyclicBarrier#reset()
         */
        public void reset() {
            barrier.reset();
        }

        /**
         * @see java.lang.Object#clone()
         */
        @Override
        protected Object clone()  {
            BarrierWrapper barrierWrapper=  null;
            try {
                barrierWrapper = (BarrierWrapper) super.clone();
                barrierWrapper.barrier = this.barrier;
            } catch (CloneNotSupportedException e) {
                //Cannot happen
            }
            return barrierWrapper;
        }
    }

    private static final long serialVersionUID = 3;

    private transient BarrierWrapper barrier;

    private int groupSize;
    
    private long timeoutInMs;

    // Ensure transient object is created by the server
    private Object readResolve(){
        createBarrier();
        return this;
    }

    /**
     * @return Returns the numThreads.
     */
    public int getGroupSize() {
        return groupSize;
    }

    /**
     * @param numThreads
     *            The numThreads to set.
     */
    public void setGroupSize(int numThreads) {
        this.groupSize = numThreads;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long delay() {
        if(getGroupSize()>=0) {
            int arrival = 0;
            try {
                if(timeoutInMs==0) {
                    arrival = this.barrier.await();                    
                } else if(timeoutInMs > 0){
                    arrival = this.barrier.await(timeoutInMs, TimeUnit.MILLISECONDS);
                } else {
                    throw new IllegalArgumentException("Negative value for timeout:"+timeoutInMs+" in Synchronizing Timer "+getName());
                }
            } catch (InterruptedException | BrokenBarrierException e) {
                return 0;
            } catch (TimeoutException e) {
                if (log.isWarnEnabled()) {
                    log.warn("SyncTimer {} timeouted waiting for users after: {}ms", getName(), getTimeoutInMs());
                }
                return 0;
            } finally {
                if(arrival == 0) {
                    barrier.reset();
                }
            }
        }
        return 0;
    }

    /**
     * We have to control the cloning process because we need some cross-thread
     * communication if our synctimers are to be able to determine when to block
     * and when to release.
     */
    @Override
    public Object clone() {
        SyncTimer newTimer = (SyncTimer) super.clone();
        newTimer.barrier = barrier;
        return newTimer;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void testEnded() {
        this.testEnded(null);
    }

    /**
     * Reset timerCounter
     */
    @Override
    public void testEnded(String host) {
        createBarrier();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void testStarted() {
        testStarted(null);
    }

    /**
     * Reset timerCounter
     */
    @Override
    public void testStarted(String host) {
        createBarrier();
    }

    /**
     *
     */
    private void createBarrier() {
        if(getGroupSize() == 0) {
            // Lazy init
            this.barrier = new BarrierWrapper();
        } else {
            this.barrier = new BarrierWrapper(getGroupSize());
        }
    }

    @Override
    public void threadStarted() {
        if(getGroupSize() == 0) {
            int numThreadsInGroup = JMeterContextService.getContext().getThreadGroup().getNumThreads();
            // Unique Barrier creation ensured by synchronized setup
            this.barrier.setup(numThreadsInGroup);
        }
    }

    @Override
    public void threadFinished() {
        // NOOP
    }

    /**
     * @return the timeoutInMs
     */
    public long getTimeoutInMs() {
        return timeoutInMs;
    }

    /**
     * @param timeoutInMs the timeoutInMs to set
     */
    public void setTimeoutInMs(long timeoutInMs) {
        this.timeoutInMs = timeoutInMs;
    }
}
