/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * ---------------------------------------------------------------------
 *
 * Portions of this file were modified and ported to Vala by Victor Vintorez in 2026.
 * These modifications are licensed under the European Union Public License (EUPL),
 * Version 1.2 or later. You may obtain a copy of the License at:
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 */

using GLib;

namespace MaterialColorUtilities {
	/**
	 * CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
	 * code and viewing conditions.
	 *
	 * <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
	 * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
	 * measuring distances between colors.
	 *
	 * <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
	 * the color. Color appearance models such as CAM16 also use information about the environment where
	 * the color was observed, known as the viewing conditions.
	 *
	 * <p>For example, white under the traditional assumption of a midday sun white point is accurately
	 * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
	 */
	public sealed class Cam16 : Object {
		// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
		public const double[,] XYZ_TO_CAM16RGB = {
			{ 0.401288, 0.650173, -0.051461 },
			{ -0.250268, 1.204414, 0.045854 },
			{ -0.002079, 0.048952, 0.953127 }
		};

		// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
		public const double[,] CAM16RGB_TO_XYZ = {
			{ 1.8620678, -1.0112547, 0.14918678 },
			{ 0.38752654, 0.62144744, -0.00897398 },
			{ -0.01584150, -0.03412294, 1.0499644 }
		};

		/** Hue in CAM16 */
		public double hue { get; private set; }

		/** Chroma in CAM16 */
		public double chroma { get; private set; }

		/** Lightness in CAM16 */
		public double j { get; private set; }

		/**
		 * Brightness in CAM16.
		 *
		 * <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
		 * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
		 * lighting.
		 */
		public double q { get; private set; }

		/**
		 * Colorfulness in CAM16.
		 *
		 * <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
		 * more colorful outside than inside, but it has the same chroma in both environments.
		 */
		public double m { get; private set; }

		/**
		 * Saturation in CAM16.
		 *
		 * <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
		 * relative to the color's own brightness, where chroma is colorfulness relative to white.
		 */
		public double s { get; private set; }

		/** Lightness coordinate in CAM16-UCS */
		public double jstar { get; private set; }

		/** a* coordinate in CAM16-UCS */
		public double astar { get; private set; }

		/** b* coordinate in CAM16-UCS */
		public double bstar { get; private set; }

		/**
		 * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
		 * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
		 * distances between colors.
		 */
		public double distance(Cam16 other) {
			double d_j = jstar - other.jstar;
			double d_a = astar - other.astar;
			double d_b = bstar - other.bstar;
			double d_e_prime = Math.sqrt(d_j * d_j + d_a * d_a + d_b * d_b);
			double d_e = 1.41 * Math.pow(d_e_prime, 0.63);
			return d_e;
		}

		/**
		 * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
		 * combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
		 * method that constructs from 3 of those dimensions. This constructor is intended for those
		 * methods to use to return all possible dimensions.
		 *
		 * @param hue for example, red, orange, yellow, green, etc.
		 * @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
		 *     perceptually accurate.
		 * @param j lightness
		 * @param q brightness; ratio of lightness to white point's lightness
		 * @param m colorfulness
		 * @param s saturation; ratio of chroma to white point's chroma
		 * @param jstar CAM16-UCS J coordinate
		 * @param astar CAM16-UCS a coordinate
		 * @param bstar CAM16-UCS b coordinate
		 */
		private Cam16(double hue,
			double chroma,
			double j,
			double q,
			double m,
			double s,
			double jstar,
			double astar,
			double bstar) {
			this.hue = hue;
			this.chroma = chroma;
			this.j = j;
			this.q = q;
			this.m = m;
			this.s = s;
			this.jstar = jstar;
			this.astar = astar;
			this.bstar = bstar;
		}

		/**
		 * Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions.
		 *
		 * @param argb ARGB representation of a color.
		 */
		public static Cam16 from_int(int argb) {
			return from_int_in_viewing_conditions(argb, ViewingConditions.DEFAULT);
		}

		/**
		 * Create a CAM16 color from a color in defined viewing conditions.
		 *
		 * @param argb ARGB representation of a color.
		 * @param viewingConditions Information about the environment where the color was observed.
		 */
		// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
		// may differ at runtime due to floating point imprecision, keeping the values the same, and
		// accurate, across implementations takes precedence.
		static Cam16 from_int_in_viewing_conditions(int argb, ViewingConditions viewing_conditions) {
			// Transform ARGB int to XYZ
			int red = (argb & 0x00ff0000) >> 16;
			int green = (argb & 0x0000ff00) >> 8;
			int blue = (argb & 0x000000ff);
			double red_l = ColorUtils.linearized(red);
			double green_l = ColorUtils.linearized(green);
			double blue_l = ColorUtils.linearized(blue);
			double x = 0.41233895 * red_l + 0.35762064 * green_l + 0.18051042 * blue_l;
			double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l;
			double z = 0.01932141 * red_l + 0.11916382 * green_l + 0.95034478 * blue_l;

			return from_xyz_in_viewing_conditions(x, y, z, viewing_conditions);
		}

		public static Cam16 from_xyz_in_viewing_conditions(double x, double y, double z, ViewingConditions viewing_conditions) {
			// Transform XYZ to 'cone'/'rgb' responses
			double[,] matrix = XYZ_TO_CAM16RGB;
			double r_t = (x * matrix[0, 0]) + (y * matrix[0, 1]) + (z * matrix[0, 2]);
			double g_t = (x * matrix[1, 0]) + (y * matrix[1, 1]) + (z * matrix[1, 2]);
			double b_t = (x * matrix[2, 0]) + (y * matrix[2, 1]) + (z * matrix[2, 2]);

			// Discount illuminant
			double r_d = viewing_conditions.rgb_d[0] * r_t;
			double g_d = viewing_conditions.rgb_d[1] * g_t;
			double b_d = viewing_conditions.rgb_d[2] * b_t;

			// Chromatic adaptation
			double r_a_f = Math.pow(viewing_conditions.fl * Math.fabs(r_d) / 100.0, 0.42);
			double g_a_f = Math.pow(viewing_conditions.fl * Math.fabs(g_d) / 100.0, 0.42);
			double b_a_f = Math.pow(viewing_conditions.fl * Math.fabs(b_d) / 100.0, 0.42);
			double r_a = MathUtils.signum(r_d) * 400.0 * r_a_f / (r_a_f + 27.13);
			double g_a = MathUtils.signum(g_d) * 400.0 * g_a_f / (g_a_f + 27.13);
			double b_a = MathUtils.signum(b_d) * 400.0 * b_a_f / (b_a_f + 27.13);

			// redness-greenness
			double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
			// yellowness-blueness
			double b = (r_a + g_a - 2.0 * b_a) / 9.0;

			// auxiliary components
			double u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0;
			double p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0;

			// hue
			double atan_2 = Math.atan2(b, a);
			double atan_degrees = MathUtils.to_degrees(atan_2);
			double hue = MathUtils.sanitize_degrees_double(atan_degrees);
			double hue_radians = MathUtils.to_radians(hue);

			// achromatic response to color
			double ac = p2 * viewing_conditions.nbb;

			// CAM16 lightness and brightness
			double j = 100.0 * Math.pow(ac / viewing_conditions.aw, viewing_conditions.c * viewing_conditions.z);
			double q = 4.0 / viewing_conditions.c * Math.sqrt(j / 100.0) * (viewing_conditions.aw + 4.0) * viewing_conditions.fl_root;

			// CAM16 chroma, colorfulness, and saturation.
			double hue_prime = (hue < 20.14) ? hue + 360 : hue;
			double e_hue = 0.25 * (Math.cos(MathUtils.to_radians(hue_prime) + 2.0) + 3.8);
			double p1 = 50000.0 / 13.0 * e_hue * viewing_conditions.nc * viewing_conditions.ncb;
			double t = p1 * Math.hypot(a, b) / (u + 0.305);
			double alpha = Math.pow(1.64 - Math.pow(0.29, viewing_conditions.n), 0.73) * Math.pow(t, 0.9);

			// CAM16 chroma, colorfulness, saturation
			double c = alpha * Math.sqrt(j / 100.0);
			double m = c * viewing_conditions.fl_root;
			double s = 50.0 * Math.sqrt((alpha * viewing_conditions.c) / (viewing_conditions.aw + 4.0));

			// CAM16-UCS components
			double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
			double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
			double astar = mstar * Math.cos(hue_radians);
			double bstar = mstar * Math.sin(hue_radians);

			return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
		}

		/**
		 * @param j CAM16 lightness
		 * @param c CAM16 chroma
		 * @param h CAM16 hue
		 */
		static Cam16 from_jch(double j, double c, double h) {
			return from_jch_in_viewing_conditions(j, c, h, ViewingConditions.DEFAULT);
		}

		/**
		 * @param j CAM16 lightness
		 * @param c CAM16 chroma
		 * @param h CAM16 hue
		 * @param viewingConditions Information about the environment where the color was observed.
		 */
		private static Cam16 from_jch_in_viewing_conditions(double j, double c, double h, ViewingConditions viewing_conditions) {
			double q = 4.0 / viewing_conditions.c * Math.sqrt(j / 100.0) * (viewing_conditions.aw + 4.0) * viewing_conditions.fl_root;
			double m = c * viewing_conditions.fl_root;
			double alpha = c / Math.sqrt(j / 100.0);
			double s = 50.0 * Math.sqrt((alpha * viewing_conditions.c) / (viewing_conditions.aw + 4.0));

			double hue_radians = MathUtils.to_radians(h);
			double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
			double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
			double astar = mstar * Math.cos(hue_radians);
			double bstar = mstar * Math.sin(hue_radians);
			return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
		}

		/**
		 * Create a CAM16 color from CAM16-UCS coordinates.
		 *
		 * @param jstar CAM16-UCS lightness.
		 * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
		 *     axis.
		 * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
		 *     axis.
		 */
		public static Cam16 from_ucs(double jstar, double astar, double bstar) {
			return from_ucs_in_viewing_conditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
		}

		/**
		 * Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
		 *
		 * @param jstar CAM16-UCS lightness.
		 * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
		 *     axis.
		 * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
		 *     axis.
		 * @param viewingConditions Information about the environment where the color was observed.
		 */
		public static Cam16 from_ucs_in_viewing_conditions(double jstar, double astar, double bstar, ViewingConditions viewing_conditions) {
			double m = Math.hypot(astar, bstar);
			double m2 = Math.expm1(m * 0.0228) / 0.0228;
			double c = m2 / viewing_conditions.fl_root;
			double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
			if (h < 0.0) {
				h += 360.0;
			}
			double j = jstar / (1.0 - (jstar - 100.0) * 0.007);
			return from_jch_in_viewing_conditions(j, c, h, viewing_conditions);
		}

		/**
		 * ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
		 * which are near-identical to the default viewing conditions for sRGB.
		 */
		public int to_int() {
			return viewed(ViewingConditions.DEFAULT);
		}

		/**
		 * ARGB representation of the color, in defined viewing conditions.
		 *
		 * @param viewingConditions Information about the environment where the color will be viewed.
		 * @return ARGB representation of color
		 */
		public int viewed(ViewingConditions viewing_conditions) {
			double[] xyz = xyz_in_viewing_conditions(viewing_conditions);
			return ColorUtils.argb_from_xyz(xyz[0], xyz[1], xyz[2]);
		}

		public double[] xyz_in_viewing_conditions(ViewingConditions viewing_conditions, double[]? return_array = null) {
			double alpha = (chroma == 0.0 || j == 0.0) ? 0.0 : chroma / Math.sqrt(j / 100.0);

			double t = Math.pow(alpha / Math.pow(1.64 - Math.pow(0.29, viewing_conditions.n), 0.73), 1.0 / 0.9);
			double h_rad = MathUtils.to_radians(hue);

			double e_hue = 0.25 * (Math.cos(h_rad + 2.0) + 3.8);
			double ac = viewing_conditions.aw * Math.pow(j / 100.0, 1.0 / viewing_conditions.c / viewing_conditions.z);
			double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.nc * viewing_conditions.ncb;
			double p2 = (ac / viewing_conditions.nbb);

			double h_sin = Math.sin(h_rad);
			double h_cos = Math.cos(h_rad);

			double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * h_cos + 108.0 * t * h_sin);
			double a = gamma * h_cos;
			double b = gamma * h_sin;
			double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
			double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
			double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;

			double r_c_base = Math.fmax(0, (27.13 * Math.fabs(r_a)) / (400.0 - Math.fabs(r_a)));
			double r_c = MathUtils.signum(r_a) * (100.0 / viewing_conditions.fl) * Math.pow(r_c_base, 1.0 / 0.42);
			double g_c_base = Math.fmax(0, (27.13 * Math.fabs(g_a)) / (400.0 - Math.fabs(g_a)));
			double g_c = MathUtils.signum(g_a) * (100.0 / viewing_conditions.fl) * Math.pow(g_c_base, 1.0 / 0.42);
			double b_c_base = Math.fmax(0, (27.13 * Math.fabs(b_a)) / (400.0 - Math.fabs(b_a)));
			double b_c = MathUtils.signum(b_a) * (100.0 / viewing_conditions.fl) * Math.pow(b_c_base, 1.0 / 0.42);
			double r_f = r_c / viewing_conditions.rgb_d[0];
			double g_f = g_c / viewing_conditions.rgb_d[1];
			double b_f = b_c / viewing_conditions.rgb_d[2];

			double[,] matrix = CAM16RGB_TO_XYZ;
			double x = (r_f * matrix[0, 0]) + (g_f * matrix[0, 1]) + (b_f * matrix[0, 2]);
			double y = (r_f * matrix[1, 0]) + (g_f * matrix[1, 1]) + (b_f * matrix[1, 2]);
			double z = (r_f * matrix[2, 0]) + (g_f * matrix[2, 1]) + (b_f * matrix[2, 2]);

			if (return_array != null) {
				return_array[0] = x;
				return_array[1] = y;
				return_array[2] = z;
				return return_array;
			} else {
				return new double[] { x, y, z };
			}
		}
	}
}
