﻿// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Symbols;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// A program location in source code.
    /// </summary>
    [DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
    public abstract class Location
    {
        internal Location()
        {
        }

        /// <summary>
        /// Location kind (None/SourceFile/MetadataFile).
        /// </summary>
        public abstract LocationKind Kind { get; }

        /// <summary>
        /// Returns true if the location represents a specific location in a source code file.
        /// </summary>
        [MemberNotNullWhen(true, nameof(SourceTree))]
        public bool IsInSource { get { return SourceTree != null; } }

        /// <summary>
        /// Returns true if the location is in metadata.
        /// </summary>
        public bool IsInMetadata { get { return MetadataModuleInternal != null; } }

        /// <summary>
        /// The syntax tree this location is located in or <c>null</c> if not in a syntax tree.
        /// </summary>
        public virtual SyntaxTree? SourceTree { get { return null; } }

        /// <summary>
        /// Returns the metadata module the location is associated with or <c>null</c> if the module is not available.
        /// </summary>
        /// <remarks>
        /// Might return null even if <see cref="IsInMetadata"/> returns true. The module symbol might not be available anymore, 
        /// for example, if the location is serialized and deserialized.
        /// </remarks>
        public IModuleSymbol? MetadataModule { get { return (IModuleSymbol?)MetadataModuleInternal?.GetISymbol(); } }

        internal virtual IModuleSymbolInternal? MetadataModuleInternal { get { return null; } }

        /// <summary>
        /// The location within the syntax tree that this location is associated with.
        /// </summary>
        /// <remarks>
        /// If <see cref="IsInSource"/> returns False this method returns an empty <see cref="TextSpan"/> which starts at position 0.
        /// </remarks>
        public virtual TextSpan SourceSpan { get { return default(TextSpan); } }

        /// <summary>
        /// Gets the location in terms of path, line and column.
        /// </summary>
        /// <returns>
        /// <see cref="FileLinePositionSpan"/> that contains path, line and column information.
        /// 
        /// Returns an invalid span (see <see cref="FileLinePositionSpan.IsValid"/>) if the information is not available.
        /// 
        /// The values are not affected by line mapping directives (#line in C# or #ExternalSource in VB).
        /// </returns>
        public virtual FileLinePositionSpan GetLineSpan()
        {
            return default(FileLinePositionSpan);
        }

        /// <summary>
        /// Gets the location in terms of path, line and column after applying source line mapping directives
        /// (<c>#line</c> in C# or <c>#ExternalSource</c> in VB).
        /// </summary>
        /// <returns>
        /// <see cref="FileLinePositionSpan"/> that contains file, line and column information,
        /// or an invalid span (see <see cref="FileLinePositionSpan.IsValid"/>) if not available.
        /// </returns>
        public virtual FileLinePositionSpan GetMappedLineSpan()
        {
            return default(FileLinePositionSpan);
        }

        // Derived classes should provide value equality semantics.
        public abstract override bool Equals(object? obj);
        public abstract override int GetHashCode();

        public override string ToString()
        {
            string result = Kind.ToString();
            if (IsInSource)
            {
                result += "(" + this.SourceTree?.FilePath + this.SourceSpan + ")";
            }
            else if (IsInMetadata)
            {
                if (this.MetadataModuleInternal != null)
                {
                    result += "(" + this.MetadataModuleInternal.Name + ")";
                }
            }
            else
            {
                var pos = GetLineSpan();
                if (pos.Path != null)
                {
                    // user-visible line and column counts are 1-based, but internally are 0-based.
                    result += "(" + pos.Path + "@" + (pos.StartLinePosition.Line + 1) + ":" + (pos.StartLinePosition.Character + 1) + ")";
                }
            }

            return result;
        }

        public static bool operator ==(Location? left, Location? right)
        {
            if (object.ReferenceEquals(left, null))
            {
                return object.ReferenceEquals(right, null);
            }

            return left.Equals(right);
        }

        public static bool operator !=(Location? left, Location? right)
        {
            return !(left == right);
        }

        protected virtual string GetDebuggerDisplay()
        {
            string result = this.GetType().Name;
            var pos = GetLineSpan();
            if (pos.Path != null)
            {
                // user-visible line and column counts are 1-based, but internally are 0-based.
                result += "(" + pos.Path + "@" + (pos.StartLinePosition.Line + 1) + ":" + (pos.StartLinePosition.Character + 1) + ")";
            }

            return result;
        }

        /// <summary>
        /// A location of kind LocationKind.None. 
        /// </summary>
        public static Location None { get { return NoLocation.Singleton; } }

        /// <summary>
        /// Creates an instance of a <see cref="Location"/> for a span in a <see cref="SyntaxTree"/>.
        /// </summary>
        public static Location Create(SyntaxTree syntaxTree, TextSpan textSpan)
        {
            if (syntaxTree == null)
            {
                throw new ArgumentNullException(nameof(syntaxTree));
            }

            return new SourceLocation(syntaxTree, textSpan);
        }

        /// <summary>
        /// Creates an instance of a <see cref="Location"/> for a span in a file.
        /// </summary>
        public static Location Create(string filePath, TextSpan textSpan, LinePositionSpan lineSpan)
        {
            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            return new ExternalFileLocation(filePath, textSpan, lineSpan);
        }

        /// <summary>
        /// Creates an instance of a <see cref="Location"/> for a span in a file with a mapped file and span.
        /// </summary>
        public static Location Create(string filePath, TextSpan textSpan, LinePositionSpan lineSpan, string mappedFilePath, LinePositionSpan mappedLineSpan)
        {
            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            if (mappedFilePath == null)
            {
                throw new ArgumentNullException(nameof(mappedFilePath));
            }

            return new ExternalFileLocation(filePath, textSpan, lineSpan, mappedFilePath, mappedLineSpan);
        }
    }
}
