/*
 * JBoss, Home of Professional Open Source
 * Copyright 2015, Red Hat, Inc. and/or its affiliates, 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.as.quickstarts.threadracing;

import org.jboss.as.quickstarts.threadracing.results.ChampionshipStandings;
import org.jboss.as.quickstarts.threadracing.results.RaceResult;
import org.jboss.as.quickstarts.threadracing.results.RaceResults;

import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The awesome Java EE thread race. It's the core of the app logic, but has no usage of Java EE technologies, thus of limited interest to study.
 *
 * @author Eduardo Martins
 */
public class Race {

    /**
     * a barrier used to sync racers for the race start
     */
    private CyclicBarrier startBarrier;

    /**
     * a count down latch used by the race to know when all racers finished/aborted
     */
    private CountDownLatch endCountDownLatch;

    /**
     * provides the positions to racer's who finish the race, the initial value is the first position, which increments on a racer finish
     */
    private AtomicInteger donePosition;

    /**
     * provides the position to racer's who abandon the race, the initial value is the last possible position, which decrements on a racer abort
     */
    private AtomicInteger abortedPosition;

    /**
     * the race result
     */
    private RaceResult result;

    /**
     * the race's environment
     */
    private final Map<String, String> environment;

    /**
     * the race's broadcaster
     */
    private final RaceBroadcaster broadcaster;

    /**
     * the past race results
     */
    private final RaceResults results;

    /**
     * the racer #1
     */
    private final Racer racer1;

    /**
     * the racer #2
     */
    private final Racer racer2;

    /**
     * the racer #3
     */
    private final Racer racer3;

    /**
     * the racer #4
     */
    private final Racer racer4;

    /**
     * Creates a new race with the specified racers and environment.
     * @param racer1
     * @param racer2
     * @param racer3
     * @param racer4
     * @param environment
     * @param broadcaster the broadcaster that will be used to update fans about the race progress.
     * @param results the race results
     */
    public Race(Racer racer1, Racer racer2, Racer racer3, Racer racer4, Map<String, String> environment, RaceBroadcaster broadcaster, RaceResults results) {
        this.racer1 = racer1;
        this.racer2 = racer2;
        this.racer3 = racer3;
        this.racer4 = racer4;
        this.environment = environment;
        this.broadcaster = broadcaster;
        this.results = results;
    }

    /**
     * Starts the race.
     * @throws Exception if there is an unexpected issue with the race, such as racers taking too long being ready to start, or not finishing the race in time.
     */
    public synchronized void run() throws Exception {
        reset();
        broadcaster.start();
        registerRacers();
        startEngines();
        startRace();
        awaitEnd();
        processResult();
    }

    /**
     * Resets the race state.
     */
    private void reset() {
        startBarrier = new CyclicBarrier(5);
        endCountDownLatch = new CountDownLatch(4);
        donePosition = new AtomicInteger(1);
        abortedPosition = new AtomicInteger(4);
        result = new RaceResult();
    }

    /**
     * Registers all racers.
     */
    private void registerRacers() {
        racer1.setRegistration(new Registration(racer1, 1));
        racer2.setRegistration(new Registration(racer2, 2));
        racer3.setRegistration(new Registration(racer3, 3));
        racer4.setRegistration(new Registration(racer4, 4));
    }

    /**
     * Starts racer's engines.
     */
    private void startEngines() {
        broadcaster.startYourEngines();
        racer1.startEngine();
        racer2.startEngine();
        racer3.startEngine();
        racer4.startEngine();
    }

    /**
     * Starts the race. Note that the race only starts when all racers are ready, i.e. all waiting at the start barrier, and for that to happen there is a 30 seconds timeout.
     * @throws Exception if the race start has expired
     */
    private void startRace() throws Exception {
        broadcaster.readySetGo();
        startBarrier.await(30, TimeUnit.SECONDS);
    }

    /**
     * Awaits the race to finish. If the race is still in progress this method will block and wait for the race to end. The wait has a timeout of 90 seconds.
     * @throws Exception if the race end has expired.
     */
    private void awaitEnd() throws Exception {
        endCountDownLatch.await(90, TimeUnit.SECONDS);
        broadcaster.raceEnd();
    }

    /**
     * Process the race result.
     */
    private void processResult() {
        results.add(result);
        broadcaster.raceResult(result);
        broadcaster.championshipStandings(new ChampionshipStandings().addAll(results).getEntryList());
    }

    /**
     * The racer's registration.
     */
    public class Registration {

        private final int number;

        /**
         * the registration's racer
         */
        private final Racer racer;

        /**
         *
         * @param racer
         */
        private Registration(Racer racer, int number) {
            this.number = number;
            this.racer = racer;
            broadcast("joins the race.");
        }

        /**
         * The racer is reader to start the race.
         * @throws Exception
         */
        public void ready() throws Exception {
            startBarrier.await(30, TimeUnit.SECONDS);
        }

        /**
         * The racer has finished the race.
         */
        public void done() {
            int racerPosition = donePosition.getAndIncrement();
            broadcast("finished the race.");
            result.setPosition(this, racerPosition);
            endCountDownLatch.countDown();
        }

        /**
         * The racer has aborted the race.
         */
        public void aborted(Throwable t) {
            int racerPosition = abortedPosition.getAndDecrement();
            t.printStackTrace();
            broadcast("aborted the race. Reason: " + (t != null ? t.getMessage() : "N/A)"));
            result.setPosition(this, racerPosition);
            endCountDownLatch.countDown();
        }

        /**
         * Retrieves the race's environment.
         * @return
         */
        public Map<String, String> getEnvironment() {
            return environment;
        }

        /**
         * Retrieves the registered racer.
         * @return
         */
        public Racer getRacer() {
            return racer;
        }

        /**
         * Retrieves the registration number.
         * @return
         */
        public int getNumber() {
            return number;
        }

        /**
         * Broadcasts a msg with respect to a race event.
         * @param msg
         */
        public void broadcast(String msg) {
            broadcaster.raceProgress(racer.getName() + ' ' + msg);
        }
    }
}
