// 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.

namespace Roslyn.LanguageServer.Protocol;

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.LanguageServer;

/// <summary>
/// Struct that may contain a <typeparamref name="T1"/> or a <typeparamref name="T2"/>.
/// </summary>
/// <typeparam name="T1">The first type this struct is designed to contain.</typeparam>
/// <typeparam name="T2">The second type this struct is designed to contain.</typeparam>
[JsonConverter(typeof(SumConverter))]
internal struct SumType<T1, T2> : ISumType, IEquatable<SumType<T1, T2>>
    where T1 : notnull
    where T2 : notnull
{
    static SumType()
    {
        SumTypeUtils.ValidateTypeParameter(typeof(T1));
        SumTypeUtils.ValidateTypeParameter(typeof(T2));
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2}"/> struct containing a <typeparamref name="T1"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2}"/>.</param>
    public SumType(T1 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2}"/> struct containing a <typeparamref name="T2"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2}"/>.</param>
    public SumType(T2 val)
    {
        this.Value = val;
    }

    /// <inheritdoc/>
    public object? Value { get; }

    /// <summary>
    /// Gets the value as the first specified type.
    /// </summary>
    public T1 First => (T1)this;

    /// <summary>
    /// Gets the value as the second specified type.
    /// </summary>
    public T2 Second => (T2)this;

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1"/> with a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2>(T1 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1?"/> with a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2>?(T1? val) => val is null ? null : new SumType<T1, T2>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2"/> with a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2>(T2 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2?"/> with a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2>?(T2? val) => val is null ? null : new SumType<T1, T2>(val);

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2}"/> to an instance of <typeparamref name="T1"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2}"/> does not contain an instance of <typeparamref name="T1"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T1(SumType<T1, T2> sum) => sum.Value is T1 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2}"/> to an instance of <typeparamref name="T2"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2}"/> does not contain an instance of <typeparamref name="T2"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T2(SumType<T1, T2> sum) => sum.Value is T2 tVal ? tVal : throw new InvalidCastException();

    public static bool operator ==(SumType<T1, T2> left, SumType<T1, T2> right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(SumType<T1, T2> left, SumType<T1, T2> right)
    {
        return !(left == right);
    }

    /// <summary>
    /// Tries to get the value as the first specified type.
    /// </summary>
    /// <param name="value">the value in the specified type.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetFirst([MaybeNullWhen(false)] out T1 value)
    {
        if (this.Value is T1 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the second specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetSecond([MaybeNullWhen(false)] out T2 value)
    {
        if (this.Value is T2 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Runs a delegate corresponding to which type is contained inside this instance.
    /// </summary>
    /// <typeparam name="TResult">The type that all the delegates will return.</typeparam>
    /// <param name="firstMatch">Delegate to handle the case where this instance contains a <typeparamref name="T1"/>.</param>
    /// <param name="secondMatch">Delegate to handle the case where this instance contains a <typeparamref name="T2"/>.</param>
    /// <param name="defaultMatch">
    /// Delegate to handle the case where this instance is uninhabited. If this delegate isn't provided the default
    /// <typeparamref name="TResult"/> will be returned instead.
    /// </param>
    /// <returns>The <typeparamref name="TResult"/> instance created by the delegate corresponding to the current type stored in this instance.</returns>
    public TResult Match<TResult>(Func<T1, TResult> firstMatch, Func<T2, TResult> secondMatch, Func<TResult>? defaultMatch = null)
    {
        if (firstMatch == null)
        {
            throw new ArgumentNullException(nameof(firstMatch));
        }

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

        if (this.Value is T1 tOne)
        {
            return firstMatch(tOne);
        }

        if (this.Value is T2 tTwo)
        {
            return secondMatch(tTwo);
        }

        if (defaultMatch != null)
        {
            return defaultMatch();
        }

#pragma warning disable CS8603 // Possible null reference return.
        return default(TResult);
#pragma warning restore CS8603 // Possible null reference return.
    }

    /// <inheritdoc/>
    public override bool Equals(object obj)
    {
        return obj is SumType<T1, T2> type && this.Equals(type);
    }

    /// <inheritdoc/>
    public bool Equals(SumType<T1, T2> other)
    {
        return EqualityComparer<object?>.Default.Equals(this.Value, other.Value);
    }

    /// <inheritdoc/>
    public override int GetHashCode()
    {
        return -1937169414 + EqualityComparer<object?>.Default.GetHashCode(this.Value);
    }
}

/// <summary>
/// Struct that may contain a <typeparamref name="T1"/>, a <typeparamref name="T2"/>, or a <typeparamref name="T3"/>.
/// </summary>
/// <typeparam name="T1">The first type this struct is designed to contain.</typeparam>
/// <typeparam name="T2">The second type this struct is designed to contain.</typeparam>
/// <typeparam name="T3">The third type this struct is designed to contain.</typeparam>
[JsonConverter(typeof(SumConverter))]
internal struct SumType<T1, T2, T3> : ISumType, IEquatable<SumType<T1, T2, T3>>
    where T1 : notnull
    where T2 : notnull
    where T3 : notnull
{
    static SumType()
    {
        SumTypeUtils.ValidateTypeParameter(typeof(T1));
        SumTypeUtils.ValidateTypeParameter(typeof(T2));
        SumTypeUtils.ValidateTypeParameter(typeof(T3));
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3}"/> struct containing a <typeparamref name="T1"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3}"/>.</param>
    public SumType(T1 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3}"/> struct containing a <typeparamref name="T2"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3}"/>.</param>
    public SumType(T2 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3}"/> struct containing a <typeparamref name="T3"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3}"/>.</param>
    public SumType(T3 val)
    {
        this.Value = val;
    }

    /// <inheritdoc/>
    public object? Value { get; }

    /// <summary>
    /// Gets the value as the first specified type.
    /// </summary>
    public T1 First => (T1)this;

    /// <summary>
    /// Gets the value as the second specified type.
    /// </summary>
    public T2 Second => (T2)this;

    /// <summary>
    /// Gets the value as the third specified type.
    /// </summary>
    public T3 Third => (T3)this;

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3>(T1 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1?"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3>?(T1? val) => val is null ? null : new SumType<T1, T2, T3>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3>(T2 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2?"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3>?(T2? val) => val is null ? null : new SumType<T1, T2, T3>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T3"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3>(T3 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T3?"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3>?(T3? val) => val is null ? null : new SumType<T1, T2, T3>(val);

    /// <summary>
    /// Implicitly wraps an instance of <see cref="SumType{T1, T2}"/> with a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to wrap.</param>
    public static implicit operator SumType<T1, T2, T3>(SumType<T1, T2> sum)
        => sum.Match(
            (v) => new SumType<T1, T2, T3>(v),
            (v) => new SumType<T1, T2, T3>(v));

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3}"/> into a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to downcast.</param>
    public static explicit operator SumType<T1, T2>(SumType<T1, T2, T3> sum)
    {
        if (sum.Value is T1 tOne)
        {
            return tOne;
        }

        if (sum.Value is T2 tTwo)
        {
            return tTwo;
        }

        throw new InvalidCastException();
    }

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3}"/> to an instance of <typeparamref name="T1"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3}"/> does not contain an instance of <typeparamref name="T1"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T1(SumType<T1, T2, T3> sum) => sum.Value is T1 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2}"/> to an instance of <typeparamref name="T2"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3}"/> does not contain an instance of <typeparamref name="T2"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T2(SumType<T1, T2, T3> sum) => sum.Value is T2 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3}"/> to an instance of <typeparamref name="T3"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3}"/> does not contain an instance of <typeparamref name="T3"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T3(SumType<T1, T2, T3> sum) => sum.Value is T3 tVal ? tVal : throw new InvalidCastException();

    public static bool operator ==(SumType<T1, T2, T3> left, SumType<T1, T2, T3> right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(SumType<T1, T2, T3> left, SumType<T1, T2, T3> right)
    {
        return !(left == right);
    }

    /// <summary>
    /// Tries to get the value as the first specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetFirst([MaybeNullWhen(false)] out T1 value)
    {
        if (this.Value is T1 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the second specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetSecond([MaybeNullWhen(false)] out T2 value)
    {
        if (this.Value is T2 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the third specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetThird([MaybeNullWhen(false)] out T3 value)
    {
        if (this.Value is T3 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Runs a delegate corresponding to which type is contained inside this instance.
    /// </summary>
    /// <typeparam name="TResult">The type that all the delegates will return.</typeparam>
    /// <param name="firstMatch">Delegate to handle the case where this instance contains a <typeparamref name="T1"/>.</param>
    /// <param name="secondMatch">Delegate to handle the case where this instance contains a <typeparamref name="T2"/>.</param>
    /// <param name="thirdMatch">Delegate to handle the case where this instance contains a <typeparamref name="T3"/>.</param>
    /// <param name="defaultMatch">
    /// Delegate to handle the case where this instance is uninhabited. If this delegate isn't provided the default
    /// <typeparamref name="TResult"/> will be returned instead.
    /// </param>
    /// <returns>The <typeparamref name="TResult"/> instance created by the delegate corresponding to the current type stored in this instance.</returns>
    public TResult Match<TResult>(Func<T1, TResult> firstMatch, Func<T2, TResult> secondMatch, Func<T3, TResult> thirdMatch, Func<TResult>? defaultMatch = null)
    {
        if (firstMatch == null)
        {
            throw new ArgumentNullException(nameof(firstMatch));
        }

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

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

        if (this.Value is T1 tOne)
        {
            return firstMatch(tOne);
        }

        if (this.Value is T2 tTwo)
        {
            return secondMatch(tTwo);
        }

        if (this.Value is T3 tThree)
        {
            return thirdMatch(tThree);
        }

        if (defaultMatch != null)
        {
            return defaultMatch();
        }

#pragma warning disable CS8603 // Possible null reference return.
        return default(TResult);
#pragma warning restore CS8603 // Possible null reference return.
    }

    /// <inheritdoc/>
    public override bool Equals(object obj)
    {
        return obj is SumType<T1, T2, T3> type && this.Equals(type);
    }

    /// <inheritdoc/>
    public bool Equals(SumType<T1, T2, T3> other)
    {
        return EqualityComparer<object?>.Default.Equals(this.Value, other.Value);
    }

    /// <inheritdoc/>
    public override int GetHashCode()
    {
        return -1937169414 + EqualityComparer<object?>.Default.GetHashCode(this.Value);
    }
}

/// <summary>
/// Struct that may contain a <typeparamref name="T1"/>, a <typeparamref name="T2"/>, a <typeparamref name="T3"/>, or a <typeparamref name="T4"/>.
/// </summary>
/// <typeparam name="T1">The first type this struct is designed to contain.</typeparam>
/// <typeparam name="T2">The second type this struct is designed to contain.</typeparam>
/// <typeparam name="T3">The third type this struct is designed to contain.</typeparam>
/// <typeparam name="T4">The fourth type this struct is designed to contain.</typeparam>
[JsonConverter(typeof(SumConverter))]
internal struct SumType<T1, T2, T3, T4> : ISumType, IEquatable<SumType<T1, T2, T3, T4>>
    where T1 : notnull
    where T2 : notnull
    where T3 : notnull
    where T4 : notnull
{
    static SumType()
    {
        SumTypeUtils.ValidateTypeParameter(typeof(T1));
        SumTypeUtils.ValidateTypeParameter(typeof(T2));
        SumTypeUtils.ValidateTypeParameter(typeof(T3));
        SumTypeUtils.ValidateTypeParameter(typeof(T4));
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3, T4}"/> struct containing a <typeparamref name="T1"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3, T4}"/>.</param>
    public SumType(T1 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3, T4}"/> struct containing a <typeparamref name="T2"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3, T4}"/>.</param>
    public SumType(T2 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3, T4}"/> struct containing a <typeparamref name="T3"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3, T4}"/>.</param>
    public SumType(T3 val)
    {
        this.Value = val;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SumType{T1, T2, T3, T4}"/> struct containing a <typeparamref name="T4"/>.
    /// </summary>
    /// <param name="val">The value to store in the <see cref="SumType{T1, T2, T3, T4}"/>.</param>
    public SumType(T4 val)
    {
        this.Value = val;
    }

    /// <inheritdoc/>
    public object? Value { get; }

    /// <summary>
    /// Gets the value as the first specified type.
    /// </summary>
    public T1 First => (T1)this;

    /// <summary>
    /// Gets the value as the second specified type.
    /// </summary>
    public T2 Second => (T2)this;

    /// <summary>
    /// Gets the value as the third specified type.
    /// </summary>
    public T3 Third => (T3)this;

    /// <summary>
    /// Gets the value as the fourth specified type.
    /// </summary>
    public T4 Fourth => (T4)this;

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(T1 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T1?"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3, T4>?(T1? val) => val is null ? null : new SumType<T1, T2, T3, T4>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(T2 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T2?"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3, T4>?(T2? val) => val is null ? null : new SumType<T1, T2, T3, T4>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T3"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(T3 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T3?"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3, T4>?(T3? val) => val is null ? null : new SumType<T1, T2, T3, T4>(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T4"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(T4 val) => new(val);

    /// <summary>
    /// Implicitly wraps a value of type <typeparamref name="T4?"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="val">Value to be wrapped.</param>
    public static implicit operator SumType<T1, T2, T3, T4>?(T4? val) => val is null ? null : new SumType<T1, T2, T3, T4>(val);

    /// <summary>
    /// Implicitly wraps an instance of <see cref="SumType{A, B}"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(SumType<T1, T2> sum)
        => sum.Match(
            (v) => new SumType<T1, T2, T3, T4>(v),
            (v) => new SumType<T1, T2, T3, T4>(v));

    /// <summary>
    /// Implicitly wraps an instance of <see cref="SumType{T1, T2, T3}"/> with a <see cref="SumType{T1, T2, T3, T4}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to wrap.</param>
    public static implicit operator SumType<T1, T2, T3, T4>(SumType<T1, T2, T3> sum)
        => sum.Match(
            (v) => new SumType<T1, T2, T3, T4>(v),
            (v) => new SumType<T1, T2, T3, T4>(v),
            (v) => new SumType<T1, T2, T3, T4>(v));

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> into a <see cref="SumType{T1, T2}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to downcast.</param>
    public static explicit operator SumType<T1, T2>(SumType<T1, T2, T3, T4> sum)
    {
        if (sum.Value is T1 tOne)
        {
            return tOne;
        }

        if (sum.Value is T2 tTwo)
        {
            return tTwo;
        }

        throw new InvalidCastException();
    }

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> into a <see cref="SumType{T1, T2, T3}"/>.
    /// </summary>
    /// <param name="sum">Sum instance to downcast.</param>
    public static explicit operator SumType<T1, T2, T3>(SumType<T1, T2, T3, T4> sum)
    {
        if (sum.Value is T1 tOne)
        {
            return tOne;
        }

        if (sum.Value is T2 tTwo)
        {
            return tTwo;
        }

        if (sum.Value is T3 tThree)
        {
            return tThree;
        }

        throw new InvalidCastException();
    }

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> to an instance of <typeparamref name="T1"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3, T4}"/> does not contain an instance of <typeparamref name="T1"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T1(SumType<T1, T2, T3, T4> sum) => sum.Value is T1 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> to an instance of <typeparamref name="T2"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3, T4}"/> does not contain an instance of <typeparamref name="T2"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T2(SumType<T1, T2, T3, T4> sum) => sum.Value is T2 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> to an instance of <typeparamref name="T3"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3, T4}"/> does not contain an instance of <typeparamref name="T3"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T3(SumType<T1, T2, T3, T4> sum) => sum.Value is T3 tVal ? tVal : throw new InvalidCastException();

    /// <summary>
    /// Attempts to cast an instance of <see cref="SumType{T1, T2, T3, T4}"/> to an instance of <typeparamref name="T4"/>.
    /// </summary>
    /// <exception cref="InvalidCastException">Thrown if this instance of <see cref="SumType{T1, T2, T3, T4}"/> does not contain an instance of <typeparamref name="T4"/>.</exception>
    /// <param name="sum">Instance to unwrap.</param>
    public static explicit operator T4(SumType<T1, T2, T3, T4> sum) => sum.Value is T4 tVal ? tVal : throw new InvalidCastException();

    public static bool operator ==(SumType<T1, T2, T3, T4> left, SumType<T1, T2, T3, T4> right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(SumType<T1, T2, T3, T4> left, SumType<T1, T2, T3, T4> right)
    {
        return !(left == right);
    }

    /// <summary>
    /// Tries to get the value as the first specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetFirst([MaybeNullWhen(false)] out T1 value)
    {
        if (this.Value is T1 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the second specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetSecond([MaybeNullWhen(false)] out T2 value)
    {
        if (this.Value is T2 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the third specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetThird([MaybeNullWhen(false)] out T3 value)
    {
        if (this.Value is T3 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Tries to get the value as the fourth specified type.
    /// </summary>
    /// <param name="value">the value in the specified type/>.</param>
    /// <returns><see langword="true"/> if the type matches.</returns>
    public bool TryGetFourth([MaybeNullWhen(false)] out T4 value)
    {
        if (this.Value is T4 typeValue)
        {
            value = typeValue;
            return true;
        }

        value = default;
        return false;
    }

    /// <summary>
    /// Runs a delegate corresponding to which type is contained inside this instance.
    /// </summary>
    /// <typeparam name="TResult">The type that all the delegates will return.</typeparam>
    /// <param name="firstMatch">Delegate to handle the case where this instance contains a <typeparamref name="T1"/>.</param>
    /// <param name="secondMatch">Delegate to handle the case where this instance contains a <typeparamref name="T2"/>.</param>
    /// <param name="thirdMatch">Delegate to handle the case where this instance contains a <typeparamref name="T3"/>.</param>
    /// <param name="fourthMatch">Delegate to handle the case where this instance contains a <typeparamref name="T4"/>.</param>
    /// <param name="defaultMatch">
    /// Delegate to handle the case where this instance is uninhabited. If this delegate isn't provided the default
    /// <typeparamref name="TResult"/> will be returned instead.
    /// </param>
    /// <returns>The <typeparamref name="TResult"/> instance created by the delegate corresponding to the current type stored in this instance.</returns>
    public TResult Match<TResult>(Func<T1, TResult> firstMatch, Func<T2, TResult> secondMatch, Func<T3, TResult> thirdMatch, Func<T4, TResult> fourthMatch, Func<TResult>? defaultMatch = null)
    {
        if (firstMatch == null)
        {
            throw new ArgumentNullException(nameof(firstMatch));
        }

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

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

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

        if (this.Value is T1 tOne)
        {
            return firstMatch(tOne);
        }

        if (this.Value is T2 tTwo)
        {
            return secondMatch(tTwo);
        }

        if (this.Value is T3 tThree)
        {
            return thirdMatch(tThree);
        }

        if (this.Value is T4 tFour)
        {
            return fourthMatch(tFour);
        }

        if (defaultMatch != null)
        {
            return defaultMatch();
        }

#pragma warning disable CS8603 // Possible null reference return.
        return default(TResult);
#pragma warning restore CS8603 // Possible null reference return.
    }

    /// <inheritdoc/>
    public override bool Equals(object obj)
    {
        return obj is SumType<T1, T2, T3, T4> type && this.Equals(type);
    }

    /// <inheritdoc/>
    public bool Equals(SumType<T1, T2, T3, T4> other)
    {
        return EqualityComparer<object?>.Default.Equals(this.Value, other.Value);
    }

    /// <inheritdoc/>
    public override int GetHashCode()
    {
        return -1937169414 + EqualityComparer<object?>.Default.GetHashCode(this.Value);
    }
}

/// <summary>
/// Utility methods for <see cref="ISumType"/> implementations.
/// </summary>
internal static class SumTypeUtils
{
    /// <summary>
    /// Validates that <paramref name="type"/> is a valid type parameter for a SumType.
    /// </summary>
    /// <param name="type">The type to validate.</param>
    /// <exception cref="NotSupportedException">If <paramref name="type"/> is not supported as a type parameter for a
    /// SumType.</exception>
    public static void ValidateTypeParameter(Type type)
    {
        if (typeof(ISumType).IsAssignableFrom(type))
        {
            throw new NotSupportedException(LanguageServerProtocolResources.NestedSumType);
        }
    }

    public static TCommon Unify<TCommon, TDerived>(this SumType<TCommon, TDerived> sumType)
        where TCommon : notnull
        where TDerived : notnull, TCommon
        => sumType.Match(common => common, derived => derived);

    public static TCommon[] Unify<TCommon, TDerived>(this SumType<TCommon[], TDerived[]> sumType)
        where TDerived : TCommon
        => sumType.Match(common => common, derived => Array.ConvertAll(derived, d => (TCommon)d));

    public static Dictionary<TKey, object> AsUntyped<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
        where TKey : notnull
        where TValue : notnull, ISumType
        => dictionary.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value);
}
