﻿// 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.

#nullable disable

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices.Implementation.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.InternalElements
{
    /// <summary>
    /// This is the base class of all code elements.
    /// </summary>
    public abstract class AbstractCodeElement : AbstractCodeModelObject, ICodeElementContainer<AbstractCodeElement>, EnvDTE.CodeElement, EnvDTE80.CodeElement2
    {
        private readonly ComHandle<EnvDTE.FileCodeModel, FileCodeModel> _fileCodeModel;
        private readonly int? _nodeKind;

        internal AbstractCodeElement(
            CodeModelState state,
            FileCodeModel fileCodeModel,
            int? nodeKind = null)
            : base(state)
        {
            Debug.Assert(fileCodeModel != null);

            _fileCodeModel = new ComHandle<EnvDTE.FileCodeModel, FileCodeModel>(fileCodeModel);
            _nodeKind = nodeKind;
        }

        internal FileCodeModel FileCodeModel
            => _fileCodeModel.Object;

        protected SyntaxTree GetSyntaxTree()
            => FileCodeModel.GetSyntaxTree();

        protected Document GetDocument()
            => FileCodeModel.GetDocument();

        protected SemanticModel GetSemanticModel()
            => FileCodeModel.GetSemanticModel();

        protected ProjectId GetProjectId()
            => FileCodeModel.GetProjectId();

        internal IGlobalOptionService GlobalOptions
            => FileCodeModel.GlobalOptions;

        internal bool IsValidNode()
        {
            if (!TryLookupNode(out var node))
            {
                return false;
            }

            if (_nodeKind != null &&
                _nodeKind.Value != node.RawKind)
            {
                return false;
            }

            return true;
        }

        internal virtual SyntaxNode LookupNode()
        {
            if (!TryLookupNode(out var node))
            {
                throw Exceptions.ThrowEFail();
            }

            return node;
        }

        internal abstract bool TryLookupNode(out SyntaxNode node);

        internal virtual ISymbol LookupSymbol()
        {
            var semanticModel = GetSemanticModel();
            var node = LookupNode();
            return semanticModel.GetDeclaredSymbol(node);
        }

        protected void UpdateNode<T>(Action<SyntaxNode, T> updater, T value)
        {
            FileCodeModel.EnsureEditor(() =>
            {
                var node = LookupNode();
                updater(node, value);
            });
        }

        public abstract EnvDTE.vsCMElement Kind { get; }

        protected virtual string GetName()
        {
            var node = LookupNode();
            return CodeModelService.GetName(node);
        }

        protected virtual void SetName(string value)
            => UpdateNode(FileCodeModel.UpdateName, value);

        public string Name
        {
            get { return GetName(); }
            set { SetName(value); }
        }

        protected virtual string GetFullName()
        {
            var node = LookupNode();
            var semanticModel = GetSemanticModel();
            return CodeModelService.GetFullName(node, semanticModel);
        }

        public string FullName
        {
            get { return GetFullName(); }
        }

        public abstract object Parent { get; }

        public abstract EnvDTE.CodeElements Children { get; }

        EnvDTE.CodeElements ICodeElementContainer<AbstractCodeElement>.GetCollection()
            => Children;

        protected virtual EnvDTE.CodeElements GetCollection()
            => GetCollection<AbstractCodeElement>(Parent);

        public virtual EnvDTE.CodeElements Collection
        {
            get { return GetCollection(); }
        }

        private LineFormattingOptions GetLineFormattingOptions()
            => State.ThreadingContext.JoinableTaskFactory.Run(() => GetDocument().GetLineFormattingOptionsAsync(GlobalOptions, CancellationToken.None).AsTask());

        public EnvDTE.TextPoint StartPoint
        {
            get
            {
                var options = GetLineFormattingOptions();
                var point = CodeModelService.GetStartPoint(LookupNode(), options);
                if (point == null)
                {
                    return null;
                }

                return FileCodeModel.TextManagerAdapter.CreateTextPoint(FileCodeModel, point.Value);
            }
        }

        public EnvDTE.TextPoint EndPoint
        {
            get
            {
                var options = GetLineFormattingOptions();
                var point = CodeModelService.GetEndPoint(LookupNode(), options);
                if (point == null)
                {
                    return null;
                }

                return FileCodeModel.TextManagerAdapter.CreateTextPoint(FileCodeModel, point.Value);
            }
        }

        public virtual EnvDTE.TextPoint GetStartPoint(EnvDTE.vsCMPart part)
        {
            var options = GetLineFormattingOptions();
            var point = CodeModelService.GetStartPoint(LookupNode(), options, part);
            if (point == null)
            {
                return null;
            }

            return FileCodeModel.TextManagerAdapter.CreateTextPoint(FileCodeModel, point.Value);
        }

        public virtual EnvDTE.TextPoint GetEndPoint(EnvDTE.vsCMPart part)
        {
            var options = GetLineFormattingOptions();
            var point = CodeModelService.GetEndPoint(LookupNode(), options, part);
            if (point == null)
            {
                return null;
            }

            return FileCodeModel.TextManagerAdapter.CreateTextPoint(FileCodeModel, point.Value);
        }

        public virtual EnvDTE.vsCMInfoLocation InfoLocation
        {
            get
            {
                // The default implementation assumes project-located elements...
                return EnvDTE.vsCMInfoLocation.vsCMInfoLocationProject;
            }
        }

        public virtual bool IsCodeType
        {
            get { return false; }
        }

        public EnvDTE.ProjectItem ProjectItem
        {
            get { return FileCodeModel.Parent; }
        }

        public string ExtenderCATID
        {
            get { throw new NotImplementedException(); }
        }

        protected virtual object GetExtenderNames()
            => throw Exceptions.ThrowENotImpl();

        public object ExtenderNames
        {
            get { return GetExtenderNames(); }
        }

        protected virtual object GetExtender(string name)
            => throw Exceptions.ThrowENotImpl();

        public object get_Extender(string extenderName)
            => GetExtender(extenderName);

        public string ElementID
        {
            get { throw new NotImplementedException(); }
        }

        public virtual void RenameSymbol(string newName)
        {
            if (string.IsNullOrEmpty(newName))
            {
                throw new ArgumentException();
            }

            CodeModelService.Rename(LookupSymbol(), newName, this.Workspace, this.State.ProjectCodeModelFactory);
        }

        protected virtual Document DeleteCore(Document document)
        {
            var node = LookupNode();
            return CodeModelService.Delete(document, node);
        }

        /// <summary>
        /// Delete the element from the source file.
        /// </summary>
        internal void Delete()
        {
            FileCodeModel.PerformEdit(document =>
            {
                return DeleteCore(document);
            });
        }

        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Required by interface")]
        public string get_Prototype(int flags)
            => CodeModelService.GetPrototype(LookupNode(), LookupSymbol(), (PrototypeFlags)flags);
    }
}
