// 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.Collections.Generic;
using System.Globalization;
using System.IO;

using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;

#nullable disable

namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// This class defines the touch task.
    /// </summary>
    public class Touch : TaskExtension, IIncrementalTask
    {
        private MessageImportance messageImportance;

        /// <summary>
        /// Forces a touch even if the file to be touched is read-only.
        /// </summary>
        public bool ForceTouch { get; set; }

        /// <summary>
        /// Creates the file if it doesn't exist.
        /// </summary>
        public bool AlwaysCreate { get; set; }

        /// <summary>
        /// Specifies a specific time other than current.
        /// </summary>
        public string Time { get; set; }

        /// <summary>
        /// File(s) to touch.
        /// </summary>
        [Required]
        public ITaskItem[] Files { get; set; }

        /// <summary>
        /// Output of this task - which files were touched.
        /// </summary>
        [Output]
        public ITaskItem[] TouchedFiles { get; set; }

        /// <summary>
        /// Importance: high, normal, low (default normal)
        /// </summary>
        public string Importance { get; set; }

        /// <summary>
        /// Question the incremental nature of this task.
        /// </summary>
        /// <remarks>When Question is true, skip touching the disk to avoid causing incremental issue.
        /// Unless the file doesn't exists, in which case, error out.</remarks>
        public bool FailIfNotIncremental { get; set; }

        /// <summary>
        /// Implementation of the execute method.
        /// </summary>
        /// <returns></returns>
        internal bool ExecuteImpl(
            FileExists fileExists,
            FileCreate fileCreate,
            GetAttributes fileGetAttributes,
            SetAttributes fileSetAttributes,
            SetLastAccessTime fileSetLastAccessTime,
            SetLastWriteTime fileSetLastWriteTime)
        {
            // See what time we are touching all files to
            DateTime touchDateTime;
            try
            {
                touchDateTime = GetTouchDateTime();
            }
            catch (FormatException e)
            {
                Log.LogErrorWithCodeFromResources("Touch.TimeSyntaxIncorrect", e.Message);
                return false;
            }

            // Go through all files and touch 'em
            bool retVal = true;
            var touchedItems = new List<ITaskItem>();
            var touchedFilesSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            foreach (ITaskItem file in Files)
            {
                string path = FileUtilities.FixFilePath(file.ItemSpec);
                // For speed, eliminate duplicates caused by poor targets authoring
                if (touchedFilesSet.Contains(path))
                {
                    continue;
                }

                // Touch the file.  If the file was touched successfully then add it to our array of
                // touched items.
                if
                (
                    TouchFile(
                        path,
                        touchDateTime,
                        fileExists,
                        fileCreate,
                        fileGetAttributes,
                        fileSetAttributes,
                        fileSetLastAccessTime,
                        fileSetLastWriteTime))
                {
                    touchedItems.Add(file);
                }
                else
                {
                    retVal = false;
                }

                // Add even on failure to avoid reattempting
                touchedFilesSet.Add(path);
            }

            // Now, set the property that indicates which items we touched.  Note that we
            // touch all the items
            TouchedFiles = touchedItems.ToArray();
            return retVal;
        }

        /// <summary>
        /// Run the task
        /// </summary>
        /// <returns></returns>
        public override bool Execute()
        {
            if (string.IsNullOrEmpty(Importance))
            {
                messageImportance = MessageImportance.Normal;
            }
            else
            {
                if (!Enum.TryParse(Importance, ignoreCase: true, out messageImportance))
                {
                    Log.LogErrorWithCodeFromResources("Message.InvalidImportance", Importance);
                    return false;
                }
            }

            return ExecuteImpl(
                File.Exists,
                File.Create,
                File.GetAttributes,
                File.SetAttributes,
                File.SetLastAccessTime,
                File.SetLastWriteTime);
        }

        /// <summary>
        /// Helper method creates a file.
        /// </summary>
        /// <param name="file"></param>
        /// <param name="fileCreate"></param>
        /// <returns>"true" if the file was created.</returns>
        private bool CreateFile(
            string file,
            FileCreate fileCreate)
        {
            try
            {
                using (FileStream fs = fileCreate(file))
                {
                }
            }
            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
            {
                Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file, e.Message);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Helper method touches a file.
        /// </summary>
        /// <returns>"True" if the file was touched.</returns>
        private bool TouchFile(
            string file,
            DateTime dt,
            FileExists fileExists,
            FileCreate fileCreate,
            GetAttributes fileGetAttributes,
            SetAttributes fileSetAttributes,
            SetLastAccessTime fileSetLastAccessTime,
            SetLastWriteTime fileSetLastWriteTime)
        {
            if (!fileExists(file))
            {
                // If the file does not exist then we check if we need to create it.
                if (AlwaysCreate)
                {
                    if (FailIfNotIncremental)
                    {
                        Log.LogWarningFromResources("Touch.CreatingFile", file, "AlwaysCreate");
                    }
                    else
                    {
                        Log.LogMessageFromResources(messageImportance, "Touch.CreatingFile", file, "AlwaysCreate");
                    }

                    if (!CreateFile(file, fileCreate))
                    {
                        return false;
                    }
                }
                else
                {
                    Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file);
                    return false;
                }
            }

            if (FailIfNotIncremental)
            {
                Log.LogWarningFromResources("Touch.Touching", file);
            }
            else
            {
                Log.LogMessageFromResources(messageImportance, "Touch.Touching", file);
            }

            // If the file is read only then we must either issue an error, or, if the user so
            // specified, make the file temporarily not read only.
            bool needToRestoreAttributes = false;
            FileAttributes faOriginal = fileGetAttributes(file);
            if ((faOriginal & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
            {
                if (ForceTouch)
                {
                    try
                    {
                        FileAttributes faNew = (faOriginal & ~FileAttributes.ReadOnly);
                        fileSetAttributes(file, faNew);
                        needToRestoreAttributes = true;
                    }
                    catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                    {
                        string lockedFileMessage = LockCheck.GetLockedFileMessage(file);
                        Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file, e.Message, lockedFileMessage);
                        return false;
                    }
                }
            }

            // Do the actual touch operation
            bool retVal = true;
            try
            {
                fileSetLastAccessTime(file, dt);
                fileSetLastWriteTime(file, dt);
            }
            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
            {
                string lockedFileMessage = LockCheck.GetLockedFileMessage(file);
                Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file, e.Message, lockedFileMessage);
                return false;
            }
            finally
            {
                if (needToRestoreAttributes)
                {
                    // Attempt to restore the attributes.  If we fail here, then there is
                    // not much we can do.
                    try
                    {
                        fileSetAttributes(file, faOriginal);
                    }
                    catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                    {
                        Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file, e.Message);
                        retVal = false;
                    }
                }
            }

            return retVal;
        }

        //-----------------------------------------------------------------------------------
        // Helper methods
        //-----------------------------------------------------------------------------------
        private DateTime GetTouchDateTime()
        {
            // If we have a specified time to which files need to be built then attempt
            // to parse it from the Time property.  Otherwise, we get the current time.
            if (string.IsNullOrEmpty(Time))
            {
                return DateTime.Now;
            }

            return DateTime.Parse(Time, DateTimeFormatInfo.InvariantInfo);
        }
    }
}
