import { match } from 'ts-pattern';

import type { Image } from '../Image.js';
import type { Mask } from '../Mask.js';
import { imageToOutputMask } from '../utils/getOutputImage.js';

import huang from './thresholds/huang.js';
import intermodes from './thresholds/intermodes.js';
import isodata from './thresholds/isodata.js';
import li from './thresholds/li.js';
import maxEntropy from './thresholds/maxEntropy.js';
import mean from './thresholds/mean.js';
import minError from './thresholds/minError.js';
import minimum from './thresholds/minimum.js';
import moments from './thresholds/moments.js';
import { otsu } from './thresholds/otsu.js';
import percentile from './thresholds/percentile.js';
import renyiEntropy from './thresholds/renyiEntropy.js';
import shanbhag from './thresholds/shanbhag.js';
import { triangle } from './thresholds/triangle.js';
import yen from './thresholds/yen.js';

export const ThresholdAlgorithm = {
  HUANG: 'huang',
  INTERMODES: 'intermodes',
  ISODATA: 'isodata',
  LI: 'li',
  MAX_ENTROPY: 'maxEntropy',
  MEAN: 'mean',
  MIN_ERROR: 'minError',
  MINIMUM: 'minimum',
  MOMENTS: 'moments',
  OTSU: 'otsu',
  PERCENTILE: 'percentile',
  RENYI_ENTROPY: 'renyiEntropy',
  SHANBHAG: 'shanbhag',
  TRIANGLE: 'triangle',
  YEN: 'yen',
} as const;
export type ThresholdAlgorithm =
  (typeof ThresholdAlgorithm)[keyof typeof ThresholdAlgorithm];

interface ThresholdOptionsBase {
  /**
   * Number of slots that histogram can have. Slots must be a power of 2.
   */
  slots?: number;
  /**
   * Image to use as the output.
   */
  out?: Mask;
}

export interface ThresholdOptionsThreshold extends ThresholdOptionsBase {
  /**
   * Threshold value that should be used. Threshold is a value in range [0,1],
   * which will be interpreted as a percentage of image.maxValue.
   */
  threshold: number;
}

export interface ThresholdOptionsAlgorithm extends ThresholdOptionsBase {
  /**
   * Specify a function to computes the threshold value.
   *   @default `'otsu'`
   */
  algorithm?: ThresholdAlgorithm;
}

export type ThresholdOptions =
  | ThresholdOptionsThreshold
  | ThresholdOptionsAlgorithm;

/**
 * Compute threshold value for an image using the specified algorithm.
 * @param image - The grey image.
 * @param options - Threshold options.
 * @returns The threshold value for the image.
 */
export function computeThreshold(
  image: Image,
  options: ThresholdOptionsAlgorithm = {},
): number {
  const { algorithm = 'otsu', slots } = options;
  if (image.channels !== 1) {
    throw new TypeError(
      'threshold can only be computed on images with one channel',
    );
  }
  const histogram = image.histogram({ slots });
  const scale = slots ? 2 ** image.bitDepth / slots : 1;

  return match(algorithm)
    .with('huang', () => huang(histogram) * scale)
    .with('intermodes', () => intermodes(histogram) * scale)
    .with('isodata', () => isodata(histogram) * scale)
    .with('li', () => li(histogram, image.size) * scale)
    .with('maxEntropy', () => maxEntropy(histogram, image.size) * scale)
    .with('mean', () => mean(histogram, image.size) * scale)
    .with('minimum', () => minimum(histogram) * scale)
    .with('minError', () => minError(histogram, image.size) * scale)
    .with('moments', () => moments(histogram, image.size) * scale)
    .with('otsu', () => otsu(histogram, image.size) * scale)
    .with('percentile', () => percentile(histogram) * scale)
    .with('renyiEntropy', () => renyiEntropy(histogram, image.size) * scale)
    .with('shanbhag', () => shanbhag(histogram, image.size) * scale)
    .with('triangle', () => triangle(histogram) * scale)
    .with('yen', () => yen(histogram, image.size) * scale)
    .exhaustive();
}

// See: https://docs.opencv.org/4.0.1/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576
/**
 * Create a black and white image based on a threshold value.
 * @param image - The grey image to convert.
 * @param options - Threshold options.
 * @returns The resulting mask.
 */
export function threshold(image: Image, options: ThresholdOptions = {}): Mask {
  let thresholdValue: number;

  if ('threshold' in options) {
    const threshold = options.threshold;
    if (threshold < 0 || threshold > 1) {
      throw new RangeError('threshold must be a value between 0 and 1');
    }
    thresholdValue = threshold * image.maxValue;
  } else {
    thresholdValue = computeThreshold(image, options);
  }
  const result = imageToOutputMask(image, options);
  for (let i = 0; i < image.size; i++) {
    result.setBitByIndex(
      i,
      image.getValueByIndex(i, 0) > thresholdValue ? 1 : 0,
    );
  }
  return result;
}
