// 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.ComponentModel;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Utilities;
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;

#nullable disable

namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities
{
    /// <summary>
    /// Specifies how the application checks for updates.
    /// </summary>
    [ComVisible(false)]
    public enum UpdateMode
    {
        /// <summary>
        /// Check for updates in the background, after the application starts.
        /// </summary>
        Background,
        /// <summary>
        /// Check for updates in the foreground, before the application starts.
        /// </summary>
        Foreground
    }

    /// <summary>
    /// Specifies the units for the update interval.
    /// </summary>
    [ComVisible(false)]
    public enum UpdateUnit
    {
        /// <summary>
        /// Update interval is in hours.
        /// </summary>
        Hours,
        /// <summary>
        /// Update interval is in days.
        /// </summary>
        Days,
        /// <summary>
        /// Update interval is in weeks.
        /// </summary>
        Weeks
    }

    /// <summary>
    /// Describes a ClickOnce deployment manifest.
    /// </summary>
    [ComVisible(false)]
    [XmlRoot("DeployManifest")]
    public sealed class DeployManifest : Manifest
    {
        private string _createDesktopShortcut;
        private string _deploymentUrl;
        private string _disallowUrlActivation;
        private AssemblyReference _entryPoint;
        private string _errorReportUrl;
        private string _install = "true";
        private string _mapFileExtensions;
        private string _minimumRequiredVersion;
        private string _product;
        private string _publisher;
        private string _suiteName;
        private string _supportUrl;
        private string _trustUrlParameters;
        private string _updateEnabled;
        private string _updateInterval = "0";
        private string _updateMode;
        private string _updateUnit = "days";
        private CompatibleFrameworkCollection _compatibleFrameworkList;
        private List<CompatibleFramework> _compatibleFrameworks = new List<CompatibleFramework>();
        private string _targetFrameworkMoniker;

        private const string _redistListFolder = "RedistList";
        private const string _redistListFile = "FrameworkList.xml";

        /// <summary>
        /// Initializes a new instance of the DeployManifest class.
        /// </summary>
        public DeployManifest()
        {
        }

        /// <summary>
        /// Initializes a new instance of the DeployManifest class.
        /// </summary>
        public DeployManifest(string targetFrameworkMoniker)
        {
            DiscoverCompatFrameworks(targetFrameworkMoniker);
        }

        private void DiscoverCompatFrameworks(string moniker)
        {
            if (!string.IsNullOrEmpty(moniker))
            {
                var frameworkName = new FrameworkNameVersioning(moniker);
                if (frameworkName.Version.Major >= 4)
                {
                    _compatibleFrameworks.Clear();
                    DiscoverCompatibleFrameworks(frameworkName);
                }
            }
        }

        private void DiscoverCompatibleFrameworks(FrameworkNameVersioning frameworkName)
        {
            FrameworkNameVersioning installableFrameworkName = GetInstallableFrameworkName(frameworkName);

            // if profile is null or empty.
            if (string.IsNullOrEmpty(installableFrameworkName.Profile))
            {
                _compatibleFrameworks.Add(GetFullCompatFramework(installableFrameworkName));
            }
            else
            {
                _compatibleFrameworks.Add(GetSubsetCompatFramework(installableFrameworkName));
                _compatibleFrameworks.Add(GetFullCompatFramework(installableFrameworkName));
            }
        }

        /// <summary>
        /// codes from GetInstallableFrameworkForTargetFxInternal in
        /// env/vscore/package/FxMultiTargeting/FrameworkMultiTargetingInternal.cs
        /// </summary>
        private static FrameworkNameVersioning GetInstallableFrameworkName(FrameworkNameVersioning frameworkName)
        {
            string installableFramework = null;
            FrameworkNameVersioning installableFrameworkObj;

            IList<string> referenceAssemblyPaths = GetPathToReferenceAssemblies(frameworkName);

            if (referenceAssemblyPaths?.Count > 0)
            {
                // the first one in the list is the reference assembly path for the requested TFM
                string referenceAssemblyPath = referenceAssemblyPaths[0];

                // Get the redistlist file path
                string redistListFilePath = GetRedistListFilePath(referenceAssemblyPath);

                if (FileSystems.Default.FileExists(redistListFilePath))
                {
                    installableFramework = GetInstallableFramework(redistListFilePath);
                }
            }

            // If the installable framework value is not in the redist, there was no redist, or no matching FX we return the sent TFM,
            // this means frameworks that are installable themselves don't need to specify this property
            // and that all unknown frameworks are assumed to be installable.
            if (installableFramework == null)
            {
                installableFrameworkObj = frameworkName;
            }
            else
            {
                try
                {
                    installableFrameworkObj = new FrameworkNameVersioning(installableFramework);
                }
                catch (ArgumentException)
                {
                    // Redist list data was invalid, behave as if it was not defined.
                    installableFrameworkObj = frameworkName;
                }
            }

            return installableFrameworkObj;
        }

        private static string GetRedistListFilePath(string referenceAssemblyPath)
        {
            string redistListPath = Path.Combine(referenceAssemblyPath, _redistListFolder);
            return Path.Combine(redistListPath, _redistListFile);
        }

        private static IList<string> GetPathToReferenceAssemblies(FrameworkNameVersioning targetFrameworkMoniker)
        {
            IList<string> targetFrameworkPaths = null;
            try
            {
                targetFrameworkPaths = ToolLocationHelper.GetPathToReferenceAssemblies(targetFrameworkMoniker);

                // this returns the chained reference assemblies folders of the framework
                // ordered from highest to lowest version
            }
            catch (InvalidOperationException)
            {
                // The chained dirs does not exist
                // or could not read redistlist for chain
            }

            return targetFrameworkPaths;
        }

        /// <summary>
        /// Gets the InstallableFramework by reading the 'InstallableFramework' attribute in the redist file of the target framework
        /// </summary>
        /// <param name="redistListFilePath">the path to the redistlist file</param>
        /// <returns>InstallableFramework</returns>
        private static string GetInstallableFramework(string redistListFilePath)
        {
            string installableFramework = null;

            try
            {
                var doc = new XmlDocument();
                var xrSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true };
                FileStream fs = File.OpenRead(redistListFilePath);
                using (XmlReader xr = XmlReader.Create(fs, xrSettings))
                {
                    doc.Load(xr);
                    XmlNode fileListNode = doc.DocumentElement;
                    XmlAttribute nameattr = fileListNode?.Attributes["InstallableFramework"];
                    if (!String.IsNullOrEmpty(nameattr?.Value))
                    {
                        installableFramework = nameattr.Value;
                    }
                }
            }
            catch (Exception)
            {
            }

            return installableFramework;
        }

        private static CompatibleFramework GetSubsetCompatFramework(FrameworkNameVersioning frameworkName)
        {
            CompatibleFramework compat = GetFullCompatFramework(frameworkName);
            compat.Profile = frameworkName.Profile;

            return compat;
        }

        private static CompatibleFramework GetFullCompatFramework(FrameworkNameVersioning frameworkName)
        {
            var compat = new CompatibleFramework
            {
                Version = frameworkName.Version.ToString(),
                SupportedRuntime = PatchCLRVersion(Util.GetClrVersion(frameworkName.Version.ToString())),
                Profile = "Full"
            };

            return compat;
        }

        /// <summary>
        /// conver (MajorVersion).(MinorVersion).(Build).(Revision) to (MajorVersion).(MinorVersion).(Build)
        /// </summary>
        /// <param name="version"></param>
        /// <returns></returns>
        private static string PatchCLRVersion(string version)
        {
            try
            {
                var ver = new Version(version);
                var result = new Version(ver.Major, ver.Minor, ver.Build);
                return result.ToString();
            }
            catch (ArgumentException)
            {
                // continue
            }
            catch (FormatException)
            {
                // continue
            }
            catch (OverflowException)
            {
                // continue
            }

            return version;
        }

        /// <summary>
        /// Specifies whether the application install will create a shortcut on the desktop
        /// If True, the installation will create a shortcut to the application on the desktop.
        /// The default is False
        /// If Install is False, this value will be ignored
        /// </summary>
        [XmlIgnore]
        public bool CreateDesktopShortcut
        {
            get => ConvertUtil.ToBoolean(_createDesktopShortcut);
            set => _createDesktopShortcut = (value ? "true" : null);
        }

        /// <summary>
        /// Specifies the target framework moniker of this project.
        /// </summary>
        [XmlIgnore]
        public string TargetFrameworkMoniker
        {
            get => _targetFrameworkMoniker;
            set
            {
                _targetFrameworkMoniker = value;
                DiscoverCompatFrameworks(_targetFrameworkMoniker);
            }
        }

        /// <summary>
        /// A collection of CompatibleFrameworks
        /// </summary>
        [XmlIgnore]
        public CompatibleFrameworkCollection CompatibleFrameworks
        {
            get
            {
                if (_compatibleFrameworkList == null && _compatibleFrameworks != null)
                {
                    _compatibleFrameworkList = new CompatibleFrameworkCollection(_compatibleFrameworks);
                }
                return _compatibleFrameworkList;
            }
        }

        /// <summary>
        /// Specifies the update location for the application.
        /// If this input is not specified then no update location will be defined for the application.
        /// However, if application updates are specified then the update location must be specified.
        /// The specified value should be a fully qualified URL or UNC path.
        /// </summary>
        [XmlIgnore]
        public string DeploymentUrl
        {
            get => _deploymentUrl;
            set => _deploymentUrl = value;
        }

        /// <summary>
        /// Specifies whether the application should be blocked from being activated via a URL.
        /// If this option is True then application can only be activated from the user's Start menu.
        /// The default is False.
        /// This option is ignored if the Install property is False.
        /// </summary>
        [XmlIgnore]
        public bool DisallowUrlActivation
        {
            get => ConvertUtil.ToBoolean(_disallowUrlActivation);
            set => _disallowUrlActivation = value ? "true" : null; // NOTE: disallowUrlActivation=false is implied, and Fusion prefers the false case to be unspecified
        }

        [XmlIgnore]
        public override AssemblyReference EntryPoint
        {
            get => _entryPoint;
            set => _entryPoint = value;
        }

        /// <summary>
        /// Specifies the link to use if there is a failure launching the application.
        /// The specified value should be a fully qualified URL or UNC path.
        /// </summary>
        [XmlIgnore]
        public string ErrorReportUrl
        {
            get => _errorReportUrl;
            set => _errorReportUrl = value;
        }

        /// <summary>
        /// Specifies whether the application is an installed application or an online only application.
        /// If this flag is True the application will be installed on the user's Start menu, and can be removed from the Add/Remove Programs dialog.
        /// If this flag is False then the application is intended for online use from a web page.
        /// The default is True.
        /// </summary>
        [XmlIgnore]
        public bool Install
        {
            get => ConvertUtil.ToBoolean(_install);
            set => _install = Convert.ToString(value, CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Specifies whether or not the ".deploy" file extension mapping is used.
        /// If this flag is true then every application file is published with a ".deploy" file extension.
        /// This option is useful for web server security to limit the number of file extensions that need to be unblocked to enable ClickOnce application deployment.
        /// The default is false.
        /// </summary>
        [XmlIgnore]
        public bool MapFileExtensions
        {
            get => ConvertUtil.ToBoolean(_mapFileExtensions);
            set => _mapFileExtensions = value ? "true" : null; // NOTE: mapFileExtensions=false is implied, and Fusion prefers the false case to be unspecified
        }

        /// <summary>
        /// Specifies whether or not the user can skip the update.
        /// If the user has a version less than the minimum required, he or she will not have the option to skip the update.
        /// The default is to have no minimum required version.
        /// This input only applies when Install is True.
        /// </summary>
        [XmlIgnore]
        public string MinimumRequiredVersion
        {
            get => _minimumRequiredVersion;
            set => _minimumRequiredVersion = value;
        }

        internal override void OnAfterLoad()
        {
            base.OnAfterLoad();
            if (_entryPoint == null && AssemblyReferences?.Count > 0)
            {
                _entryPoint = AssemblyReferences[0];
                _entryPoint.ReferenceType = AssemblyReferenceType.ClickOnceManifest;
            }
        }

        internal override void OnBeforeSave()
        {
            base.OnBeforeSave();
            if (AssemblyIdentity != null && String.IsNullOrEmpty(AssemblyIdentity.PublicKeyToken))
            {
                AssemblyIdentity.PublicKeyToken = "0000000000000000";
            }
        }

        /// <summary>
        /// Specifies the name of the application.
        /// If this input is not specified then the name is inferred from the identity of the generated manifest.
        /// This name is used for the shortcut name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog.
        /// </summary>
        [XmlIgnore]
        public string Product
        {
            get => _product;
            set => _product = value;
        }

        /// <summary>
        /// Specifies the publisher of the application.
        /// If this input is not specified then the name is inferred from the registered user, or the identity of the generated manifest.
        /// This name is used for the folder name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog.
        /// </summary>
        [XmlIgnore]
        public string Publisher
        {
            get => _publisher;
            set => _publisher = value;
        }

        /// <summary>
        /// Specifies the suite name of the application.
        /// This name is used for the sub-folder name on the Start menu (as a child of the publisher)
        /// </summary>
        [XmlIgnore]
        public string SuiteName
        {
            get => _suiteName;
            set => _suiteName = value;
        }

        /// <summary>
        /// Specifies the link that appears in the Add/Remove Programs dialog for the application.
        /// The specified value should be a fully qualified URL or UNC path.
        /// </summary>
        [XmlIgnore]
        public string SupportUrl
        {
            get => _supportUrl;
            set => _supportUrl = value;
        }

        /// <summary>
        /// Specifies whether or not URL query-string parameters should be made available to the application.
        /// The default is False indicating that parameters will not be available to the application.
        /// </summary>
        [XmlIgnore]
        public bool TrustUrlParameters
        {
            get => ConvertUtil.ToBoolean(_trustUrlParameters);
            set => _trustUrlParameters = value ? "true" : null;
            // NOTE: trustUrlParameters=false is implied, and Fusion prefers the false case to be unspecified
        }

        /// <summary>
        /// Indicates whether or not the application is updatable.
        /// The default is False.
        /// This input only applies when Install is True.
        /// </summary>
        [XmlIgnore]
        public bool UpdateEnabled
        {
            get => ConvertUtil.ToBoolean(_updateEnabled);
            set => _updateEnabled = Convert.ToString(value, CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Specifies the update interval for the application.
        /// The default is zero.
        /// This input only applies when Install and UpdateEnabled are both True.
        /// </summary>
        [XmlIgnore]
        public int UpdateInterval
        {
            get
            {
                try { return Convert.ToInt32(_updateInterval, CultureInfo.InvariantCulture); }
                catch (ArgumentException) { return 1; }
                catch (FormatException) { return 1; }
            }
            set => _updateInterval = Convert.ToString(value, CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Specifies whether updates should be checked in the foreground before starting the application, or in the background as the application is running.
        /// The default is "Background".
        /// This input only applies when Install and UpdateEnabled are both True.
        /// </summary>
        [XmlIgnore]
        public UpdateMode UpdateMode
        {
            get
            {
                try { return (UpdateMode)Enum.Parse(typeof(UpdateMode), _updateMode, true); }
                catch (FormatException) { return UpdateMode.Foreground; }
                catch (ArgumentException) { return UpdateMode.Foreground; }
            }
            set => _updateMode = value.ToString();
        }

        /// <summary>
        /// Specifies the units for UpdateInterval input.
        /// This input only applies when Install and UpdateEnabled are both True.
        /// </summary>
        [XmlIgnore]
        public UpdateUnit UpdateUnit
        {
            get
            {
                try { return (UpdateUnit)Enum.Parse(typeof(UpdateUnit), _updateUnit, true); }
                catch (FormatException) { return UpdateUnit.Days; }
                catch (ArgumentException) { return UpdateUnit.Days; }
            }
            set => _updateUnit = value.ToString();
        }

        public override void Validate()
        {
            base.Validate();
            ValidateDeploymentProvider();
            ValidateMinimumRequiredVersion();
            ValidatePlatform();
            ValidateEntryPoint();
        }

        private void ValidateDeploymentProvider()
        {
            if (!String.IsNullOrEmpty(_deploymentUrl) && PathUtil.IsLocalPath(_deploymentUrl))
            {
                OutputMessages.AddWarningMessage("GenerateManifest.InvalidDeploymentProvider");
            }
        }

        private void ValidateEntryPoint()
        {
            if (_entryPoint != null)
            {
                if (!String.IsNullOrEmpty(_entryPoint.TargetPath) && !_entryPoint.TargetPath.EndsWith(
                        ".manifest",
                        StringComparison.OrdinalIgnoreCase))
                {
                    OutputMessages.AddErrorMessage("GenerateManifest.InvalidEntryPoint", _entryPoint.ToString());
                }

                string manifestPath = _entryPoint.ResolvedPath;
                if (manifestPath == null)
                {
                    manifestPath = Path.Combine(Path.GetDirectoryName(SourcePath), _entryPoint.TargetPath);
                }
                if (FileSystems.Default.FileExists(manifestPath))
                {
                    ApplicationManifest entryPointManifest = ManifestReader.ReadManifest(manifestPath, false) as ApplicationManifest;
                    if (entryPointManifest != null)
                    {
                        if (Install)
                        {
                            if (entryPointManifest.HostInBrowser)
                            {
                                OutputMessages.AddErrorMessage("GenerateManifest.HostInBrowserNotOnlineOnly");
                            }
                        }
                        else
                        {
                            if (entryPointManifest.FileAssociations?.Count > 0)
                            {
                                OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsNotInstalled");
                            }
                        }
                    }
                }
            }
        }

        private void ValidateMinimumRequiredVersion()
        {
            if (!String.IsNullOrEmpty(_minimumRequiredVersion))
            {
                var v1 = new Version(_minimumRequiredVersion);
                var v2 = new Version(AssemblyIdentity.Version);
                if (v1 > v2)
                {
                    OutputMessages.AddErrorMessage("GenerateManifest.GreaterMinimumRequiredVersion");
                }
            }
        }

        #region " XmlSerializer "

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("CreateDesktopShortcut")]
        public string XmlCreateDesktopShortcut
        {
            get => _createDesktopShortcut?.ToLowerInvariant();
            set => _createDesktopShortcut = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlArray("CompatibleFrameworks")]
        public CompatibleFramework[] XmlCompatibleFrameworks
        {
            get => _compatibleFrameworks.Count > 0 ? _compatibleFrameworks.ToArray() : null;
            set => _compatibleFrameworks = new List<CompatibleFramework>(value);
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("DeploymentUrl")]
        public string XmlDeploymentUrl
        {
            get => _deploymentUrl;
            set => _deploymentUrl = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("DisallowUrlActivation")]
        public string XmlDisallowUrlActivation
        {
            get => _disallowUrlActivation?.ToLowerInvariant();
            set => _disallowUrlActivation = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("ErrorReportUrl")]
        public string XmlErrorReportUrl
        {
            get => _errorReportUrl;
            set => _errorReportUrl = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("Install")]
        public string XmlInstall
        {
            get => !String.IsNullOrEmpty(_install) ? _install.ToLower(CultureInfo.InvariantCulture) : "true";  // NOTE: Install attribute shouldn't be null in the manifest, so specify install="true" by default
            set => _install = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("MapFileExtensions")]
        public string XmlMapFileExtensions
        {
            get => _mapFileExtensions?.ToLowerInvariant();
            set => _mapFileExtensions = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("MinimumRequiredVersion")]
        public string XmlMinimumRequiredVersion
        {
            get => _minimumRequiredVersion;
            set => _minimumRequiredVersion = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("Product")]
        public string XmlProduct
        {
            get => _product;
            set => _product = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("Publisher")]
        public string XmlPublisher
        {
            get => _publisher;
            set => _publisher = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("SuiteName")]
        public string XmlSuiteName
        {
            get => _suiteName;
            set => _suiteName = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("SupportUrl")]
        public string XmlSupportUrl
        {
            get => _supportUrl;
            set => _supportUrl = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("TrustUrlParameters")]
        public string XmlTrustUrlParameters
        {
            get => _trustUrlParameters?.ToLowerInvariant();
            set => _trustUrlParameters = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("UpdateEnabled")]
        public string XmlUpdateEnabled
        {
            get => _updateEnabled?.ToLower(CultureInfo.InvariantCulture);
            set => _updateEnabled = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("UpdateInterval")]
        public string XmlUpdateInterval
        {
            get => _updateInterval;
            set => _updateInterval = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("UpdateMode")]
        public string XmlUpdateMode
        {
            get => _updateMode;
            set => _updateMode = value;
        }

        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("UpdateUnit")]
        public string XmlUpdateUnit
        {
            get => _updateUnit?.ToLower(CultureInfo.InvariantCulture);
            set => _updateUnit = value;
        }

        #endregion
    }
}
