﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.EditAndContinue
{
    // EncEditSessionInfo is populated on a background thread and then read from the UI thread
    internal sealed class EditSessionTelemetry
    {
        internal readonly struct Data
        {
            public readonly ImmutableArray<(ushort EditKind, ushort SyntaxKind, Guid projectId)> RudeEdits;
            public readonly ImmutableArray<string> EmitErrorIds;
            public readonly ImmutableArray<Guid> ProjectsWithValidDelta;
            public readonly EditAndContinueCapabilities Capabilities;
            public readonly bool HadCompilationErrors;
            public readonly bool HadRudeEdits;
            public readonly bool HadValidChanges;
            public readonly bool HadValidInsignificantChanges;
            public readonly bool InBreakState;
            public readonly bool IsEmpty;
            public readonly bool Committed;

            public Data(EditSessionTelemetry telemetry)
            {
                Contract.ThrowIfNull(telemetry._inBreakState);

                RudeEdits = telemetry._rudeEdits.AsImmutable();
                EmitErrorIds = telemetry._emitErrorIds.AsImmutable();
                ProjectsWithValidDelta = telemetry._projectsWithValidDelta.AsImmutable();
                HadCompilationErrors = telemetry._hadCompilationErrors;
                HadRudeEdits = telemetry._hadRudeEdits;
                HadValidChanges = telemetry._hadValidChanges;
                HadValidInsignificantChanges = telemetry._hadValidInsignificantChanges;
                InBreakState = telemetry._inBreakState.Value;
                Capabilities = telemetry._capabilities;
                IsEmpty = telemetry.IsEmpty;
                Committed = telemetry._committed;
            }
        }

        private readonly object _guard = new();

        // Limit the number of reported items to limit the size of the telemetry event (max total size is 64K).
        private const int MaxReportedProjectIds = 20;

        private readonly HashSet<(ushort, ushort, Guid)> _rudeEdits = new();
        private readonly HashSet<string> _emitErrorIds = new();
        private readonly HashSet<Guid> _projectsWithValidDelta = new();

        private bool _hadCompilationErrors;
        private bool _hadRudeEdits;
        private bool _hadValidChanges;
        private bool _hadValidInsignificantChanges;
        private bool? _inBreakState;
        private bool _committed;

        private EditAndContinueCapabilities _capabilities;

        public Data GetDataAndClear()
        {
            lock (_guard)
            {
                var data = new Data(this);
                _rudeEdits.Clear();
                _emitErrorIds.Clear();
                _projectsWithValidDelta.Clear();
                _hadCompilationErrors = false;
                _hadRudeEdits = false;
                _hadValidChanges = false;
                _hadValidInsignificantChanges = false;
                _inBreakState = null;
                _capabilities = EditAndContinueCapabilities.None;
                _committed = false;
                return data;
            }
        }

        public bool IsEmpty => !(_hadCompilationErrors || _hadRudeEdits || _hadValidChanges || _hadValidInsignificantChanges);

        public void SetBreakState(bool value)
            => _inBreakState = value;

        public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray<string> errorsIds)
        {
            lock (_guard)
            {
                _emitErrorIds.AddRange(errorsIds);

                switch (summary)
                {
                    case ProjectAnalysisSummary.NoChanges:
                        break;

                    case ProjectAnalysisSummary.CompilationErrors:
                        _hadCompilationErrors = true;
                        break;

                    case ProjectAnalysisSummary.RudeEdits:
                        _hadRudeEdits = true;
                        break;

                    case ProjectAnalysisSummary.ValidChanges:
                        _hadValidChanges = true;

                        if (errorsIds.IsEmpty && _projectsWithValidDelta.Count < MaxReportedProjectIds)
                        {
                            _projectsWithValidDelta.Add(projectTelemetryId);
                        }

                        break;

                    case ProjectAnalysisSummary.ValidInsignificantChanges:
                        _hadValidInsignificantChanges = true;
                        break;

                    default:
                        throw ExceptionUtilities.UnexpectedValue(summary);
                }
            }
        }

        public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, ImmutableArray<Diagnostic> emitDiagnostics)
            => LogProjectAnalysisSummary(summary, projectTelemetryId, emitDiagnostics.SelectAsArray(d => d.Severity == DiagnosticSeverity.Error, d => d.Id));

        public void LogRudeEditDiagnostics(ImmutableArray<RudeEditDiagnostic> diagnostics, Guid projectTelemetryId)
        {
            lock (_guard)
            {
                foreach (var diagnostic in diagnostics)
                {
                    _rudeEdits.Add(((ushort)diagnostic.Kind, diagnostic.SyntaxKind, projectTelemetryId));
                }
            }
        }

        public void LogRuntimeCapabilities(EditAndContinueCapabilities capabilities)
        {
            lock (_guard)
            {
                Debug.Assert(_capabilities == EditAndContinueCapabilities.None || _capabilities == capabilities);
                _capabilities = capabilities;
            }
        }

        internal void LogCommitted()
            => _committed = true;
    }
}
