// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Options.Providers;
using Microsoft.Internal.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Settings;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Options
{
    internal abstract class AbstractSettingsManagerOptionSerializer : ForegroundThreadAffinitizedObject, IOptionSerializer
    {
        // NOTE: This service is not public or intended for use by teams/individuals outside of Microsoft. Any data stored is subject to deletion without warning.
        [Guid("9B164E40-C3A2-4363-9BC5-EB4039DEF653")]
        private class SVsSettingsPersistenceManager {};

        protected readonly ISettingsManager Manager;
        private readonly IOptionService _optionService;

        public AbstractSettingsManagerOptionSerializer(IServiceProvider serviceProvider, IOptionService optionService)
            : base(assertIsForeground: true) // The GetService call requires being on the UI thread or else it will marshal and risk deadlock
        {
            Contract.ThrowIfNull(serviceProvider);
            Contract.ThrowIfNull(optionService);

            this.storageKeyToOptionMap = new Lazy<ImmutableDictionary<string, IOption>>(CreateStorageKeyToOptionMap, isThreadSafe: true);

            this.Manager = (ISettingsManager)serviceProvider.GetService(typeof(SVsSettingsPersistenceManager));
            this._optionService = optionService;

            // While the settings persistence service should be available in all SKUs it is possible an ISO shell author has undefined the
            // contributing package. In that case persistence of settings won't work (we don't bother with a backup solution for persistence
            // as the scenario seems exceedingly unlikely), but we shouldn't crash the IDE.
            if (this.Manager != null)
            {
                ISettingsSubset settingsSubset = this.Manager.GetSubset(SettingStorageRoot + "*");
                settingsSubset.SettingChangedAsync += OnSettingChangedAsync;
            }
        }

        protected abstract string SettingStorageRoot { get; }

        protected abstract bool SupportsOption(IOption option, string languageName);

        protected virtual string GetStorageKeyForOption(IOption option)
        {
            return SettingStorageRoot + option.Name;
        }

        protected static IEnumerable<KeyValuePair<string, IOption>> GetOptionInfoFromTypeFields(IEnumerable<Type> types, BindingFlags flags, Func<FieldInfo, KeyValuePair<string, IOption>> transform)
        {
            return GetOptionInfoFromTypeFields(types, flags, transform, filter: null);
        }

        private Task OnSettingChangedAsync(object sender, PropertyChangedEventArgs args)
        {
            IOption option;
            if (this.StorageKeyToOptionMap.TryGetValue(args.PropertyName, out option))
            {
                this.SetChangedOption(this._optionService, option, LanguageName);
            }

            return SpecializedTasks.Default<object>();
        }

        protected abstract string LanguageName { get; }

        private void SetChangedOption(IOptionService optionService, IOption option, string languageName)
        {
            OptionKey key = new OptionKey(option, languageName);

            object currentValue;
            if (this.TryFetch(key, out currentValue))
            {
                OptionSet optionSet = optionService.GetOptions();
                optionSet = optionSet.WithChangedOption(key, currentValue);

                optionService.SetOptions(optionSet);
            }
        }

        private Lazy<ImmutableDictionary<string, IOption>> storageKeyToOptionMap;

        protected abstract ImmutableDictionary<string, IOption> CreateStorageKeyToOptionMap();

        protected ImmutableDictionary<string, IOption> StorageKeyToOptionMap
        {
            get
            {
                return this.storageKeyToOptionMap.Value;
            }
        }
       
        protected KeyValuePair<string, IOption> GetOptionInfo(FieldInfo fieldInfo)
        {
            var value = (IOption)fieldInfo.GetValue(null);
            return new KeyValuePair<string, IOption>(GetStorageKeyForOption(value), value);
        }

        protected static IEnumerable<KeyValuePair<string, IOption>> GetOptionInfoFromTypeFields(IEnumerable<Type> types, BindingFlags flags, Func<FieldInfo, KeyValuePair<string, IOption>> transform, Predicate<FieldInfo> filter)
        {
            var values = new List<KeyValuePair<string, IOption>>();
            foreach (Type type in types)
            {
                FieldInfo[] fields = type.GetFields(flags) ?? new FieldInfo[0];
                Func<FieldInfo, bool> localFilter = (fi) =>
                    {
                        if (!(fi.GetValue(null) is IOption))
                        {
                            return false;
                        }

                        return (filter != null) ? filter(fi) : true;
                    };

                values.AddRange(fields.Where(localFilter).Select(transform));
            }

            return values;
        }        

        public virtual bool TryFetch(OptionKey optionKey, out object value)
        {
            value = null;

            if (this.Manager == null)
            {
                Debug.Fail("Manager field is unexpectedly null.");
                return false;
            }

            if (!SupportsOption(optionKey.Option, optionKey.Language))
            {
                value = null;
                return false;
            }

            var storageKey = GetStorageKeyForOption(optionKey.Option);
            value = this.Manager.GetValueOrDefault(storageKey, optionKey.Option.DefaultValue);

            return true;
        }

        public virtual bool TryPersist(OptionKey optionKey, object value)
        {
            if (this.Manager == null)
            {
                Debug.Fail("Manager field is unexpectedly null.");
                return false;
            }

            if (!SupportsOption(optionKey.Option, optionKey.Language))
            {
                return false;
            }

            var storageKey = GetStorageKeyForOption(optionKey.Option);
            this.Manager.SetValueAsync(storageKey, value, isMachineLocal: false);
            return true;
        }
    }
}