import * as MRD from './MRD';
import {
  functionF,
  lchabToLab,
  labToLchab,
  labToXyz,
  xyzToLinearRgb,
  linearRgbToRgb,
  rgbToRgb255,
  rgbToHex,
  ILLUMINANT_C,
  ILLUMINANT_D65,
  SRGB,
} from './colorspace';
import {
  mod,
  clamp,
  polarToCartesian,
  circularLerp,
  multMatrixVector,
  Vector3,
} from './arithmetic';

/**
 * Converts Munsell value to Y (of XYZ) based on the formula in the ASTM
 * D1535-18e1.
 * @param v - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @returns {number} Y
 */
export const munsellValueToY = (v: number): number => {
  return v * (1.1914 + v * (-0.22533 + v * (0.23352 + v * (-0.020484 + v * 0.00081939)))) * 0.01;
};

/**
 * Converts Munsell value to L* (of CIELAB).
 * @param v - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @returns {number} L*
 */
export const munsellValueToL = (v: number): number => {
  return 116 * functionF(munsellValueToY(v)) - 16;
};

// These converters process a dark color (value < 1) separately because the
// values of the Munsell Renotation Data (all.dat) are not evenly distributed:
// [0, 0.2, 0.4, 0.6, 0.8, 1, 2, 3, ..., 10].

// In the following functions, the actual value equals scaledValue/5 if dark is
// true; the actual chroma equals to halfChroma*2.

const mhvcToLchabAllIntegerCase = (
  hue40: number,
  scaledValue: number,
  halfChroma: number,
  dark = false,
): Vector3 => {
  // This function deals with the case where H, V, and C are all integers.
  // If chroma is larger than 50, C * ab is linearly extrapolated.

  // This function does no range checks: hue40 must be in {0, 1, ..., 39};
  // scaledValue must be in {0, 1, ..., 10} if dark is false, and {0, 1, ..., 6}
  // if dark is true; halfChroma must be a non-negative integer.
  if (dark) {
    // Value is in {0, 0.2, 0.4, 0.6, 0.8, 1}.
    if (halfChroma <= 25) {
      return [
        MRD.mrdLTableDark[scaledValue],
        MRD.mrdCHTableDark[hue40][scaledValue][halfChroma][0],
        MRD.mrdCHTableDark[hue40][scaledValue][halfChroma][1],
      ];
    } else {
      // Linearly extrapolates a color outside the MRD.
      const cstarab = MRD.mrdCHTableDark[hue40][scaledValue][25][0];
      const factor = halfChroma / 25;
      return [
        MRD.mrdLTableDark[scaledValue],
        cstarab * factor,
        MRD.mrdCHTableDark[hue40][scaledValue][25][1],
      ];
    }
  } else {
    if (halfChroma <= 25) {
      return [
        MRD.mrdLTable[scaledValue],
        MRD.mrdCHTable[hue40][scaledValue][halfChroma][0],
        MRD.mrdCHTable[hue40][scaledValue][halfChroma][1],
      ];
    } else {
      const cstarab = MRD.mrdCHTable[hue40][scaledValue][25][0];
      const factor = halfChroma / 25;
      return [
        MRD.mrdLTable[scaledValue],
        cstarab * factor,
        MRD.mrdCHTable[hue40][scaledValue][25][1],
      ];
    }
  }
};

// Deals with the case where V and C are integer.
const mhvcToLchabValueChromaIntegerCase = (
  hue40: number,
  scaledValue: number,
  halfChroma: number,
  dark = false,
): Vector3 => {
  const hue1 = Math.floor(hue40);
  const hue2 = mod(Math.ceil(hue40), 40);
  const [lstar, cstarab1, hab1] = mhvcToLchabAllIntegerCase(hue1, scaledValue, halfChroma, dark);
  if (hue1 === hue2) {
    return [lstar, cstarab1, hab1];
  } else {
    const [, cstarab2, hab2] = mhvcToLchabAllIntegerCase(hue2, scaledValue, halfChroma, dark);
    if (hab1 === hab2 || mod(hab2 - hab1, 360) >= 180) {
      // FIXME: was workaround for the rare
      // case hab1 exceeds hab2, which will be removed after some test.
      return [lstar, cstarab1, hab1];
    } else {
      const hab = circularLerp(hue40 - hue1, hab1, hab2, 360);
      const cstarab =
        (cstarab1 * mod(hab2 - hab, 360)) / mod(hab2 - hab1, 360) +
        (cstarab2 * mod(hab - hab1, 360)) / mod(hab2 - hab1, 360);
      return [lstar, cstarab, hab];
    }
  }
};

// Deals with the case where V is integer.
const mhvcToLchabValueIntegerCase = (
  hue40: number,
  scaledValue: number,
  halfChroma: number,
  dark = false,
): Vector3 => {
  const halfChroma1 = Math.floor(halfChroma);
  const halfChroma2 = Math.ceil(halfChroma);
  if (halfChroma1 === halfChroma2) {
    return mhvcToLchabValueChromaIntegerCase(hue40, scaledValue, halfChroma, dark);
  } else {
    const [lstar, cstarab1, hab1] = mhvcToLchabValueChromaIntegerCase(
      hue40,
      scaledValue,
      halfChroma1,
      dark,
    );
    const [, cstarab2, hab2] = mhvcToLchabValueChromaIntegerCase(
      hue40,
      scaledValue,
      halfChroma2,
      dark,
    );
    const [astar1, bstar1] = polarToCartesian(cstarab1, hab1, 360);
    const [astar2, bstar2] = polarToCartesian(cstarab2, hab2, 360);
    const astar = astar1 * (halfChroma2 - halfChroma) + astar2 * (halfChroma - halfChroma1);
    const bstar = bstar1 * (halfChroma2 - halfChroma) + bstar2 * (halfChroma - halfChroma1);
    return labToLchab(lstar, astar, bstar);
  }
};

const mhvcToLchabGeneralCase = (
  hue40: number,
  scaledValue: number,
  halfChroma: number,
  dark = false,
): Vector3 => {
  const actualValue = dark ? scaledValue * 0.2 : scaledValue;
  const scaledValue1 = Math.floor(scaledValue);
  const scaledValue2 = Math.ceil(scaledValue);
  const lstar = munsellValueToL(actualValue);
  if (scaledValue1 === scaledValue2) {
    return mhvcToLchabValueIntegerCase(hue40, scaledValue1, halfChroma, dark);
  } else if (scaledValue1 === 0) {
    // If the given color is so dark (V < 0.2) that it is out of MRD, we use the
    // fact that the chroma and hue of LCHab corresponds roughly to that of
    // Munsell.
    const [, cstarab, hab] = mhvcToLchabValueIntegerCase(hue40, 1, halfChroma, dark);
    return [lstar, cstarab, hab];
  } else {
    const [lstar1, cstarab1, hab1] = mhvcToLchabValueIntegerCase(
      hue40,
      scaledValue1,
      halfChroma,
      dark,
    );
    const [lstar2, cstarab2, hab2] = mhvcToLchabValueIntegerCase(
      hue40,
      scaledValue2,
      halfChroma,
      dark,
    );
    const [astar1, bstar1] = polarToCartesian(cstarab1, hab1, 360);
    const [astar2, bstar2] = polarToCartesian(cstarab2, hab2, 360);
    const astar =
      (astar1 * (lstar2 - lstar)) / (lstar2 - lstar1) +
      (astar2 * (lstar - lstar1)) / (lstar2 - lstar1);
    const bstar =
      (bstar1 * (lstar2 - lstar)) / (lstar2 - lstar1) +
      (bstar2 * (lstar - lstar1)) / (lstar2 - lstar1);
    return labToLchab(lstar, astar, bstar);
  }
};

/**
 * Converts Munsell HVC to LCHab. Note that the returned value is under
 * **Illuminant C**. I don't recommend you use this function
 * if you are not sure what that means.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @returns {Array} [L*, C*ab, hab]
 */
export const mhvcToLchab = (hue100: number, value: number, chroma: number): Vector3 => {
  const hue40 = mod(hue100 * 0.4, 40);
  const value10 = clamp(value, 0, 10);
  const halfChroma = Math.max(0, chroma) * 0.5;
  if (value >= 1) {
    return mhvcToLchabGeneralCase(hue40, value10, halfChroma, false);
  } else {
    return mhvcToLchabGeneralCase(hue40, value10 * 5, halfChroma, true);
  }
};

const hueNames = ['R', 'YR', 'Y', 'GY', 'G', 'BG', 'B', 'PB', 'P', 'RP'];

/**
 * Converts Munsell Color string to Munsell HVC.
 * @param munsellStr - is the standard Munsell Color code.
 * @returns {Array} [hue100, value, chroma]
 * @throws {SyntaxError} if the given string is invalid.
 */
export const munsellToMhvc = (munsellStr: string): Vector3 => {
  const nums = munsellStr
    .split(/[^a-z0-9.-]+/)
    .filter(Boolean)
    .map((str) => Number(str));
  const words = munsellStr.match(/[A-Z]+/);
  if (words === null) throw new SyntaxError(`Doesn't contain hue names: ${munsellStr}`);
  const hueName = words[0];
  const hueNumber = hueNames.indexOf(hueName);
  if (hueName === 'N') {
    return [0, nums[0], 0];
  } else if (nums.length !== 3) {
    throw new SyntaxError(`Doesn't contain 3 numbers: ${nums}`);
  } else if (hueNumber === -1) {
    // achromatic
    throw new SyntaxError(`Invalid hue designator: ${hueName}`);
  } else {
    return [hueNumber * 10 + nums[0], nums[1], nums[2]];
  }
};

/**
 * Converts Munsell Color string to LCHab. Note that the returned value is under
 * **Illuminant C**. I don't recommend you use this function
 * if you are not sure what that means.
 * @param munsellStr - is the standard Munsell Color code.
 * @returns {Array} [L*, C*ab, hab]
 */
export const munsellToLchab = (munsellStr: string): Vector3 => {
  return mhvcToLchab(...munsellToMhvc(munsellStr));
};

/**
 * Converts Munsell HVC to CIELAB. Note that the returned value is under
 * **Illuminant C**. I don't recommend you use this function
 * if you are not sure what that means.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @returns {Array} [L*, a*, b*]
 */
export const mhvcToLab = (hue100: number, value: number, chroma: number): Vector3 => {
  return lchabToLab(...mhvcToLchab(hue100, value, chroma));
};

/**
 * Converts Munsell Color string to CIELAB. Note that the returned value is under
 * **Illuminant C**. I don't recommend you use this function
 * if you are not sure what that means.
 * @param munsellStr
 * @returns {Array} [L*, a*, b*]
 */
export const munsellToLab = (munsellStr: string): Vector3 => {
  return mhvcToLab(...munsellToMhvc(munsellStr));
};

/**
 * Converts Munsell HVC to XYZ.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @param [illuminant]
 * @returns {Array} [X, Y, Z]
 */
export const mhvcToXyz = (
  hue100: number,
  value: number,
  chroma: number,
  illuminant = ILLUMINANT_D65,
): Vector3 => {
  // Uses Bradford transformation
  return multMatrixVector(
    illuminant.catMatrixCToThis,
    labToXyz(...mhvcToLab(hue100, value, chroma), ILLUMINANT_C),
  );
};

/**
 * Converts Munsell Color string to XYZ.
 * @param munsellStr
 * @param [illuminant]
 * @returns {Array} [X, Y, Z]
 */
export const munsellToXyz = (munsellStr: string, illuminant = ILLUMINANT_D65): Vector3 => {
  return mhvcToXyz(...munsellToMhvc(munsellStr), illuminant);
};

/**
 * Converts Munsell HVC to linear RGB.
 * @param hue100 - is in the circle group R/100Z. Any real
 * number is accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds
 * the interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero
 * if it is negative.
 * @param [rgbSpace]
 * @returns {Array} [linear R, linear G, linear B]
 */
export const mhvcToLinearRgb = (
  hue100: number,
  value: number,
  chroma: number,
  rgbSpace = SRGB,
): Vector3 => {
  return xyzToLinearRgb(...mhvcToXyz(hue100, value, chroma, rgbSpace.illuminant), rgbSpace);
};

/**
 * Converts Munsell Color string to linear RGB.
 * @param munsellStr
 * @param [rgbSpace]
 * @returns {Array} [linear R, linear G, linear B]
 */
export const munsellToLinearRgb = (munsellStr: string, rgbSpace = SRGB): Vector3 => {
  return mhvcToLinearRgb(...munsellToMhvc(munsellStr), rgbSpace);
};

/**
 * Converts Munsell HVC to gamma-corrected RGB.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @param [rgbSpace]
 * @returns {Array} [R, G, B]
 */
export const mhvcToRgb = (
  hue100: number,
  value: number,
  chroma: number,
  rgbSpace = SRGB,
): Vector3 => {
  return linearRgbToRgb(...mhvcToLinearRgb(hue100, value, chroma, rgbSpace), rgbSpace);
};

/**
 * Converts Munsell Color string to gamma-corrected RGB.
 * @param munsellStr
 * @param [rgbSpace]
 * @returns {Array} [R, G, B]
 */
export const munsellToRgb = (munsellStr: string, rgbSpace = SRGB): Vector3 => {
  return mhvcToRgb(...munsellToMhvc(munsellStr), rgbSpace);
};

/**
 * Converts Munsell HVC to quantized RGB.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @param [clamp] - If true, the returned value will be clamped
 * to the range [0, 255].
 * @param [rgbSpace]
 * @returns {Array} [R255, G255, B255]
 */
export const mhvcToRgb255 = (
  hue100: number,
  value: number,
  chroma: number,
  clamp = true,
  rgbSpace = SRGB,
): Vector3 => {
  return rgbToRgb255(...mhvcToRgb(hue100, value, chroma, rgbSpace), clamp);
};

/**
 * Converts Munsell Color string to quantized RGB.
 * @param munsellStr
 * @param [clamp] - If true, the returned value will be clamped
 * to the range [0, 255].
 * @param [rgbSpace]
 * @returns {Array} [R255, G255, B255]
 */
export const munsellToRgb255 = (munsellStr: string, clamp = true, rgbSpace = SRGB): Vector3 => {
  return mhvcToRgb255(...munsellToMhvc(munsellStr), clamp, rgbSpace);
};

/**
 * Converts Munsell HVC to 24-bit hex color.
 * @param hue100 - is in the circle group R/100Z. Any real number is
 * accepted.
 * @param value - will be in [0, 10]. Clamped if it exceeds the
 * interval.
 * @param chroma - will be in [0, +inf). Assumed to be zero if it is
 * negative.
 * @param [rgbSpace]
 * @returns {string} hex color "#XXXXXX"
 */
export const mhvcToHex = (
  hue100: number,
  value: number,
  chroma: number,
  rgbSpace = SRGB,
): string => {
  return rgbToHex(...mhvcToRgb(hue100, value, chroma, rgbSpace));
};

/**
 * Converts Munsell Color string to 24-bit hex color.
 * @param munsellStr
 * @param [rgbSpace]
 * @returns {string} hex color "#XXXXXX"
 */
export const munsellToHex = (munsellStr: string, rgbSpace = SRGB): string => {
  return mhvcToHex(...munsellToMhvc(munsellStr), rgbSpace);
};

/**
 * Converts Munsell HVC to string. `N`, the code for achromatic colors, is used
 * when the chroma becomes zero w.r.t. the specified number of digits.
 * @param hue100
 * @param value
 * @param chroma
 * @param [digits] - is the number of digits after the decimal
 * point. Must be non-negative integer. Note that the units digit of the hue
 * prefix is assumed to be already after the decimal point.
 * @returns {string} Munsell Color code
 */
export const mhvcToMunsell = (
  hue100: number,
  value: number,
  chroma: number,
  digits = 1,
): string => {
  const canonicalHue100 = mod(hue100, 100);
  const huePrefix = canonicalHue100 % 10;
  const hueNumber = Math.round((canonicalHue100 - huePrefix) / 10);
  // If the hue prefix is 0, we use 10 with the previous hue name instead, which is a
  // common practice in the Munsell system.
  const hueDigits = Math.max(digits - 1, 0);
  const fixedHuePrefix = huePrefix.toFixed(hueDigits);
  const hueStr =
    parseFloat(fixedHuePrefix) === 0
      ? Number(10).toFixed(hueDigits) + hueNames[mod(hueNumber - 1, 10)]
      : huePrefix.toFixed(hueDigits) + hueNames[hueNumber];
  const chromaStr = chroma.toFixed(digits);
  const valueStr = value.toFixed(digits);
  if (parseFloat(chromaStr) === 0) {
    return `N ${valueStr}`;
  } else {
    return `${hueStr} ${valueStr}/${chromaStr}`;
  }
};
