﻿// 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.Linq;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// The LineDirectiveMap is created to enable translating positions, using the #line directives
    /// in a file. The basic implementation creates an ordered array of line mapping entries, one
    /// for each #line directive in the file (plus one at the beginning). If the file has no
    /// directives, then the array has just one element in it. To map line numbers, a binary search
    /// of the mapping entries is done and nearest line mapping is applied.
    /// </summary>
    internal abstract partial class LineDirectiveMap<TDirective>
        where TDirective : SyntaxNode
    {
        internal readonly ImmutableArray<LineMappingEntry> Entries;

        // Get all active #line directives under trivia into the list, in source code order.
        protected abstract bool ShouldAddDirective(TDirective directive);

        // Given a directive and the previous entry, create a new entry.
        protected abstract LineMappingEntry GetEntry(TDirective directive, SourceText sourceText, LineMappingEntry previous);

        // Creates the first entry with language specific content
        protected abstract LineMappingEntry InitializeFirstEntry();

        protected LineDirectiveMap(SyntaxTree syntaxTree)
        {
            // Accumulate all the directives, in source code order
            var syntaxRoot = (SyntaxNodeOrToken)syntaxTree.GetRoot();
            IList<TDirective> directives = syntaxRoot.GetDirectives<TDirective>(filter: ShouldAddDirective);
            Debug.Assert(directives != null);

            // Create the entry map.
            Entries = CreateEntryMap(syntaxTree, directives);
        }

        // Given a span and a default file name, return a FileLinePositionSpan that is the mapped
        // span, taking into account line directives.
        public FileLinePositionSpan TranslateSpan(SourceText sourceText, string treeFilePath, TextSpan span)
        {
            var unmappedStartPos = sourceText.Lines.GetLinePosition(span.Start);
            var unmappedEndPos = sourceText.Lines.GetLinePosition(span.End);
            var entry = FindEntry(unmappedStartPos.Line);

            return TranslateSpan(entry, treeFilePath, unmappedStartPos, unmappedEndPos);
        }

        protected FileLinePositionSpan TranslateSpan(in LineMappingEntry entry, string treeFilePath, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            string path = entry.MappedPathOpt ?? treeFilePath;
            var span = entry.State == PositionState.RemappedSpan ?
                TranslateEnhancedLineDirectiveSpan(entry, unmappedStartPos, unmappedEndPos) :
                TranslateLineDirectiveSpan(entry, unmappedStartPos, unmappedEndPos);
            return new FileLinePositionSpan(path, span, hasMappedPath: entry.MappedPathOpt != null);
        }

        private static LinePositionSpan TranslateLineDirectiveSpan(in LineMappingEntry entry, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            return new LinePositionSpan(translatePosition(entry, unmappedStartPos), translatePosition(entry, unmappedEndPos));

            static LinePosition translatePosition(in LineMappingEntry entry, LinePosition unmapped)
            {
                int mappedLine = unmapped.Line - entry.UnmappedLine + entry.MappedLine;
                return (mappedLine == -1) ? new LinePosition(unmapped.Character) : new LinePosition(mappedLine, unmapped.Character);
            }
        }

        private static LinePositionSpan TranslateEnhancedLineDirectiveSpan(in LineMappingEntry entry, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            // A span starting on the first line, at or before 'UnmappedCharacterOffset' is
            // mapped to the entire 'MappedSpan', regardless of the size of the unmapped span,
            // even if the unmapped span ends before 'UnmappedCharacterOffset'.
            if (unmappedStartPos.Line == entry.UnmappedLine &&
                unmappedStartPos.Character <= entry.UnmappedCharacterOffset.GetValueOrDefault())
            {
                return entry.MappedSpan;
            }

            // A span starting on the first line after 'UnmappedCharacterOffset', or starting on
            // a subsequent line, is mapped to a span of corresponding size.
            return new LinePositionSpan(translatePosition(entry, unmappedStartPos), translatePosition(entry, unmappedEndPos));

            static LinePosition translatePosition(in LineMappingEntry entry, LinePosition unmapped)
            {
                return new LinePosition(
                    unmapped.Line - entry.UnmappedLine + entry.MappedSpan.Start.Line,
                    unmapped.Line == entry.UnmappedLine ?
                        entry.MappedSpan.Start.Character + unmapped.Character - entry.UnmappedCharacterOffset.GetValueOrDefault() :
                        unmapped.Character);
            }
        }

        /// <summary>
        /// Determines whether the position is considered to be hidden from the debugger or not.
        /// </summary>
        public abstract LineVisibility GetLineVisibility(SourceText sourceText, int position);

        /// <summary>
        /// Combines TranslateSpan and IsHiddenPosition to not search the entries twice when emitting sequence points
        /// </summary>
        internal abstract FileLinePositionSpan TranslateSpanAndVisibility(SourceText sourceText, string treeFilePath, TextSpan span, out bool isHiddenPosition);

        /// <summary>
        /// Are there any hidden regions in the map?
        /// </summary>
        /// <returns>True if there's at least one hidden region in the map.</returns>
        public bool HasAnyHiddenRegions()
        {
            return this.Entries.Any(static e => e.State == PositionState.Hidden);
        }

        // Find the line mapped entry with the largest unmapped line number <= lineNumber.
        protected LineMappingEntry FindEntry(int lineNumber)
        {
            int r = FindEntryIndex(lineNumber);

            return this.Entries[r];
        }

        // Find the index of the line mapped entry with the largest unmapped line number <= lineNumber.
        protected int FindEntryIndex(int lineNumber)
        {
            int r = Entries.BinarySearch(new LineMappingEntry(lineNumber));
            return r >= 0 ? r : ((~r) - 1);
        }

        // Given the ordered list of all directives in the file, return the ordered line mapping
        // entry for the file. This always starts with the null mapped that maps line 0 to line 0.
        private ImmutableArray<LineMappingEntry> CreateEntryMap(SyntaxTree tree, IList<TDirective> directives)
        {
            var entries = ArrayBuilder<LineMappingEntry>.GetInstance(directives.Count + 1);

            var current = InitializeFirstEntry();
            entries.Add(current);

            if (directives.Count > 0)
            {
                var sourceText = tree.GetText();
                foreach (var directive in directives)
                {
                    current = GetEntry(directive, sourceText, current);
                    entries.Add(current);
                }
            }

#if DEBUG
            // Make sure the entries array is correctly sorted. 
            for (int i = 0; i < entries.Count - 1; ++i)
            {
                Debug.Assert(entries[i].CompareTo(entries[i + 1]) < 0);
            }
#endif

            return entries.ToImmutableAndFree();
        }

        protected abstract LineVisibility GetUnknownStateVisibility(int index);

        /// <summary>
        /// The caller is expected to not call this if <see cref="Entries"/> is empty.
        /// </summary>
        public IEnumerable<LineMapping> GetLineMappings(TextLineCollection lines)
        {
            Debug.Assert(Entries.Length > 1);

            var current = Entries[0];

            // the first entry is always initialized to unmapped:
            Debug.Assert(
                current.State is PositionState.Unmapped or PositionState.Unknown &&
                current.UnmappedLine == 0 &&
                current.MappedLine == 0 &&
                current.MappedPathOpt == null);

            for (int i = 1; i < Entries.Length; i++)
            {
                var next = Entries[i];

                int unmappedEndLine = next.UnmappedLine - 2;
                Debug.Assert(unmappedEndLine >= current.UnmappedLine - 1);

                // Skip empty spans - two consecutive #line directives or #line on the first line.
                if (unmappedEndLine >= current.UnmappedLine)
                {
                    // C#: Span ends just at the start of the line containing #line directive
                    //
                    // #line Current "file1"
                    // [|....\n
                    // ...........\n|]
                    // #line Next "file2"
                    //
                    // VB: Span starts at the beginning of the line following the #ExternalSource directive and ends at the start of the line preceding #End ExternalSource.
                    // #ExternalSource("file", 1)
                    // [|....\n
                    // ...........\n|]
                    // #End ExternalSource

                    var endLine = lines[unmappedEndLine];
                    int lineLength = endLine.EndIncludingLineBreak - endLine.Start;

                    yield return CreateLineMapping(current, unmappedEndLine, lineLength, currentIndex: i - 1);
                }

                current = next;
            }

            var lastLine = lines[^1];

            // Last span (unless the last #line/#End ExternalSource is on the last line):
            // #line Current "file1"
            // [|....\n
            // ...........\n|]
            //
            // #End ExternalSource
            // [|....\n
            // ...........\n|]
            if (current.UnmappedLine <= lastLine.LineNumber)
            {
                int lineLength = lastLine.EndIncludingLineBreak - lastLine.Start;
                int unmappedEndLine = lastLine.LineNumber;

                yield return CreateLineMapping(current, unmappedEndLine, lineLength, currentIndex: Entries.Length - 1);
            }
        }

        private LineMapping CreateLineMapping(in LineMappingEntry entry, int unmappedEndLine, int lineLength, int currentIndex)
        {
            var unmapped = new LinePositionSpan(
                new LinePosition(entry.UnmappedLine, character: 0),
                new LinePosition(unmappedEndLine, lineLength));

            if (entry.State == PositionState.Hidden ||
                entry.State == PositionState.Unknown && GetUnknownStateVisibility(currentIndex) == LineVisibility.Hidden)
            {
                return new LineMapping(unmapped, characterOffset: null, mappedSpan: default);
            }

            string path = entry.MappedPathOpt ?? string.Empty;
            bool hasMappedPath = entry.MappedPathOpt != null;

            if (entry.State == PositionState.RemappedSpan)
            {
                return new LineMapping(
                    unmapped,
                    characterOffset: entry.UnmappedCharacterOffset,
                    new FileLinePositionSpan(path, entry.MappedSpan, hasMappedPath));
            }

            var mappedSpan = new LinePositionSpan(
                new LinePosition(entry.MappedLine, character: 0),
                new LinePosition(entry.MappedLine + unmappedEndLine - entry.UnmappedLine, lineLength));
            var mapped = new FileLinePositionSpan(path, mappedSpan, hasMappedPath);
            return new LineMapping(unmapped, characterOffset: null, mapped);
        }
    }
}
