import {
  polarToCartesian,
  cartesianToPolar,
  multMatrixVector,
  clamp as _clamp,
  Vector3,
  Matrix33,
} from './arithmetic';

const CONST1 = 216 / 24389;
const CONST2 = 24389 / 27 / 116;
const CONST3 = 16 / 116;

export const functionF = (x: number): number => {
  // Called in XYZ -> Lab conversion
  if (x > CONST1) {
    return Math.pow(x, 0.3333333333333333);
  } else {
    return CONST2 * x + CONST3;
  }
};

export const labToLchab = (lstar: number, astar: number, bstar: number): Vector3 => {
  return [lstar, ...cartesianToPolar(astar, bstar, 360)];
};

export const lchabToLab = (lstar: number, Cstarab: number, hab: number): Vector3 => {
  return [lstar, ...polarToCartesian(Cstarab, hab, 360)];
};

export class Illuminant {
  X: number;
  Z: number;
  catMatrixCToThis: Matrix33;
  catMatrixThisToC: Matrix33;
  constructor(X: number, Z: number, catMatrixCToThis: Matrix33, catMatrixThisToC: Matrix33) {
    this.X = X;
    this.Z = Z;
    this.catMatrixCToThis = catMatrixCToThis;
    this.catMatrixThisToC = catMatrixThisToC;
  }
}

// The following data are based on dufy. I use the Bradford transformation as CAT.
export const ILLUMINANT_D65 = new Illuminant(
  0.950428061568676,
  1.08891545904089,
  [
    [0.9904112147597705, -0.00718628493839008, -0.011587161829988951],
    [-0.012395677058354078, 1.01560663662526, -0.0029181533414322086],
    [-0.003558889496942143, 0.006762494889396557, 0.9182865019746504],
  ],
  [
    [1.0098158523233767, 0.007060316533713093, 0.012764537821734395],
    [0.012335983421444891, 0.9846986027789835, 0.003284857773421468],
    [0.003822773174044815, -0.007224207660971385, 1.0890100329203007],
  ],
);
export const ILLUMINANT_C = new Illuminant(
  0.9807171421603395,
  1.182248923134197,
  [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1],
  ],
  [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1],
  ],
);

const DELTA = 6 / 29;
const CONST4 = 3 * DELTA * DELTA;

export const lToY = (lstar: number): number => {
  const fy = (lstar + 16) / 116;
  return fy > DELTA ? fy * fy * fy : (fy - CONST3) * CONST4;
};

export const labToXyz = (
  lstar: number,
  astar: number,
  bstar: number,
  illuminant = ILLUMINANT_D65,
): Vector3 => {
  const fy = (lstar + 16) / 116;
  const fx = fy + astar * 0.002;
  const fz = fy - bstar * 0.005;
  const Xw = illuminant.X;
  const Zw = illuminant.Z;
  return [
    fx > DELTA ? fx * fx * fx * Xw : (fx - CONST3) * CONST4 * Xw,
    fy > DELTA ? fy * fy * fy : (fy - CONST3) * CONST4,
    fz > DELTA ? fz * fz * fz * Zw : (fz - CONST3) * CONST4 * Zw,
  ];
};

export const xyzToLab = (X: number, Y: number, Z: number, illuminant = ILLUMINANT_D65): Vector3 => {
  const [fX, fY, fZ] = [X / illuminant.X, Y, Z / illuminant.Z].map(functionF);
  return [116 * fY - 16, 500 * (fX - fY), 200 * (fY - fZ)];
};

const createLinearizer = (gamma: number): ((x: number) => number) => {
  // Returns a function for inverse gamma-correction (not used for sRGB).
  const reciprocal = 1 / gamma;
  return (x) => {
    return x >= 0 ? Math.pow(x, reciprocal) : -Math.pow(-x, reciprocal);
  };
};

const createDelinearizer = (gamma: number): ((x: number) => number) => {
  // Returns a function for gamma correction (not used for sRGB).
  return (x) => {
    return x >= 0 ? Math.pow(x, gamma) : -Math.pow(-x, gamma);
  };
};

export class RGBSpace {
  matrixThisToXyz: Matrix33;
  matrixXyzToThis: Matrix33;
  linearizer: (x: number) => number;
  delinearizer: (x: number) => number;
  illuminant: Illuminant;
  constructor(
    matrixThisToXyz: Matrix33,
    matrixXyzToThis: Matrix33,
    linearizer = createLinearizer(2.2),
    delinearizer = createDelinearizer(2.2),
    illuminant = ILLUMINANT_D65,
  ) {
    this.matrixThisToXyz = matrixThisToXyz;
    this.matrixXyzToThis = matrixXyzToThis;
    this.linearizer = linearizer;
    this.delinearizer = delinearizer;
    this.illuminant = illuminant;
  }
}

const CONST5 = 0.0031308 * 12.92;

// The following data are based on dufy.
export const SRGB = new RGBSpace(
  [
    [0.4124319639872968, 0.3575780371782625, 0.1804592355313134],
    [0.21266023143094992, 0.715156074356525, 0.07218369421252536],
    [0.01933274831190452, 0.11919267905942081, 0.9504186404649174],
  ],
  [
    [3.240646461582504, -1.537229731776316, -0.49856099408961585],
    [-0.969260718909152, 1.876000564872059, 0.04155578980259398],
    [0.05563672378977863, -0.2040013205625215, 1.0570977520057931],
  ],
  (x) => {
    // Below is actually the linearizer of bg-sRGB.
    if (x > CONST5) {
      return Math.pow((0.055 + x) / 1.055, 2.4);
    } else if (x < -CONST5) {
      return -Math.pow((0.055 - x) / 1.055, 2.4);
    } else {
      return x / 12.92;
    }
  },
  (x) => {
    // Below is actually the delinearizer of bg-sRGB.
    if (x > 0.0031308) {
      return Math.pow(x, 1 / 2.4) * 1.055 - 0.055;
    } else if (x < -0.0031308) {
      return -Math.pow(-x, 1 / 2.4) * 1.055 + 0.055;
    } else {
      return x * 12.92;
    }
  },
);

export const ADOBE_RGB = new RGBSpace(
  [
    [0.5766645233146432, 0.18556215235063508, 0.18820138590339738],
    [0.29734264483411293, 0.6273768008045281, 0.07528055436135896],
    [0.027031149530373878, 0.07069034375262295, 0.991193965757893],
  ],
  [
    [2.0416039047109305, -0.5650114025085637, -0.3447340526026908],
    [-0.969223190031607, 1.8759279278672774, 0.04155418080089159],
    [0.01344622799042258, -0.11837953662156253, 1.015322039041507],
  ],
  createDelinearizer(563 / 256),
  createLinearizer(563 / 256),
);

export const xyzToLinearRgb = (X: number, Y: number, Z: number, rgbSpace = SRGB): Vector3 => {
  return multMatrixVector(rgbSpace.matrixXyzToThis, [X, Y, Z]);
};

export const linearRgbToXyz = (lr: number, lg: number, lb: number, rgbSpace = SRGB): Vector3 => {
  return multMatrixVector(rgbSpace.matrixThisToXyz, [lr, lg, lb]);
};

export const linearRgbToRgb = (lr: number, lg: number, lb: number, rgbSpace = SRGB): Vector3 => {
  return [lr, lg, lb].map(rgbSpace.delinearizer) as Vector3;
};

export const rgbToLinearRgb = (r: number, g: number, b: number, rgbSpace = SRGB): Vector3 => {
  return [r, g, b].map(rgbSpace.linearizer) as Vector3;
};

export const rgbToRgb255 = (r: number, g: number, b: number, clamp = true): Vector3 => {
  if (clamp) {
    return [r, g, b].map((x) => _clamp(Math.round(x * 255), 0, 255)) as Vector3;
  } else {
    return [r, g, b].map((x) => Math.round(x * 255)) as Vector3;
  }
};

export const rgb255ToRgb = (r255: number, g255: number, b255: number): Vector3 => {
  return [r255 / 255, g255 / 255, b255 / 255];
};

export const rgbToHex = (r: number, g: number, b: number): string => {
  const toHex = (x: number) =>
    Math.round(_clamp(x, 0, 1) * 255)
      .toString(16)
      .padStart(2, '0');
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};

export const hexToRgb = (hex: string): Vector3 => {
  switch (hex.length) {
    case 7: // #XXXXXX
    case 9: // #XXXXXXXX
      return [
        parseInt(hex.slice(1, 3), 16) / 255,
        parseInt(hex.slice(3, 5), 16) / 255,
        parseInt(hex.slice(5, 7), 16) / 255,
      ];
    case 4: // #XXX
    case 5: // #XXXX
      return [parseInt(hex[1], 16) / 15, parseInt(hex[2], 16) / 15, parseInt(hex[3], 16) / 15];
    default:
      throw SyntaxError(`The length of hex color is invalid: ${hex}`);
  }
};
