/*
 * Copyright (c) 2002-2018, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.utils;

import java.io.BufferedReader;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import static org.jline.terminal.TerminalBuilder.PROP_COLOR_DISTANCE;

public class Colors {

    // @spotless:off

    /**
     * Default 256 colors palette
     */
    public static final int[] DEFAULT_COLORS_256 = {
            // 16 ansi
            0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0,
            0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff,

            // 6x6x6 color cube
            0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff,
            0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff,
            0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff,
            0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff,
            0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff,
            0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff,

            0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff,
            0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff,
            0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff,
            0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff,
            0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff,
            0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff,

            0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff,
            0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff,
            0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff,
            0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff,
            0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff,
            0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff,

            0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff,
            0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff,
            0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff,
            0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff,
            0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff,
            0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff,

            0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff,
            0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff,
            0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff,
            0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff,
            0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff,
            0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff,

            0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff,
            0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff,
            0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff,
            0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff,
            0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7, 0xffd7ff,
            0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff,

            // 24 grey ramp
            0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
            0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e,
            0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee,
    };

    /**
     * Default 88 colors palette
     */
    public static final int[] DEFAULT_COLORS_88 = {
            // 16 ansi
            0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0,
            0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff,

            // 4x4x4 color cube
            0x000000, 0x00008b, 0x0000cd, 0x0000ff,
            0x008b00, 0x008b8b, 0x008bcd, 0x008bff,
            0x00cd00, 0x00cd8b, 0x00cdcd, 0x00cdff,
            0x00ff00, 0x00ff8b, 0x00ffcd, 0x00ffff,

            0x8b0000, 0x8b008b, 0x8b00cd, 0x8b00ff,
            0x8b8b00, 0x8b8b8b, 0x8b8bcd, 0x8b8bff,
            0x8bcd00, 0x8bcd8b, 0x8bcdcd, 0x8bcdff,
            0x8bff00, 0x8bff8b, 0x8bffcd, 0x8bffff,

            0xcd0000, 0xcd008b, 0xcd00cd, 0xcd00ff,
            0xcd8b00, 0xcd8b8b, 0xcd8bcd, 0xcd8bff,
            0xcdcd00, 0xcdcd8b, 0xcdcdcd, 0xcdcdff,
            0xcdff00, 0xcdff8b, 0xcdffcd, 0xcdffff,

            0xff0000, 0xff008b, 0xff00cd, 0xff00ff,
            0xff8b00, 0xff8b8b, 0xff8bcd, 0xff8bff,
            0xffcd00, 0xffcd8b, 0xffcdcd, 0xffcdff,
            0xffff00, 0xffff8b, 0xffffcd, 0xffffff,

            // 8 grey ramp
            0x2e2e2e, 0x5c5c5c, 0x737373, 0x8b8b8b, 0xa2a2a2, 0xb9b9b9, 0xd0d0d0, 0xe7e7e7,
    };

    // @spotless:on

    /**
     * D50 illuminant for CAM color spaces
     */
    public static final double[] D50 = new double[]{96.422f, 100.0f, 82.521f};
    /**
     * D65 illuminant for CAM color spaces
     */
    public static final double[] D65 = new double[]{95.047, 100.0, 108.883};

    /**
     * Average surrounding for CAM color spaces
     */
    public static final double[] averageSurrounding = new double[]{1.0, 0.690, 1.0};
    /**
     * Dim surrounding for CAM color spaces
     */
    public static final double[] dimSurrounding = new double[]{0.9, 0.590, 0.9};
    /**
     * Dark surrounding for CAM color spaces
     */
    public static final double[] darkSurrounding = new double[]{0.8, 0.525, 0.8};
    /**
     * Lightness
     */
    public static final int J = 0;
    /**
     * Brightness
     */
    public static final int Q = 1;
    /**
     * Chroma
     */
    public static final int C = 2;
    /**
     * Colorfulness
     */
    public static final int M = 3;
    /**
     * Saturation
     */
    public static final int s = 4;
    /**
     * Hue Composition / Hue Quadrature
     */
    public static final int H = 5;
    /**
     * Hue
     */
    public static final int h = 6;
    static final int SUR_F = 0;
    static final int SUR_C = 1;
    static final int SUR_N_C = 2;
    static final int VC_X_W = 0;
    static final int VC_Y_W = 1;
    static final int VC_Z_W = 2;
    static final int VC_L_A = 3;
    static final int VC_Y_B = 4;
    static final int VC_F = 5;
    static final int VC_C = 6;
    static final int VC_N_C = 7;
    static final int VC_Z = 8;
    static final int VC_N = 9;
    static final int VC_N_BB = 10;
    static final int VC_N_CB = 11;
    static final int VC_A_W = 12;
    static final int VC_F_L = 13;
    static final int VC_D_RGB_R = 14;
    static final int VC_D_RGB_G = 15;
    static final int VC_D_RGB_B = 16;
    /**
     * sRGB encoding environment
     */
    public static final double[] sRGB_encoding_environment = vc(D50, 64.0, 64.0 / 5, dimSurrounding);
    /**
     * sRGB typical environment
     */
    public static final double[] sRGB_typical_environment = vc(D50, 200.0, 200.0 / 5, averageSurrounding);
    /**
     * Adobe RGB environment
     */
    public static final double[] AdobeRGB_environment = vc(D65, 160.0, 160.0 / 5, averageSurrounding);
    private static final int L = 0;
    private static final int A = 1;
    private static final int B = 2;
    private static final int X = 0;
    private static final int Y = 1;
    private static final int Z = 2;
    private static final double kl = 2.0;
    private static final double kc = 1.0;
    private static final double kh = 1.0;
    private static final double k1 = 0.045;
    private static final double k2 = 0.015;
    private static final double epsilon = 216.0 / 24389.0;
    private static final double kappa = 24389.0 / 27.0;
    private static int[] COLORS_256 = DEFAULT_COLORS_256;
    private static Map<String, Integer> COLOR_NAMES;

    public static void setRgbColors(int[] colors) {
        if (colors == null || colors.length != 256) {
            throw new IllegalArgumentException();
        }
        COLORS_256 = colors;
    }

    public static int rgbColor(int col) {
        return COLORS_256[col];
    }

    public static Integer rgbColor(String name) {
        if (COLOR_NAMES == null) {
            Map<String, Integer> colors = new LinkedHashMap<>();
            try (InputStream is = InfoCmp.class.getResourceAsStream("colors.txt");
                 BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
                br.lines()
                        .map(String::trim)
                        .filter(s -> !s.startsWith("#"))
                        .filter(s -> !s.isEmpty())
                        .forEachOrdered(s -> {
                            colors.put(s, colors.size());
                        });
                COLOR_NAMES = colors;
            } catch (IOException e) {
                throw new IOError(e);
            }
        }
        return COLOR_NAMES.get(name);
    }

    public static int roundColor(int col, int max) {
        return roundColor(col, max, null);
    }

    public static int roundColor(int col, int max, String dist) {
        if (col >= max) {
            int c = COLORS_256[col];
            col = roundColor(c, COLORS_256, max, dist);
        }
        return col;
    }

    public static int roundRgbColor(int r, int g, int b, int max) {
        return roundColor((r << 16) + (g << 8) + b, COLORS_256, max, (String) null);
    }

    static int roundColor(int color, int[] colors, int max, String dist) {
        return roundColor(color, colors, max, getDistance(dist));
    }

    static int roundColor(int color, int[] colors, int max, Distance distance) {
        double best_distance = Integer.MAX_VALUE;
        int best_index = Integer.MAX_VALUE;
        for (int idx = 0; idx < max; idx++) {
            double d = distance.compute(color, colors[idx]);
            if (d <= best_distance) {
                best_index = idx;
                best_distance = d;
            }
        }
        return best_index;
    }

    static Distance getDistance(String dist) {
        if (dist == null) {
            dist = System.getProperty(PROP_COLOR_DISTANCE, "cie76");
        }
        return new NamedDistance(dist, doGetDistance(dist));
    }

    private static Distance doGetDistance(String dist) {
        if (dist.equals("rgb")) {
            return (p1, p2) -> {
                // rgb: see https://www.compuphase.com/cmetric.htm
                double[] c1 = rgb(p1);
                double[] c2 = rgb(p2);
                double rmean = (c1[0] + c2[0]) / 2.0;
                double[] w = {2.0 + rmean, 4.0, 3.0 - rmean};
                return scalar(c1, c2, w);
            };
        }
        if (dist.matches("rgb\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) {
            return (p1, p2) -> scalar(rgb(p1), rgb(p2), getWeights(dist));
        }
        if (dist.equals("lab") || dist.equals("cie76")) {
            return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2));
        }
        if (dist.matches("lab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) {
            double[] w = getWeights(dist);
            return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2), new double[]{w[0], w[1], w[1]});
        }
        if (dist.equals("cie94")) {
            return (p1, p2) -> cie94(rgb2cielab(p1), rgb2cielab(p2));
        }
        if (dist.equals("cie00") || dist.equals("cie2000")) {
            return (p1, p2) -> cie00(rgb2cielab(p1), rgb2cielab(p2));
        }
        if (dist.equals("cam02")) {
            return (p1, p2) -> cam02(p1, p2, sRGB_typical_environment);
        }
        if (dist.equals("camlab")) {
            return (p1, p2) -> {
                double[] c1 = camlab(p1, sRGB_typical_environment);
                double[] c2 = camlab(p2, sRGB_typical_environment);
                return scalar(c1, c2);
            };
        }
        if (dist.matches("camlab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) {
            return (p1, p2) -> {
                double[] c1 = camlab(p1, sRGB_typical_environment);
                double[] c2 = camlab(p2, sRGB_typical_environment);
                double[] w = getWeights(dist);
                return scalar(c1, c2, new double[]{w[0], w[1], w[1]});
            };
        }
        if (dist.matches("camlch")) {
            return (p1, p2) -> {
                double[] c1 = camlch(p1, sRGB_typical_environment);
                double[] c2 = camlch(p2, sRGB_typical_environment);
                return camlch(c1, c2);
            };
        }
        if (dist.matches("camlch\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) {
            return (p1, p2) -> {
                double[] c1 = camlch(p1, sRGB_typical_environment);
                double[] c2 = camlch(p2, sRGB_typical_environment);
                double[] w = getWeights(dist);
                return camlch(c1, c2, w);
            };
        }
        throw new IllegalArgumentException("Unsupported distance function: " + dist);
    }

    private static double[] getWeights(String dist) {
        String[] weights =
                dist.substring(dist.indexOf('(') + 1, dist.length() - 1).split(",");
        return Stream.of(weights).mapToDouble(Double::parseDouble).toArray();
    }

    private static double scalar(double[] c1, double[] c2, double[] w) {
        return sqr((c1[0] - c2[0]) * w[0]) + sqr((c1[1] - c2[1]) * w[1]) + sqr((c1[2] - c2[2]) * w[2]);
    }

    private static double scalar(double[] c1, double[] c2) {
        return sqr(c1[0] - c2[0]) + sqr(c1[1] - c2[1]) + sqr(c1[2] - c2[2]);
    }

    private static double cie94(double[] lab1, double[] lab2) {
        double dl = lab1[L] - lab2[L];
        double da = lab1[A] - lab2[A];
        double db = lab1[B] - lab2[B];
        double c1 = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]);
        double c2 = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]);
        double dc = c1 - c2;
        double dh = da * da + db * db - dc * dc;
        dh = dh < 0.0 ? 0.0 : Math.sqrt(dh);
        double sl = 1.0;
        double sc = 1.0 + k1 * c1;
        double sh = 1.0 + k2 * c1;
        double dLKlsl = dl / (kl * sl);
        double dCkcsc = dc / (kc * sc);
        double dHkhsh = dh / (kh * sh);
        return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh;
    }

    private static double cie00(double[] lab1, double[] lab2) {
        double c_star_1_ab = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]);
        double c_star_2_ab = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]);
        double c_star_average_ab = (c_star_1_ab + c_star_2_ab) / 2.0;
        double c_star_average_ab_pot_3 = c_star_average_ab * c_star_average_ab * c_star_average_ab;
        double c_star_average_ab_pot_7 = c_star_average_ab_pot_3 * c_star_average_ab_pot_3 * c_star_average_ab;
        double G = 0.5 * (1.0 - Math.sqrt(c_star_average_ab_pot_7 / (c_star_average_ab_pot_7 + 6103515625.0))); // 25^7
        double a1_prime = (1.0 + G) * lab1[A];
        double a2_prime = (1.0 + G) * lab2[A];
        double C_prime_1 = Math.sqrt(a1_prime * a1_prime + lab1[B] * lab1[B]);
        double C_prime_2 = Math.sqrt(a2_prime * a2_prime + lab2[B] * lab2[B]);
        double h_prime_1 = (Math.toDegrees(Math.atan2(lab1[B], a1_prime)) + 360.0) % 360.0;
        double h_prime_2 = (Math.toDegrees(Math.atan2(lab2[B], a2_prime)) + 360.0) % 360.0;
        double delta_L_prime = lab2[L] - lab1[L];
        double delta_C_prime = C_prime_2 - C_prime_1;
        double h_bar = Math.abs(h_prime_1 - h_prime_2);
        double delta_h_prime;
        if (C_prime_1 * C_prime_2 == 0.0) {
            delta_h_prime = 0.0;
        } else if (h_bar <= 180.0) {
            delta_h_prime = h_prime_2 - h_prime_1;
        } else if (h_prime_2 <= h_prime_1) {
            delta_h_prime = h_prime_2 - h_prime_1 + 360.0;
        } else {
            delta_h_prime = h_prime_2 - h_prime_1 - 360.0;
        }
        double delta_H_prime = 2.0 * Math.sqrt(C_prime_1 * C_prime_2) * Math.sin(Math.toRadians(delta_h_prime / 2.0));
        double L_prime_average = (lab1[L] + lab2[L]) / 2.0;
        double C_prime_average = (C_prime_1 + C_prime_2) / 2.0;
        double h_prime_average;
        if (C_prime_1 * C_prime_2 == 0.0) {
            h_prime_average = 0.0;
        } else if (h_bar <= 180.0) {
            h_prime_average = (h_prime_1 + h_prime_2) / 2.0;
        } else if ((h_prime_1 + h_prime_2) < 360.0) {
            h_prime_average = (h_prime_1 + h_prime_2 + 360.0) / 2.0;
        } else {
            h_prime_average = (h_prime_1 + h_prime_2 - 360.0) / 2.0;
        }
        double L_prime_average_minus_50 = L_prime_average - 50.0;
        double L_prime_average_minus_50_square = L_prime_average_minus_50 * L_prime_average_minus_50;
        double T = 1.0
                - 0.17 * Math.cos(Math.toRadians(h_prime_average - 30.0))
                + 0.24 * Math.cos(Math.toRadians(h_prime_average * 2.0))
                + 0.32 * Math.cos(Math.toRadians(h_prime_average * 3.0 + 6.0))
                - 0.20 * Math.cos(Math.toRadians(h_prime_average * 4.0 - 63.0));
        double S_L =
                1.0 + ((0.015 * L_prime_average_minus_50_square) / Math.sqrt(20.0 + L_prime_average_minus_50_square));
        double S_C = 1.0 + 0.045 * C_prime_average;
        double S_H = 1.0 + 0.015 * T * C_prime_average;
        double h_prime_average_minus_275_div_25 = (h_prime_average - 275.0) / (25.0);
        double h_prime_average_minus_275_div_25_square =
                h_prime_average_minus_275_div_25 * h_prime_average_minus_275_div_25;
        double delta_theta = 30.0 * Math.exp(-h_prime_average_minus_275_div_25_square);
        double C_prime_average_pot_3 = C_prime_average * C_prime_average * C_prime_average;
        double C_prime_average_pot_7 = C_prime_average_pot_3 * C_prime_average_pot_3 * C_prime_average;
        double R_C = 2.0 * Math.sqrt(C_prime_average_pot_7 / (C_prime_average_pot_7 + 6103515625.0)); // 25^7
        double R_T = -Math.sin(Math.toRadians(2.0 * delta_theta)) * R_C;
        double dLKlsl = delta_L_prime / (kl * S_L);
        double dCkcsc = delta_C_prime / (kc * S_C);
        double dHkhsh = delta_H_prime / (kh * S_H);
        return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh + R_T * dCkcsc * dHkhsh;
    }

    private static double cam02(int p1, int p2, double[] vc) {
        double[] c1 = jmh2ucs(camlch(p1, vc));
        double[] c2 = jmh2ucs(camlch(p2, vc));
        return scalar(c1, c2);
    }

    private static double[] jmh2ucs(double[] lch) {
        double sJ = ((1.0 + 100 * 0.007) * lch[0]) / (1.0 + 0.007 * lch[0]);
        double sM = ((1.0 / 0.0228) * Math.log(1.0 + 0.0228 * lch[1]));
        double a = sM * Math.cos(Math.toRadians(lch[2]));
        double b = sM * Math.sin(Math.toRadians(lch[2]));
        return new double[]{sJ, a, b};
    }

    static double camlch(double[] c1, double[] c2) {
        return camlch(c1, c2, new double[]{1.0, 1.0, 1.0});
    }

    static double camlch(double[] c1, double[] c2, double[] w) {
        // normalize weights to correlate range
        double lightnessWeight = w[0] / 100.0;
        double colorfulnessWeight = w[1] / 120.0;
        double hueWeight = w[2] / 360.0;
        // calculate sort-of polar distance
        double dl = (c1[0] - c2[0]) * lightnessWeight;
        double dc = (c1[1] - c2[1]) * colorfulnessWeight;
        double dh = hueDifference(c1[2], c2[2], 360.0) * hueWeight;
        return dl * dl + dc * dc + dh * dh;
    }

    private static double hueDifference(double hue1, double hue2, double c) {
        double difference = (hue2 - hue1) % c;
        double ch = c / 2;
        if (difference > ch) difference -= c;
        if (difference < -ch) difference += c;
        return difference;
    }

    private static double[] rgb(int color) {
        int r = (color >> 16) & 0xFF;
        int g = (color >> 8) & 0xFF;
        int b = (color >> 0) & 0xFF;
        return new double[]{r / 255.0, g / 255.0, b / 255.0};
    }

    static double[] rgb2xyz(int color) {
        return rgb2xyz(rgb(color));
    }

    static double[] rgb2cielab(int color) {
        return rgb2cielab(rgb(color));
    }

    static double[] camlch(int color) {
        return camlch(color, sRGB_typical_environment);
    }

    static double[] camlch(int color, double[] vc) {
        return xyz2camlch(rgb2xyz(color), vc);
    }

    static double[] camlab(int color) {
        return camlab(color, sRGB_typical_environment);
    }

    static double[] camlab(int color, double[] vc) {
        return lch2lab(camlch(color, vc));
    }

    static double[] lch2lab(double[] lch) {
        double toRad = Math.PI / 180;
        return new double[]{lch[0], lch[1] * Math.cos(lch[2] * toRad), lch[1] * Math.sin(lch[2] * toRad)};
    }

    private static double[] xyz2camlch(double[] xyz, double[] vc) {
        double[] XYZ = new double[]{xyz[0] * 100.0, xyz[1] * 100.0, xyz[2] * 100.0};
        double[] cam = forwardTransform(XYZ, vc);
        return new double[]{cam[J], cam[M], cam[h]};
    }

    /**
     * CIECAM02 appearance correlates
     */
    private static double[] forwardTransform(double[] XYZ, double[] vc) {
        // calculate sharpened cone response
        double[] RGB = forwardPreAdaptationConeResponse(XYZ);
        // calculate corresponding (sharpened) cone response considering various luminance level and surround conditions
        // in D
        double[] RGB_c = forwardPostAdaptationConeResponse(RGB, vc);
        // calculate HPE equal area cone fundamentals
        double[] RGBPrime = CAT02toHPE(RGB_c);
        // calculate response-compressed postadaptation cone response
        double[] RGBPrime_a = forwardResponseCompression(RGBPrime, vc);
        // calculate achromatic response
        double A = (2.0 * RGBPrime_a[0] + RGBPrime_a[1] + RGBPrime_a[2] / 20.0 - 0.305) * vc[VC_N_BB];
        // calculate lightness
        double J = 100.0 * Math.pow(A / vc[VC_A_W], vc[VC_Z] * vc[VC_C]);
        // calculate redness-greenness and yellowness-blueness color opponent values
        double a = RGBPrime_a[0] + (-12.0 * RGBPrime_a[1] + RGBPrime_a[2]) / 11.0;
        double b = (RGBPrime_a[0] + RGBPrime_a[1] - 2.0 * RGBPrime_a[2]) / 9.0;
        // calculate hue angle
        double h = (Math.toDegrees(Math.atan2(b, a)) + 360.0) % 360.0;
        // calculate eccentricity
        double e = ((12500.0 / 13.0) * vc[VC_N_C] * vc[VC_N_CB]) * (Math.cos(Math.toRadians(h) + 2.0) + 3.8);
        // get t
        double t = e
                * Math.sqrt(Math.pow(a, 2.0) + Math.pow(b, 2.0))
                / (RGBPrime_a[0] + RGBPrime_a[1] + 1.05 * RGBPrime_a[2]);
        // calculate brightness
        double Q = (4.0 / vc[VC_C]) * Math.sqrt(J / 100.0) * (vc[VC_A_W] + 4.0) * Math.pow(vc[VC_F_L], 0.25);
        // calculate the correlates of chroma, colorfulness, and saturation
        double C = Math.signum(t)
                * Math.pow(Math.abs(t), 0.9)
                * Math.sqrt(J / 100.0)
                * Math.pow(1.64 - Math.pow(0.29, vc[VC_N]), 0.73);
        double M = C * Math.pow(vc[VC_F_L], 0.25);
        double s = 100.0 * Math.sqrt(M / Q);
        // calculate hue composition
        double H = calculateH(h);
        return new double[]{J, Q, C, M, s, H, h};
    }

    private static double calculateH(double h) {
        if (h < 20.14) h = h + 360;
        double i;
        if (h >= 20.14 && h < 90.0) { // index i = 1
            i = (h - 20.14) / 0.8;
            return 100.0 * i / (i + (90 - h) / 0.7);
        } else if (h < 164.25) { // index i = 2
            i = (h - 90) / 0.7;
            return 100.0 + 100.0 * i / (i + (164.25 - h));
        } else if (h < 237.53) { // index i = 3
            i = (h - 164.25);
            return 200.0 + 100.0 * i / (i + (237.53 - h) / 1.2);
        } else if (h <= 380.14) { // index i = 4
            i = (h - 237.53) / 1.2;
            double H = 300.0 + 100.0 * i / (i + (380.14 - h) / 0.8);
            // don't use 400 if we can use 0
            if (H <= 400.0 && H >= 399.999) H = 0;
            return H;
        } else {
            throw new IllegalArgumentException("h outside assumed range 0..360: " + h);
        }
    }

    private static double[] forwardResponseCompression(double[] RGB, double[] vc) {
        double[] result = new double[3];
        for (int channel = 0; channel < RGB.length; channel++) {
            if (RGB[channel] >= 0) {
                double n = Math.pow(vc[VC_F_L] * RGB[channel] / 100.0, 0.42);
                result[channel] = 400.0 * n / (n + 27.13) + 0.1;
            } else {
                double n = Math.pow(-1.0 * vc[VC_F_L] * RGB[channel] / 100.0, 0.42);
                result[channel] = -400.0 * n / (n + 27.13) + 0.1;
            }
        }
        return result;
    }

    private static double[] forwardPostAdaptationConeResponse(double[] RGB, double[] vc) {
        return new double[]{vc[VC_D_RGB_R] * RGB[0], vc[VC_D_RGB_G] * RGB[1], vc[VC_D_RGB_B] * RGB[2]};
    }

    public static double[] CAT02toHPE(double[] RGB) {
        double[] RGBPrime = new double[3];
        RGBPrime[0] = 0.7409792 * RGB[0] + 0.2180250 * RGB[1] + 0.0410058 * RGB[2];
        RGBPrime[1] = 0.2853532 * RGB[0] + 0.6242014 * RGB[1] + 0.0904454 * RGB[2];
        RGBPrime[2] = -0.0096280 * RGB[0] - 0.0056980 * RGB[1] + 1.0153260 * RGB[2];
        return RGBPrime;
    }

    private static double[] forwardPreAdaptationConeResponse(double[] XYZ) {
        double[] RGB = new double[3];
        RGB[0] = 0.7328 * XYZ[0] + 0.4296 * XYZ[1] - 0.1624 * XYZ[2];
        RGB[1] = -0.7036 * XYZ[0] + 1.6975 * XYZ[1] + 0.0061 * XYZ[2];
        RGB[2] = 0.0030 * XYZ[0] + 0.0136 * XYZ[1] + 0.9834 * XYZ[2];
        return RGB;
    }

    static double[] vc(double[] xyz_w, double L_A, double Y_b, double[] surrounding) {
        double[] vc = new double[17];
        vc[VC_X_W] = xyz_w[0];
        vc[VC_Y_W] = xyz_w[1];
        vc[VC_Z_W] = xyz_w[2];
        vc[VC_L_A] = L_A;
        vc[VC_Y_B] = Y_b;
        vc[VC_F] = surrounding[SUR_F];
        vc[VC_C] = surrounding[SUR_C];
        vc[VC_N_C] = surrounding[SUR_N_C];

        double[] RGB_w = forwardPreAdaptationConeResponse(xyz_w);
        double D =
                Math.max(0.0, Math.min(1.0, vc[VC_F] * (1.0 - (1.0 / 3.6) * Math.pow(Math.E, (-L_A - 42.0) / 92.0))));
        double Yw = xyz_w[1];
        double[] RGB_c = new double[]{
                (D * Yw / RGB_w[0]) + (1.0 - D), (D * Yw / RGB_w[1]) + (1.0 - D), (D * Yw / RGB_w[2]) + (1.0 - D),
        };

        // calculate increase in brightness and colorfulness caused by brighter viewing environments
        double L_Ax5 = 5.0 * L_A;
        double k = 1.0 / (L_Ax5 + 1.0);
        double kpow4 = Math.pow(k, 4.0);
        vc[VC_F_L] = 0.2 * kpow4 * (L_Ax5) + 0.1 * Math.pow(1.0 - kpow4, 2.0) * Math.pow(L_Ax5, 1.0 / 3.0);

        // calculate response compression on J and C caused by background lightness.
        vc[VC_N] = Y_b / Yw;
        vc[VC_Z] = 1.48 + Math.sqrt(vc[VC_N]);

        vc[VC_N_BB] = 0.725 * Math.pow(1.0 / vc[VC_N], 0.2);
        vc[VC_N_CB] = vc[
                VC_N_BB]; // chromatic contrast factors (calculate increase in J, Q, and C caused by dark backgrounds)

        // calculate achromatic response to white
        double[] RGB_wc = new double[]{RGB_c[0] * RGB_w[0], RGB_c[1] * RGB_w[1], RGB_c[2] * RGB_w[2]};
        double[] RGBPrime_w = CAT02toHPE(RGB_wc);
        double[] RGBPrime_aw = new double[3];
        for (int channel = 0; channel < RGBPrime_w.length; channel++) {
            if (RGBPrime_w[channel] >= 0) {
                double n = Math.pow(vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42);
                RGBPrime_aw[channel] = 400.0 * n / (n + 27.13) + 0.1;
            } else {
                double n = Math.pow(-1.0 * vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42);
                RGBPrime_aw[channel] = -400.0 * n / (n + 27.13) + 0.1;
            }
        }
        vc[VC_A_W] = (2.0 * RGBPrime_aw[0] + RGBPrime_aw[1] + RGBPrime_aw[2] / 20.0 - 0.305) * vc[VC_N_BB];
        vc[VC_D_RGB_R] = RGB_c[0];
        vc[VC_D_RGB_G] = RGB_c[1];
        vc[VC_D_RGB_B] = RGB_c[2];
        return vc;
    }

    public static double[] rgb2cielab(double[] rgb) {
        return xyz2lab(rgb2xyz(rgb));
    }

    private static double[] rgb2xyz(double[] rgb) {
        double vr = pivotRgb(rgb[0]);
        double vg = pivotRgb(rgb[1]);
        double vb = pivotRgb(rgb[2]);
        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
        double x = vr * 0.4124564 + vg * 0.3575761 + vb * 0.1804375;
        double y = vr * 0.2126729 + vg * 0.7151522 + vb * 0.0721750;
        double z = vr * 0.0193339 + vg * 0.1191920 + vb * 0.9503041;
        return new double[]{x, y, z};
    }

    private static double pivotRgb(double n) {
        return n > 0.04045 ? Math.pow((n + 0.055) / 1.055, 2.4) : n / 12.92;
    }

    private static double[] xyz2lab(double[] xyz) {
        double fx = pivotXyz(xyz[0]);
        double fy = pivotXyz(xyz[1]);
        double fz = pivotXyz(xyz[2]);
        double l = 116.0 * fy - 16.0;
        double a = 500.0 * (fx - fy);
        double b = 200.0 * (fy - fz);
        return new double[]{l, a, b};
    }

    private static double pivotXyz(double n) {
        return n > epsilon ? Math.cbrt(n) : (kappa * n + 16) / 116;
    }

    private static double sqr(double n) {
        return n * n;
    }

    @FunctionalInterface
    interface Distance {
        double compute(int c1, int c2);
    }

    private static class NamedDistance implements Distance {
        private final String name;
        private final Distance delegate;

        public NamedDistance(String name, Distance delegate) {
            this.name = name;
            this.delegate = delegate;
        }

        @Override
        public double compute(int c1, int c2) {
            return delegate.compute(c1, c2);
        }

        @Override
        public String toString() {
            return name;
        }
    }
}
