﻿// 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.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.InternalElements;
using Microsoft.VisualStudio.LanguageServices.Implementation.Interop;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel
{
    public sealed partial class FileCodeModel
    {
        private const int ElementAddedDispId = 1;
        private const int ElementChangedDispId = 2;
        private const int ElementDeletedDispId = 3;
        private const int ElementDeletedDispId2 = 4;

        public void FireEvents()
        {
            _ = _codeElementTable.CleanUpDeadObjectsAsync(State.ProjectCodeModelFactory.Listener).ReportNonFatalErrorAsync();

            if (this.IsZombied)
            {
                // file is removed from the solution. this can happen if a fireevent is enqueued to foreground notification service
                // but the file itself is removed from the solution before it has a chance to run
                return;
            }

            if (!TryGetDocument(out var document))
            {
                // file is removed from the solution. this can happen if a fireevent is enqueued to foreground notification service
                // but the file itself is removed from the solution before it has a chance to run
                return;
            }

            // TODO(DustinCa): Enqueue unknown change event if a file is closed without being saved.
            var oldTree = _lastSyntaxTree;
            var newTree = document.GetRequiredSyntaxTreeSynchronously(CancellationToken.None);

            _lastSyntaxTree = newTree;

            if (oldTree == newTree ||
                oldTree.IsEquivalentTo(newTree, topLevel: true))
            {
                return;
            }

            var eventQueue = this.CodeModelService.CollectCodeModelEvents(oldTree, newTree);
            if (eventQueue.Count == 0)
            {
                return;
            }

            var projectCodeModel = this.State.ProjectCodeModelFactory.GetProjectCodeModel(document.Project.Id);
            if (projectCodeModel == null)
            {
                return;
            }

            if (!projectCodeModel.TryGetCachedFileCodeModel(this.Workspace.GetFilePath(GetDocumentId()), out _))
            {
                return;
            }

            var extensibility = (EnvDTE80.IVsExtensibility2)this.State.ServiceProvider.GetService(typeof(EnvDTE.IVsExtensibility));
            if (extensibility == null)
                return;

            foreach (var codeModelEvent in eventQueue)
            {
                GetElementsForCodeModelEvent(codeModelEvent, out var element, out var parentElement);

                if (codeModelEvent.Type == CodeModelEventType.Add)
                {
                    extensibility.FireCodeModelEvent(ElementAddedDispId, element, EnvDTE80.vsCMChangeKind.vsCMChangeKindUnknown);
                }
                else if (codeModelEvent.Type == CodeModelEventType.Remove)
                {
                    extensibility.FireCodeModelEvent3(ElementDeletedDispId2, parentElement, element, EnvDTE80.vsCMChangeKind.vsCMChangeKindUnknown);
                    extensibility.FireCodeModelEvent(ElementDeletedDispId, element, EnvDTE80.vsCMChangeKind.vsCMChangeKindUnknown);
                }
                else if (codeModelEvent.Type.IsChange())
                {
                    extensibility.FireCodeModelEvent(ElementChangedDispId, element, ConvertToChangeKind(codeModelEvent.Type));
                }
                else
                {
                    Debug.Fail("Invalid event type: " + codeModelEvent.Type);
                }
            }

            return;
        }

        private static EnvDTE80.vsCMChangeKind ConvertToChangeKind(CodeModelEventType eventType)
        {
            EnvDTE80.vsCMChangeKind result = 0;

            if ((eventType & CodeModelEventType.Rename) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindRename;
            }

            if ((eventType & CodeModelEventType.Unknown) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindUnknown;
            }

            if ((eventType & CodeModelEventType.BaseChange) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindBaseChange;
            }

            if ((eventType & CodeModelEventType.TypeRefChange) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindTypeRefChange;
            }

            if ((eventType & CodeModelEventType.SigChange) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindSignatureChange;
            }

            if ((eventType & CodeModelEventType.ArgChange) != 0)
            {
                result |= EnvDTE80.vsCMChangeKind.vsCMChangeKindArgumentChange;
            }

            return result;
        }

        // internal for testing
        internal void GetElementsForCodeModelEvent(CodeModelEvent codeModelEvent, out EnvDTE.CodeElement? element, out object? parentElement)
        {
            parentElement = GetParentElementForCodeModelEvent(codeModelEvent);

            if (codeModelEvent.Node == null)
            {
                element = this.CodeModelService.CreateUnknownRootNamespaceCodeElement(this.State, this);
            }
            else if (this.CodeModelService.IsParameterNode(codeModelEvent.Node))
            {
                element = GetParameterElementForCodeModelEvent(codeModelEvent, parentElement);
            }
            else if (this.CodeModelService.IsAttributeNode(codeModelEvent.Node))
            {
                element = GetAttributeElementForCodeModelEvent(codeModelEvent, parentElement);
            }
            else if (this.CodeModelService.IsAttributeArgumentNode(codeModelEvent.Node))
            {
                element = GetAttributeArgumentElementForCodeModelEvent(codeModelEvent, parentElement);
            }
            else
            {
                if (codeModelEvent.Type == CodeModelEventType.Remove)
                {
                    element = this.CodeModelService.CreateUnknownCodeElement(this.State, this, codeModelEvent.Node);
                }
                else
                {
                    element = this.GetOrCreateCodeElement<EnvDTE.CodeElement>(codeModelEvent.Node);
                }
            }

            if (element == null)
            {
                Debug.Fail("We should have created an element for this event!");
            }

            Debug.Assert(codeModelEvent.Type != CodeModelEventType.Remove || parentElement != null);
        }

        private object? GetParentElementForCodeModelEvent(CodeModelEvent codeModelEvent)
        {
            if (this.CodeModelService.IsParameterNode(codeModelEvent.Node) ||
                this.CodeModelService.IsAttributeArgumentNode(codeModelEvent.Node))
            {
                if (codeModelEvent.ParentNode != null)
                {
                    return this.GetOrCreateCodeElement<EnvDTE.CodeElement>(codeModelEvent.ParentNode);
                }
            }
            else if (this.CodeModelService.IsAttributeNode(codeModelEvent.Node))
            {
                if (codeModelEvent.ParentNode != null)
                {
                    return this.GetOrCreateCodeElement<EnvDTE.CodeElement>(codeModelEvent.ParentNode);
                }
                else
                {
                    return this;
                }
            }
            else if (codeModelEvent.Type == CodeModelEventType.Remove)
            {
                if (codeModelEvent.ParentNode != null &&
                    codeModelEvent.ParentNode.Parent != null)
                {
                    return this.GetOrCreateCodeElement<EnvDTE.CodeElement>(codeModelEvent.ParentNode);
                }
                else
                {
                    return this;
                }
            }

            return null;
        }

        private EnvDTE.CodeElement? GetParameterElementForCodeModelEvent(CodeModelEvent codeModelEvent, object? parentElement)
            => parentElement switch
            {
                EnvDTE.CodeDelegate parentDelegate => GetParameterElementForCodeModelEvent(codeModelEvent, parentDelegate.Parameters, parentElement),
                EnvDTE.CodeFunction parentFunction => GetParameterElementForCodeModelEvent(codeModelEvent, parentFunction.Parameters, parentElement),
                EnvDTE80.CodeProperty2 parentProperty => GetParameterElementForCodeModelEvent(codeModelEvent, parentProperty.Parameters, parentElement),
                _ => null,
            };

        private EnvDTE.CodeElement? GetParameterElementForCodeModelEvent(CodeModelEvent codeModelEvent, EnvDTE.CodeElements? parentParameters, object? parentElement)
        {
            if (parentParameters == null)
            {
                return null;
            }

            var parameterName = this.CodeModelService.GetName(codeModelEvent.Node);

            if (codeModelEvent.Type == CodeModelEventType.Remove)
            {
                var parentCodeElement = ComAggregate.TryGetManagedObject<AbstractCodeMember>(parentElement);
                if (parentCodeElement != null)
                {
                    return (EnvDTE.CodeElement)CodeParameter.Create(this.State, parentCodeElement, parameterName);
                }
            }
            else
            {
                return parentParameters.Item(parameterName);
            }

            return null;
        }

        private EnvDTE.CodeElement? GetAttributeElementForCodeModelEvent(CodeModelEvent codeModelEvent, object? parentElement)
        {
            var node = codeModelEvent.Node;
            var parentNode = codeModelEvent.ParentNode;
            var eventType = codeModelEvent.Type;
            switch (parentElement)
            {
                case EnvDTE.CodeType parentType:
                    return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentType.Attributes, parentElement);
                case EnvDTE.CodeFunction parentFunction:
                    return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentFunction.Attributes, parentElement);
                case EnvDTE.CodeProperty parentProperty:
                    return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentProperty.Attributes, parentElement);
                case EnvDTE80.CodeEvent parentEvent:
                    return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentEvent.Attributes, parentElement);
                case EnvDTE.CodeVariable parentVariable:
                    return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentVariable.Attributes, parentElement);
                case EnvDTE.FileCodeModel parentFileCodeModel:
                    {
                        var fileCodeModel = ComAggregate.GetManagedObject<FileCodeModel>(parentElement);
                        parentNode = fileCodeModel.GetSyntaxRoot();

                        return GetAttributeElementForCodeModelEvent(node, parentNode, eventType, parentFileCodeModel.CodeElements, parentElement);
                    }
            }

            return null;
        }

        private EnvDTE.CodeElement? GetAttributeElementForCodeModelEvent(SyntaxNode node, SyntaxNode parentNode, CodeModelEventType eventType, EnvDTE.CodeElements? elementsToSearch, object parentObject)
        {
            if (elementsToSearch == null)
            {
                return null;
            }

            CodeModelService.GetAttributeNameAndOrdinal(parentNode, node, out var name, out var ordinal);

            if (eventType == CodeModelEventType.Remove)
            {
                if (parentObject is EnvDTE.CodeElement)
                {
                    var parentCodeElement = ComAggregate.TryGetManagedObject<AbstractCodeElement>(parentObject);
                    if (parentCodeElement != null)
                    {
                        return (EnvDTE.CodeElement)CodeAttribute.Create(this.State, this, parentCodeElement, name, ordinal);
                    }
                }
                else if (parentObject is EnvDTE.FileCodeModel)
                {
                    var parentFileCodeModel = ComAggregate.TryGetManagedObject<FileCodeModel>(parentObject);
                    if (parentFileCodeModel != null && parentFileCodeModel == this)
                    {
                        return (EnvDTE.CodeElement)CodeAttribute.Create(this.State, this, null, name, ordinal);
                    }
                }
            }
            else
            {
                var testOrdinal = 0;
                foreach (EnvDTE.CodeElement element in elementsToSearch)
                {
                    if (element.Kind != EnvDTE.vsCMElement.vsCMElementAttribute)
                    {
                        continue;
                    }

                    if (element.Name == name)
                    {
                        if (ordinal == testOrdinal)
                        {
                            return element;
                        }

                        testOrdinal++;
                    }
                }
            }

            return null;
        }

        private EnvDTE.CodeElement? GetAttributeArgumentElementForCodeModelEvent(CodeModelEvent codeModelEvent, object? parentElement)
        {
            if (parentElement is EnvDTE80.CodeAttribute2 parentAttribute)
            {
                return GetAttributeArgumentForCodeModelEvent(codeModelEvent, parentAttribute.Arguments, parentElement);
            }

            return null;
        }

        private EnvDTE.CodeElement? GetAttributeArgumentForCodeModelEvent(CodeModelEvent codeModelEvent, EnvDTE.CodeElements? parentAttributeArguments, object parentElement)
        {
            if (parentAttributeArguments == null)
            {
                return null;
            }

            CodeModelService.GetAttributeArgumentParentAndIndex(codeModelEvent.Node, out _, out var ordinal);

            if (codeModelEvent.Type == CodeModelEventType.Remove)
            {
                var parentCodeElement = ComAggregate.TryGetManagedObject<CodeAttribute>(parentElement);
                if (parentCodeElement != null)
                {
                    return (EnvDTE.CodeElement)CodeAttributeArgument.Create(this.State, parentCodeElement, ordinal);
                }
            }
            else
            {
                return parentAttributeArguments.Item(ordinal + 1); // Needs to be 1-based to call back into code model
            }

            return null;
        }
    }
}
