﻿// 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.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.Collections;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.InternalElements
{
    [ComVisible(true)]
    [ComDefaultInterface(typeof(EnvDTE80.CodeParameter2))]
    public sealed class CodeParameter : AbstractCodeElement, EnvDTE.CodeParameter, EnvDTE80.CodeParameter2, IParameterKind
    {
        internal static EnvDTE.CodeParameter Create(
            CodeModelState state,
            AbstractCodeMember parent,
            string name)
        {
            var element = new CodeParameter(state, parent, name);
            return (EnvDTE.CodeParameter)ComAggregate.CreateAggregatedObject(element);
        }

        private readonly ParentHandle<AbstractCodeMember> _parentHandle;
        private readonly string _name;

        private CodeParameter(
            CodeModelState state,
            AbstractCodeMember parent,
            string name)
            : base(state, parent.FileCodeModel)
        {
            _parentHandle = new ParentHandle<AbstractCodeMember>(parent);
            _name = name;
        }

        private IParameterSymbol ParameterSymbol
        {
            get { return (IParameterSymbol)LookupSymbol(); }
        }

        private void UpdateNodeAndReacquireParentNodeKey<T>(Action<SyntaxNode, T> parameterUpdater, T value)
        {
            void updater(SyntaxNode n, T v)
            {
                var parentNode = _parentHandle.Value.LookupNode();
                var parentNodePath = new SyntaxPath(parentNode);

                parameterUpdater(n, v);

                _parentHandle.Value.ReacquireNodeKey(parentNodePath, CancellationToken.None);
            }

            UpdateNode(updater, value);
        }

        protected override EnvDTE.CodeElements GetCollection()
            => GetCollection<CodeParameter>(Parent);

        protected override string GetName()
            => _name;

        protected override string GetFullName()
        {
            var node = LookupNode();
            if (node == null)
            {
                return string.Empty;
            }

            return CodeModelService.GetParameterFullName(node);
        }

        internal override bool TryLookupNode(out SyntaxNode node)
        {
            node = null;

            var parentNode = _parentHandle.Value.LookupNode();
            if (parentNode == null)
            {
                return false;
            }

            if (!CodeModelService.TryGetParameterNode(parentNode, _name, out var parameterNode))
            {
                return false;
            }

            node = parameterNode;
            return node != null;
        }

        public override EnvDTE.vsCMElement Kind
        {
            get { return EnvDTE.vsCMElement.vsCMElementParameter; }
        }

        public override object Parent
        {
            get { return _parentHandle.Value; }
        }

        EnvDTE.CodeElement EnvDTE.CodeParameter.Parent
        {
            get { return (EnvDTE.CodeElement)Parent; }
        }

        EnvDTE.CodeElement EnvDTE80.CodeParameter2.Parent
        {
            get { return (EnvDTE.CodeElement)Parent; }
        }

        public override EnvDTE.CodeElements Children
        {
            get { return this.Attributes; }
        }

        public EnvDTE.CodeElements Attributes
        {
            get { return AttributeCollection.Create(this.State, this); }
        }

        public string DocComment
        {
            get
            {
                return string.Empty;
            }

            set
            {
                throw Exceptions.ThrowENotImpl();
            }
        }

        public EnvDTE.CodeTypeRef Type
        {
            get
            {
                return CodeTypeRef.Create(this.State, this, GetProjectId(), ParameterSymbol.Type);
            }

            set
            {
                UpdateNodeAndReacquireParentNodeKey(FileCodeModel.UpdateType, value);
            }
        }

        public EnvDTE80.vsCMParameterKind ParameterKind
        {
            get
            {
                return CodeModelService.GetParameterKind(LookupNode());
            }

            set
            {
                UpdateNodeAndReacquireParentNodeKey(FileCodeModel.UpdateParameterKind, value);
            }
        }

        public string DefaultValue
        {
            get
            {
                return CodeModelService.GetInitExpression(LookupNode());
            }

            set
            {
                UpdateNode(FileCodeModel.UpdateInitExpression, value);
            }
        }

        public EnvDTE.CodeAttribute AddAttribute(string name, string value, object position)
        {
            return FileCodeModel.EnsureEditor(() =>
            {
                return FileCodeModel.AddAttribute(LookupNode(), name, value, position);
            });
        }

        void IParameterKind.SetParameterPassingMode(PARAMETER_PASSING_MODE passingMode)
            => this.ParameterKind = this.CodeModelService.UpdateParameterKind(ParameterKind, passingMode);

        void IParameterKind.SetParameterArrayDimensions(int dimensions)
        {
            var type = this.ParameterSymbol.Type;
            var compilation = this.FileCodeModel.GetCompilation();

            var elementType = type is IArrayTypeSymbol
                ? ((IArrayTypeSymbol)type).ElementType
                : type;

            // The original C# implementation had a weird behavior where it wold allow setting array dimensions
            // to 0 to create an array with a single rank.
            var rank = Math.Max(dimensions, 1);
            var newType = compilation.CreateArrayTypeSymbol(elementType, rank);

            this.Type = CodeTypeRef.Create(this.State, this, GetProjectId(), newType);
        }

        int IParameterKind.GetParameterArrayCount()
        {
            var arrayType = this.ParameterSymbol.Type as IArrayTypeSymbol;
            var count = 0;

            while (arrayType != null)
            {
                count++;
                arrayType = arrayType.ElementType as IArrayTypeSymbol;
            }

            return count;
        }

        int IParameterKind.GetParameterArrayDimensions(int index)
        {
            if (index < 0)
            {
                throw Exceptions.ThrowEInvalidArg();
            }

            var arrayType = this.ParameterSymbol.Type as IArrayTypeSymbol;
            var count = 0;

            while (count < index && arrayType != null)
            {
                count++;
                arrayType = arrayType.ElementType as IArrayTypeSymbol;
            }

            if (arrayType == null)
            {
                throw Exceptions.ThrowEInvalidArg();
            }

            return arrayType.Rank;
        }

        PARAMETER_PASSING_MODE IParameterKind.GetParameterPassingMode()
        {
            var parameterKind = this.ParameterKind;

            if ((parameterKind & EnvDTE80.vsCMParameterKind.vsCMParameterKindRef) != 0)
            {
                return PARAMETER_PASSING_MODE.cmParameterTypeInOut;
            }
            else if ((parameterKind & EnvDTE80.vsCMParameterKind.vsCMParameterKindOut) != 0)
            {
                return PARAMETER_PASSING_MODE.cmParameterTypeOut;
            }
            else
            {
                return PARAMETER_PASSING_MODE.cmParameterTypeIn;
            }
        }
    }
}
