// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Utilities;

#nullable disable

namespace Microsoft.Build.Tasks
{
    /// <remarks>
    /// Base class for task state files.
    /// </remarks>
    internal abstract class StateFileBase
    {
        // Current version for serialization. This should be changed when breaking changes
        // are made to this class.
        // Note: Consider that changes can break VS2015 RTM which did not have a version check.
        // Version 4/5 - VS2017.7:
        //   Unify .NET Core + Full Framework. Custom serialization on some types that are no
        //   longer [Serializable].
        internal const byte CurrentSerializationVersion = 6;

        // Version this instance is serialized with.
        private byte _serializedVersion = CurrentSerializationVersion;

        /// <summary>
        /// True if <see cref="SerializeCache"/> should create the state file and serialize ourselves, false otherwise.
        /// </summary>
        internal virtual bool HasStateToSave => true;

        /// <summary>
        /// Writes the contents of this object out to the specified file.
        /// </summary>
        internal virtual void SerializeCache(string stateFile, TaskLoggingHelper log, bool serializeEmptyState = false)
        {
            try
            {
                if (!string.IsNullOrEmpty(stateFile))
                {
                    if (FileSystems.Default.FileExists(stateFile))
                    {
                        File.Delete(stateFile);
                    }

                    if (serializeEmptyState || HasStateToSave)
                    {
                        using (var s = new FileStream(stateFile, FileMode.CreateNew))
                        {
                            var translator = BinaryTranslator.GetWriteTranslator(s);
                            translator.Translate(ref _serializedVersion);
                            Translate(translator);
                        }
                    }
                }
            }
            // If there was a problem writing the file (like it's read-only or locked on disk, for
            // example), then eat the exception and log a warning.  Otherwise, rethrow.
            catch (Exception e) when (!ExceptionHandling.NotExpectedSerializationException(e))
            {
                // Not being able to serialize the cache is not an error, but we let the user know anyway.
                // Don't want to hold up processing just because we couldn't read the file.
                log.LogWarningWithCodeFromResources("General.CouldNotWriteStateFile", stateFile, e.Message);
            }
        }

        public abstract void Translate(ITranslator translator);

        /// <summary>
        /// Reads the specified file from disk into a StateFileBase derived object.
        /// </summary>
        internal static T DeserializeCache<T>(string stateFile, TaskLoggingHelper log) where T : StateFileBase
        {
            T retVal = null;

            // First, we read the cache from disk if one exists, or if one does not exist, we create one.
            try
            {
                if (!string.IsNullOrEmpty(stateFile) && FileSystems.Default.FileExists(stateFile))
                {
                    using (FileStream s = File.OpenRead(stateFile))
                    {
                        using var translator = BinaryTranslator.GetReadTranslator(s, InterningBinaryReader.PoolingBuffer);

                        byte version = 0;
                        translator.Translate(ref version);
                        // If the version is wrong, log a message not a warning. This could be a valid cache with the wrong version preventing correct deserialization.
                        // For the latter case, internals may be unexpectedly null.
                        if (version != CurrentSerializationVersion)
                        {
                            log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, log.FormatResourceString("General.IncompatibleStateFileType"));
                            return null;
                        }

                        var constructors = typeof(T).GetConstructors();
                        foreach (var constructor in constructors)
                        {
                            var parameters = constructor.GetParameters();
                            if (parameters.Length == 1 && parameters[0].ParameterType == typeof(ITranslator))
                            {
                                retVal = constructor.Invoke([translator]) as T;
                            }
                        }

                        if (retVal == null)
                        {
                            log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile,
                                log.FormatResourceString("General.IncompatibleStateFileType"));
                        }
                    }
                }
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                // The deserialization process seems like it can throw just about
                // any exception imaginable.  Catch them all here.
                // Not being able to deserialize the cache is not an error, but we let the user know anyway.
                // Don't want to hold up processing just because we couldn't read the file.
                log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, e.Message);
            }

            return retVal;
        }

        /// <summary>
        /// Deletes the state file from disk
        /// </summary>
        /// <param name="stateFile"></param>
        /// <param name="log"></param>
        internal static void DeleteFile(string stateFile, TaskLoggingHelper log)
        {
            try
            {
                if (!string.IsNullOrEmpty(stateFile))
                {
                    if (FileSystems.Default.FileExists(stateFile))
                    {
                        File.Delete(stateFile);
                    }
                }
            }
            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
            {
                log.LogWarningWithCodeFromResources("General.CouldNotDeleteStateFile", stateFile, e.Message);
            }
        }
    }
}
