using ImageMagick;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Ocr.Tesseract.Extensions;

/// <summary>
/// Extensions for the <see cref="MagickImage"/> type
/// </summary>
public static class MagickImageExtensions
{
  public static double GetIntensity(this MagickImage image)
  {
    using var pixels = image.GetPixels();
    double totalIntensity = pixels.Sum(pixel =>
      (pixel.GetChannel(0) + pixel.GetChannel(1) + pixel.GetChannel(2)) / 3.0);
    return totalIntensity / pixels.Count();
  }

  public static MagickImage ThresholdAdaptive(
    this MagickImage image,
    int width, int height, Channels channel = Channels.Gray
  )
  {
    image.AdaptiveThreshold(width, height, channel);
    return image;
  }

  public static MagickImage NegateColors(
    this MagickImage image
  )
  {
    image.Negate();
    return image;
  }

  public static MagickImage NormalizeImage(this MagickImage image)
  {
    image.Normalize();
    return image;
  }

  public static MagickImage ToBinary(this MagickImage image)
  {
    image.Depth = 1;
    image.SetBitDepth(1);
    image.ColorType = ColorType.Bilevel;
    return image;
  }

  public static MagickImage AddBorder(
    this MagickImage image,
    int size, IMagickColor<ushort> color
  )
  {
    image.BorderColor = color;
    image.Border(size);
    return image;
  }

  public static MagickImage RemoveAlpha(this MagickImage image, IMagickColor<ushort> color)
  {
    image.BackgroundColor = color;
    image.Alpha(AlphaOption.Remove);
    image.Alpha(AlphaOption.Off);
    return image;
  }

  public static MagickImage Resample(this MagickImage image, double density, DensityUnit unit)
  {
    image.Density = new Density(density, unit);
    image.Resample(new PointD(density));
    return image;
  }

  private static int[] hLineKernel =
  {
    2, 2, 2,
    -1, -1, -1,
    2, 2, 2,
    // -1, -1, -1
  };

  private static int[] vLineKernel =
  {
    -1, 2, -1,
    -1, 2, -1,
    -1, 2, -1
  };

  public static MagickImage GetHLines(this MagickImage image, out IEnumerable<(int, int)> coords)
  {
    var pixels = image.GetPixels();
    coords = Filter(image, hLineKernel, pixels);
    return image;
  }

  public static MagickImage GetVLines(this MagickImage image, out IEnumerable<(int, int)> coords)
  {
    var pixels = image.GetPixels();
    coords = Filter(image, vLineKernel, pixels);
    return image;
  }

  /// <summary>
  /// Applies the provided <paramref name="kernel"/> and returns a list of coordinates where the filter matches
  /// </summary>
  /// <param name="kernel"></param>
  /// <returns></returns>
  /// <exception cref="ArgumentException"></exception>
  private static IEnumerable<(int, int)> Filter(
    IMagickImage image, int[] kernel, IPixelCollection<ushort> pixels
  )
  {
    const int kernelSize = 3;

    if (kernel.Length != (kernelSize * kernelSize))
    {
      throw new ArgumentException($"Filter kernels must be a {kernelSize}x{kernelSize} matrix",
        nameof(kernel));
    }

    (int, int)? current = null;
    for (int x = 0; x < image.Width; x += kernelSize)
    {
      for (int y = 0; y < image.Height; y += kernelSize)
      {
        if (IsMatch(x, y))
        {
          current ??= (x, y);
        }
        else if (current.HasValue)
        {
          yield return current.Value;
          current = null;
        }
      }
    }

    bool IsMatch(int x, int y)
    {
      for (int kernelX = 0; kernelX < 3; kernelX++)
      {
        for (int kernelY = 0; kernelY < 3; kernelY++)
        {
          var k = kernel[kernelX + (kernelSize * kernelY)];
          var v = pixels![x + kernelX, y + kernelY]!.GetChannel((int)PixelChannel.Gray);

          switch (k)
          {
            case > 0 when v <= 0:
            case < 0 when v > 0:
              return false;
          }
        }
      }

      return true;
    }
  }

  public static MagickImage ResizeImage(
    this MagickImage image,
    float factor,
    FilterType filterType,
    PixelInterpolateMethod interpolateMethod
  )
  {
    return image.ResizeImage(
      (int)Math.Round(image.Width * factor),
      (int)Math.Round(image.Height * factor),
      filterType,
      interpolateMethod
    );
  }

  public static MagickImage ResizeImage(
    this MagickImage image,
    int width,
    int height,
    FilterType filterType,
    PixelInterpolateMethod interpolateMethod
  )
  {
    image.FilterType = filterType;
    image.Interpolate = interpolateMethod;
    image.Resize(new MagickGeometry(width, height));
    return image;
  }

  public static MagickImage ToRgb(
    this MagickImage image
  )
  {
    image.ColorSpace = ColorSpace.sRGB;
    image.ColorType = ColorType.TrueColorAlpha;
    return image;
  }

  public static MagickImage ToGrayscale(
    this MagickImage image
  )
  {
    image.Grayscale();
    return image;
  }

  public static MagickImage Negate(this MagickImage image)
  {
    image.Negate();
    return image;
  }

  public static MagickImage GrayscaleBonW(this MagickImage image)
  {
    var intensity = image.GetIntensity();
    var maxDepth = Math.Pow(2, image.DetermineBitDepth());

    image.Grayscale();
    if (intensity < maxDepth / 2f)
    {
      image.NegateGrayscale();
    }

    return image;
  }


  public static MagickImage Fill(
    this MagickImage image,
    IMagickColor<ushort> color,
    IEnumerable<IMagickGeometry> geometries
  )
  {
    var drawables = new Drawables().FillColor(color);

    foreach (var component in geometries)
    {
      drawables.Rectangle(
        component.X,
        component.Y,
        component.X + component.Width,
        component.Height + component.Y
      );
    }

    drawables.Draw(image);
    return image;
  }

  public static MagickImage GetConnectedComponents(
    this MagickImage image,
    IConnectedComponentsSettings settings,
    out IReadOnlyCollection<IConnectedComponent<ushort>> components
  )
  {
    var tImage = image.CloneImage();
    components = tImage.ConnectedComponents(settings);
    return image;
  }

  /// <inheritdoc cref="MagickImage.Clone()" />
  public static MagickImage CloneImage(this MagickImage image)
  {
    return (MagickImage)image.Clone();
  }
}
