/*
 * 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 {
	/**
	 * Design utilities using color temperature theory.
	 *
	 * <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
	 * calculations when needed.
	 */
	public sealed class TemperatureCache : Object {
		public Hct input { get; private set; }

		private Hct? precomputed_complement;
		private GenericArray<Hct>? precomputed_hcts_by_temp;
		private GenericArray<Hct>? precomputed_hcts_by_hue;
		private HashTable<Hct, double?>? precomputed_temps_by_hct;

		/**
		 * Create a cache that allows calculation of ex. complementary and analogous colors.
		 *
		 * @param input Color to find complement/analogous colors of. Any colors will have the same tone,
		 *     and chroma as the input color, modulo any restrictions due to the other hues having lower
		 *     limits on chroma.
		 */
		public TemperatureCache(Hct input) {
			this.input = input;
		}

		/**
		 * A color that complements the input color aesthetically.
		 *
		 * <p>In art, this is usually described as being across the color wheel. History of this shows
		 * intent as a color that is just as cool-warm as the input color is warm-cool.
		 */
		public Hct get_complement() {
			if (precomputed_complement != null) {
				return precomputed_complement;
			}

			double coldest_hue = get_coldest().hue;
			double coldest_temp = get_temps_by_hct().lookup(get_coldest());

			double warmest_hue = get_warmest().hue;
			double warmest_temp = get_temps_by_hct().lookup(get_warmest());
			double range = warmest_temp - coldest_temp;
			bool start_hue_is_coldest_to_warmest = is_between(input.hue, coldest_hue, warmest_hue);
			double start_hue = start_hue_is_coldest_to_warmest ? warmest_hue : coldest_hue;
			double end_hue = start_hue_is_coldest_to_warmest ? coldest_hue : warmest_hue;
			double direction_of_rotation = 1.0;
			double smallest_error = 1000.0;
			Hct answer = get_hcts_by_hue()[(int) Math.round(input.hue)];

			double complement_relative_temp = (1.0 - get_relative_temperature(input));
			// Find the color in the other section, closest to the inverse percentile
			// of the input color. This is the complement.
			for (double hue_addend = 0.0; hue_addend <= 360.0; hue_addend += 1.0) {
				double hue = MathUtils.sanitize_degrees_double(start_hue + direction_of_rotation * hue_addend);
				if (!is_between(hue, start_hue, end_hue)) {
					continue;
				}
				Hct possible_answer = get_hcts_by_hue()[(int) Math.round(hue)];
				double relative_temp = (get_temps_by_hct().lookup(possible_answer) - coldest_temp) / range;
				double error = Math.fabs(complement_relative_temp - relative_temp);
				if (error < smallest_error) {
					smallest_error = error;
					answer = possible_answer;
				}
			}
			precomputed_complement = answer;
			return precomputed_complement;
		}

		/**
		 * A set of colors with differing hues, equidistant in temperature.
		 *
		 * <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12
		 * sections. This method allows provision of either of those values.
		 *
		 * <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
		 *
		 * @param count The number of colors to return, includes the input color.
		 * @param divisions The number of divisions on the color wheel.
		 */
		public GenericArray<Hct> get_analogous_colors(int count = 5, int divisions = 12) {
			// The starting hue is the hue of the input color.
			int start_hue = (int) Math.round(input.hue);
			Hct start_hct = get_hcts_by_hue()[start_hue];
			double last_temp = get_relative_temperature(start_hct);

			var all_colors = new GenericArray<Hct> ();
			all_colors.add(start_hct);

			double absolute_total_temp_delta = 0.0;
			for (int i = 0; i < 360; i++) {
				int hue = MathUtils.sanitize_degrees_int(start_hue + i);
				Hct hct = get_hcts_by_hue()[hue];
				double temp = get_relative_temperature(hct);
				double temp_delta = Math.fabs(temp - last_temp);
				last_temp = temp;
				absolute_total_temp_delta += temp_delta;
			}

			int hue_addend = 1;
			double temp_step = absolute_total_temp_delta / (double) divisions;
			double total_temp_delta = 0.0;
			last_temp = get_relative_temperature(start_hct);
			while (all_colors.length < divisions) {
				int hue = MathUtils.sanitize_degrees_int(start_hue + hue_addend);
				Hct hct = get_hcts_by_hue()[hue];
				double temp = get_relative_temperature(hct);
				double temp_delta = Math.fabs(temp - last_temp);
				total_temp_delta += temp_delta;

				double desired_total_temp_delta_for_index = (all_colors.length * temp_step);
				bool index_satisfied = total_temp_delta >= desired_total_temp_delta_for_index;
				int index_addend = 1;
				// Keep adding this hue to the answers until its temperature is
				// insufficient. This ensures consistent behavior when there aren't
				// `divisions` discrete steps between 0 and 360 in hue with `tempStep`
				// delta in temperature between them.
				//
				// For example, white and black have no analogues: there are no other
				// colors at T100/T0. Therefore, they should just be added to the array
				// as answers.
				while (index_satisfied && all_colors.length < divisions) {
					all_colors.add(hct);
					desired_total_temp_delta_for_index = ((all_colors.length + index_addend) * temp_step);
					index_satisfied = total_temp_delta >= desired_total_temp_delta_for_index;
					index_addend++;
				}
				last_temp = temp;
				hue_addend++;

				if (hue_addend > 360) {
					while (all_colors.length < divisions) {
						all_colors.add(hct);
					}
					break;
				}
			}

			var answers = new GenericArray<Hct> ();
			answers.add(input);

			int ccw_count = (int) Math.floor(((double) count - 1.0) / 2.0);
			for (int i = 1; i < (ccw_count + 1); i++) {
				int index = 0 - i;
				while (index < 0) {
					index = all_colors.length + index;
				}
				if (index >= all_colors.length) {
					index = index % all_colors.length;
				}
				answers.insert(0, all_colors[index]);
			}

			int cw_count = count - ccw_count - 1;
			for (int i = 1; i < (cw_count + 1); i++) {
				int index = i;
				while (index < 0) {
					index = all_colors.length + index;
				}
				if (index >= all_colors.length) {
					index = index % all_colors.length;
				}
				answers.add(all_colors[index]);
			}

			return answers;
		}

		/**
		 * Temperature relative to all colors with the same chroma and tone.
		 *
		 * @param hct HCT to find the relative temperature of.
		 * @return Value on a scale from 0 to 1.
		 */
		public double get_relative_temperature(Hct hct = input) {
			double range = get_temps_by_hct().lookup(get_warmest()) - get_temps_by_hct().lookup(get_coldest());
			double difference_from_coldest = get_temps_by_hct().lookup(hct) - get_temps_by_hct().lookup(get_coldest());
			// Handle when there's no difference in temperature between warmest and
			// coldest: for example, at T100, only one color is available, white.
			if (range == 0.0) {
				return 0.5;
			}
			return difference_from_coldest / range;
		}

		/**
		 * Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
		 * warm.
		 *
		 * <p>Color science has researched emotion and harmony, which art uses to select colors. Warm-cool
		 * is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in
		 * Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and 21.
		 *
		 * <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
		 * Return value has these properties:<br>
		 * - Values below 0 are cool, above 0 are warm.<br>
		 * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br>
		 * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
		 */
		public static double raw_temperature(Hct color) {
			double[] lab = ColorUtils.lab_from_argb(color.to_int());
			double hue = MathUtils.sanitize_degrees_double(MathUtils.to_degrees(Math.atan2(lab[2], lab[1])));
			double chroma = Math.hypot(lab[1], lab[2]);
			return -0.5 + 0.02 * Math.pow(chroma, 1.07) * Math.cos(MathUtils.to_radians(MathUtils.sanitize_degrees_double(hue - 50.0)));
		}

		/** Coldest color with same chroma and tone as input. */
		private Hct get_coldest() {
			return get_hcts_by_temp()[0];
		}

		/**
		 * HCTs for all colors with the same chroma/tone as the input.
		 *
		 * <p>Sorted by hue, ex. index 0 is hue 0.
		 */
		private GenericArray<Hct> get_hcts_by_hue() {
			if (precomputed_hcts_by_hue != null) {
				return precomputed_hcts_by_hue;
			}
			var hcts = new GenericArray<Hct> ();
			for (double hue = 0.0; hue <= 360.0; hue += 1.0) {
				Hct color_at_hue = Hct.from(hue, input.chroma, input.tone);
				hcts.add(color_at_hue);
			}
			precomputed_hcts_by_hue = hcts;
			return precomputed_hcts_by_hue;
		}

		/**
		 * HCTs for all colors with the same chroma/tone as the input.
		 *
		 * <p>Sorted from coldest first to warmest last.
		 */
		private GenericArray<Hct> get_hcts_by_temp() {
			if (precomputed_hcts_by_temp != null) {
				return precomputed_hcts_by_temp;
			}

			var hcts = new GenericArray<Hct> ();
			var by_hue = get_hcts_by_hue();
			for (int i = 0; i < by_hue.length; i++) {
				hcts.add(by_hue[i]);
			}
			hcts.add(input);
			hcts.sort_with_data((a, b) => {
				double temp_a = get_temps_by_hct().lookup(a);
				double temp_b = get_temps_by_hct().lookup(b);
				if (temp_a < temp_b)return -1;
				if (temp_a > temp_b)return 1;
				return 0;
			});
			precomputed_hcts_by_temp = hcts;
			return precomputed_hcts_by_temp;
		}

		/** Keys of HCTs in getHctsByTemp, values of raw temperature. */
		private HashTable<Hct, double?> get_temps_by_hct() {
			if (precomputed_temps_by_hct != null) {
				return precomputed_temps_by_hct;
			}

			var by_hue = get_hcts_by_hue();
			var all_hcts = new GenericArray<Hct> ();
			for (int i = 0; i < by_hue.length; i++) {
				all_hcts.add(by_hue[i]);
			}
			all_hcts.add(input);

			var temperatures_by_hct = new HashTable<Hct, double?> (Hct.hash, Hct.equal);
			for (int i = 0; i < all_hcts.length; i++) {
				temperatures_by_hct.insert(all_hcts[i], raw_temperature(all_hcts[i]));
			}

			precomputed_temps_by_hct = temperatures_by_hct;
			return precomputed_temps_by_hct;
		}

		/** Warmest color with same chroma and tone as input. */
		private Hct get_warmest() {
			var by_temp = get_hcts_by_temp();
			return by_temp[by_temp.length - 1];
		}

		/** Determines if an angle is between two other angles, rotating clockwise. */
		private static bool is_between(double angle, double a, double b) {
			if (a < b) {
				return a <= angle && angle <= b;
			}
			return a <= angle || angle <= b;
		}
	}
}
