/*
 * 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 {
	/**
	 * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
	 * based on suitability.
	 *
	 * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
	 * muddied, while curating the high cluster count to a much smaller number of appropriate choices.
	 */
	public sealed class Score : Object {
		private const double TARGET_CHROMA = 48.0;
		private const double WEIGHT_PROPORTION = 0.7;
		private const double WEIGHT_CHROMA_ABOVE = 0.3;
		private const double WEIGHT_CHROMA_BELOW = 0.1;
		private const double CUTOFF_CHROMA = 5.0;
		private const double CUTOFF_EXCITED_PROPORTION = 0.01;

		private Score() {}

		/**
		 * Given a map with keys of colors and values of how often the color appears, rank the colors
		 * based on suitability for being used for a UI theme.
		 *
		 * @param colorsToPopulation map with keys of colors and values of how often the color appears,
		 *     usually from a source image.
		 * @param desired max count of colors to be returned in the list.
		 * @param fallbackColorArgb color to be returned if no other options available.
		 * @param filter whether to filter out undesireable combinations.
		 * @return Colors sorted by suitability for a UI theme. The most suitable color is the first item,
		 *     the least suitable is the last. There will always be at least one color returned. If all
		 *     the input colors were not suitable for a theme, a default fallback color will be provided,
		 *     Google Blue.
		 */
		public static int[] score(HashTable<int, int> colors_to_population,
		                          int desired = 4,
		                          int fallback_color_argb = (int) 0xff4285f4,
		                          bool filter = true) {
			// Get the HCT color for each Argb value, while finding the per hue count and
			// total count.
			var colors_hct = new GenericArray<Hct> ();
			int[] hue_population = new int[360];
			double population_sum = 0.0;

			var iter = HashTableIter<int, int> (colors_to_population);
			int entry_key;
			int entry_value;

			while (iter.next(out entry_key, out entry_value)) {
				Hct hct = Hct.from_int(entry_key);
				colors_hct.add(hct);
				int hue = (int) Math.floor(hct.hue);
				hue_population[hue] += entry_value;
				population_sum += entry_value;
			}

			// Hues with more usage in neighboring 30 degree slice get a larger number.
			double[] hue_excited_proportions = new double[360];
			for (int hue = 0; hue < 360; hue++) {
				double proportion = hue_population[hue] / population_sum;
				for (int i = hue - 14; i < hue + 16; i++) {
					int neighbour_hue = MathUtils.sanitize_degrees_int(i);
					hue_excited_proportions[neighbour_hue] += proportion;
				}
			}

			// Scores each HCT color based on usage and chroma, while optionally
			// filtering out values that do not have enough chroma or usage.
			var scored_hcts = new GenericArray<ScoredHct> ();
			for (int i = 0; i < colors_hct.length; i++) {
				Hct hct = colors_hct[i];
				int hue = MathUtils.sanitize_degrees_int((int) Math.round(hct.hue));
				double proportion = hue_excited_proportions[hue];
				if (filter && (hct.chroma < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
					continue;
				}

				double proportion_score = proportion * 100.0 * WEIGHT_PROPORTION;
				double chroma_weight = hct.chroma < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
				double chroma_score = (hct.chroma - TARGET_CHROMA) * chroma_weight;
				double score = proportion_score + chroma_score;
				scored_hcts.add(new ScoredHct(hct, score));
			}
			// Sorted so that colors with higher scores come first.
			scored_hcts.sort(ScoredHct.compare);

			// Iterates through potential hue differences in degrees in order to select
			// the colors with the largest distribution of hues possible. Starting at
			// 90 degrees(maximum difference for 4 colors) then decreasing down to a
			// 15 degree minimum.
			var chosen_colors = new GenericArray<Hct> ();
			for (int difference_degrees = 90; difference_degrees >= 15; difference_degrees--) {
				chosen_colors = new GenericArray<Hct> ();
				for (int i = 0; i < scored_hcts.length; i++) {
					Hct hct = scored_hcts[i].hct;
					bool has_duplicate_hue = false;
					for (int j = 0; j < chosen_colors.length; j++) {
						if (MathUtils.difference_degrees(hct.hue, chosen_colors[j].hue) < difference_degrees) {
							has_duplicate_hue = true;
							break;
						}
					}
					if (!has_duplicate_hue) {
						chosen_colors.add(hct);
					}
					if (chosen_colors.length >= desired) {
						break;
					}
				}
				if (chosen_colors.length >= desired) {
					break;
				}
			}
			if (chosen_colors.length == 0) {
				return new int[] { fallback_color_argb };
			}
			int[] colors = new int[chosen_colors.length];
			for (int i = 0; i < chosen_colors.length; i++) {
				colors[i] = chosen_colors[i].to_int();
			}
			return colors;
		}
	}

	private class ScoredHct {
		public Hct hct { get; private set; }
		public double score { get; private set; }

		public ScoredHct(Hct hct, double score) {
			this.hct = hct;
			this.score = score;
		}

		public static int compare(ScoredHct a, ScoredHct b) {
			if (a.score < b.score) {
				return 1;
			} else if (a.score > b.score) {
				return -1;
			}
			return 0;
		}
	}
}
