import java.io.IOException;
import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;

/**
 *  Generic example of master/worker program.
 *  "Tasks" just consist of sleeping for a specified time; times to wait are
 *  generatedly randomly from 1 to a specified maximum.
 *
 *  Common code for master process for client/server versions using sockets.
 */
abstract public class MasterWorkerSocketsMaster {

    /* variables to be used by all threads */
    
    protected boolean verbose = false;
    private TaskQueue taskQueue;
    private int totalTaskTime = 0;
    private int maxWorkerTotalTaskTime;
    private int minWorkerTotalTaskTime;

    /* methods for use by subclasses */

    protected void initialize(int numTasks, int maxTaskTime, boolean _verbose) {
        verbose = _verbose;

        maxWorkerTotalTaskTime = 0;
        minWorkerTotalTaskTime = maxTaskTime * numTasks;
    }

    protected void buildTaskQueue(int numTasks, int maxTaskTime) {
        taskQueue = new TaskQueue(numTasks, maxTaskTime);
    }

    protected void waitUntilTaskQueueEmpty() {
        synchronized (taskQueue) {
            while (!taskQueue.isEmpty()) {
                try {
                    taskQueue.wait();
                }
                catch (InterruptedException e) {
                    System.err.println("this should not happen");
                    e.printStackTrace();
                }
            }
        }
    }

    protected void printSummaryInfo(int numTasks, int maxTaskTime) {
        System.out.printf("number of tasks = %d\n", numTasks);
        System.out.printf("max task time = %d\n", maxTaskTime);
        System.out.printf("total task time = %d\n", totalTaskTime);
        System.out.printf("total task time in workers ranges from %d to %d\n",
                minWorkerTotalTaskTime, maxWorkerTotalTaskTime);
    }

    /* methods for use by threads */

    /**
     * Update global variables at end of worker.
     */
    private synchronized void updateGlobals(int workerTotalTaskTime) {
        maxWorkerTotalTaskTime =
            Math.max(maxWorkerTotalTaskTime, workerTotalTaskTime);
        minWorkerTotalTaskTime =
            Math.min(minWorkerTotalTaskTime, workerTotalTaskTime);
        totalTaskTime += workerTotalTaskTime;
    }

    /* utility methods */

    /**
     * Get hostname for socket.
     */
    protected static String clientAddress(Socket s) {
        return s.getInetAddress().getHostName();
    }

    /**
     * Class for threads to communicate with workers.
     */
    protected class WorkerTalker implements Runnable {

        private int myID;
        private Socket socket;

        public WorkerTalker(int ID, Socket s) {

            myID = ID;
            socket = s;
        }

        public void run() {

            ObjectInputStream input = null;
            ObjectOutputStream output = null;

            try {
                output = new ObjectOutputStream(socket.getOutputStream());
                input = new ObjectInputStream(socket.getInputStream());

                /* send ID to worker process */
                output.writeObject(new Integer(myID));

                FakeTasks.Task t;
                /* get tasks from queue until queue is empty */
                while ((t = taskQueue.get()) != null) {
                    /* send task to worker process, receive result */
                    output.writeObject(t);
                    FakeTasks.TaskResult result = 
                        (FakeTasks.TaskResult) input.readObject();
                    if (verbose) {
                        System.out.printf("(worker %d) %s\n",
                                myID, FakeTasks.toString(t, result));
                    }
                }
                /* 
                 * send shutdown task to worker
                 * TODO:  improve this?
                 *   seems like we should be able to just close the output 
                 *   stream, but that produces "Broken pipe" exceptions in the
                 *   workers
                 */
                output.writeObject(new ShutdownTask());
                /* receive summary */
                MasterWorkerSocketsWorker.Summary summary =
                (MasterWorkerSocketsWorker.Summary) input.readObject();
                System.out.printf(
                    "worker %d number of tasks = %d, total task time = %d\n", 
                    myID, summary.numTasks, summary.totalTaskTime);
                /* thread-safe update to globals */
                updateGlobals(summary.totalTaskTime);
                /* shut things down */
                socket.shutdownOutput();
                output.close();
                input.close();
                socket.close();
            }
            catch (ClassNotFoundException e) { 
                System.err.println("error in client " + myID + ":\n\t" + e);
            }
            catch (IOException e) { 
                System.err.println("error in client " + myID + ":\n\t" + e);
            }
        }
    }

    /**
     * Class for shutdown task.
     * (Anything that's serializable and not a FakeTasks.Task would work.)
     */
    public static class ShutdownTask implements Serializable {
        private static final long serialVersionUID = 1L;
    }

    /**
     * Class for task queue (thread-safe).
     */
    private static class TaskQueue {
        private List<FakeTasks.Task> tasks;

        public TaskQueue(int numTasks, int maxTaskTime) {
            tasks = FakeTasks.randomTasks(numTasks, maxTaskTime);
        }

        /** Check whether queue is empty. */
        public synchronized boolean isEmpty() {
            return tasks.isEmpty();
        }

        /** Get a task (returns null if queue is empty). */
        public synchronized FakeTasks.Task get() {
            if (tasks.isEmpty()) {
                notifyAll();
                return null;
            }
            else {
                return tasks.remove(0);
            }
        }
    }
}