/*
 * 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 {
	/**
	 * 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)
	 *
	 * <p>This class caches intermediate values of the CAM16 conversion process that depend only on
	 * viewing conditions, enabling speed ups.
	 */
	public sealed class ViewingConditions : Object {
		/** sRGB-like viewing conditions. */
		private static ViewingConditions _default = null;
		public static ViewingConditions DEFAULT {
			get {
				if (_default == null) {
					_default = ViewingConditions.default_with_background_lstar(50.0);
				}
				return _default;
			}
		}

		public double aw { get; private set; }
		public double nbb { get; private set; }
		public double ncb { get; private set; }
		public double c { get; private set; }
		public double nc { get; private set; }
		public double n { get; private set; }
		public double[] rgb_d;
		public double fl { get; private set; }
		public double fl_root { get; private set; }
		public double z { get; private set; }

		/**
		 * Create ViewingConditions from a simple, physically relevant, set of parameters.
		 *
		 * @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
		 *     afternoon
		 * @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
		 *     the room where the color is viewed. Can be calculated from lux by multiplying lux by
		 *     0.0586. default = 11.72, or 200 lux.
		 * @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
		 *     L*a*b*. default = 50.0
		 * @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
		 *     like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
		 *     night. 2.0 means there is no difference between the lighting on the color and around it.
		 *     default = 2.0
		 * @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
		 *     such as knowing an apple is still red in green light. default = false, the eye does not
		 *     perform this process on self-luminous objects like displays.
		 */
		public static ViewingConditions make(double[] white_point,
		                                     double adapting_luminance,
		                                     double background_lstar,
		                                     double surround,
		                                     bool discounting_illuminant) {
			// A background of pure black is non-physical and leads to infinities that represent the idea
			// that any color viewed in pure black can't be seen.
			background_lstar = Math.fmax(0.1, background_lstar);
			// Transform white point XYZ to 'cone'/'rgb' responses
			double[,] matrix = Cam16.XYZ_TO_CAM16RGB;
			double[] xyz = white_point;
			double r_w = (xyz[0] * matrix[0, 0]) + (xyz[1] * matrix[0, 1]) + (xyz[2] * matrix[0, 2]);
			double g_w = (xyz[0] * matrix[1, 0]) + (xyz[1] * matrix[1, 1]) + (xyz[2] * matrix[1, 2]);
			double b_w = (xyz[0] * matrix[2, 0]) + (xyz[1] * matrix[2, 1]) + (xyz[2] * matrix[2, 2]);
			double f = 0.8 + (surround / 10.0);
			double c = (f >= 0.9) ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0)) : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
			double d = discounting_illuminant ? 1.0 : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adapting_luminance - 42.0) / 92.0)));
			d = MathUtils.clamp_double(0.0, 1.0, d);
			double nc = f;
			double[] rgb_d = new double[] { d* (100.0 / r_w) + 1.0 - d, d* (100.0 / g_w) + 1.0 - d, d* (100.0 / b_w) + 1.0 - d };
			double k = 1.0 / (5.0 * adapting_luminance + 1.0);
			double k4 = k * k * k * k;
			double k4_f = 1.0 - k4;
			double fl = (k4 * adapting_luminance) + (0.1 * k4_f * k4_f * Math.cbrt(5.0 * adapting_luminance));
			double n = (ColorUtils.y_from_lstar(background_lstar) / white_point[1]);
			double z = 1.48 + Math.sqrt(n);
			double nbb = 0.725 / Math.pow(n, 0.2);
			double ncb = nbb;
			double[] rgb_a_factors = new double[] {
				Math.pow(fl * rgb_d[0] * r_w / 100.0, 0.42),
				Math.pow(fl * rgb_d[1] * g_w / 100.0, 0.42),
				Math.pow(fl * rgb_d[2] * b_w / 100.0, 0.42)
			};

			double[] rgb_a = new double[] {
				(400.0 * rgb_a_factors[0]) / (rgb_a_factors[0] + 27.13),
				(400.0 * rgb_a_factors[1]) / (rgb_a_factors[1] + 27.13),
				(400.0 * rgb_a_factors[2]) / (rgb_a_factors[2] + 27.13)
			};

			double aw = ((2.0 * rgb_a[0]) + rgb_a[1] + (0.05 * rgb_a[2])) * nbb;
			return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgb_d, fl, Math.pow(fl, 0.25), z);
		}

		/**
		 * Create sRGB-like viewing conditions with a custom background lstar.
		 *
		 * <p>Default viewing conditions have a lstar of 50, midgray.
		 */
		public static ViewingConditions default_with_background_lstar(double lstar) {
			return ViewingConditions.make(
			                              ColorUtils.white_point_d65(),
			                              (200.0 / Math.PI * ColorUtils.y_from_lstar(50.0) / 100.0),
			                              lstar,
			                              2.0,
			                              false
			);
		}

		/**
		 * Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
		 * for technical color science terminology, this class would not benefit from documenting them
		 * individually. A brief overview is available in the CAM16 specification, and a complete overview
		 * requires a color science textbook, such as Fairchild's Color Appearance Models.
		 */
		private ViewingConditions(double n,
			double aw,
			double nbb,
			double ncb,
			double c,
			double nc,
			double[] rgb_d,
			double fl,
			double fl_root,
			double z) {
			this.n = n;
			this.aw = aw;
			this.nbb = nbb;
			this.ncb = ncb;
			this.c = c;
			this.nc = nc;
			this.rgb_d = rgb_d;
			this.fl = fl;
			this.fl_root = fl_root;
			this.z = z;
		}
	}
}
