From 37fd8bff815355f826d515cf39d43a4a0397ad23 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Sat, 15 Jul 2017 22:41:31 +0100
Subject: [PATCH] Start filling in all the missing saving/loading logic

---
 Nibriboard/Nibriboard.csproj                 |  3 +
 Nibriboard/RippleSpace/Plane.cs              | 68 +++++++++++++++++++-
 Nibriboard/RippleSpace/RippleSpaceManager.cs | 47 ++++++++++++++
 Nibriboard/Utilities.cs                      | 19 ++++++
 Nibriboard/packages.config                   |  1 +
 5 files changed, 135 insertions(+), 3 deletions(-)

diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj
index 7e57ec1..d0f1615 100644
--- a/Nibriboard/Nibriboard.csproj
+++ b/Nibriboard/Nibriboard.csproj
@@ -53,6 +53,9 @@
     <Reference Include="Newtonsoft.Json">
       <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
     </Reference>
+    <Reference Include="SharpCompress">
+      <HintPath>..\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll</HintPath>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Program.cs" />
diff --git a/Nibriboard/RippleSpace/Plane.cs b/Nibriboard/RippleSpace/Plane.cs
index 6e6f443..bd7f0d5 100644
--- a/Nibriboard/RippleSpace/Plane.cs
+++ b/Nibriboard/RippleSpace/Plane.cs
@@ -4,6 +4,9 @@ using System.Threading.Tasks;
 using System.IO;
 using System.Runtime.Serialization.Formatters.Binary;
 using System.Runtime.Serialization;
+using SharpCompress.Writers;
+using SharpCompress.Common;
+using SharpCompress.Readers;
 
 namespace Nibriboard.RippleSpace
 {
@@ -36,6 +39,7 @@ namespace Nibriboard.RippleSpace
 		/// <summary>
 		/// The number of chunks in a square around (0, 0) that should always be
 		/// loaded.
+		/// Works like a radius.
 		/// </summary>
 		public int PrimaryChunkAreaSize = 10;
 
@@ -83,7 +87,7 @@ namespace Nibriboard.RippleSpace
 			}
 		}
 
-		public Plane(string inName, int inChunkSize)
+		public Plane(string inName, int inChunkSize, string inStorageDirectoryRoot)
 		{
 			Name = inName;
 			ChunkSize = inChunkSize;
@@ -93,8 +97,33 @@ namespace Nibriboard.RippleSpace
 			// Note that the primary chunk area is a radius around (0, 0) - not the diameter
 			SoftLoadedChunkLimit = PrimaryChunkAreaSize * PrimaryChunkAreaSize * 4;
 
-			StorageDirectory = $"./Planes/{Name}";
+			StorageDirectory = inStorageDirectoryRoot + Name;
+			if(File.Exists(StorageDirectory))
+				throw new InvalidOperationException($"Error: The unpacked storage directory {StorageDirectory} already exists!");
+			Directory.CreateDirectory(StorageDirectory);
 		}
+
+		private async Task LoadPrimaryChunks()
+		{
+			List<ChunkReference> primaryChunkRefs = new List<ChunkReference>();
+
+			ChunkReference currentRef = new ChunkReference(this, -PrimaryChunkAreaSize, -PrimaryChunkAreaSize);
+			while(currentRef.Y < PrimaryChunkAreaSize)
+			{
+				primaryChunkRefs.Add(currentRef.Clone() as ChunkReference);
+
+				currentRef.X++;
+
+				if(currentRef.X > PrimaryChunkAreaSize)
+				{
+					currentRef.X = -PrimaryChunkAreaSize;
+					currentRef.Y++;
+				}
+			}
+
+			await FetchChunks(primaryChunkRefs);
+		}
+
 		/// <summary>
 		/// Fetches a list of chunks by a list of chunk refererences.
 		/// </summary>
@@ -102,6 +131,7 @@ namespace Nibriboard.RippleSpace
 		/// <returns>The chunks attached to the specified chunk references.</returns>
 		public async Task<List<Chunk>> FetchChunks(List<ChunkReference> chunkRefs)
 		{
+			// todo Paralellise loading with https://www.nuget.org/packages/AsyncEnumerator
 			List<Chunk> chunks = new List<Chunk>();
 			foreach(ChunkReference chunkRef in chunkRefs)
 				chunks.Add(await FetchChunk(chunkRef));
@@ -180,6 +210,20 @@ namespace Nibriboard.RippleSpace
 			}
 		}
 
+		public void Save(Stream destination)
+		{
+			WriterOptions packingOptions = new WriterOptions(CompressionType.GZip);
+
+			IEnumerable<string> chunkFiles = Directory.GetFiles(StorageDirectory);
+			using(IWriter packer = WriterFactory.Open(destination, ArchiveType.Tar, packingOptions))
+			{
+				foreach(string nextChunkFile in chunkFiles)
+				{
+					packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile);
+				}
+			}
+		}
+
 		/// <summary>
 		/// Handles chunk updates from the individual loaded chunks on this plane.
 		/// Re-emits chunk updates it catches wind of at plane-level.
@@ -200,5 +244,23 @@ namespace Nibriboard.RippleSpace
 			// Make the chunk update bubble up to plane-level
 			OnChunkUpdate(sender, eventArgs);
 		}
-	}
+
+		public static async Task<Plane> FromFile(string inName, int inChunkSize, string inStorageDirectoryRoot, string sourceFilename)
+		{
+			Plane loadedPlane = new Plane(inName, inChunkSize, inStorageDirectoryRoot);
+
+			// Unpack the plane to the temporary directory
+			using(Stream sourceStream = File.OpenRead(sourceFilename))
+			using(IReader unpacker = ReaderFactory.Open(sourceStream))
+			{
+				unpacker.WriteAllToDirectory(loadedPlane.StorageDirectory);
+			}
+
+			// Load the primary chunks from disk inot the plane
+			await loadedPlane.LoadPrimaryChunks();
+
+			return loadedPlane;
+		}
+
+}
 }
diff --git a/Nibriboard/RippleSpace/RippleSpaceManager.cs b/Nibriboard/RippleSpace/RippleSpaceManager.cs
index 686fd12..7f775c4 100644
--- a/Nibriboard/RippleSpace/RippleSpaceManager.cs
+++ b/Nibriboard/RippleSpace/RippleSpaceManager.cs
@@ -1,12 +1,19 @@
 ﻿using System;
+using System.IO;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Diagnostics;
+using SharpCompress.Readers;
 
 namespace Nibriboard.RippleSpace
 {
 	public class RippleSpaceManager
 	{
+		/// <summary>
+		/// The temporary directory in which we are currently storing our unpacked planes temporarily.
+		/// </summary>
+		public string UnpackedDirectory;
+
 		/// <summary>
 		/// The master list of planes that this PlaneManager is in charge of. 
 		/// </summary>
@@ -26,6 +33,12 @@ namespace Nibriboard.RippleSpace
 
 		public RippleSpaceManager()
 		{
+			// Create a temporary directory in which to store our unpacked planes
+			UnpackedDirectory = Path.GetTempFileName();
+			File.Delete(UnpackedDirectory);
+			UnpackedDirectory += "/";
+			Directory.CreateDirectory(UnpackedDirectory);
+
 			Log.WriteLine("[RippleSpace] New blank ripplespace initialised.");
 		}
 
@@ -89,5 +102,39 @@ namespace Nibriboard.RippleSpace
 			}
 
 		}
+
+		public async Task<RippleSpaceManager> FromFile(string filename)
+		{
+			if(!File.Exists(filename))
+				throw new FileNotFoundException($"Error: Couldn't find the packed ripplespace at {filename}");
+
+			RippleSpaceManager rippleSpace = new RippleSpaceManager();
+
+			using(Stream packedRippleSpaceStream = File.OpenRead(filename))
+			using(IReader rippleSpaceUnpacker = ReaderFactory.Open(packedRippleSpaceStream))
+			{
+				Log.WriteLine($"[Core] Unpacking ripplespace packed with {rippleSpaceUnpacker.ArchiveType} from {filename}.");
+				rippleSpaceUnpacker.WriteAllToDirectory(UnpackedDirectory);
+			}
+			Log.WriteLine("[Core] done!");
+
+			if(!File.Exists(rippleSpace.UnpackedDirectory + "index.list"))
+				throw new InvalidDataException($"Error: The packed ripplespace at {filename} doesn't appear to contain an index file.");
+
+			Log.WriteLine("[Core] Importing planes");
+
+			StreamReader planes = new StreamReader(rippleSpace.UnpackedDirectory + "index.list");
+			List<Task> planeReaders = new List<Task>();
+			string nextPlane;
+			while((nextPlane = await planes.ReadLineAsync()) != null)
+			{
+				planeReaders.Add(Plane.FromFile(rippleSpace.UnpackedDirectory + nextPlane));
+			}
+			await Task.WhenAll(planeReaders);
+
+			Log.WriteLine("[Core] done!");
+
+			return rippleSpace;
+		}
 	}
 }
diff --git a/Nibriboard/Utilities.cs b/Nibriboard/Utilities.cs
index d829ecd..46d307d 100644
--- a/Nibriboard/Utilities.cs
+++ b/Nibriboard/Utilities.cs
@@ -6,6 +6,12 @@ namespace Nibriboard
 {
 	public static class Utilities
 	{
+		/// <summary>
+		/// Deserialises an object from it's binary representation.
+		/// </summary>
+		/// <typeparam name="T">The type to cast the deseralised object into.</typeparam>
+		/// <param name="sourceStream">The source stream to deseralise an object from.</param>
+		/// <returns>The object deserialised from the given stream.</returns>
 		public static async Task<T> DeserialiseBinaryObject<T>(Stream sourceStream)
 		{
 			return await Task.Run(() => {
@@ -13,5 +19,18 @@ namespace Nibriboard
 				return (T)formatter.Deserialize(sourceStream);
 			});
 		}
+
+		/// <summary>
+		/// Serialises an object and outputs a binary representation to the given stream.
+		/// </summary>
+		/// <param name="target">The target to serialise.</param>
+		/// <param name="destinationStream">The destination stream to send the binary representation to.</param>
+		public static async Task SerialiseToBinary(object target, Stream destinationStream)
+		{
+			await Task.Run(() => {
+				BinaryFormatter formatter = new BinaryFormatter();
+				formatter.Serialize(destinationStream, target);
+			});
+		}
 	}
 }
diff --git a/Nibriboard/packages.config b/Nibriboard/packages.config
index e899f6b..8c0a927 100644
--- a/Nibriboard/packages.config
+++ b/Nibriboard/packages.config
@@ -3,4 +3,5 @@
   <package id="IotWeb" version="0.8.6" targetFramework="net452" />
   <package id="MimeSharp" version="1.0.0" targetFramework="net45" />
   <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
+  <package id="SharpCompress" version="0.17.1" targetFramework="net461" />
 </packages>
\ No newline at end of file
