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

#nullable disable

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 source 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);
    }
}
