﻿// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
    internal sealed class DiagnosticTableItem : TableItem
    {
        public readonly DiagnosticData Data;

        private DiagnosticTableItem(
            Workspace workspace,
            DiagnosticData data,
            string? projectName,
            Guid projectGuid,
            string[] projectNames,
            Guid[] projectGuids)
            : base(workspace, projectName, projectGuid, projectNames, projectGuids)
        {
            Contract.ThrowIfNull(data);
            Data = data;
        }

        internal static DiagnosticTableItem Create(Workspace workspace, DiagnosticData data)
        {
            GetProjectNameAndGuid(workspace, data.ProjectId, out var projectName, out var projectGuid);
            return new DiagnosticTableItem(workspace, data, projectName, projectGuid, projectNames: Array.Empty<string>(), projectGuids: Array.Empty<Guid>());
        }

        public override TableItem WithAggregatedData(string[] projectNames, Guid[] projectGuids)
            => new DiagnosticTableItem(Workspace, Data, projectName: null, projectGuid: Guid.Empty, projectNames, projectGuids);

        public override DocumentId? DocumentId
            => Data.DocumentId;

        public override ProjectId? ProjectId
            => Data.ProjectId;

        // TODO: use of OriginalFileSpan seems very suspect here.  It is used for navigation.  But we should likely
        // navigate to the remapped position. (Unless navigation already handles that?  Unclear what the
        // invariants/expectations are between these two components).
        public override LinePosition GetOriginalPosition()
            => Data.DataLocation.UnmappedFileSpan.StartLinePosition;

        public override string GetOriginalFilePath()
            => Data.DataLocation.UnmappedFileSpan.Path;

        public override bool EqualsIgnoringLocation(TableItem other)
        {
            if (other is not DiagnosticTableItem otherDiagnosticItem)
            {
                return false;
            }

            var diagnostic = Data;
            var otherDiagnostic = otherDiagnosticItem.Data;

            // everything same except location
            return diagnostic.Id == otherDiagnostic.Id &&
                   diagnostic.ProjectId == otherDiagnostic.ProjectId &&
                   diagnostic.DocumentId == otherDiagnostic.DocumentId &&
                   diagnostic.Category == otherDiagnostic.Category &&
                   diagnostic.Severity == otherDiagnostic.Severity &&
                   diagnostic.WarningLevel == otherDiagnostic.WarningLevel &&
                   diagnostic.Message == otherDiagnostic.Message;
        }

        /// <summary>
        /// Used to group diagnostics that only differ in the project they come from.
        /// We want to avoid displaying diagnostic multuple times when it is reported from 
        /// multi-targeted projects and/or files linked to multiple projects.
        /// Note that a linked file is represented by unique <see cref="DocumentId"/> in each project it is linked to,
        /// so we don't include <see cref="DocumentId"/> in the comparison.
        /// </summary>
        internal sealed class GroupingComparer : IEqualityComparer<DiagnosticData>, IEqualityComparer<DiagnosticTableItem>
        {
            public static readonly GroupingComparer Instance = new();

            public bool Equals(DiagnosticData left, DiagnosticData right)
            {
                if (ReferenceEquals(left, right))
                    return true;

                if (left is null || right is null)
                    return false;

                var leftLocation = left.DataLocation;
                var rightLocation = right.DataLocation;

                // location-less or project level diagnostic:
                if (left.DocumentId == null || right.DocumentId == null)
                    return left.Equals(right);

                return
                    leftLocation.UnmappedFileSpan == rightLocation.UnmappedFileSpan &&
                    left.Severity == right.Severity &&
                    left.IsSuppressed == right.IsSuppressed &&
                    left.Id == right.Id &&
                    left.Message == right.Message;
            }

            public int GetHashCode(DiagnosticData data)
            {
                var location = data.DataLocation;

                // location-less or project level diagnostic:
                if (data.DocumentId == null)
                    return data.GetHashCode();

                return
                    Hash.Combine(location.UnmappedFileSpan.GetHashCode(),
                    Hash.Combine(data.IsSuppressed,
                    Hash.Combine(data.Id, ((int)data.Severity).GetHashCode())));
            }

            public bool Equals(DiagnosticTableItem left, DiagnosticTableItem right)
            {
                if (ReferenceEquals(left, right))
                {
                    return true;
                }

                if (left is null || right is null)
                {
                    return false;
                }

                return Equals(left.Data, right.Data);
            }

            public int GetHashCode(DiagnosticTableItem item)
                => GetHashCode(item.Data);
        }
    }
}
