// <copyright file="MeterProviderBuilderExtensions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

#nullable enable

using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenTelemetry.Internal;
using OpenTelemetry.Resources;

namespace OpenTelemetry.Metrics
{
    /// <summary>
    /// Contains extension methods for the <see cref="MeterProviderBuilder"/> class.
    /// </summary>
    public static class MeterProviderBuilderExtensions
    {
        /// <summary>
        /// Adds a reader to the provider.
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="reader"><see cref="MetricReader"/>.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder, MetricReader reader)
        {
            Guard.ThrowIfNull(reader);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.AddReader(reader);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Adds a reader to the provider.
        /// </summary>
        /// <remarks>
        /// Note: The type specified by <typeparamref name="T"/> will be
        /// registered as a singleton service into application services.
        /// </remarks>
        /// <typeparam name="T">Reader type.</typeparam>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddReader<T>(this MeterProviderBuilder meterProviderBuilder)
            where T : MetricReader
        {
            meterProviderBuilder.ConfigureServices(services => services.TryAddSingleton<T>());

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.AddReader(sp.GetRequiredService<T>());
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Adds a reader to the provider.
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="implementationFactory">The factory that creates the service.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddReader(
            this MeterProviderBuilder meterProviderBuilder,
            Func<IServiceProvider, MetricReader> implementationFactory)
        {
            Guard.ThrowIfNull(implementationFactory);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.AddReader(implementationFactory(sp));
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Add metric view, which can be used to customize the Metrics outputted
        /// from the SDK. The views are applied in the order they are added.
        /// </summary>
        /// <remarks>See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view.</remarks>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="instrumentName">Name of the instrument, to be used as part of Instrument selection criteria.</param>
        /// <param name="name">Name of the view. This will be used as name of resulting metrics stream.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, string name)
        {
            if (!MeterProviderBuilderSdk.IsValidInstrumentName(name))
            {
                throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name));
            }

            if (instrumentName.IndexOf('*') != -1)
            {
                throw new ArgumentException(
                    $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " +
                    $"contains a wildcard character. This is not allowed when using a view to " +
                    $"rename a metric stream as it would lead to conflicting metric stream names.",
                    nameof(instrumentName));
            }

            meterProviderBuilder.AddView(instrumentName, new MetricStreamConfiguration { Name = name });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Add metric view, which can be used to customize the Metrics outputted
        /// from the SDK. The views are applied in the order they are added.
        /// </summary>
        /// <remarks>See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view.</remarks>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="instrumentName">Name of the instrument, to be used as part of Instrument selection criteria.</param>
        /// <param name="metricStreamConfiguration">Aggregation configuration used to produce metrics stream.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, MetricStreamConfiguration metricStreamConfiguration)
        {
            Guard.ThrowIfNullOrWhitespace(instrumentName);
            Guard.ThrowIfNull(metricStreamConfiguration);

            if (metricStreamConfiguration.Name != null && instrumentName.IndexOf('*') != -1)
            {
                throw new ArgumentException(
                    $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " +
                    $"contains a wildcard character. This is not allowed when using a view to " +
                    $"rename a metric stream as it would lead to conflicting metric stream names.",
                    nameof(instrumentName));
            }

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    if (instrumentName.IndexOf('*') != -1)
                    {
                        var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*");
                        var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
                        meterProviderBuilderSdk.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null);
                    }
                    else
                    {
                        meterProviderBuilderSdk.AddView(instrument => instrument.Name.Equals(instrumentName, StringComparison.OrdinalIgnoreCase) ? metricStreamConfiguration : null);
                    }
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Add metric view, which can be used to customize the Metrics outputted
        /// from the SDK. The views are applied in the order they are added.
        /// </summary>
        /// <remarks>
        /// <list type="bullet">
        /// <item>Note: An invalid <see cref="MetricStreamConfiguration"/>
        /// returned from <paramref name="viewConfig"/> will cause the
        /// view to be ignored, no error will be
        /// thrown at runtime.</item>
        /// <item>See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view.</item>
        /// </list>
        /// </remarks>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="viewConfig">Function to configure aggregation based on the instrument.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, Func<Instrument, MetricStreamConfiguration?> viewConfig)
        {
            Guard.ThrowIfNull(viewConfig);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.AddView(viewConfig);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Sets the maximum number of Metric streams supported by the MeterProvider.
        /// When no Views are configured, every instrument will result in one metric stream,
        /// so this control the numbers of instruments supported.
        /// When Views are configured, a single instrument can result in multiple metric streams,
        /// so this control the number of streams.
        /// </summary>
        /// <remarks>
        /// If an instrument is created, but disposed later, this will still be contributing to the limit.
        /// This may change in the future.
        /// </remarks>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="maxMetricStreams">Maximum number of metric streams allowed.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder meterProviderBuilder, int maxMetricStreams)
        {
            Guard.ThrowIfOutOfRange(maxMetricStreams, min: 1);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.SetMaxMetricStreams(maxMetricStreams);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Sets the maximum number of MetricPoints allowed per metric stream.
        /// This limits the number of unique combinations of key/value pairs used
        /// for reporting measurements.
        /// </summary>
        /// <remarks>
        /// If a particular key/value pair combination is used at least once,
        /// it will contribute to the limit for the life of the process.
        /// This may change in the future. See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/2360.
        /// </remarks>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="maxMetricPointsPerMetricStream">Maximum maximum number of metric points allowed per metric stream.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream)
        {
            Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Sets the <see cref="ResourceBuilder"/> from which the Resource associated with
        /// this provider is built from. Overwrites currently set ResourceBuilder.
        /// You should usually use <see cref="ConfigureResource(MeterProviderBuilder, Action{ResourceBuilder})"/> instead
        /// (call <see cref="ResourceBuilder.Clear"/> if desired).
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="resourceBuilder"><see cref="ResourceBuilder"/> from which Resource will be built.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder)
        {
            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.SetResourceBuilder(resourceBuilder);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Modify the <see cref="ResourceBuilder"/> from which the Resource associated with
        /// this provider is built from in-place.
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="configure">An action which modifies the provided <see cref="ResourceBuilder"/> in-place.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action<ResourceBuilder> configure)
        {
            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.ConfigureResource(configure);
                }
            });

            return meterProviderBuilder;
        }

        /// <summary>
        /// Sets the <see cref="ExemplarFilter"/> to be used for this provider.
        /// This is applied to all the metrics from this provider.
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <param name="exemplarFilter"><see cref="ExemplarFilter"/> ExemplarFilter to use.</param>
        /// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
        internal static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter)
        {
            Guard.ThrowIfNull(exemplarFilter);

            meterProviderBuilder.ConfigureBuilder((sp, builder) =>
            {
                if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
                {
                    meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter);
                }
            });

            return meterProviderBuilder;
        }

#pragma warning disable SA1202 // `public` members should come before `internal` members
        /// <summary>
        /// Run the given actions to initialize the <see cref="MeterProvider"/>.
        /// </summary>
        /// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
        /// <returns><see cref="MeterProvider"/>.</returns>
        public static MeterProvider? Build(this MeterProviderBuilder meterProviderBuilder)
        {
            if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase)
            {
                return meterProviderBuilderBase.InvokeBuild();
            }

            return null;
        }
#pragma warning restore SA1202
    }
}
