/*
Copyright (C) 2009  Diego Darriba

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package es.uvigo.darwin.prottest.facade.strategy;

import es.uvigo.darwin.prottest.exe.ParallelModelEstimator;

import mpi.MPI;
import mpi.Request;
import es.uvigo.darwin.prottest.exe.RunEstimator;
import es.uvigo.darwin.prottest.global.options.ApplicationOptions;
import es.uvigo.darwin.prottest.model.Model;
import es.uvigo.darwin.prottest.observer.ObservableModelUpdater;
import es.uvigo.darwin.prottest.util.checkpoint.CheckPointManager;
import es.uvigo.darwin.prottest.util.collection.ModelCollection;
import es.uvigo.darwin.prottest.util.collection.SingleModelCollection;
import es.uvigo.darwin.prottest.util.comparator.ModelWeightComparator;
import es.uvigo.darwin.prottest.util.exception.ProtTestInternalException;

/**
 * The Class ImprovedDynamicDistributionStrategy.
 */
public class HybridDistributionStrategy extends DistributionStrategy {

    /** The Constant TAG_SEND_REQUEST. */
    private static final int TAG_SEND_REQUEST = 1;
    /** MPJ Tag for sending a new model. */
    private static final int TAG_EXIST_MORE_MODELS = 2;
    /** The Constant TAG_SEND_MODEL. */
    private static final int TAG_SEND_MODEL = 3;
    private int maxPEs;
    /** The number of available PEs. */
    int availablePEs;
    /** The root model request. */
    boolean rootModelRequest;
    /** The root model. */
    Model rootModel;
    private ParallelModelEstimator pme;
    private Model[] computedModels;
    private MultipleDistributor distributor;

    /**
     * Instantiates a new improved dynamic distribution strategy.
     *
     * @param mpjMe the rank of the current process in MPJ
     * @param mpjSize the size of the MPJ communicator
     * @param options the application options
     * @param cpManager the checkpoint manager, it can be null if checkpointing is not supported
     */
    public HybridDistributionStrategy(int mpjMe, int mpjSize, ApplicationOptions options, CheckPointManager cpManager) {
        this(mpjMe, mpjSize, options, cpManager, Runtime.getRuntime().availableProcessors());
    }

    /**
     * Instantiates a new improved dynamic distribution strategy.
     *
     * @param mpjMe the rank of the current process in MPJ
     * @param mpjSize the size of the MPJ communicator
     * @param options the application options
     * @param cpManager the checkpoint manager, it can be null if checkpointing is not supported
     */
    public HybridDistributionStrategy(int mpjMe, int mpjSize, ApplicationOptions options, CheckPointManager cpManager, int numberOfThreads) {
        super(mpjMe, mpjSize, options, cpManager);
        if (mpjSize == 1) {
            throw new ProtTestInternalException("Dynamic Distribution Strategy" + " requires at least 2 processors");
        }
        itemsPerProc = new int[mpjSize];
        displs = new int[mpjSize];
        modelSet = new SingleModelCollection(options.getAlignment());
        maxPEs = numberOfThreads;
        availablePEs = maxPEs;
        pme = new ParallelModelEstimator(maxPEs, options.getAlignment());
        pme.addObserver(this);
    }

    /* (non-Javadoc)
     * @see es.uvigo.darwin.prottest.facade.strategy.DistributionStrategy#distribute(es.uvigo.darwin.prottest.util.collection.ModelCollection)
     */
    public Model[] distribute(ModelCollection arrayListModel, ModelWeightComparator comparator) {

        distributor = new MultipleDistributor(this, arrayListModel, comparator, mpjMe, mpjSize);
        Thread distributorThread = new Thread(distributor);
        distributorThread.start();
        numberOfModels = arrayListModel.size();
        request();

        computationDone();

        return computedModels;
    }

    /* (non-Javadoc)
     * @see es.uvigo.darwin.prottest.facade.strategy.DistributionStrategy#request()
     */
    public void request() {

        startTime = System.currentTimeMillis();

//        List<RunEstimator> runenvList = new ArrayList<RunEstimator>();

        while (true) {
            // send request to root
            Model[] modelToReceive = null;
            Model model = null;
            if (mpjMe > 0) {
                int[] sendMessage = {availablePEs};
                Request modelRequest = MPI.COMM_WORLD.Isend(sendMessage, 0, 1, MPI.INT, 0, TAG_SEND_REQUEST);
                // prepare reception
                modelToReceive = new Model[1];
                boolean[] notification = new boolean[1];
                // wait for request
                modelRequest.Wait();

                Request notifyRecv = MPI.COMM_WORLD.Irecv(notification, 0, 1, MPI.BOOLEAN, 0, TAG_EXIST_MORE_MODELS);
                notifyRecv.Wait();

                if (notification[0]) {
                    // receive model
                    Request modelReceive = MPI.COMM_WORLD.Irecv(modelToReceive, 0, 1, MPI.OBJECT, 0, TAG_SEND_MODEL);
                    modelReceive.Wait();
                    model = modelToReceive[0];
                } else {
                    break;
                }
            } else {
                // This strategy is an easy way to avoid the problem of thread-safety
                // in MPJ-Express. It works correctly, but it also causes to introduce
                // coupling amongst this class and Distributor, having to define two
                // public attributes: rootModelRequest and rootModel.
                rootModelRequest = true;
                while (rootModelRequest) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new ProtTestInternalException("Thread interrupted");
                    }
                }
                model = rootModel;
                if (model == null) {
                    break;
                }
            }

            if (model != null) {
                availablePEs -= MultipleDistributor.getPEs(model, maxPEs);
                // compute
                modelSet.add(model);
                RunEstimator runenv =
                        factory.createRunEstimator(options, model, MultipleDistributor.getPEs(model, maxPEs));
//                runenv.addObserver(this);
                pme.execute(runenv);

                while (availablePEs <= 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new ProtTestInternalException("Thread interrupted");
                    }
                }
            }
        }

        endTime = System.currentTimeMillis();

        while (pme.hasMoreTasks()) {
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                throw new ProtTestInternalException("Thread interrupted");
            }
        }

        if (mpjMe > 0) {
            gather();
        } else {
            computedModels = gather();
        }

    }

    /* (non-Javadoc)
     * @see es.uvigo.darwin.prottest.facade.ProtTestFacade#update(es.uvigo.darwin.prottest.observer.Observable, java.lang.Object)
     */
    @Override
    public void update(ObservableModelUpdater o, Model model, ApplicationOptions options) {
        if (options != null && model.isComputed()) {
            availablePEs += MultipleDistributor.getPEs(model, maxPEs);
        }
        super.update(o, model, options);
    }

    /**
     * Gathers the models of all processors into the root one. This method should
     * be called by every processor after computing likelihood value of whole model set.
     * 
     * This method will return an empty array of models for every non-root processor
     * 
     * @return the array of gathered models 
     */
    private Model[] gather() {

        Model[] allModels = new Model[numberOfModels];
        if (distributor != null) {
            itemsPerProc = distributor.getItemsPerProc();
            displs = distributor.getDispls();
        }

        MPI.COMM_WORLD.Bcast(itemsPerProc, 0, mpjSize, MPI.INT, 0);

        // gathering optimized models
        MPI.COMM_WORLD.Gatherv(modelSet.toArray(new Model[0]), 0, modelSet.size(), MPI.OBJECT,
                allModels, 0, itemsPerProc, displs, MPI.OBJECT, 0);

        return allModels;
    }
}
