using System.Collections;

namespace Common.Distance;

public static class Calculator
{
  /// <summary>
  /// Calculates the levenshtein distance between two enumerables
  /// </summary>
  /// <typeparam name="T"></typeparam>
  /// <param name="reference"></param>
  /// <param name="hypothesis"></param>
  /// <returns></returns>
  public static double GetDistance<T>(T reference, T? hypothesis)
    where T : IEnumerable
  {
    // Setup
    var refArr = reference.Cast<object>().ToArray();
    var hypArr = hypothesis?.Cast<object>().ToArray() ?? Array.Empty<object>();

    var distance = new int[refArr.Length + 1, hypArr.Length + 1];

    // Fill matrix
    for (var x = 0; x <= refArr.Length; x++)
    {
      // Reference on X axis
      distance[x, 0] = x;
    }

    for (var y = 0; y <= hypArr.Length; y++)
    {
      // Hypothesis on Y axis
      distance[0, y] = y;
    }

    // Calculate distance
    for (var x = 0; x < refArr.Length; x++)
    {
      for (var y = 0; y < hypArr.Length; y++)
      {
        // BL Cost depends on whether the two elements are equal
        var cost = Equals(refArr[x], hypArr[y]) ? 0 : 1;

        // Apply distance mask
        var c1 = distance[x, y] + cost; // Bottom left

        var c2 = distance[x, y + 1] + 1; // Top left
        var c3 = distance[x + 1, y] + 1; // Bottom right

        distance[x + 1, y + 1] = Min(c1, c2, c3); // Top right
      }
    }

    return distance[refArr.Length, hypArr.Length];
  }

  private static T Min<T>(params T[] values)
  {
    if (!values.Any())
    {
      throw new ArgumentException("Array cannot be empty", nameof(values));
    }

    return values.Min()!;
  }
}