using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using Serilog;
using Zio;
namespace MfGames.Nitride.IO.Contents;
///
/// An operation that writes out entities to a file system.
///
[WithProperties]
public partial class WriteFiles : FileSystemOperationBase, IOperation
{
private readonly IValidator validator;
private Dictionary> factories;
public WriteFiles(
IValidator validator,
ILogger logger,
IFileSystem fileSystem)
: base(fileSystem)
{
this.Logger = logger;
this.validator = validator;
this.TextEncoding = Encoding.UTF8;
this.factories = new Dictionary>
{
[typeof(IBinaryContent)] = GetBinaryStream,
[typeof(ITextContent)] = this.GetTextStream,
};
}
public ILogger Logger { get; set; } = null!;
public Dictionary> StreamFactories
{
get => this.factories;
set => this.factories = value ?? throw new ArgumentNullException(nameof(value));
}
///
/// Gets or sets the encoding to force any text output.
///
public Encoding TextEncoding { get; set; }
///
/// Writes out all the files to the given file system using the paths
/// currently stored in the `UPath` component. Only files that have
/// a path and a registered writer will be written.
///
/// The entities to parse.
/// The same list of entities without changes.
public override IEnumerable Run(IEnumerable entities)
{
this.validator.ValidateAndThrow(this);
IEnumerable results = entities.SelectEntity(this.Process)
.ToList();
return results;
}
public WriteFiles WithFileSystem(IFileSystem fileSystem)
{
this.FileSystem = fileSystem;
return this;
}
private static Stream GetBinaryStream(IContent content)
{
return ((IBinaryContent)content).GetStream();
}
private Stream GetTextStream(IContent content)
{
// See if we can convert the stream first. If that is the case, then
// we don't have to load it entirely in memory.
if (content is IBinaryContentConvertable convertable)
{
return convertable.ToBinaryContent()
.GetStream();
}
// We have the load the text into memory and convert it.
var textContent = (ITextContent)content;
string text = textContent.GetReader()
.ReadToEnd();
var stream = new MemoryStream();
var writer = new StreamWriter(stream, this.TextEncoding ?? Encoding.UTF8);
writer.Write(text);
writer.Flush();
stream.Position = 0;
return stream;
}
///
/// Internal method for writing out the entity. This handles the
/// registered writers to allow for multiple `IContent` types being
/// written out automatically.
///
/// The entity to write out.
/// The path of the entity.
/// The entity passed in.
private Entity Process(
Entity entity,
UPath path)
{
// See if we have any content. If we don't, then there is nothing
// to do.
if (!entity.HasContent())
{
return entity;
}
// First see if we have a factory for the exact type of content.
IContent content = entity.GetContent();
if (this.factories.TryGetValue(content.GetType(), out Func? getStream))
{
Stream stream = getStream(content);
return this.Process(entity, path, stream);
}
// If we have an easy conversion, then use that so we don't have to
// walk up the tree looking for one we do have.
if (content is IBinaryContentConvertable binaryConvertable
&& this.factories.TryGetValue(typeof(IBinaryContent), out Func? binaryContent))
{
Stream stream = binaryContent(binaryConvertable.ToBinaryContent());
return this.Process(entity, path, stream);
}
if (content is ITextContentConvertable textConvertable
&& this.factories.TryGetValue(typeof(ITextContent), out Func? textContent))
{
Stream stream = textContent(textConvertable.ToTextContent());
return this.Process(entity, path, stream);
}
// For everything else, we have to find a content that we have a
// registered type for by walking up the inheritance tree and
// finding the right type.
List types = new() { content.GetType() };
while (types.Count > 0)
{
// Check to see if we have any of these types.
Func? found = types
.Select(x => this.factories.TryGetValue(x, out Func? factory) ? factory : null)
.FirstOrDefault(x => x != null);
if (found != null)
{
Stream stream = found(content);
return this.Process(entity, path, stream);
}
// We didn't find one, so add all the parent types and try
// again with the new list.
types = types.SelectMany(x => new[] { x.BaseType }.Union(x.GetInterfaces()))
.Where(x => x != null)
.Select(x => x!)
.ToList();
}
// If we got this far, we never found a content to handle.
throw new InvalidOperationException(
"Cannot write out entity "
+ path
+ " because cannot determine how to get a stream out content type "
+ content.GetType()
.FullName
+ ". To resolve, register a function to this.StreamFactories.");
}
///
/// Writes out a stream to the given path in the file system.
///
/// The entity being written out.
/// The path to write out, directories will be created.
/// The stream to write out.
/// The entity passed in.
private Entity Process(
Entity entity,
UPath path,
Stream stream)
{
// Make sure we have the directory structure.
UPath directory = path.GetDirectory();
if (directory != "/")
{
this.FileSystem.CreateDirectory(directory);
}
// Write out the file.
using Stream fileStream = this.FileSystem.CreateFile(path);
stream.CopyTo(fileStream);
stream.Close();
// Return the entity because we've written out the files.
return entity;
}
}