///////////////////////////////////////////////////////////////////////////////
//
//  Microsoft Research Singularity
//
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//

using System;
using System.Diagnostics;
using System.Collections;
using System.Threading;
using Microsoft.SingSharp;
using Microsoft.Singularity.UnitTest;
using Microsoft.Singularity.Applications;
using Microsoft.Singularity.Directory;
using Microsoft.Singularity.Io;
using Microsoft.Singularity.Channels;

namespace Microsoft.Singularity.Applications
{
    /// <summary>
    ///     Test cases for creating processes in parallel.
    /// </summary>
    internal sealed class TestProcessCreation
    {
        private const int numThreads = 16;
        private const int iterations = 16;
        private const string commandLine = "fib";
        private static int idGenerator;
        private static ManualResetEvent evtGate;

        /// <summary>
        ///     Run test to create processes. The child processes don't output 
        ///     to the console. Instead, we swallow their output with a null pipe.
        /// </summary>
        internal static void RunWithNoChildOutput()
        {
            Console.Write("    test parallel process creation with no child output... ");

            evtGate = new ManualResetEvent(false);

            // Start the worker threads
            Thread[] threads = new Thread[numThreads];
            for (int i = 0; i < threads.Length; i++) {
                threads[i] = new Thread(new ThreadStart(ProcessCreationMainWithNoOutput));
                ((!)threads[i]).Start();
                Thread.Yield();
            }

            // Give the worker threads a bit time to initialize
            // then signal the start
            Thread.Sleep(1000);
            evtGate.Set();

            // Wait for all worker threads to finish
            for (int i = 0; i < threads.Length; i++) {
                ((!)threads[i]).Join();
            }

            // If we didn't crash, then this test is successful.
            Console.WriteLine("OK");
        }

        /// <summary>
        ///     Run test to create processes in parallel. The child
        ///     processes output to the console through PipeMultiplexer.
        /// </summary>
        internal static void RunWithChildOutput()
        {
            Console.Write("    test parallel process creation with child output... ");

            evtGate = new ManualResetEvent(false);

            // Start the worker threads
            WorkerThread[] threads = new WorkerThread[numThreads];
            for (int i = 0; i < threads.Length; i++) {
                PipeMultiplexer! mux = CreatePipeMultiplexer();
                WorkerThread worker = new WorkerThread(mux);
                worker.thread = new Thread(worker.ThreadRoutine);
                threads[i] = worker;
            }

            // We want to create all of the muxes first, then create
            // all of the threads, then start them.

            foreach (WorkerThread! worker in threads) {
                worker.thread.Start();
                Thread.Yield();
            }

            // Give the worker threads a bit time to initialize
            // then signal the start
            Thread.Sleep(1000);
            evtGate.Set();

            // Wait for all worker threads to finish
            foreach (WorkerThread! worker in threads) {
                worker.thread.Join();
            }

            foreach (WorkerThread! worker in threads) {
                PipeMultiplexer! mux = worker.muxRef.Acquire();
                delete mux;
            }

            // If we didn't crash, then this test is successful.
            Console.WriteLine();
            Console.WriteLine("OK");
        }

        /// <summary>
        ///     Thread main function that creates processes. The child
        ///     processes don't output to the console. Instead, we swallow
        ///     their output with a null pipe.
        /// </summary>
        private static void ProcessCreationMainWithNoOutput()
        {
            int id = Interlocked.Increment(ref idGenerator) - 1;

            NullPipeControl.Imp:START! control = StartNullPipe();
            DirectoryServiceContract.Imp! directoryService = 
                    DirectoryService.NewClientEndpoint();

            // Wait for the signal
            evtGate.WaitOne();

            // Create many processes
            Process[] children = new Process[iterations];
            try {
                for (int i = 0; i < iterations; i++) {
                    UnicodePipeContract.Imp! childStdInImp;
                    UnicodePipeContract.Exp! childStdInExp;
                    UnicodePipeContract.NewChannel(out childStdInImp, out childStdInExp);

                    UnicodePipeContract.Imp! childStdOutImp = CreateNullPipeClient(control);

                    //Manifest manifest;
                    children[i] = Binder.CreateProcess(directoryService,
                            new string[1]{commandLine}, childStdInExp, childStdOutImp);
                    delete childStdInImp;
                    if (children[i] == null) {
                        throw new ProcessCreateException("Failed to create process");
                    }
                    
                    ((!)children[i]).Start();
                    if (id == 0) {
                        Console.Write('.');
                    }
                }
            }
            finally {
                delete directoryService;
                delete control;
            }

            // Wait for all children to finish
            for (int i = 0; i < children.Length; i++) {
                ((!)children[i]).Join();
            }
        }

        private contract NullPipeControl 
        {
            in message NewInput(UnicodePipeContract.Exp:EXPMOVABLE! client);
            out message Ack();

            state START: NewInput? -> Ack! -> START;
        }

        private static UnicodePipeContract.Imp:READY! CreateNullPipeClient(
                NullPipeControl.Imp:START! control) 
        {
            UnicodePipeContract.Imp! client;
            UnicodePipeContract.Exp! exp;
            UnicodePipeContract.NewChannel(out client, out exp, 
                        UnicodePipeContract.EXPMOVABLE.Value);
            control.SendNewInput(exp);
            switch receive {
                case control.Ack():
                    switch receive {
                        case client.Moved():
                            return client;

                        case client.ChannelClosed():
                            throw new ProcessCreateException(
                                    "Failed to create null pipe: client receve");
                    }

                 case control.ChannelClosed():
                    throw new ProcessCreateException(
                            "Failed to create null pipe: control receive");
            }
        }

        private static NullPipeControl.Imp:START! StartNullPipe()
        {
            NullPipeControl.Imp! controlImp;
            NullPipeControl.Exp! controlExp;
            NullPipeControl.NewChannel(out controlImp, out controlExp);

            NullPipeThread pipeThread = new NullPipeThread(controlExp);

            Thread t = new Thread(new ThreadStart(pipeThread.PumpMessage));
            t.Start();
            return controlImp;
        }

        private class NullPipeThread 
        {
            TRef<NullPipeControl.Exp:START>! controlExp;

            internal NullPipeThread([Claims] NullPipeControl.Exp:START! control)
            {
                this.controlExp = new TRef<NullPipeControl.Exp:START>(control);
            }

            internal void PumpMessage() 
            {
                NullPipeControl.Exp:START! control = this.controlExp.Acquire();
                ESet<UnicodePipeContract.Exp:READY> clients = 
                    new ESet<UnicodePipeContract.Exp:READY>();

                bool done = false;

                try {
                    while (!done) {
                        switch receive {

                        case control.NewInput(client):
                            client.SendMoved();
                            clients.Add(client);
                            control.SendAck();
                            break;

                        case control.ChannelClosed():
                            done = true; 
                            break;

                        case client.Write(buffer, index, count) in clients:
                            client.SendAckWrite(buffer);
                            clients.Add(client);
                            continue;

                        case client.ChannelClosed() in clients:
                            delete client;
                            continue;
                        }
                    }
                }
                finally {
                    clients.Dispose();
                    delete control;
                }
            }
        }

        private class WorkerThread
        {
            public WorkerThread([Claims]PipeMultiplexer! mux)
            {
                this.muxRef = new TContainer<PipeMultiplexer>(mux);
            }

            public TContainer<PipeMultiplexer>! muxRef;

            public Thread thread;

            public void ThreadRoutine()
            {
                int totalProcessesCreated = 0;
                int goalCountProcessesCreated = 0x100;
                int maxActiveProcesses = 0x10;

                ArrayList processes = new ArrayList();

                PipeMultiplexer! mux = muxRef.Acquire();
                
                DirectoryServiceContract.Imp! rootdir = DirectoryService.NewClientEndpoint();

                try {

                    for (;;) {
                        Thread.Yield();

                        // scan child processes and remove dead ones
                        int i = 0;
                        while (i < processes.Count) {
                            Process! process = (Process!)processes[i];
                            if (process.Join(TimeSpan.Zero)) {
                                // oooo, it's dead.
                                // Debug.WriteLine(String.Format("yay, process {0} died", process.Id));
                                process.Dispose(true);
                                processes.RemoveAt(i);
                            }
                            else {
                                // process not dead yet
                                i++;
                            }
                        }


                        bool canCreateProcess = (processes.Count < maxActiveProcesses
                            && totalProcessesCreated < goalCountProcessesCreated);

                        if (canCreateProcess) {
                            string[] childArgs = new string[1]{ commandLine };
                            Process process = Binder.CreateProcess(rootdir, childArgs, mux);
                            if (process == null) {
                                Debug.WriteLine("FAILED to create child process.");
                                Debugger.Break();
                                break;
                            }
                            processes.Add(process);
                            process.Start();
                            // Debug.WriteLine(String.Format("created child process (id {0}), this is {1}/{2}", process.Id, totalProcessesCreated, goalCountProcessesCreated));
                            totalProcessesCreated++;
                        }

                        if (!canCreateProcess) {
                            // Debug.WriteLine("snore");
                            Thread.Sleep(250);
                        }

                        if (processes.Count == 0 && totalProcessesCreated == goalCountProcessesCreated) {
                            Debug.WriteLine("goal has been created.  worker thread terminating.");
                            break;
                        }
                    }

                    foreach (Process! process in processes) {
                        Debug.WriteLine("Terminating child process: " + process.Id);
                        process.Stop();
                    }
                    foreach (Process! process in processes) {
                        Debug.WriteLine("Waiting for child process: " + process.Id);
                        process.Join();
                        process.Dispose(true);
                    }
                    processes.Clear();
                }
                finally {
                    delete rootdir;
                    muxRef.Release(mux);
                }
            }
        }

        // Redirect our standard output into a multiplexer so we can interleave
        // output from child processes
        private static PipeMultiplexer! CreatePipeMultiplexer()
        {
            // Swap our real stdOut with a newly created one
            UnicodePipeContract.Exp! newOutputExp;
            UnicodePipeContract.Imp! newOutputImp;
            UnicodePipeContract.NewChannel(out newOutputImp, out newOutputExp);
            UnicodePipeContract.Imp stdOut = ConsoleOutput.Swap(newOutputImp);
            if (stdOut == null) {
                throw new Exception("test expects a STDOUT pipe");
            }
            // Use a mux to splice our own output together with the child
            // processes we will run.
            return PipeMultiplexer.Start(stdOut, newOutputExp);
        }

    }
}
