//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Commands;
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
using Microsoft.PowerShell.EditorServices.Symbols;

namespace Microsoft.PowerShell.EditorServices.CodeLenses
{
    /// <summary>
    /// Provides the "reference" code lens by extracting document symbols.
    /// </summary>
    internal class ReferencesCodeLensProvider : FeatureProviderBase, ICodeLensProvider
    {
        private static readonly Location[] s_emptyLocationArray = new Location[0];

        /// <summary>
        /// The editor session code lenses are being provided from.
        /// </summary>
        private EditorSession _editorSession;

        /// <summary>
        /// The document symbol provider to supply symbols to generate the code lenses.
        /// </summary>
        private IDocumentSymbolProvider _symbolProvider;

        /// <summary>
        /// Construct a new ReferencesCodeLensProvider for a given EditorSession.
        /// </summary>
        /// <param name="editorSession"></param>
        public ReferencesCodeLensProvider(EditorSession editorSession)
        {
            _editorSession = editorSession;

            // TODO: Pull this from components
            _symbolProvider = new ScriptDocumentSymbolProvider(
                editorSession.PowerShellContext.LocalPowerShellVersion.Version);
        }

        /// <summary>
        /// Get all reference code lenses for a given script file.
        /// </summary>
        /// <param name="scriptFile">The PowerShell script file to get code lenses for.</param>
        /// <returns>An array of CodeLenses describing all functions in the given script file.</returns>
        public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile)
        {
            var acc = new List<CodeLens>();
            foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile))
            {
                if (sym.SymbolType == SymbolType.Function)
                {
                    acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion));
                }
            }

            return acc.ToArray();
        }

        /// <summary>
        /// Take a codelens and create a new codelens object with updated references.
        /// </summary>
        /// <param name="codeLens">The old code lens to get updated references for.</param>
        /// <param name="cancellationToken">The cancellation token for this request.</param>
        /// <returns>A new code lens object describing the same data as the old one but with updated references.</returns>
        public async Task<CodeLens> ResolveCodeLensAsync(
            CodeLens codeLens,
            CancellationToken cancellationToken)
        {
            ScriptFile[] references = _editorSession.Workspace.ExpandScriptReferences(
                codeLens.File);

            SymbolReference foundSymbol = _editorSession.LanguageService.FindFunctionDefinitionAtLocation(
                codeLens.File,
                codeLens.ScriptExtent.StartLineNumber,
                codeLens.ScriptExtent.StartColumnNumber);

            FindReferencesResult referencesResult = await _editorSession.LanguageService.FindReferencesOfSymbolAsync(
                foundSymbol,
                references,
                _editorSession.Workspace);

            Location[] referenceLocations;
            if (referencesResult == null)
            {
                referenceLocations = s_emptyLocationArray;
            }
            else
            {
                var acc = new List<Location>();
                foreach (SymbolReference foundReference in referencesResult.FoundReferences)
                {
                    if (!NotReferenceDefinition(foundSymbol, foundReference))
                    {
                        continue;
                    }

                    acc.Add(new Location
                    {
                        Uri = GetFileUri(foundReference.FilePath),
                        Range = foundReference.ScriptRegion.ToRange()
                    });
                }
                referenceLocations = acc.ToArray();
            }

            return new CodeLens(
                codeLens,
                new ClientCommand(
                    "editor.action.showReferences",
                    GetReferenceCountHeader(referenceLocations.Length),
                    new object[]
                    {
                        codeLens.File.DocumentUri,
                        codeLens.ScriptExtent.ToRange().Start,
                        referenceLocations,
                    }
                    ));
        }

        /// <summary>
        /// Check whether a SymbolReference is not a reference to another defined symbol.
        /// </summary>
        /// <param name="definition">The symbol definition that may be referenced.</param>
        /// <param name="reference">The reference symbol to check.</param>
        /// <returns>True if the reference is not a reference to the definition, false otherwise.</returns>
        private static bool NotReferenceDefinition(
            SymbolReference definition,
            SymbolReference reference)
        {
            return
                definition.ScriptRegion.StartLineNumber != reference.ScriptRegion.StartLineNumber
                || definition.SymbolType != reference.SymbolType
                || !string.Equals(definition.SymbolName, reference.SymbolName, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Get a URI for a given file path.
        /// </summary>
        /// <param name="filePath">A file path that may be prefixed with URI scheme already.</param>
        /// <returns>A URI to the file.</returns>
        private static string GetFileUri(string filePath)
        {
            // If the file isn't untitled, return a URI-style path
            return
                !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory")
                    ? new Uri("file://" + filePath).AbsoluteUri
                    : filePath;
        }

        /// <summary>
        /// Get the code lens header for the number of references on a definition,
        /// given the number of references.
        /// </summary>
        /// <param name="referenceCount">The number of references found for a given definition.</param>
        /// <returns>The header string for the reference code lens.</returns>
        private static string GetReferenceCountHeader(int referenceCount)
        {
            if (referenceCount == 1)
            {
                return "1 reference";
            }

            var sb = new StringBuilder(14); // "100 references".Length = 14
            sb.Append(referenceCount);
            sb.Append(" references");
            return sb.ToString();
        }
    }
}