using Ocr.Report.Models;
using ReportGeneration.Abstract.Model;
using ReportGeneration.Interface;

namespace Ocr.Report;

public class ReportGenerator : IDisposable
{
  private IDocumentGenerator Document { get; }

  private ICollection<ImageStats> Images { get; }

  public ReportGenerator(
    IDocumentGenerator document,
    IEnumerable<ImageStats> data
  )
  {
    Images = data.ToArray();

    Document = document;
    document.Open();
  }

  public ReportGenerator(
    string title,
    IDocumentGenerator document,
    IEnumerable<ImageStats> data
  ) : this(document, data) => AddTitle(title);

  #region Writing

  public ReportGenerator AddTitle(string text)
  {
    Document.AppendHeading(1, text);
    return this;
  }

  public ReportGenerator AddProcessorStats(string title)
  {
    Document.AppendHeading(2, title);

    var processors = new Dictionary<string, ICollection<ImageStats>>();
    foreach (var image in Images)
    {
      foreach (var processor in image.Processors)
      {
        if (processors.TryGetValue(processor.Name, out var images))
        {
          images.Add(image);
        }
        else
        {
          processors.Add(processor.Name, new List<ImageStats> { image });
        }
      }
    }

    foreach (var (processor, images) in processors)
    {
      var ordered = images
        .Select(i => (Stats: i, Distance: i
            .Processors
            .Where(p => p.Name.Equals(processor))
            .Select(p => p.Distance)
            .Average()
          ))
        .OrderBy(i => i.Distance)
        .ToArray();

      Document
        .AppendHeading(3, processor)
        .AppendTable(2, table =>
          {
            table.AppendHeader(new[] { "Image", "Preview", "Distance" });

            foreach (var (stats, distance) in ordered)
            {
              var imgPath = Path.Combine("results", $"{processor}.00.{stats.ImageName}.png");

              table.AppendRow(new[]
              {
                $"<a href=\"#{stats.ImageName}\">{stats.ImageName}</a>",
                Document.FormatImage(imgPath, new Bounds(0, 150)),
                distance.ToString("F2")
              });
            }
          }
        );
    }

    return this;
  }

  public ReportGenerator AddImageStatsFull(string title)
  {
    Document.AppendHeading(2, title);

    foreach (var stat in Images)
    {
      Document
        .AppendHeading(3, stat.ImageName)
        .AppendParagraph(
          Document.FormatImage(Path.Combine("img", stat.ImageName), new Bounds(0, 350))
        )
        .AppendTable(
          stat.Reference.Count + 5,
          table =>
          {
            table.AppendHeader(stat
              .Reference
              .Prepend("Image")
              .Prepend("Perfect matches")
              .Prepend("CER (avg)")
              .Prepend("WER")
              .Prepend("Elapsed")
              .Prepend("Processor")
            );

            var processors = stat.Processors
              .OrderBy(s => s.Distance)
              .ThenBy(s => s.ProcessingTime);

            foreach (var processor in processors)
            {
              var imgPath = Path.Combine("results", $"{processor.Name}.00.{stat.ImageName}.png");

              table.AppendRow(processor.Words
                .Select(s => s.ToString() ?? string.Empty)
                .Prepend(Document.FormatImage(imgPath, new Bounds(0, 150)))
                .Prepend($"{processor.Words.Count(w => w.Distance == 0)} / {processor.Words.Count}")
                .Prepend(processor.Words.Average(s => s.Distance).ToString("F2"))
                .Prepend($"{processor.Distance * 100:F1}%")
                .Prepend($"{processor.ProcessingTime * 1000:F1}ms")
                .Prepend(processor.Name)
              );
            }
          })
        .AppendParagraph(
          $"Comparison data generated based on {stat.Reference.Count} tagged words."
        );
    }

    return this;
  }

  public ReportGenerator AddComparison(
    string title,
    Func<IEnumerable<double>, (double, double)> evaluationFunc
  )
  {
    var lookup = Images
      .SelectMany(s => s.Processors)
      .ToLookup(p => p.Name);

    Document.AppendHeading(2, title);

    var byWer = lookup
      .Select(g =>
      {
        var (value, deviation) = evaluationFunc(g.Select(p => p.Distance * 100));
        return (
          Name: g.Key,
          Value: value,
          Deviation: deviation
        );
      })
      .OrderBy(g => g.Value);
    Document.AppendHeading(3, "WER");
    AppendComparison(("Error", "%"), byWer);

    var byCer = lookup
      .Select(g =>
      {
        var (value, deviation) =
          evaluationFunc(g.SelectMany(p => p.Words, (_, word) => word.Distance));
        return (
          Name: g.Key,
          Value: value,
          Deviation: deviation
        );
      })
      .OrderBy(g => g.Value);
    Document.AppendHeading(3, "CER");
    AppendComparison(("Changes", string.Empty), byCer);

    var byTime = lookup
      .Select(g =>
      {
        var (value, deviation) = evaluationFunc(g.Select(p => p.ProcessingTime * 1000));
        return (
          Name: g.Key,
          Value: value,
          Deviation: deviation
        );
      })
      .OrderBy(g => g.Value);
    Document.AppendHeading(3, "Time");
    AppendComparison(("Time", "ms"), byTime);

    return this;
  }

  private void AppendComparison(
    (string name, string unit)? valueInfo,
    IEnumerable<(string, double, double)> values
  )
  {
    const int context = 5;

    var tValues = values.ToArray();
    var tContext = Math.Min(tValues.Length / 2, context);

    Document.AppendTable(3, table =>
    {
      table
        .AppendHeader(new[]
        {
          "Processor",
          valueInfo?.name ?? "Value",
          "Deviation"
        })
        .AppendRows(tValues
          .Take(tContext)
          .Select(MakeRow))
        .AppendRow("...")
        .AppendRows(
          tValues
            .TakeLast(tContext)
            .Select(MakeRow)
        );

      return;

      string[] MakeRow((string, double, double) v) =>
        new[]
        {
          v.Item1,
          v.Item2.ToString("F2") + valueInfo?.unit,
          v.Item3.ToString("F2") + valueInfo?.unit
        };
    });
  }

  #endregion

  #region IDisposable

  protected virtual void Dispose(bool disposing)
  {
    if (disposing)
    {
      Document.Dispose();
    }
  }

  /// <inheritdoc />
  public void Dispose()
  {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  #endregion
}
