///////////////////////////////////////////////////////////////////////////////
//
//  Microsoft Research Singularity
//
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//
//  Note:   Perform short test of some RamDisk and RamDiskClientManager operations.
//          Used MemStress.cs as an application template.
//

using System;
using System.Collections;
using System.Threading;

using Microsoft.Singularity.UnitTest;
using Microsoft.Singularity.V1.Services;

using Microsoft.Contracts;
using Microsoft.SingSharp;
using Microsoft.SingSharp.Reflection;
using Microsoft.Singularity.Applications;
using Microsoft.Singularity.Channels;
using Microsoft.Singularity.Directory;
using Microsoft.Singularity.Configuration;
using Microsoft.Singularity.Io;

using Microsoft.Singularity.Services.RamDisk;
using Microsoft.Singularity.Services.RamDisk.Contracts;

[assembly: Transform(typeof(ApplicationResourceTransform))]

namespace Microsoft.Singularity.Applications
{
    [ConsoleCategory(DefaultAction=true)]
    internal class Parameters {
        [InputEndpoint("data")]
        public readonly TRef<UnicodePipeContract.Exp:READY> Stdin;

        [OutputEndpoint("data")]
        public readonly TRef<UnicodePipeContract.Imp:READY> Stdout;

        reflective internal Parameters();

        internal int AppMain() {
            return RamDiskTest.AppMain(this);
        }
    }

    public class RamDiskTest
    {
        internal static int AppMain(Parameters! config)
        {
            const int diskBytes = 2000000 /*bytes*/;

            RamDiskControlContract.Imp   managerImp = null;
            DirectoryServiceContract.Imp dsImp      = null;
            DiskDeviceContract.Imp       diskImp    = null;
            try {
                managerImp = ConnectToRamDiskClientManager();
                
                string! diskPath = CreateTest(managerImp, diskBytes);
                dsImp = DirectoryService.NewClientEndpoint();
                diskImp = OpenDevice(dsImp, diskPath);
                
                int lastSector = (diskBytes - 1 + RamDisk.SECTOR_SIZE - 1)/RamDisk.SECTOR_SIZE;
                BasicReadWriteTest(diskImp, 0/*sector*/,  RamDisk.SECTOR_SIZE);
                BasicReadWriteTest(diskImp, lastSector,   RamDisk.SECTOR_SIZE);
                BasicReadWriteTest(diskImp, lastSector/2, RamDisk.SECTOR_SIZE);
                for (int size = RamDisk.SECTOR_SIZE; size < diskBytes; size *= 2) {
                    BasicReadWriteTest(diskImp, 0/*sector*/, size);
                }
                BasicReadWriteTest(diskImp, 0/*sector*/, diskBytes);

                BasicReadWriteErrorTest(diskImp, lastSector + 1, RamDisk.SECTOR_SIZE);
                BasicReadWriteErrorTest(diskImp, lastSector, 2*RamDisk.SECTOR_SIZE);
                BasicReadWriteErrorTest(diskImp, 0/*sector*/, diskBytes + RamDisk.SECTOR_SIZE);
                BasicReadWriteErrorTest(diskImp, lastSector/2, (lastSector + 1 - lastSector/2 + 1) * RamDisk.SECTOR_SIZE);

                RandomReadWriteTest(diskImp, diskBytes);

                EnsureDestroyFailsWithInUse(managerImp, diskPath);

                delete diskImp;
                diskImp = null;

                Destroy(managerImp, diskPath, false/*force*/);
                EnsureDoesNotExist(dsImp, diskPath);

                OutOfMemoryTest(managerImp);
            }
            catch (Exception ex) {
                Console.WriteLine("Error: " + ex.Message + "\n" + ex.StackTrace);
            }
            finally {
                delete managerImp;
                delete dsImp;
                delete diskImp;
            }
            return 0;
        }

        private static void BasicReadWriteTest(DiskDeviceContract.Imp:Ready! diskImp,
                                               int                           sector,
                                               int                           size)
        {
            byte[] in ExHeap data = null;
            try {
                data = new[ExHeap] byte[size];
                for (int i = 0; i < data.Length; i++) {
                    data[i] = (byte)i;
                }
                data = Write(diskImp, 0, data);

                delete data;
                data = new[ExHeap] byte[size];
                
                data = Read(diskImp, 0, data);
                for (int i = 0; i < data.Length; i++) {
                    if (data[i] != (byte)i) {
                        throw new Exception(String.Format("Data did not match after write-read sequence: at byte {0} got {1}, expected {2}",
                                                          i, data[i], (byte)i));
                    }
                }
            }
            finally {
                delete data;
            }
        }

        private static void BasicReadWriteErrorTest(DiskDeviceContract.Imp:Ready! imp,
                                                    int                           sector,
                                                    int                           size)
        {
            byte[] in ExHeap data = new[ExHeap] byte[size];
            imp.SendRead(data, 0, (ulong)size, (ulong)sector);
            switch receive {
                case imp.AckRead(outBuffer):
                    delete outBuffer;
                    throw new Exception("RamDisk read operation succeeded where expected to fail");
                case imp.NakRead():
                    break;
                case imp.ChannelClosed():
                    throw new Exception("RamDisk closed channel prematurely");
            }

            data = new[ExHeap] byte[size];
            imp.SendWrite(data, 0, (ulong)size, (ulong)sector);
            switch receive {
                case imp.AckWrite(outBuffer):
                    throw new Exception("RamDisk write operation succeeded where expected to fail");
                case imp.NakWrite():
                    break;
                case imp.ChannelClosed():
                    throw new Exception("RamDisk closed channel prematurely");
            }
        }

        private static void RandomReadWriteTest(DiskDeviceContract.Imp:Ready! diskImp,
                                                int                           diskBytes)
        {
            const int seed = 840799514; // Just an arbitrary value
            const int sectorsToWrite = 1000;
            int numSectors = (diskBytes - 1 + RamDisk.SECTOR_SIZE - 1)/RamDisk.SECTOR_SIZE + 1;
            Random r = new Random(seed);
            ArrayList! visitedSectors = new ArrayList();
            
            byte[]! in ExHeap data = new[ExHeap] byte[RamDisk.SECTOR_SIZE];
            try {
                byte[]! gcData = new byte[RamDisk.SECTOR_SIZE];
                for (int i = 0; i < sectorsToWrite; i++) {
                    int sectorNum = r.Next(numSectors);
                    if (visitedSectors.Contains(sectorNum)) {
                        continue;
                    }
                    else {
                        visitedSectors.Add(sectorNum);
                    }

                    r.NextBytes(gcData);
                    
                    Bitter.FromByteArray(data, 0, RamDisk.SECTOR_SIZE, gcData, 0);
                    data = Write(diskImp, (ulong)sectorNum, data);
                }

                visitedSectors.Clear();

                r = new Random(seed);
                for (int i = 0; i < sectorsToWrite; i++) {
                    int sectorNum = r.Next(numSectors);
                    if (visitedSectors.Contains(sectorNum)) {
                        continue;
                    }
                    else {
                        visitedSectors.Add(sectorNum);
                    }

                    r.NextBytes(gcData);
                    
                    data = Read(diskImp, (ulong)sectorNum, data);
                    for (int byteNum = 0; byteNum < data.Length; byteNum++) {
                        if (data[byteNum] != gcData[byteNum]) {
                            throw new Exception(
                                String.Format("Data did not match after write-read sequence: at sector {0}, byte {1}, got {2}, expected {3}",
                                              sectorNum, byteNum, data[byteNum], gcData[byteNum]));
                        }
                    }
                }
            }
            finally {
                delete data;
            }
        }

        private static RamDiskControlContract.Imp! ConnectToRamDiskClientManager()
        {
            DirectoryServiceContract.Imp! dsExp =
                DirectoryService.NewClientEndpoint();

            RamDiskControlContract.Imp! fccImp;
            RamDiskControlContract.Exp! fccExp;
            RamDiskControlContract.NewChannel(out fccImp, out fccExp);
            try {
                dsExp.SendBind(
                    Bitter.FromString2(
                        RamDiskControlContract.ManagerControlPath
                        ),
                    fccExp);

                switch receive {
                    case dsExp.AckBind():
                        switch receive {
                            case fccImp.Success():
                                return fccImp;
                            case fccImp.ChannelClosed():
                                throw new Exception("RamDisk client manager closed channel prematurely");
                        }
                        // unreached
                    case dsExp.NakBind(exp, errorCode):
                        delete exp;
                        delete fccImp;
                        throw new Exception("RamDisk client manager rejected bind");
 
                    case dsExp.ChannelClosed():
                        delete fccImp;
                        throw new Exception("RamDisk client manager closed channel prematurely");
                }
            }
            finally {
                delete dsExp;
            }
        }
        
        private static void OutOfMemoryTest(RamDiskControlContract.Imp! controller)
        {
            controller.SendCreate(1000000000000L);

            switch receive {
                case controller.CreateSuccess(diskPath):
                    delete diskPath;
                    throw new Exception("Creation of very large disk succeeded where expected to fail with out of memory");
                case controller.Fail(error):
                    if (error != RamDiskContractErrorCode.OutOfMemory) {
                        throw new Exception("Creation of very large disk failed with unexpected error code " + error + ", expected to fail with out of memory");
                    }
                    return;
                case controller.ChannelClosed():
                    throw new Exception("RamDisk client manager closed channel prematurely");
            }
        }

        private static string! CreateTest(RamDiskControlContract.Imp! controller,
                                          ulong                       diskSizeBytes)
        {
            controller.SendCreate(diskSizeBytes);

            switch receive {
                case controller.CreateSuccess(diskPath):
                    string result = Bitter.ToString2(diskPath);
                    delete diskPath;
                    return result;
                case controller.Fail(error):
                    throw new Exception("Create disk failed with error code " + error);
                case controller.ChannelClosed():
                    throw new Exception("RamDisk client manager closed channel prematurely");
            }
        }

        private static DiskDeviceContract.Imp:Ready!
        OpenDevice(DirectoryServiceContract.Imp:Ready! nsImp,
                   string!                             deviceName)
        {
            DiskDeviceContract.Exp! diskExp;
            DiskDeviceContract.Imp! diskImp;
            DiskDeviceContract.NewChannel(out diskImp, out diskExp);

            nsImp.SendBind(Bitter.FromString2(deviceName), diskExp);
            switch receive {
                case nsImp.AckBind():
                    switch receive {
                        case diskImp.Success():
                            return diskImp;
                        case diskImp.ContractNotSupported():
                            delete diskImp;
                            throw new Exception("RamDisk does not support disk contract");
                        case diskImp.ChannelClosed():
                            delete diskImp;
                            throw new Exception("RamDisk closed channel prematurely");
                    }
                    // unreached
                case nsImp.NakBind(badExp, errorCode):
                    delete diskImp;
                    delete badExp;
                    throw new Exception("Failed to bind disk '" + deviceName + "' with error code " + errorCode);
                case nsImp.ChannelClosed():
                    delete diskImp;
                    throw new Exception("Directory Service closed channel prematurely");
            }
        }

        private static void EnsureDoesNotExist(DirectoryServiceContract.Imp:Ready! nsImp,
                                               string!                             fileName)
        {
            DiskDeviceContract.Exp! fileExp;
            DiskDeviceContract.Imp! fileImp;
            DiskDeviceContract.NewChannel(out fileImp, out fileExp);

            nsImp.SendBind(Bitter.FromString2(fileName), fileExp);
            switch receive {
                case nsImp.AckBind():
                    throw new Exception("File expected to be missing exists");
                case nsImp.NakBind(badExp, errorCode):
                    delete fileImp;
                    delete badExp;
                    if (errorCode != ErrorCode.NotFound) {
                        throw new Exception("Failed to bind file '" + fileName + "' expected to be missing with unexpected error code " + errorCode);
                    }
                    return;
                case nsImp.ChannelClosed():
                    delete fileImp;
                    throw new Exception("Directory Service closed channel prematurely");
            }
        }

        private static byte[]! in ExHeap Read(DiskDeviceContract.Imp:Ready! imp,
                                              ulong                      sectorId,
                                              [Claims] byte[]! in ExHeap data)

        {
            ulong dataLength = (ulong)data.Length;
            imp.SendRead(data, 0, dataLength, sectorId);
            switch receive {
                case imp.AckRead(outBuffer):
                    return outBuffer;
                case imp.NakRead():
                    throw new Exception("RamDisk rejected read operation");
                case imp.ChannelClosed():
                    throw new Exception("RamDisk closed channel prematurely");
            }
        }

        private static byte[]! in ExHeap Write(DiskDeviceContract.Imp:Ready! imp,
                                               ulong                         sectorId,
                                               [Claims] byte[]! in ExHeap    data)
        {
            imp.SendWrite(data, 0, (ulong)data.Length, sectorId);
            switch receive {
                case imp.AckWrite(outBuffer):
                    return outBuffer;
                case imp.NakWrite():
                    throw new Exception("RamDisk rejected write operation");
                case imp.ChannelClosed():
                    throw new Exception("RamDisk closed channel prematurely");
            }
        }

        private static void EnsureDestroyFailsWithInUse(RamDiskControlContract.Imp! controller,
                                                        string!                     diskPath)
        {
            char[] in ExHeap diskPathExHeap = Bitter.FromString2(diskPath);
            controller.SendDestroy(diskPathExHeap, false/*force*/);

            switch receive {
                case controller.Success():
                    throw new Exception("Destroy disk succeeded where expected to fail with in-use error");
                case controller.Fail(errorCode):
                    if (errorCode != RamDiskContractErrorCode.IsInUse) {
                        throw new Exception("Destroy disk failed with unexpected error code " + errorCode + ", expected in-use");
                    }
                    return;
                case controller.ChannelClosed():
                    throw new Exception("RamDisk client manager closed channel prematurely");
            }
        }

        private static void Destroy(RamDiskControlContract.Imp! controller,
                                    string!                     diskPath,
                                    bool                        force)
        {
            char[] in ExHeap diskPathExHeap = Bitter.FromString2(diskPath);
            controller.SendDestroy(diskPathExHeap, force);

            switch receive {
                case controller.Success():
                    return;
                case controller.Fail(error):
                    throw new Exception("Destroy disk failed with error code " + error);
                case controller.ChannelClosed():
                    throw new Exception("RamDisk client manager closed channel prematurely");
            }
        }
    }
}


