// 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 System.Linq;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Build.Tasks;
using Microsoft.Cci;
using Microsoft.Cci.Extensions;
using Microsoft.Cci.Extensions.CSharp;
using Microsoft.Cci.Filters;
using Microsoft.Cci.Writers;
using Microsoft.Cci.Writers.CSharp;
using Microsoft.Cci.Writers.Syntax;

namespace Microsoft.DotNet.GenAPI
{
    public class GenAPITask : BuildTask
    {
        private const string InternalsVisibleTypeName = "System.Runtime.CompilerServices.InternalsVisibleToAttribute";
        private const string DefaultFileHeader =
                "//------------------------------------------------------------------------------\r\n" +
                "// <auto-generated>\r\n" +
                "//     This code was generated by a tool.\r\n" +
                "//     GenAPI Version: {0}\r\n" +
                "//\r\n" +
                "//     Changes to this file may cause incorrect behavior and will be lost if\r\n" +
                "//     the code is regenerated.\r\n" +
                "// </auto-generated>\r\n" +
                "//------------------------------------------------------------------------------\r\n";
        
        private WriterType _writerType;
        private SyntaxWriterType _syntaxWriterType;
        private DocIdKinds _docIdKinds = Cci.Writers.DocIdKinds.All;

        /// <summary>
        /// Path for an specific assembly or a directory to get all assemblies.
        /// </summary>
        [Required]
        public string Assembly { get; set; }

        /// <summary>
        /// Delimited (',' or ';') set of paths to use for resolving assembly references.
        /// </summary>
        public string LibPath { get; set; }

        /// <summary>
        /// Specify a api list in the DocId format of which APIs to include.
        /// </summary>
        public string ApiList { get; set; }

        /// <summary>
        /// Output path. Default is the console. Can specify an existing directory as well and
        /// then a file will be created for each assembly with the matching name of the assembly.
        /// </summary>
        public string OutputPath { get; set; }

        /// <summary>
        /// Specify a file with an alternate header content to prepend to output.
        /// </summary>
        public string HeaderFile { get; set; }

        /// <summary>
        /// Specify the writer type to use. Legal values: CSDecl, DocIds, TypeForwards, TypeList. Default is CSDecl.
        /// </summary>
        public string WriterType 
        { 
            get => _writerType.ToString(); 
            set => _writerType = string.IsNullOrWhiteSpace(value) ? default : (WriterType) Enum.Parse(typeof(WriterType), value, true); 
        }

        /// <summary>
        /// Specific the syntax writer type. Only used if the writer is CSDecl. Legal values: Text, Html, Xml. Default is Text.
        /// </summary>
        public string SyntaxWriterType 
        { 
            get => _syntaxWriterType.ToString(); 
            set => _syntaxWriterType = string.IsNullOrWhiteSpace(value) ? default : (SyntaxWriterType)Enum.Parse(typeof(SyntaxWriterType), value, true); 
        }

        /// <summary>
        /// Only include API of the specified kinds. Legal values: A, Assembly, Namespace, N, T, Type, Field, F, P, Property, Method, M, Event, E, All. Default is All.
        /// </summary>
        public string DocIdKinds 
        { 
            get => _docIdKinds.ToString(); 
            set => _docIdKinds = string.IsNullOrWhiteSpace(value) ? Cci.Writers.DocIdKinds.All : (DocIdKinds)Enum.Parse(typeof(DocIdKinds), value, true); 
        }

        /// <summary>
        /// Method bodies should throw PlatformNotSupportedException.
        /// </summary>
        public string ExceptionMessage { get; set; }

        /// <summary>
        /// Include global prefix for compilation.
        /// </summary>
        public bool GlobalPrefix { get; set; }

        /// <summary>
        /// Specify a api list in the DocId format of which APIs to exclude.
        /// </summary>
        public string ExcludeApiList { get; set; }

        /// <summary>
        /// Specify a list in the DocId format of which attributes should be excluded from being applied on apis.
        /// </summary>
        public string ExcludeAttributesList { get; set; }

        /// <summary>
        /// [CSDecl] Resolve type forwards and include its members.
        /// </summary>
        public bool FollowTypeForwards { get; set; }

        /// <summary>
        /// [CSDecl] Include only API's not CS code that compiles.
        /// </summary>
        public bool ApiOnly { get; set; }

        /// <summary>
        /// Include all API's not just public APIs. Default is public only.
        /// </summary>
        public bool All { get; set; }

        /// <summary>
        /// Include both internal and public APIs if assembly contains an InternalsVisibleTo attribute. Otherwise, include only public APIs.
        /// </summary>
        public bool RespectInternals { get; set; }

        /// <summary>
        /// Exclude APIs marked with a CompilerGenerated attribute.
        /// </summary>
        public bool ExcludeCompilerGenerated { get; set; }

        /// <summary>
        /// [CSDecl] Include member headings for each type of member.
        /// </summary>
        public bool MemberHeadings { get; set; }

        /// <summary>
        /// [CSDecl] Highlight overridden base members.
        /// </summary>
        public bool HighlightBaseMembers { get; set; }

        /// <summary>
        /// [CSDecl] Highlight interface implementation members.
        /// </summary>
        public bool HighlightInterfaceMembers { get; set; }

        /// <summary>
        /// [CSDecl] Include base types, interfaces, and attributes, even when those types are filtered.
        /// </summary>
        public bool AlwaysIncludeBase { get; set; }

        /// <summary>
        /// [CSDecl] Specify one or more namespace+type list(s) in the DocId format of types that should be wrapped inside an #if with the symbol specified in the <c>Symbol</c> metadata item.
        /// If the <c>Symbol</c> metadata item is empty the types won't be wrapped but will still be output after all other types, this can be used in combination with <see cref="DefaultCondition" /> to wrap all other types.
        /// </summary>
        public ITaskItem[] ConditionalTypeLists { get; set; }

        /// <summary>
        /// [CSDecl] #if condition to apply to all types not included in <see cref="ConditionalTypeLists" />.
        /// </summary>
        public string DefaultCondition { get; set; }

        /// <summary>
        /// Exclude members when return value or parameter types are excluded.
        /// </summary>
        public bool ExcludeMembers { get; set; }

        /// <summary>
        /// [CSDecl] Language Version to target.
        /// </summary>
        public string LangVersion { get; set; }

        public override bool Execute()
        {
            HostEnvironment host = new();
            host.UnableToResolve += (sender, e) =>
                Log.LogWarning("Unable to resolve assembly '{0}' referenced by '{1}'.", e.Unresolved.ToString(), e.Referrer.ToString());

            host.UnifyToLibPath = true;
            if (!string.IsNullOrWhiteSpace(LibPath))
            {
                host.AddLibPaths(HostEnvironment.SplitPaths(LibPath));
            }

            IEnumerable<IAssembly> assemblies = host.LoadAssemblies(HostEnvironment.SplitPaths(Assembly));
            if (!assemblies.Any())
            {
                Log.LogError("ERROR: Failed to load any assemblies from '{0}'", Assembly);
                return false;
            }

            string headerText = GetHeaderText(HeaderFile, _writerType, _syntaxWriterType);
            bool loopPerAssembly = Directory.Exists(OutputPath);

            if (loopPerAssembly)
            {
                foreach (IAssembly assembly in assemblies)
                {
                    using (TextWriter output = GetOutput(OutputPath, GetFilename(assembly, _writerType, _syntaxWriterType)))
                    using (IStyleSyntaxWriter syntaxWriter = GetSyntaxWriter(output, _writerType, _syntaxWriterType))
                    {
                        ICciWriter writer = null;
                        try
                        {
                            if (headerText != null)
                            {
                                output.Write(headerText);
                            }

                            bool includeInternals = RespectInternals &&
                                assembly.Attributes.HasAttributeOfType(InternalsVisibleTypeName);
                            writer = GetWriter(output, syntaxWriter, includeInternals);
                            writer.WriteAssemblies(new IAssembly[] { assembly });
                        }
                        finally
                        {
                            if (writer is CSharpWriter csWriter)
                            {
                                csWriter.Dispose();
                            }
                        }
                    }
                }
            }
            else
            {
                using (TextWriter output = GetOutput(OutputPath))
                using (IStyleSyntaxWriter syntaxWriter = GetSyntaxWriter(output, _writerType, _syntaxWriterType))
                {
                    ICciWriter writer = null;
                    try
                    {
                        if (headerText != null)
                        {
                            output.Write(headerText);
                        }

                        bool includeInternals = RespectInternals &&
                            assemblies.Any(assembly => assembly.Attributes.HasAttributeOfType(InternalsVisibleTypeName));
                        writer = GetWriter(output, syntaxWriter, includeInternals);
                        writer.WriteAssemblies(assemblies);
                    }
                    finally
                    {
                        if (writer is CSharpWriter csWriter)
                        {
                            csWriter.Dispose();
                        }
                    }
                }
            }

            return !Log.HasLoggedErrors;
        }

        private static string GetHeaderText(string headerFile, WriterType writerType, SyntaxWriterType syntaxWriterType)
        {
            if (!string.IsNullOrEmpty(headerFile))
            {
                return File.ReadAllText(headerFile);
            }

            string defaultHeader = string.Empty;
            // This header is for CS source only
            if ((writerType == GenAPI.WriterType.CSDecl || writerType == GenAPI.WriterType.TypeForwards) &&
                syntaxWriterType == GenAPI.SyntaxWriterType.Text)
            {
                // Write default header (culture-invariant, so that the generated file will not be language-dependent)
                defaultHeader = string.Format(CultureInfo.InvariantCulture,
                    DefaultFileHeader, typeof(GenAPITask).Assembly.GetName().Version.ToString());
            }

            return defaultHeader;
        }

        private static TextWriter GetOutput(string outFilePath, string filename = "")
        {
            // If this is a null, empty, whitespace, or a directory use console
            if (string.IsNullOrWhiteSpace(outFilePath))
                return Console.Out;

            if (Directory.Exists(outFilePath) && !string.IsNullOrEmpty(filename))
            {
                return File.CreateText(Path.Combine(outFilePath, filename));
            }

            return File.CreateText(outFilePath);
        }

        private static string GetFilename(IAssembly assembly, WriterType writer, SyntaxWriterType syntax)
        {
            string name = assembly.Name.Value;
            return writer switch
            {
                GenAPI.WriterType.DocIds or GenAPI.WriterType.TypeForwards => name + ".txt",
                _ => syntax switch
                {
                    GenAPI.SyntaxWriterType.Xml => name + ".xml",
                    GenAPI.SyntaxWriterType.Html => name + ".html",
                    _ => name + ".cs",
                },
            };
        }

        private static ICciFilter GetFilter(
            string apiList,
            bool all,
            bool includeInternals,
            bool apiOnly,
            bool excludeCompilerGenerated,
            string excludeApiList,
            bool excludeMembers,
            string excludeAttributesList,
            bool includeForwardedTypes)
        {
            ICciFilter includeFilter;
            if (!string.IsNullOrWhiteSpace(apiList))
            {
                includeFilter = new DocIdIncludeListFilter(apiList);
            }
            else if (all)
            {
                includeFilter = new IncludeAllFilter();
            }
            else if (includeInternals)
            {
                includeFilter = new InternalsAndPublicCciFilter(excludeAttributes: apiOnly, includeForwardedTypes);
            }
            else
            {
                includeFilter = new PublicOnlyCciFilter(excludeAttributes: apiOnly, includeForwardedTypes);
            }

            if (excludeCompilerGenerated)
            {
                includeFilter = new IntersectionFilter(includeFilter, new ExcludeCompilerGeneratedCciFilter());
            }

            if (!string.IsNullOrWhiteSpace(excludeApiList))
            {
                includeFilter = new IntersectionFilter(includeFilter, new DocIdExcludeListFilter(excludeApiList, excludeMembers) { IncludeForwardedTypes = includeForwardedTypes });
            }

            if (!string.IsNullOrWhiteSpace(excludeAttributesList))
            {
                includeFilter = new IntersectionFilter(includeFilter, new ExcludeAttributesFilter(excludeAttributesList));
            }

            return includeFilter;
        }

        private static IStyleSyntaxWriter GetSyntaxWriter(TextWriter output, WriterType writer, SyntaxWriterType syntax)
        {
            if (writer != GenAPI.WriterType.CSDecl && writer != GenAPI.WriterType.TypeList)
                return null;

            return syntax switch
            {
                GenAPI.SyntaxWriterType.Xml => new OpenXmlSyntaxWriter(output),
                GenAPI.SyntaxWriterType.Html => new HtmlSyntaxWriter(output),
                _ => new TextSyntaxWriter(output) { SpacesInIndent = 4 },
            };
        }

        private ICciWriter GetWriter(TextWriter output, ISyntaxWriter syntaxWriter, bool includeInternals)
        {
            ICciFilter filter = GetFilter(
                ApiList,
                All,
                includeInternals,
                ApiOnly,
                ExcludeCompilerGenerated,
                ExcludeApiList,
                ExcludeMembers,
                ExcludeAttributesList,
                FollowTypeForwards);

            switch (_writerType)
            {
                case GenAPI.WriterType.DocIds:
                    return new DocumentIdWriter(output, filter, _docIdKinds);
                case GenAPI.WriterType.TypeForwards:
                    return new TypeForwardWriter(output, filter)
                    {
                        IncludeForwardedTypes = true
                    };
                case GenAPI.WriterType.TypeList:
                    return new TypeListWriter(syntaxWriter, filter);
                case GenAPI.WriterType.CSDecl:
                default:
                    {
                        CSharpWriter writer = new(syntaxWriter, filter, ApiOnly);
                        writer.IncludeSpaceBetweenMemberGroups = writer.IncludeMemberGroupHeadings = MemberHeadings;
                        writer.HighlightBaseMembers = HighlightBaseMembers;
                        writer.HighlightInterfaceMembers = HighlightInterfaceMembers;
                        writer.PutBraceOnNewLine = true;
                        writer.PlatformNotSupportedExceptionMessage = ExceptionMessage;
                        writer.IncludeGlobalPrefixForCompilation = GlobalPrefix;
                        writer.AlwaysIncludeBase = AlwaysIncludeBase;
                        writer.LangVersion = GetLangVersion(LangVersion);
                        writer.IncludeForwardedTypes = FollowTypeForwards;
                        writer.DefaultCondition = DefaultCondition;
                        if (ConditionalTypeLists != null)
                        {
                            writer.ConditionalTypeLists = ConditionalTypeLists.Select(c =>
                                new CSharpWriter.ConditionalTypeList
                                {
                                    Symbol = c.GetMetadata("Symbol"),
                                    Filter = new DocIdIncludeListFilter(c.ItemSpec) { IncludeForwardedTypes = FollowTypeForwards }
                                });
                        }
                        return writer;
                    }
            }
        }

        private static Version GetLangVersion(string langVersion)
        {
            string langVersionValue = langVersion ?? string.Empty;

            if (langVersionValue.Equals("default", StringComparison.OrdinalIgnoreCase))
            {
                return CSDeclarationWriter.LangVersionDefault;
            }
            else if (langVersionValue.Equals("latest", StringComparison.OrdinalIgnoreCase))
            {
                return CSDeclarationWriter.LangVersionLatest;
            }
            else if (langVersionValue.Equals("preview", StringComparison.OrdinalIgnoreCase))
            {
                return CSDeclarationWriter.LangVersionPreview;
            }
            else if (Version.TryParse(langVersionValue, out Version parsedVersion))
            {
                return parsedVersion;
            }
            else
            {
                return CSDeclarationWriter.LangVersionDefault;
            }
        }
    }
}
