import edu.rice.hj.runtime.actors.Actor;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;

import static edu.rice.hj.Module1.*;

/*
 * This class runs a actor version of computing pi by
 * setting a threshold value.
 */
public class PiActor2 {
    public static void main(final String[] args) {

        final int scale;

        // Default scale is 5000, if user does not specify one
        if (args.length > 0) {
            scale = Integer.parseInt(args[0]);
        } else {
            scale = 5000;
        }

        System.out.println("required precision = " + scale);

        final int numWorkers = 48;

        initializeHabanero();
        for (int iter = 0; iter < 8; iter++) {
            final long startTime = System.nanoTime();

            final Master2 master = new Master2(numWorkers, scale);
            finish(() -> {
                master.start();
            });

            final long endTime = System.nanoTime();
            final long execTime = (long) ((endTime - startTime) / 1e6);

            System.out.println("PI = " + master.getResult());
            System.out.println("Iteration-" + iter + " Exec Time = " + execTime + " ms.");
        }
        finalizeHabanero();
    }

    // Message classes
    private static class StopMessage2 {
        public static StopMessage2 ONLY = new StopMessage2();
    }

    private static class WorkMessage2 {
        public final int scale;
        public final int term;

        public WorkMessage2(final int scale, final int term) {
            this.scale = scale;
            this.term = term;
        }
    }

    private static class ResultMessage2 {
        public final BigDecimal result;
        public final int workerId;

        public ResultMessage2(final BigDecimal result, final int workerId) {
            this.result = result;
            this.workerId = workerId;
        }
    }

    /*
     * A master actor.
     */
    private static class Master2 extends Actor<Object> {

        // start: do not mess with these
        private final int numWorkers;
        private final int scale;
        private final Worker2[] workers;
        // end: do not mess with these

        // use  result to accumulate the value of PI
        private BigDecimal result = BigDecimal.ZERO;

        // use tolerance to decide when it is okay to request termination of workers
        private BigDecimal tolerance;

        // use counter to track number of workers that have terminated
        private AtomicInteger numWorkersTerminated = new AtomicInteger(0);
        // use counter to track how many terms have been requested
        private int numTermsRequested = 0;

        public Master2(final int numWorkers, final int scale) {
            this.numWorkers = numWorkers;
            this.scale = scale;
            this.tolerance = BigDecimal.ONE.movePointLeft(scale);
            this.workers = new Worker2[numWorkers];
        }

        public void onPostStart() {
            // now start the workers
            for (int i = 0; i < numWorkers; i++) {
                workers[i] = new Worker2(this, i);
                workers[i].start();
            }
            // TODO send some work to workers in advance, generateWork() should be useful
        }

        /**
         * Generates work for the given worker
         *
         * @param workerId the id of te worker to send work
         */
        private void generateWork(int workerId) {
            // send work request to specified worker
            final WorkMessage2 wm = new WorkMessage2(scale, numTermsRequested);
            workers[workerId].send(wm);
            // update the limit for the series requested so far
            numTermsRequested += 1;
        }

        public void requestWorkersToExit() {
            // TODO request all workers to exit via the StopMessage2
        }

        protected void process(final Object msg) {
            if (msg instanceof ResultMessage2) {
                // a message sent from a worker about the term it computed
                ResultMessage2 rm = (ResultMessage2) msg;
                result = result.add(rm.result);

                // TODO If we reached our precision, we can request workers to terminate
                // TODO else we generate some more work to keep the worker busy
            } else if (msg instanceof StopMessage2) {
                // a message sent from a worker that it is terminating
                // TODO track how many workers terminated
                // TODO master can terminate (via exit()) only if all workers have terminated
            }
        }

        public String getResult() {
            return result.toPlainString();
        }

    }

    /*
     * A worker actor.
     */
    private static class Worker2 extends Actor<Object> {

        private final Master2 master;
        private final int id;

        public Worker2(final Master2 master, final int id) {
            this.master = master;
            this.id = id;
        }

        protected void process(final Object msg) {
            if (msg instanceof StopMessage2) {
                // TODO let master know worker is terminating by sending a message
                // TODO terminate this
            } else if (msg instanceof WorkMessage2) {
                // master requested computation of a term
                WorkMessage2 wm = (WorkMessage2) msg; // do some more computation
                BigDecimal result = PiUtil.calculateBbpTerm(wm.scale, wm.term);
                master.send(new ResultMessage2(result, id));
            }
        }
    }
}