// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Globalization;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Language
{
    /// <summary>
    /// A location in a Razor file.
    /// </summary>
    public struct SourceLocation : IEquatable<SourceLocation>
    {
        /// <summary>
        /// An undefined <see cref="SourceLocation"/>.
        /// </summary>
        public static readonly SourceLocation Undefined =
            new SourceLocation(absoluteIndex: -1, lineIndex: -1, characterIndex: -1);

        /// <summary>
        /// A <see cref="SourceLocation"/> with <see cref="AbsoluteIndex"/>, <see cref="LineIndex"/>, and
        /// <see cref="CharacterIndex"/> initialized to 0.
        /// </summary>
        public static readonly SourceLocation Zero =
            new SourceLocation(absoluteIndex: 0, lineIndex: 0, characterIndex: 0);

        /// <summary>
        /// Initializes a new instance of <see cref="SourceLocation"/>.
        /// </summary>
        /// <param name="absoluteIndex">The absolute index.</param>
        /// <param name="lineIndex">The line index.</param>
        /// <param name="characterIndex">The character index.</param>
        public SourceLocation(int absoluteIndex, int lineIndex, int characterIndex)
            : this(filePath: null, absoluteIndex: absoluteIndex, lineIndex: lineIndex, characterIndex: characterIndex)
        {
        }

        /// <summary>
        /// Initializes a new instance of <see cref="SourceLocation"/>.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <param name="absoluteIndex">The absolute index.</param>
        /// <param name="lineIndex">The line index.</param>
        /// <param name="characterIndex">The character index.</param>
        public SourceLocation(string filePath, int absoluteIndex, int lineIndex, int characterIndex)
        {
            FilePath = filePath;
            AbsoluteIndex = absoluteIndex;
            LineIndex = lineIndex;
            CharacterIndex = characterIndex;
        }

        /// <summary>
        /// Path of the file.
        /// </summary>
        /// <remarks>
        /// <para>
        /// When <c>null</c>, the parser assumes the location is in the file currently being processed.
        /// </para>
        /// <para>Set property is only accessible for deserialization purposes.</para>
        /// </remarks>
        public string FilePath { get; set;  }

        /// <remarks>Set property is only accessible for deserialization purposes.</remarks>
        public int AbsoluteIndex { get; set;  }

        /// <remarks>Set property is only accessible for deserialization purposes.</remarks>
        public int LineIndex { get; set; }

        /// <remarks>Set property is only accessible for deserialization purposes.</remarks>
        public int CharacterIndex { get; set; }

        /// <summary>
        /// Creates a new instance of <see cref="SourceLocation"/> from the provided span.
        /// </summary>
        /// <param name="span">
        /// The souce span. If <c>null</c>, <see cref="SourceLocation.Undefined"/> will be returned.
        /// </param>
        /// <remarks>A <see cref="SourceLocation"/> that corresponds to the beginning of the span.</remarks>
        public static SourceLocation FromSpan(SourceSpan? span)
        {
            return span == null ?
                SourceLocation.Undefined :
                new SourceLocation(span.Value.FilePath, span.Value.AbsoluteIndex, span.Value.LineIndex, span.Value.CharacterIndex);
        }

        /// <inheritdoc />
        public override string ToString()
        {
            return string.Format(
                CultureInfo.CurrentCulture,
                "({0}:{1},{2})",
                AbsoluteIndex,
                LineIndex,
                CharacterIndex);
        }

        /// <inheritdoc />
        public override bool Equals(object obj)
        {
            return obj is SourceLocation &&
                Equals((SourceLocation)obj);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            var hashCodeCombiner = HashCodeCombiner.Start();
            hashCodeCombiner.Add(FilePath, StringComparer.Ordinal);
            hashCodeCombiner.Add(AbsoluteIndex);

            return hashCodeCombiner;
        }

        /// <inheritdoc />
        public bool Equals(SourceLocation other)
        {
            return string.Equals(FilePath, other.FilePath, StringComparison.Ordinal) &&
                AbsoluteIndex == other.AbsoluteIndex &&
                LineIndex == other.LineIndex &&
                CharacterIndex == other.CharacterIndex;
        }

        public static bool operator==(SourceLocation left, SourceLocation right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(SourceLocation left, SourceLocation right)
        {
            return !left.Equals(right);
        }
    }
}
