/*
 * 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 {
	/**
	 * A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
	 *
	 * <p>TonalPalette is intended for use in a single thread due to its stateful caching.
	 */
	public sealed class TonalPalette : Object {
		private HashTable<int, int> cache;

		/** The key color is the first tone, starting from T50, that matches the palette's chroma. */
		public Hct key_color { get; private set; }

		/** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
		public double hue { get; private set; }

		/** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
		public double chroma { get; private set; }

		/**
		 * Create tones using the HCT hue and chroma from a color.
		 *
		 * @param argb ARGB representation of a color
		 * @return Tones matching that color's hue and chroma.
		 */
		public static TonalPalette from_int(int argb) {
			return from_hct(Hct.from_int(argb));
		}

		/**
		 * Create tones using a HCT color.
		 *
		 * @param hct HCT representation of a color.
		 * @return Tones matching that color's hue and chroma.
		 */
		public static TonalPalette from_hct(Hct hct) {
			return new TonalPalette(hct.hue, hct.chroma, hct);
		}

		/**
		 * Create tones from a defined HCT hue and chroma.
		 *
		 * @param hue HCT hue
		 * @param chroma HCT chroma
		 * @return Tones matching hue and chroma.
		 */
		public static TonalPalette from_hue_and_chroma(double hue, double chroma) {
			Hct key_color = new KeyColor(hue, chroma).create();
			return new TonalPalette(hue, chroma, key_color);
		}

		private TonalPalette(double hue, double chroma, Hct key_color) {
			cache = new HashTable<int, int> (direct_hash, direct_equal);
			this.hue = hue;
			this.chroma = chroma;
			this.key_color = key_color;
		}

		/**
		 * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
		 *
		 * @param tone HCT tone, measured from 0 to 100.
		 * @return ARGB representation of a color with that tone.
		 */
		public int tone(int tone) {
			int color;
			if (!this.cache.contains(tone)) {
				if (tone == 99 && Hct.is_yellow(this.hue)) {
					color = average_argb(this.tone(98), this.tone(100));
				} else {
					color = Hct.from(this.hue, this.chroma, tone).to_int();
				}
				cache.insert(tone, color);
			} else {
				color = cache.get(tone);
			}
			return color;
		}

		/** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
		public Hct get_hct(double tone) {
			if (tone == 99.0 && Hct.is_yellow(hue)) {
				return Hct.from_int(this.tone(99));
			}

			return Hct.from(this.hue, this.chroma, tone);
		}

		private int average_argb(int argb1, int argb2) {
			int red1 = (argb1 >> 16) & 0xff;
			int green1 = (argb1 >> 8) & 0xff;
			int blue1 = argb1 & 0xff;
			int red2 = (argb2 >> 16) & 0xff;
			int green2 = (argb2 >> 8) & 0xff;
			int blue2 = argb2 & 0xff;
			int red = (int) Math.round((red1 + red2) / 2.0);
			int green = (int) Math.round((green1 + green2) / 2.0);
			int blue = (int) Math.round((blue1 + blue2) / 2.0);
			return 255 << 24 | (red & 255) << 16 | (green & 255) << 8 | (blue & 255);
		}
	}

	/** Key color is a color that represents the hue and chroma of a tonal palette. */
	private sealed class KeyColor {
		private double hue;
		private double requested_chroma;

		// Cache that maps tone to max chroma to avoid duplicated HCT calculation.
		private HashTable<int, double?> chroma_cache = new HashTable<int, double?> (direct_hash, direct_equal);
		private const double MAX_CHROMA_VALUE = 200.0;

		/** Key color is a color that represents the hue and chroma of a tonal palette */
		public KeyColor(double hue, double requested_chroma) {
			this.hue = hue;
			this.requested_chroma = requested_chroma;
		}

		/**
		 * Creates a key color from a [hue] and a [chroma]. The key color is the first tone, starting
		 * from T50, matching the given hue and chroma.
		 *
		 * @return Key color [Hct]
		 */
		public Hct create() {
			// Pivot around T50 because T50 has the most chroma available, on
			// average. Thus it is most likely to have a direct answer.
			int pivot_tone = 50;
			int tone_step_size = 1;
			// Epsilon to accept values slightly higher than the requested chroma.
			double epsilon = 0.01;

			// Binary search to find the tone that can provide a chroma that is closest
			// to the requested chroma.
			int lower_tone = 0;
			int upper_tone = 100;
			while (lower_tone < upper_tone) {
				int mid_tone = (lower_tone + upper_tone) / 2;
				bool is_ascending = max_chroma(mid_tone) < max_chroma(mid_tone + tone_step_size);
				bool sufficient_chroma = max_chroma(mid_tone) >= requested_chroma - epsilon;

				if (sufficient_chroma) {
					// Either range [lowerTone, midTone] or [midTone, upperTone] has
					// the answer, so search in the range that is closer the pivot tone.
					if (Math.fabs(lower_tone - pivot_tone) < Math.fabs(upper_tone - pivot_tone)) {
						upper_tone = mid_tone;
					} else {
						if (lower_tone == mid_tone) {
							return Hct.from(this.hue, this.requested_chroma, lower_tone);
						}
						lower_tone = mid_tone;
					}
				} else {
					// As there is no sufficient chroma in the midTone, follow the direction to the chroma
					// peak.
					if (is_ascending) {
						lower_tone = mid_tone + tone_step_size;
					} else {
						// Keep midTone for potential chroma peak.
						upper_tone = mid_tone;
					}
				}
			}

			return Hct.from(this.hue, this.requested_chroma, lower_tone);
		}

		// Find the maximum chroma for a given tone
		private double max_chroma(int tone) {
			if (!chroma_cache.contains(tone)) {
				double new_chroma = Hct.from(hue, MAX_CHROMA_VALUE, tone).chroma;
				chroma_cache.insert(tone, new_chroma);
			}
			return chroma_cache.get(tone);
		}
	}
}
