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

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Internal;
using Microsoft.Internal.Collections;

namespace System.ComponentModel.Composition
{

    /// <summary>
    ///     The exception that is thrown when one or more errors occur during composition.
    /// </summary>
    [DebuggerTypeProxy(typeof(CompositionExceptionDebuggerProxy))]
    [DebuggerDisplay("{Message}")]
    public class CompositionException : Exception
    {
        private readonly ReadOnlyCollection<CompositionError> _errors;

        /// <summary>
        ///     Initializes a new instance of the <see cref="CompositionException"/> class.
        /// </summary>
        public CompositionException()
                : this((string?)null, (Exception?)null, (IEnumerable<CompositionError>?)null)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="CompositionException"/> class
        ///     with the specified error message.
        /// </summary>
        /// <param name="message">
        ///     A <see cref="string"/> containing a message that describes the
        ///     <see cref="CompositionException"/>; or <see langword="null"/> to set
        ///     the <see cref="Exception.Message"/> property to its default value.
        /// </param>
        public CompositionException(string? message)
            : this(message, (Exception?)null, (IEnumerable<CompositionError>?)null)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="CompositionException"/> class
        ///     with the specified error message and exception that is the cause of the
        ///     exception.
        /// </summary>
        /// <param name="message">
        ///     A <see cref="string"/> containing a message that describes the
        ///     <see cref="CompositionException"/>; or <see langword="null"/> to set
        ///     the <see cref="Exception.Message"/> property to its default value.
        /// </param>
        /// <param name="innerException">
        ///     The <see cref="Exception"/> that is the underlying cause of the
        ///     <see cref="ComposablePartException"/>; or <see langword="null"/> to set
        ///     the <see cref="Exception.InnerException"/> property to <see langword="null"/>.
        /// </param>
        public CompositionException(string? message, Exception? innerException)
            : this(message, innerException, (IEnumerable<CompositionError>?)null)
        {
        }

        internal CompositionException(CompositionError error)
            : this(new CompositionError[] { error })
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="CompositionException"/> class
        ///     with the specified errors.
        /// </summary>
        /// <param name="errors">
        ///     An <see cref="IEnumerable{T}"/> of <see cref="CompositionError"/> objects
        ///     representing the errors that are the cause of the
        ///     <see cref="CompositionException"/>; or <see langword="null"/> to set the
        ///     <see cref="Errors"/> property to an empty <see cref="IEnumerable{T}"/>.
        /// </param>
        /// <exception cref="ArgumentException">
        ///     <paramref name="errors"/> contains an element that is <see langword="null"/>.
        /// </exception>
        public CompositionException(IEnumerable<CompositionError>? errors)
            : this((string?)null, (Exception?)null, errors)
        {
        }

        internal CompositionException(string? message, Exception? innerException, IEnumerable<CompositionError>? errors)
                    : base(message, innerException)
        {
            Requires.NullOrNotNullElements(errors, nameof(errors));
            _errors = Array.AsReadOnly(errors == null ? Array.Empty<CompositionError>() : errors.ToArray<CompositionError>());
        }

        /// <summary>
        ///     Gets the errors that are the cause of the exception.
        /// </summary>
        /// <value>
        ///     An <see cref="IEnumerable{T}"/> of <see cref="CompositionError"/> objects
        ///     representing the errors that are the cause of the
        ///     <see cref="CompositionException"/>.
        /// </value>
        public ReadOnlyCollection<CompositionError> Errors
        {
            get { return _errors; }
        }

        /// <summary>
        ///     Gets a message that describes the exception.
        /// </summary>
        /// <value>
        ///     A <see cref="string"/> containing a message that describes the
        ///     <see cref="CompositionException"/>.
        /// </value>
        public override string Message
        {
            get
            {
                if (Errors.Count == 0)
                {   // If there are no errors, then we simply return base.Message,
                    // which will either use the default Exception message, or if
                    // one was specified; the user supplied message.

                    return base.Message;
                }

                return BuildDefaultMessage();
            }
        }

        public ReadOnlyCollection<Exception> RootCauses
        {
            get
            {
                var errors = new List<Exception>();

                // In here return a collection of all of the exceptions in the Errors collection
                foreach (var error in Errors)
                {
                    if (error.Exception != null)
                    {
                        if (error.Exception is CompositionException ce)
                        {
                            if (ce.RootCauses.Count > 0)
                            {
                                errors.AddRange(ce.RootCauses);
                                continue;
                            }
                        }
                        errors.Add(error.Exception);
                    }
                }
                return errors.ToReadOnlyCollection<Exception>();
            }
        }

        private string BuildDefaultMessage()
        {
            List<Stack<CompositionError>> paths = CalculatePaths(this);

            StringBuilder writer = new StringBuilder();

            WriteHeader(writer, Errors.Count, paths.Count);
            WritePaths(writer, paths);

            return writer.ToString();
        }

        private static void WriteHeader(StringBuilder writer, int errorsCount, int pathCount)
        {
            if (errorsCount > 1 && pathCount > 1)
            {
                // The composition produced multiple composition errors, with {0} root causes. The root causes are provided below.
                writer.AppendFormat(
                     CultureInfo.CurrentCulture,
                     SR.CompositionException_MultipleErrorsWithMultiplePaths,
                     pathCount);
            }
            else if (errorsCount == 1 && pathCount > 1)
            {
                // The composition produced a single composition error, with {0} root causes. The root causes are provided below.
                writer.AppendFormat(
                     CultureInfo.CurrentCulture,
                     SR.CompositionException_SingleErrorWithMultiplePaths,
                     pathCount);
            }
            else
            {
                if (errorsCount != 1 || pathCount != 1)
                {
                    throw new Exception(SR.Diagnostic_InternalExceptionMessage);
                }

                // The composition produced a single composition error. The root cause is provided below.
                writer.AppendFormat(
                     CultureInfo.CurrentCulture,
                     SR.CompositionException_SingleErrorWithSinglePath,
                     pathCount);
            }

            writer.Append(' ');
            writer.AppendLine(SR.CompositionException_ReviewErrorProperty);
        }

        private static void WritePaths(StringBuilder writer, List<Stack<CompositionError>> paths)
        {
            int ordinal = 0;
            foreach (Stack<CompositionError> path in paths)
            {
                ordinal++;
                WritePath(writer, path, ordinal);
            }
        }

        private static void WritePath(StringBuilder writer, Stack<CompositionError> path, int ordinal)
        {
            writer.AppendLine();
            writer.Append(ordinal.ToString(CultureInfo.CurrentCulture));
            writer.Append(SR.CompositionException_PathsCountSeparator);
            writer.Append(' ');

            bool needsSeparator = false;
            foreach (CompositionError error in path)
            {
                if (needsSeparator)
                {
                    writer.AppendLine().Append(SR.CompositionException_ErrorPrefix).Append(' ');
                }
                needsSeparator = true;

                WriteError(writer, error);
            }
        }

        private static void WriteError(StringBuilder writer, CompositionError error)
        {
            writer.AppendLine(error.Description);

            if (error.Element != null)
            {
                WriteElementGraph(writer, error.Element);
            }
        }

        private static void WriteElementGraph(StringBuilder writer, ICompositionElement element)
        {
            // Writes the composition element and its origins in the format:
            // Element: Export --> Part --> PartDefinition --> Catalog

            writer.AppendFormat(CultureInfo.CurrentCulture, SR.CompositionException_ElementPrefix, element.DisplayName);

            while ((element = element.Origin!) != null)
            {
                writer.AppendFormat(CultureInfo.CurrentCulture, SR.CompositionException_OriginFormat, SR.CompositionException_OriginSeparator, element.DisplayName);
            }

            writer.AppendLine();
        }

        private static List<Stack<CompositionError>> CalculatePaths(CompositionException exception)
        {
            List<Stack<CompositionError>> paths = new List<Stack<CompositionError>>();

            VisitContext context = default;
            context.Path = new Stack<CompositionError>();
            context.LeafVisitor = path =>
            {
                // Take a snapshot of the path
                paths.Add(path.Copy());
            };

            VisitCompositionException(exception, context);

            return paths;
        }

        private static void VisitCompositionException(CompositionException exception, VisitContext context)
        {
            foreach (CompositionError error in exception.Errors)
            {
                VisitError(error, context);
            }

            if (exception.InnerException != null)
            {
                VisitException(exception.InnerException, context);
            }
        }

        private static void VisitError(CompositionError error, VisitContext context)
        {
            context.Path.Push(error);

            if (error.Exception == null)
            {   // This error is a root cause, so write
                // out the stack from this point

                context.LeafVisitor(context.Path);
            }
            else
            {
                VisitException(error.Exception, context);
            }

            context.Path.Pop();
        }

        private static void VisitException(Exception exception, VisitContext context)
        {
            if (exception is CompositionException composition)
            {
                VisitCompositionException(composition, context);
            }
            else
            {
                VisitError(new CompositionError(exception.Message, exception.InnerException), context);
            }
        }

        private struct VisitContext
        {
            public Stack<CompositionError> Path;
            public Action<Stack<CompositionError>> LeafVisitor;
        }
    }
}
