using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Markdown.Mapping;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;

namespace Robust.Shared.Replays;

[NotContentImplementable]
public interface IReplayRecordingManager
{
    /// <summary>
    /// Whether or not a replay recording can currently be started.
    /// </summary>
    bool CanStartRecording();

    /// <summary>
    /// This is a convenience variation of <see cref="RecordReplayMessage"/> that only records messages for server-side
    /// recordings.
    /// </summary>
    void RecordServerMessage(object obj);

    /// <summary>
    /// This is a convenience variation of <see cref="RecordReplayMessage"/> that only records messages for client-side
    /// recordings.
    /// </summary>
    void RecordClientMessage(object obj);

    /// <summary>
    /// Queues some net-serializable data to be saved by a replay recording. Does nothing if <see cref="IsRecording"/>
    /// is false.
    /// </summary>
    /// <remarks>
    /// The queued object is typically something like an <see cref="EntityEventArgs"/>, so that replays can
    /// simulate receiving networked messages. However, this can really be any serializable data and could be used
    /// for saving server-exclusive data like power net or atmos pipe-net data for replaying. Alternatively, those
    /// could also just use networked component states on entities that are in null space and hidden from all
    /// players (but still saved to replays).
    /// </remarks>
    void RecordReplayMessage(object obj);

    /// <summary>
    ///     Whether the server is currently recording replay data.
    /// </summary>
    bool IsRecording { get; }

    /// <summary>
    /// Gets the <c>state</c> object passed into <see cref="TryStartRecording"/> for the current recording.
    /// </summary>
    /// <remarks>
    /// Returns <see langword="null"/> if there is no active replay recording.
    /// </remarks>
    public object? ActiveRecordingState { get; }

    /// <summary>
    /// Processes pending write tasks and saves the replay data for the current tick. This should be called even if a
    /// replay is not currently being recorded.
    /// </summary>
    void Update(GameState? state);

    /// <summary>
    /// This gets invoked whenever a replay recording is starting. Subscribers can use this to add extra yaml data
    /// to the recording's metadata file, as well as to provide serializable messages that get replayed when the replay
    /// is initially loaded. E.g., this should contain networked events that would get sent to a newly connected client.
    /// </summary>
    event Action<MappingDataNode, List<object>> RecordingStarted;

    /// <summary>
    /// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra yaml data to the
    /// recording's metadata file.
    /// </summary>
    /// <seealso cref="RecordingStopped2"/>
    event Action<MappingDataNode> RecordingStopped;

    /// <summary>
    /// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra data to the replay.
    /// </summary>
    /// <remarks>
    /// This is effectively a more powerful version of <see cref="RecordingStopped"/>.
    /// </remarks>
    event Action<ReplayRecordingStopped> RecordingStopped2;

    /// <summary>
    /// This gets invoked after a replay recording has finished and provides information about where the replay data
    /// was saved. Note that this only means that all write tasks have started, however some of the file tasks may not
    /// have finished yet. See <see cref="WaitWriteTasks"/>.
    /// </summary>
    event Action<ReplayRecordingFinished> RecordingFinished;

    /// <summary>
    /// Tries to starts a replay recording.
    /// </summary>
    /// <param name="directory">
    /// The directory that the replay will be written to. E.g., <see cref="IResourceManager.UserData"/>.
    /// </param>
    /// <param name="name">
    /// The name of the replay. This will also determine the folder where the replay will be stored, which will be a
    /// subfolder within  <see cref="CVars.ReplayDirectory"/>. If not provided, will default to using the current time.
    /// </param>
    /// <param name="overwrite">
    /// Whether to overwrite the specified path if a folder already exists.
    /// </param>
    /// <param name="duration">
    /// Optional time limit for the recording.
    /// </param>
    /// <param name="state">
    /// An arbitrary object that is available in <see cref="ActiveRecordingState"/> and <see cref="RecordingFinished"/>.
    /// </param>
    /// <returns>Returns true if the recording was successfully started.</returns>
    bool TryStartRecording(
        IWritableDirProvider directory,
        string? name = null,
        bool overwrite = false,
        TimeSpan? duration = null,
        object? state = null);

    /// <summary>
    /// Stops an ongoing replay recording.
    /// </summary>
    void StopRecording();

    /// <summary>
    /// Returns information about the currently ongoing replay recording.
    /// </summary>
    ReplayRecordingStats GetReplayStats();

    /// <summary>
    /// Returns a task that will wait for all the current writing tasks to finish.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if we are currently recording (<see cref="IsRecording"/> true).
    /// </exception>
    Task WaitWriteTasks();

    /// <summary>
    /// Returns true if there are any currently running write tasks.
    /// </summary>
    bool IsWriting();
}

/// <summary>
/// Event object used by <see cref="IReplayRecordingManager.RecordingStopped2"/>.
/// Allows modifying metadata and adding more data to replay files.
/// </summary>
public sealed class ReplayRecordingStopped
{
    /// <summary>
    /// Mutable metadata that will be saved to the replay's metadata file.
    /// </summary>
    public required MappingDataNode Metadata { get; init; }

    /// <summary>
    /// A writer that allows arbitrary file writing into the replay file.
    /// </summary>
    public required IReplayFileWriter Writer { get; init; }

    internal ReplayRecordingStopped()
    {
    }
}

/// <summary>
/// Event data for <see cref="IReplayRecordingManager.RecordingFinished"/>.
/// </summary>
/// <param name="Directory">The writable dir provider in which the replay is being recorded.</param>
/// <param name="Path">The path to the replay in <paramref name="Directory"/>.</param>
/// <param name="State">The state object passed to <see cref="IReplayRecordingManager.TryStartRecording"/>.</param>
public record ReplayRecordingFinished(IWritableDirProvider Directory, ResPath Path, object? State);

/// <summary>
/// Statistics for an active replay recording.
/// </summary>
/// <param name="Time">The simulation time the replay has been recording for.</param>
/// <param name="Ticks">The amount of simulation ticks the replay has recorded.</param>
/// <param name="Size">The total compressed size of the replay data blobs.</param>
/// <param name="UncompressedSize">The total uncompressed size of the replay data blobs.</param>
public record struct ReplayRecordingStats(TimeSpan Time, uint Ticks, long Size, long UncompressedSize);

/// <summary>
/// Allows writing extra files directly into the replay file.
/// </summary>
/// <seealso cref="ReplayRecordingStopped"/>
/// <seealso cref="IReplayRecordingManager.RecordingStopped2"/>
[NotContentImplementable]
public interface IReplayFileWriter
{
    /// <summary>
    /// The base directory inside the replay directory you should generally be writing to.
    /// This is equivalent to <see cref="ReplayConstants.ReplayZipFolder"/>.
    /// </summary>
    ResPath BaseReplayPath { get; }

    /// <summary>
    /// Writes arbitrary data into a file in the replay.
    /// </summary>
    /// <param name="path">The file path to write to.</param>
    /// <param name="bytes">The bytes to write to the file.</param>
    /// <param name="compressionLevel">How much to compress the file.</param>
    void WriteBytes(
        ResPath path,
        ReadOnlyMemory<byte> bytes,
        CompressionLevel compressionLevel = CompressionLevel.Optimal);

    /// <summary>
    /// Writes a yaml document into a file in the replay.
    /// </summary>
    /// <param name="path">The file path to write to.</param>
    /// <param name="yaml">The yaml document to write to the file.</param>
    /// <param name="compressionLevel">How much to compress the file.</param>
    void WriteYaml(
        ResPath path,
        YamlDocument yaml,
        CompressionLevel compressionLevel = CompressionLevel.Optimal)
    {
        var memStream = new MemoryStream();
        using var writer = new StreamWriter(memStream);
        var yamlStream = new YamlStream {yaml};
        yamlStream.Save(new YamlMappingFix(new Emitter(writer)), false);
        writer.Flush();
        WriteBytes(path, memStream.AsMemory(), compressionLevel);
    }
}

/// <summary>
/// Engine-internal functions for <see cref="IReplayRecordingManager"/>.
/// </summary>
internal interface IReplayRecordingManagerInternal : IReplayRecordingManager
{
    /// <summary>
    /// Initializes the replay manager.
    /// </summary>
    void Initialize();

    /// <summary>
    /// Shut down any active replay recording, at engine shutdown.
    /// </summary>
    void Shutdown();
}
