﻿// 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 System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ChangeSignature;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Text.Classification;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ChangeSignature
{
    internal partial class ChangeSignatureDialogViewModel : AbstractNotifyPropertyChanged
    {
        private readonly IClassificationFormatMap _classificationFormatMap;
        private readonly ClassificationTypeMap _classificationTypeMap;
        private readonly INotificationService _notificationService;
        private readonly ParameterConfiguration _originalParameterConfiguration;

        // This can be changed to ParameterViewModel if we will allow adding 'this' parameter.
        private readonly ExistingParameterViewModel? _thisParameter;
        private readonly List<ParameterViewModel> _parametersWithoutDefaultValues;
        private readonly List<ParameterViewModel> _parametersWithDefaultValues;

        // This can be changed to ParameterViewModel if we will allow adding 'params' parameter.
        private readonly ExistingParameterViewModel? _paramsParameter;
        private readonly HashSet<ParameterViewModel> _disabledParameters = new();

        private readonly ImmutableArray<SymbolDisplayPart> _declarationParts;
        private bool _previewChanges;

        /// <summary>
        /// The document where the symbol we are changing signature is defined.
        /// </summary>
        private readonly Document _document;
        private readonly int _positionForTypeBinding;

        internal ChangeSignatureDialogViewModel(
            ParameterConfiguration parameters,
            ISymbol symbol,
            Document document,
            int positionForTypeBinding,
            IClassificationFormatMap classificationFormatMap,
            ClassificationTypeMap classificationTypeMap)
        {
            _originalParameterConfiguration = parameters;
            _document = document;
            _positionForTypeBinding = positionForTypeBinding;
            _classificationFormatMap = classificationFormatMap;
            _classificationTypeMap = classificationTypeMap;

            _notificationService = document.Project.Solution.Services.GetRequiredService<INotificationService>();

            // This index is displayed to users. That is why we start it from 1.
            var initialDisplayIndex = 1;

            if (parameters.ThisParameter != null)
            {
                _thisParameter = new ExistingParameterViewModel(this, parameters.ThisParameter, initialDisplayIndex++);
                _disabledParameters.Add(_thisParameter);
            }

            _declarationParts = symbol.ToDisplayParts(s_symbolDeclarationDisplayFormat);

            _parametersWithoutDefaultValues = CreateParameterViewModels(parameters.ParametersWithoutDefaultValues, ref initialDisplayIndex);
            _parametersWithDefaultValues = CreateParameterViewModels(parameters.RemainingEditableParameters, ref initialDisplayIndex);

            if (parameters.ParamsParameter != null)
            {
                _paramsParameter = new ExistingParameterViewModel(this, parameters.ParamsParameter, initialDisplayIndex++);
            }

            UpdateNameConflictMarkers();

            var selectedIndex = parameters.SelectedIndex;
            // Currently, we do not support editing the ThisParameter. 
            // Therefore, if there is such parameter, we should move the selectedIndex.
            if (parameters.ThisParameter != null && selectedIndex == 0)
            {
                // If we have at least one parameter after the ThisParameter, select the first one after This.
                // Otherwise, do not select anything.
                if (parameters.ParametersWithoutDefaultValues.Length + parameters.RemainingEditableParameters.Length > 0)
                {
                    this.SelectedIndex = 1;
                }
                else
                {
                    this.SelectedIndex = null;
                }
            }
            else
            {
                this.SelectedIndex = selectedIndex;
            }
        }

        private void UpdateNameConflictMarkers()
        {
            var parameterNameOverlapMap = new Dictionary<string, List<ParameterViewModel>>();
            foreach (var parameter in AllParameters)
            {
                if (!parameter.IsRemoved)
                {
                    parameterNameOverlapMap
                        .GetOrAdd(parameter.ParameterName, _ => new List<ParameterViewModel>())
                        .Add(parameter);
                }
                else
                {
                    parameter.HasParameterNameConflict = Visibility.Collapsed;
                }
            }

            foreach (var parameterName in parameterNameOverlapMap.Keys)
            {
                var matchingParameters = parameterNameOverlapMap[parameterName];
                if (matchingParameters.Count > 1)
                {
                    foreach (var matchingParameter in matchingParameters)
                    {
                        matchingParameter.HasParameterNameConflict = Visibility.Visible;
                    }
                }
                else
                {
                    matchingParameters.Single().HasParameterNameConflict = Visibility.Collapsed;
                }
            }

            NotifyPropertyChanged(nameof(AllParameters));
        }

        public AddParameterDialogViewModel CreateAddParameterDialogViewModel()
            => new(_document, _positionForTypeBinding);

        private List<ParameterViewModel> CreateParameterViewModels(ImmutableArray<Parameter> parameters, ref int initialIndex)
        {
            var list = new List<ParameterViewModel>();
            foreach (ExistingParameter existingParameter in parameters)
            {
                list.Add(new ExistingParameterViewModel(this, existingParameter, initialIndex));
                initialIndex++;
            }

            return list;
        }

        public int GetStartingSelectionIndex()
        {
            if (_thisParameter == null)
            {
                return 0;
            }

            if (_parametersWithDefaultValues.Count + _parametersWithoutDefaultValues.Count > 0)
            {
                return 1;
            }

            return -1;
        }

        public bool PreviewChanges
        {
            get
            {
                return _previewChanges;
            }

            set
            {
                _previewChanges = value;
            }
        }

        public bool CanRemove
        {
            get
            {
                if (!EditableParameterSelected(out var index))
                {
                    return false;
                }

                return !AllParameters[index].IsRemoved;
            }
        }

        public bool CanRestore
        {
            get
            {
                if (!EditableParameterSelected(out var index))
                {
                    return false;
                }

                return AllParameters[index].IsRemoved;
            }
        }

        private bool EditableParameterSelected(out int index)
        {
            index = -1;

            if (!AllParameters.Any())
            {
                return false;
            }

            if (!SelectedIndex.HasValue)
            {
                return false;
            }

            index = SelectedIndex.Value;

            if (index == 0 && _thisParameter != null)
            {
                return false;
            }

            return true;
        }

        internal void Remove()
        {
            if (AllParameters[_selectedIndex!.Value] is AddedParameterViewModel)
            {
                var parameterToRemove = AllParameters[_selectedIndex!.Value];

                if (_parametersWithoutDefaultValues.Contains(parameterToRemove))
                {
                    _parametersWithoutDefaultValues.Remove(parameterToRemove);
                }
                else
                {
                    _parametersWithDefaultValues.Remove(parameterToRemove);
                }
            }
            else
            {
                AllParameters[_selectedIndex!.Value].IsRemoved = true;
            }

            UpdateNameConflictMarkers();
            RemoveRestoreNotifyPropertyChanged();
        }

        internal void Restore()
        {
            AllParameters[_selectedIndex!.Value].IsRemoved = false;
            UpdateNameConflictMarkers();
            RemoveRestoreNotifyPropertyChanged();
        }

        internal void AddParameter(AddedParameter addedParameter)
        {
            if (addedParameter.IsRequired)
            {
                _parametersWithoutDefaultValues.Add(new AddedParameterViewModel(this, addedParameter));
            }
            else
            {
                _parametersWithDefaultValues.Add(new AddedParameterViewModel(this, addedParameter));
            }

            UpdateNameConflictMarkers();
            RemoveRestoreNotifyPropertyChanged();
        }

        internal void RemoveRestoreNotifyPropertyChanged()
        {
            NotifyPropertyChanged(nameof(AllParameters));
            NotifyPropertyChanged(nameof(SignatureDisplay));
            NotifyPropertyChanged(nameof(SignaturePreviewAutomationText));
            NotifyPropertyChanged(nameof(CanRemove));
            NotifyPropertyChanged(nameof(RemoveAutomationText));
            NotifyPropertyChanged(nameof(CanRestore));
            NotifyPropertyChanged(nameof(RestoreAutomationText));
        }

        internal ParameterConfiguration GetParameterConfiguration()
        {
            return new ParameterConfiguration(
                _originalParameterConfiguration.ThisParameter,
                _parametersWithoutDefaultValues.Where(p => !p.IsRemoved).Select(p => p.Parameter).ToImmutableArray(),
                _parametersWithDefaultValues.Where(p => !p.IsRemoved).Select(p => p.Parameter).ToImmutableArray(),
                (_paramsParameter == null || _paramsParameter.IsRemoved) ? null : (ExistingParameter)_paramsParameter.Parameter,
                selectedIndex: -1);
        }

        private static readonly SymbolDisplayFormat s_symbolDeclarationDisplayFormat = new(
            genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
            miscellaneousOptions:
                SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier,
            extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod,
            memberOptions:
                SymbolDisplayMemberOptions.IncludeType |
                SymbolDisplayMemberOptions.IncludeExplicitInterface |
                SymbolDisplayMemberOptions.IncludeAccessibility |
                SymbolDisplayMemberOptions.IncludeModifiers |
                SymbolDisplayMemberOptions.IncludeRef);

        private static readonly SymbolDisplayFormat s_parameterDisplayFormat = new(
            genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
            miscellaneousOptions:
                SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier,
            parameterOptions:
                SymbolDisplayParameterOptions.IncludeType |
                SymbolDisplayParameterOptions.IncludeParamsRefOut |
                SymbolDisplayParameterOptions.IncludeDefaultValue |
                SymbolDisplayParameterOptions.IncludeExtensionThis |
                SymbolDisplayParameterOptions.IncludeName);

        public TextBlock SignatureDisplay
        {
            get
            {
                // TODO: Should probably use original syntax & formatting exactly instead of regenerating here
                var displayParts = GetSignatureDisplayParts();

                var textBlock = displayParts.ToTaggedText().ToTextBlock(_classificationFormatMap, _classificationTypeMap);

                foreach (var inline in textBlock.Inlines)
                {
                    inline.FontSize = 12;
                }

                textBlock.IsEnabled = false;
                return textBlock;
            }
        }

        public string SignaturePreviewAutomationText
        {
            get
            {
                return GetSignatureDisplayParts().Select(sdp => sdp.ToString()).Join(" ");
            }
        }

        internal string TEST_GetSignatureDisplayText()
            => GetSignatureDisplayParts().Select(p => p.ToString()).Join("");

        private List<SymbolDisplayPart> GetSignatureDisplayParts()
        {
            var displayParts = new List<SymbolDisplayPart>();

            displayParts.AddRange(_declarationParts);
            displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "("));

            var first = true;
            foreach (var parameter in AllParameters.Where(p => !p.IsRemoved))
            {
                if (!first)
                {
                    displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ","));
                    displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, " "));
                }

                first = false;

                switch (parameter)
                {
                    case ExistingParameterViewModel existingParameter:
                        displayParts.AddRange(existingParameter.ParameterSymbol.ToDisplayParts(s_parameterDisplayFormat));
                        break;

                    case AddedParameterViewModel addedParameterViewModel:
                        var languageService = _document.GetRequiredLanguageService<IChangeSignatureViewModelFactoryService>();
                        displayParts.AddRange(languageService.GeneratePreviewDisplayParts(addedParameterViewModel));
                        break;

                    default:
                        throw ExceptionUtilities.UnexpectedValue(parameter.GetType().ToString());
                }
            }

            displayParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ")"));
            return displayParts;
        }

        public List<ParameterViewModel> AllParameters
        {
            get
            {
                var list = new List<ParameterViewModel>();
                if (_thisParameter != null)
                {
                    list.Add(_thisParameter);
                }

                list.AddRange(_parametersWithoutDefaultValues);
                list.AddRange(_parametersWithDefaultValues);

                if (_paramsParameter != null)
                {
                    list.Add(_paramsParameter);
                }

                return list;
            }
        }

        public bool CanMoveUp
        {
            get
            {
                if (!SelectedIndex.HasValue)
                {
                    return false;
                }

                var index = SelectedIndex.Value;
                index = _thisParameter == null ? index : index - 1;
                if (index <= 0 || index == _parametersWithoutDefaultValues.Count || index >= _parametersWithoutDefaultValues.Count + _parametersWithDefaultValues.Count)
                {
                    return false;
                }

                return true;
            }
        }

        public bool CanMoveDown
        {
            get
            {
                if (!SelectedIndex.HasValue)
                {
                    return false;
                }

                var index = SelectedIndex.Value;
                index = _thisParameter == null ? index : index - 1;
                if (index < 0 || index == _parametersWithoutDefaultValues.Count - 1 || index >= _parametersWithoutDefaultValues.Count + _parametersWithDefaultValues.Count - 1)
                {
                    return false;
                }

                return true;
            }
        }

        internal void MoveUp()
        {
            Debug.Assert(CanMoveUp);

            var index = SelectedIndex!.Value;
            index = _thisParameter == null ? index : index - 1;
            Move(index < _parametersWithoutDefaultValues.Count ? _parametersWithoutDefaultValues : _parametersWithDefaultValues, index < _parametersWithoutDefaultValues.Count ? index : index - _parametersWithoutDefaultValues.Count, delta: -1);
        }

        internal void MoveDown()
        {
            Debug.Assert(CanMoveDown);

            var index = SelectedIndex!.Value;
            index = _thisParameter == null ? index : index - 1;
            Move(index < _parametersWithoutDefaultValues.Count ? _parametersWithoutDefaultValues : _parametersWithDefaultValues, index < _parametersWithoutDefaultValues.Count ? index : index - _parametersWithoutDefaultValues.Count, delta: 1);
        }

        private void Move(List<ParameterViewModel> list, int index, int delta)
        {
            var param = list[index];
            list.RemoveAt(index);
            list.Insert(index + delta, param);

            SelectedIndex += delta;

            NotifyPropertyChanged(nameof(AllParameters));
            NotifyPropertyChanged(nameof(SignatureDisplay));
            NotifyPropertyChanged(nameof(SignaturePreviewAutomationText));
        }

        internal bool CanSubmit([NotNullWhen(false)] out string? message)
        {
            var canSubmit = AllParameters.Any(p => p.IsRemoved) ||
                AllParameters.Any(p => p is AddedParameterViewModel) ||
                    !_parametersWithoutDefaultValues.OfType<ExistingParameterViewModel>().Select(p => p.ParameterSymbol).SequenceEqual(_originalParameterConfiguration.ParametersWithoutDefaultValues.Cast<ExistingParameter>().Select(p => p.Symbol)) ||
                    !_parametersWithDefaultValues.OfType<ExistingParameterViewModel>().Select(p => p.ParameterSymbol).SequenceEqual(_originalParameterConfiguration.RemainingEditableParameters.Cast<ExistingParameter>().Select(p => p.Symbol));

            if (!canSubmit)
            {
                message = ServicesVSResources.You_must_change_the_signature;
                return false;
            }

            message = null;
            return true;
        }

        internal bool TrySubmit()
        {
            if (!CanSubmit(out var message))
            {
                _notificationService.SendNotification(message, severity: NotificationSeverity.Information);
                return false;
            }

            return true;
        }

        private bool IsDisabled(ParameterViewModel parameterViewModel)
        {
            return _disabledParameters.Contains(parameterViewModel);
        }

        private int? _selectedIndex;
        public int? SelectedIndex
        {
            get
            {
                return _selectedIndex;
            }

            set
            {
                var newSelectedIndex = value == -1 ? null : value;
                if (newSelectedIndex == _selectedIndex)
                {
                    return;
                }

                _selectedIndex = newSelectedIndex;

                NotifyPropertyChanged(nameof(CanMoveUp));
                NotifyPropertyChanged(nameof(MoveUpAutomationText));
                NotifyPropertyChanged(nameof(CanMoveDown));
                NotifyPropertyChanged(nameof(MoveDownAutomationText));
                NotifyPropertyChanged(nameof(CanRemove));
                NotifyPropertyChanged(nameof(RemoveAutomationText));
                NotifyPropertyChanged(nameof(CanRestore));
                NotifyPropertyChanged(nameof(RestoreAutomationText));
            }
        }

        public string MoveUpAutomationText
        {
            get
            {
                if (!CanMoveUp)
                {
                    return string.Empty;
                }

                return string.Format(ServicesVSResources.Move_0_above_1, AllParameters[SelectedIndex!.Value].ShortAutomationText, AllParameters[SelectedIndex!.Value - 1].ShortAutomationText);
            }
        }

        public string MoveDownAutomationText
        {
            get
            {
                if (!CanMoveDown)
                {
                    return string.Empty;
                }

                return string.Format(ServicesVSResources.Move_0_below_1, AllParameters[SelectedIndex!.Value].ShortAutomationText, AllParameters[SelectedIndex!.Value + 1].ShortAutomationText);
            }
        }

        public string RemoveAutomationText
        {
            get
            {
                if (!CanRemove)
                {
                    return string.Empty;
                }

                return string.Format(ServicesVSResources.Remove_0, AllParameters[SelectedIndex!.Value].ShortAutomationText);
            }
        }

        public string RestoreAutomationText
        {
            get
            {
                if (!CanRestore)
                {
                    return string.Empty;
                }

                return string.Format(ServicesVSResources.Restore_0, AllParameters[SelectedIndex!.Value].ShortAutomationText);
            }
        }
    }
}
