﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ColorSchemes
{
    internal partial class ColorSchemeApplier
    {
        private sealed class ClassificationVerifier
        {
            private static readonly Guid TextEditorMEFItemsColorCategory = new("75a05685-00a8-4ded-bae5-e7a50bfa929a");

            // These classification colors (0x00BBGGRR) should match the VS\EditorColors.xml file.
            // They are not in the scheme files because they are core classifications.
            private const uint DarkThemePlainText = 0x00DCDCDCu;
            private const uint DarkThemeIdentifier = DarkThemePlainText;
            private const uint DarkThemeOperator = 0x00B4B4B4u;
            private const uint DarkThemeKeyword = 0x00D69C56u;

            private const uint LightThemePlainText = 0x00000000u;
            private const uint LightThemeIdentifier = LightThemePlainText;
            private const uint LightThemeOperator = LightThemePlainText;
            private const uint LightThemeKeyword = 0x00FF0000u;

            private const string PlainTextClassificationTypeName = "plain text";

            // Dark Theme Core Classifications
            private static ImmutableDictionary<string, uint> DarkThemeForeground
                => new Dictionary<string, uint>()
                {
                    [PlainTextClassificationTypeName] = DarkThemePlainText,
                    [ClassificationTypeNames.Identifier] = DarkThemeIdentifier,
                    [ClassificationTypeNames.Keyword] = DarkThemeKeyword,
                    [ClassificationTypeNames.Operator] = DarkThemeOperator,
                }.ToImmutableDictionary();

            // Light, Blue, or AdditionalContrast Theme Core Classifications
            private static ImmutableDictionary<string, uint> BlueLightThemeForeground
                => new Dictionary<string, uint>()
                {
                    [PlainTextClassificationTypeName] = LightThemePlainText,
                    [ClassificationTypeNames.Identifier] = LightThemeIdentifier,
                    [ClassificationTypeNames.Keyword] = LightThemeKeyword,
                    [ClassificationTypeNames.Operator] = LightThemeOperator,
                }.ToImmutableDictionary();

            private readonly IThreadingContext _threadingContext;
            private readonly IVsService<IVsFontAndColorStorage> _fontAndColorStorage;
            private readonly ImmutableArray<string> _classifications;
            private readonly ImmutableDictionary<ColorSchemeName, ImmutableDictionary<Guid, ImmutableDictionary<string, uint>>> _colorSchemes;

            // The High Contrast theme is not included because we do not want to make changes when the user is in High Contrast mode.

            public ClassificationVerifier(
                IThreadingContext threadingContext,
                IVsService<IVsFontAndColorStorage> fontAndColorStorage,
                ImmutableDictionary<ColorSchemeName, ColorScheme> colorSchemes)
            {
                _threadingContext = threadingContext;
                _fontAndColorStorage = fontAndColorStorage;
                _colorSchemes = colorSchemes.ToImmutableDictionary(
                    nameAndScheme => nameAndScheme.Key,
                    nameAndScheme => nameAndScheme.Value.Themes.ToImmutableDictionary(
                        theme => theme.Guid,
                        theme => theme.Category.Colors
                            .Where(color => color.Foreground.HasValue)
                            .ToImmutableDictionary(color => color.Name, color => color.Foreground!.Value)));

                // Gather all the classifications from the core and scheme dictionaries.
                var coreClassifications = DarkThemeForeground.Keys.Concat(BlueLightThemeForeground.Keys).Distinct();
                var colorSchemeClassifications = _colorSchemes.Values.SelectMany(scheme => scheme.Values.SelectMany(theme => theme.Keys)).Distinct();
                _classifications = coreClassifications.Concat(colorSchemeClassifications).ToImmutableArray();
            }

            /// <summary>
            /// Determines if any Classification foreground colors have been customized in Fonts and Colors.
            /// </summary>
            public async Task<bool> AreForegroundColorsCustomizedAsync(
                ColorSchemeName schemeName, Guid themeId, CancellationToken cancellationToken)
            {
                // Make no changes when in high contast mode or in unknown theme.
                if (SystemParameters.HighContrast ||
                    !_colorSchemes.TryGetValue(schemeName, out var colorScheme) ||
                    !colorScheme.TryGetValue(themeId, out var colorSchemeTheme))
                {
                    return false;
                }

                // Ensure we are initialized
                var fontAndColorStorage = await _fontAndColorStorage.GetValueAsync(cancellationToken).ConfigureAwait(true);

                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                var fontAndColorUtilities = (IVsFontAndColorUtilities)fontAndColorStorage;

                var coreThemeColors = themeId == KnownColorThemes.Dark
                    ? DarkThemeForeground
                    : BlueLightThemeForeground;

                // Open Text Editor category for readonly access and do not load items if they are defaulted.
                if (fontAndColorStorage.OpenCategory(TextEditorMEFItemsColorCategory, (uint)__FCSTORAGEFLAGS.FCSF_READONLY) == VSConstants.S_OK)
                {
                    try
                    {
                        foreach (var classification in _classifications)
                        {
                            var colorItems = new ColorableItemInfo[1];
                            if (fontAndColorStorage.GetItem(classification, colorItems) != VSConstants.S_OK)
                            {
                                // Classifications that are still defaulted will not have entries.
                                continue;
                            }

                            var colorItem = colorItems[0];

                            if (IsClassificationCustomized(coreThemeColors, colorSchemeTheme, fontAndColorUtilities, colorItem, classification))
                            {
                                return true;
                            }
                        }
                    }
                    finally
                    {
                        fontAndColorStorage.CloseCategory();
                    }
                }

                return false;
            }

            /// <summary>
            /// Determines if the ColorableItemInfo's Foreground has been customized to a color that doesn't match the
            /// selected scheme.
            /// </summary>
            private bool IsClassificationCustomized(
                ImmutableDictionary<string, uint> coreThemeColors,
                ImmutableDictionary<string, uint> schemeThemeColors,
                IVsFontAndColorUtilities fontAndColorUtilities,
                ColorableItemInfo colorItem,
                string classification)
            {
                _threadingContext.ThrowIfNotOnUIThread();

                var foregroundColorRef = colorItem.crForeground;

                if (fontAndColorUtilities.GetColorType(foregroundColorRef, out var foregroundColorType) != VSConstants.S_OK)
                {
                    // Without being able to check color type, we cannot make a determination.
                    return false;
                }

                // If the color is defaulted then it isn't customized.
                if (foregroundColorType == (int)__VSCOLORTYPE.CT_AUTOMATIC)
                {
                    return false;
                }

                // Since the color type isn't default then it has been customized, we will
                // perform an additional check for RGB colors to see if the customized color
                // matches the color scheme color.
                if (foregroundColorType != (int)__VSCOLORTYPE.CT_RAW)
                {
                    return true;
                }

                if (coreThemeColors.TryGetValue(classification, out var coreColor))
                {
                    return foregroundColorRef != coreColor;
                }

                if (schemeThemeColors.TryGetValue(classification, out var schemeColor))
                {
                    return foregroundColorRef != schemeColor;
                }

                // Since Classification inheritance isn't represented in the scheme files,
                // this switch case will handle the 3 cases we expect.
                var fallbackColor = classification switch
                {
                    ClassificationTypeNames.OperatorOverloaded => coreThemeColors[ClassificationTypeNames.Operator],
                    ClassificationTypeNames.ControlKeyword => coreThemeColors[ClassificationTypeNames.Keyword],
                    _ => coreThemeColors[ClassificationTypeNames.Identifier]
                };

                return foregroundColorRef != fallbackColor;
            }
        }
    }
}
