/*
 * 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 {
	// Delegates
	public delegate TonalPalette TonalPaletteFromDynamicSchemeDelegate(DynamicScheme scheme);

	public delegate double DoubleFromDynamicSchemeDelegate(DynamicScheme scheme);

	public delegate double ? NullableDoubleFromDynamicSchemeDelegate(DynamicScheme scheme);

	public delegate DynamicColor ? DynamicColorFromDynamicSchemeDelegate(DynamicScheme scheme);

	public delegate ContrastCurve ? ContrastCurveFromDynamicSchemeDelegate(DynamicScheme scheme);

	public delegate ToneDeltaPair ? ToneDeltaPairFromDynamicSchemeDelegate(DynamicScheme scheme);

	/**
	 * A color that adjusts itself based on UI state, represented by DynamicScheme.
	 *
	 * <p>This color automatically adjusts to accommodate a desired contrast level, or other adjustments
	 * such as differing in light mode versus dark mode, or what the theme is, or what the color that
	 * produced the theme is, etc.
	 *
	 * <p>Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
	 * become closer to their background as contrast lowers, and further when contrast increases.
	 *
	 * <p>Prefer the static constructors. They provide a much more simple interface, such as requiring
	 * just a hexcode, or just a hexcode and a background.
	 *
	 * <p>Ultimately, each component necessary for calculating a color, adjusting it for a desired
	 * contrast level, and ensuring it has a certain lightness/tone difference from another color, is
	 * provided by a function that takes a DynamicScheme and returns a value. This ensures ultimate
	 * flexibility, any desired behavior of a color for any design system, but it usually unnecessary.
	 * See the default constructor for more information.
	 */
	public sealed class DynamicColor : Object {
		// Simple value fields can be proper GObject properties.
		public string name { get; private set; }
		public bool is_background { get; private set; }

		// Delegate fields cannot be GObject properties in Vala; they are kept as
		// plain public fields. DynamicColor is sealed so they cannot be overridden.
		public TonalPaletteFromDynamicSchemeDelegate palette;
		public DoubleFromDynamicSchemeDelegate tone;
		public NullableDoubleFromDynamicSchemeDelegate? chroma_multiplier;
		public DynamicColorFromDynamicSchemeDelegate? background;
		public DynamicColorFromDynamicSchemeDelegate? second_background;
		public ContrastCurveFromDynamicSchemeDelegate? contrast_curve;
		public ToneDeltaPairFromDynamicSchemeDelegate? tone_delta_pair;
		public NullableDoubleFromDynamicSchemeDelegate? opacity;

		private HashTable<DynamicScheme, Hct> hct_cache = new HashTable<DynamicScheme, Hct> (DynamicScheme.hash, DynamicScheme.equal);


		/**
		 * A constructor for DynamicColor.
		 *
		 * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
		 * flexible to ensure it can support any scenario. Functional arguments allow overriding without
		 * risks that come with subclasses.
		 *
		 * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
		 * its background is principled and matches accessibility guidance. That does not mean it's the
		 * desired approach for _every_ design system, and every color pairing, always, in every case.
		 *
		 * <p>For opaque colors (colors with alpha = 100%).
		 *
		 * @param name The name of the dynamic color.
		 * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
		 *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
		 *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
		 * @param tone Function that provides a tone, given a DynamicScheme.
		 * @param isBackground Whether this dynamic color is a background, with some other color as the
		 *     foreground.
		 * @param chroma_multiplier A function returning the chroma multiplier.
		 * @param background The background of the dynamic color (as a function of a `DynamicScheme`), if
		 *     it exists.
		 * @param secondBackground A second background of the dynamic color (as a function of a
		 *     `DynamicScheme`), if it exists.
		 * @param contrastCurve A `ContrastCurve` object specifying how its contrast against its
		 *     background should behave in various contrast levels options.
		 * @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta constraint between two
		 *     colors. One of them must be the color being constructed.
		 * @param opacity A function returning the opacity of a color, as a number between 0 and 1.
		 */
		public DynamicColor(string name,
			owned TonalPaletteFromDynamicSchemeDelegate palette,
			owned DoubleFromDynamicSchemeDelegate tone,
			bool is_background,
			owned NullableDoubleFromDynamicSchemeDelegate? chroma_multiplier = null,
			owned DynamicColorFromDynamicSchemeDelegate? background = null,
			owned DynamicColorFromDynamicSchemeDelegate? second_background = null,
			owned ContrastCurveFromDynamicSchemeDelegate? contrast_curve = null,
			owned ToneDeltaPairFromDynamicSchemeDelegate? tone_delta_pair = null,
			owned NullableDoubleFromDynamicSchemeDelegate? opacity = null) {
			this.name = name;
			this.palette = (owned) palette;
			this.tone = (owned) tone;
			this.is_background = is_background;
			this.chroma_multiplier = (owned) chroma_multiplier;
			this.background = (owned) background;
			this.second_background = (owned) second_background;
			this.contrast_curve = (owned) contrast_curve;
			this.tone_delta_pair = (owned) tone_delta_pair;
			this.opacity = (owned) opacity;
		}

		/**
		 * A convenience constructor for DynamicColor.
		 *
		 * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
		 * flexible to ensure it can support any scenario. Functional arguments allow overriding without
		 * risks that come with subclasses.
		 *
		 * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
		 * its background is principled and matches accessibility guidance. That does not mean it's the
		 * desired approach for _every_ design system, and every color pairing, always, in every case.
		 *
		 * <p>For opaque colors (colors with alpha = 100%).
		 *
		 * <p>For colors that do not have backgrounds.
		 *
		 * @param name The name of the dynamic color.
		 * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
		 *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
		 *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
		 * @param tone Function that provides a tone, given a DynamicScheme.
		 * @param isBackground Whether this dynamic color is a background, with some other color as the
		 *     foreground (defaults to false).
		 */
		public static DynamicColor from_palette(string name, owned TonalPaletteFromDynamicSchemeDelegate palette, owned DoubleFromDynamicSchemeDelegate tone, bool is_background = false) {
			return new DynamicColor(name, (owned) palette, (owned) tone, is_background);
		}

		/**
		 * Create a DynamicColor from a hex code.
		 *
		 * <p>Result has no background; thus no support for increasing/decreasing contrast for a11y.
		 *
		 * @param name The name of the dynamic color.
		 * @param argb The source color from which to extract the hue and chroma.
		 */
		public static DynamicColor from_argb(string name, int argb) {
			Hct hct = Hct.from_int(argb);
			TonalPalette palette = TonalPalette.from_int(argb);
			return DynamicColor.from_palette(name, (s) => palette, (s) => hct.tone);
		}

		/**
		 * Returns an ARGB integer (i.e. a hex code).
		 *
		 * @param scheme Defines the conditions of the user interface, for example, whether or not it is
		 *     dark mode or light mode, and what the desired contrast level is.
		 */
		public int get_argb(DynamicScheme scheme) {
			int argb = get_hct(scheme).to_int();
			if (opacity == null) {
				return argb;
			}
			double? alpha_opacity = opacity(scheme);
			if (alpha_opacity == null) {
				return argb;
			}
			int alpha = MathUtils.clamp_int(0, 255, (int) Math.round(alpha_opacity * 255));
			return (argb & 0x00ffffff) | (alpha << 24);
		}

		/**
		 * Returns an HCT object.
		 *
		 * @param scheme Defines the conditions of the user interface, for example, whether or not it is
		 *     dark mode or light mode, and what the desired contrast level is.
		 */
		public Hct get_hct(DynamicScheme scheme) {
			Hct cached_answer = hct_cache.lookup(scheme);
			if (cached_answer != null) {
				return cached_answer;
			}

			Hct answer = ColorSpecs.get_spec(scheme.spec_version).get_hct(scheme, this);
			if (hct_cache.size() > 4) {
				hct_cache.remove_all();
			}
			hct_cache.insert(scheme, answer);
			return answer;
		}

		/** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */
		public double get_tone(DynamicScheme scheme) {
			return ColorSpecs.get_spec(scheme.spec_version).get_tone(scheme, this);
		}

		/**
		 * Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
		 * that is as close to ratio as possible.
		 */
		public static double foreground_tone(double bg_tone, double ratio) {
			double lighter_tone = Contrast.lighter_unsafe(bg_tone, ratio);
			double darker_tone = Contrast.darker_unsafe(bg_tone, ratio);
			double lighter_ratio = Contrast.ratio_of_tones(lighter_tone, bg_tone);
			double darker_ratio = Contrast.ratio_of_tones(darker_tone, bg_tone);
			bool prefer_lighter = tone_prefers_light_foreground(bg_tone);

			if (prefer_lighter) {
				// "Neglible difference" handles an edge case where the initial contrast ratio is high
				// (ex. 13.0), and the ratio passed to the function is that high ratio, and both the lighter
				// and darker ratio fails to pass that ratio.
				//
				// This was observed with Tonal Spot's On Primary Container turning black momentarily between
				// high and max contrast in light mode. PC's standard tone was T90, OPC's was T10, it was
				// light mode, and the contrast level was 0.6568521221032331.
				bool negligible_difference = Math.fabs(lighter_ratio - darker_ratio) < 0.1 && lighter_ratio < ratio && darker_ratio < ratio;
				if (lighter_ratio >= ratio || lighter_ratio >= darker_ratio || negligible_difference) {
					return lighter_tone;
				} else {
					return darker_tone;
				}
			} else {
				return darker_ratio >= ratio || darker_ratio >= lighter_ratio ? darker_tone : lighter_tone;
			}
		}

		/**
		 * Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
		 * supporting it.
		 */
		public static double enable_light_foreground(double tone) {
			if (tone_prefers_light_foreground(tone) && !tone_allows_light_foreground(tone)) {
				return 49.0;
			}
			return tone;
		}

		/**
		 * People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
		 * during research for APCA.
		 *
		 * <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in order
		 * to ensure light foregrounds.
		 *
		 * <p>Since `tertiaryContainer` in dark monochrome scheme requires a tone of 60, it should not be
		 * adjusted. Therefore, 60 is excluded here.
		 */
		public static bool tone_prefers_light_foreground(double tone) {
			return Math.round(tone) < 60;
		}

		/** Tones less than ~T50 always permit white at 4.5 contrast. */
		public static bool tone_allows_light_foreground(double tone) {
			return Math.round(tone) <= 49;
		}

		public static DoubleFromDynamicSchemeDelegate get_initial_tone_from_background(owned DynamicColorFromDynamicSchemeDelegate? background) {
			if (background == null) {
				return (s) => 50.0;
			}
			return (s) => {
					   var bg_color = background(s);
					   return bg_color != null? bg_color.get_tone(s) : 50.0;
			};
		}

		public DynamicColorBuilder to_builder() {
			// Wrapper lambdas close over `this` (a ref-counted GObject), giving true
			// copy semantics: the original is not modified and both instances work
			// independently.
			var builder = new DynamicColorBuilder()
			     .set_name(this.name)
			     .set_palette((s) => this.palette(s))
			     .set_tone((s) => this.tone(s))
			     .set_is_background(this.is_background);
			if (this.chroma_multiplier != null)
				builder.set_chroma_multiplier((s) => this.chroma_multiplier(s));
			if (this.background != null)
				builder.set_background((s) => this.background(s));
			if (this.second_background != null)
				builder.set_second_background((s) => this.second_background(s));
			if (this.contrast_curve != null)
				builder.set_contrast_curve((s) => this.contrast_curve(s));
			if (this.tone_delta_pair != null)
				builder.set_tone_delta_pair((s) => this.tone_delta_pair(s));
			if (this.opacity != null)
				builder.set_opacity((s) => this.opacity(s));
			return builder;
		}
	}

	/** Builder for {@link DynamicColor}. */
	public class DynamicColorBuilder : Object {
		private string name;
		private TonalPaletteFromDynamicSchemeDelegate palette;
		private DoubleFromDynamicSchemeDelegate tone;
		private bool is_background;
		private NullableDoubleFromDynamicSchemeDelegate? chroma_multiplier;
		private DynamicColorFromDynamicSchemeDelegate? background;
		private DynamicColorFromDynamicSchemeDelegate? second_background;
		private ContrastCurveFromDynamicSchemeDelegate? contrast_curve;
		private ToneDeltaPairFromDynamicSchemeDelegate? tone_delta_pair;
		private NullableDoubleFromDynamicSchemeDelegate? opacity;

		public DynamicColorBuilder set_name(string name) {
			this.name = name;
			return this;
		}

		public DynamicColorBuilder set_palette(owned TonalPaletteFromDynamicSchemeDelegate palette) {
			this.palette = (owned) palette;
			return this;
		}

		public DynamicColorBuilder set_tone(owned DoubleFromDynamicSchemeDelegate tone) {
			this.tone = (owned) tone;
			return this;
		}

		public DynamicColorBuilder set_is_background(bool is_background) {
			this.is_background = is_background;
			return this;
		}

		public DynamicColorBuilder set_chroma_multiplier(owned NullableDoubleFromDynamicSchemeDelegate chroma_multiplier) {
			this.chroma_multiplier = (owned) chroma_multiplier;
			return this;
		}

		public DynamicColorBuilder set_background(owned DynamicColorFromDynamicSchemeDelegate background) {
			this.background = (owned) background;
			return this;
		}

		public DynamicColorBuilder set_second_background(owned DynamicColorFromDynamicSchemeDelegate second_background) {
			this.second_background = (owned) second_background;
			return this;
		}

		public DynamicColorBuilder set_contrast_curve(owned ContrastCurveFromDynamicSchemeDelegate contrast_curve) {
			this.contrast_curve = (owned) contrast_curve;
			return this;
		}

		public DynamicColorBuilder set_tone_delta_pair(owned ToneDeltaPairFromDynamicSchemeDelegate tone_delta_pair) {
			this.tone_delta_pair = (owned) tone_delta_pair;
			return this;
		}

		public DynamicColorBuilder set_opacity(owned NullableDoubleFromDynamicSchemeDelegate opacity) {
			this.opacity = (owned) opacity;
			return this;
		}

		public DynamicColorBuilder extend_spec_version(SpecVersion spec_version, DynamicColor extended_color) {
			validate_extended_color(spec_version, extended_color);

			return new DynamicColorBuilder()
			        .set_name(this.name)
			        .set_is_background(this.is_background)
			        .set_palette((s) => s.spec_version >= spec_version ? extended_color.palette(s) : this.palette(s))
			        .set_tone((s) => s.spec_version >= spec_version ? extended_color.tone(s) : this.tone(s))
			        .set_chroma_multiplier((s) => {
				unowned var chroma_multiplier = s.spec_version >= spec_version ? extended_color.chroma_multiplier : this.chroma_multiplier;
				return chroma_multiplier != null ? chroma_multiplier(s) : 1.0;
			})
			        .set_background((s) => {
				unowned var bg = s.spec_version >= spec_version ? extended_color.background : this.background;
				return bg != null ? bg(s) : null;
			})
			        .set_second_background((s) => {
				unowned var bg2 = s.spec_version >= spec_version ? extended_color.second_background : this.second_background;
				return bg2 != null ? bg2(s) : null;
			})
			        .set_contrast_curve((s) => {
				unowned var cc = s.spec_version >= spec_version ? extended_color.contrast_curve : this.contrast_curve;
				return cc != null ? cc(s) : null;
			})
			        .set_tone_delta_pair((s) => {
				unowned var tdp = s.spec_version >= spec_version ? extended_color.tone_delta_pair : this.tone_delta_pair;
				return tdp != null ? tdp(s) : null;
			})
			        .set_opacity((s) => {
				unowned var op = s.spec_version >= spec_version ? extended_color.opacity : this.opacity;
				return op != null ? op(s) : null;
			});
		}

		public DynamicColor build() {
			if (this.background == null && this.second_background != null) {
				GLib.error("IllegalArgumentException: Color %s has second_background defined, but background is not defined.".printf(name));
			}
			if (this.background == null && this.contrast_curve != null) {
				GLib.error("IllegalArgumentException: Color %s has contrast_curve defined, but background is not defined.".printf(name));
			}
			if (this.background != null && this.contrast_curve == null) {
				GLib.error("IllegalArgumentException: Color %s has background defined, but contrast_curve is not defined.".printf(name));
			}
			DoubleFromDynamicSchemeDelegate final_tone;
			if (this.tone == null) {
				final_tone = DynamicColor.get_initial_tone_from_background((owned) this.background);
			} else {
				final_tone = (owned) this.tone;
			}

			return new DynamicColor(
			                        this.name,
			                        (owned) this.palette,
			                        (owned) final_tone,
			                        this.is_background,
			                        (owned) this.chroma_multiplier,
			                        (owned) this.background,
			                        (owned) this.second_background,
			                        (owned) this.contrast_curve,
			                        (owned) this.tone_delta_pair,
			                        (owned) this.opacity
			);
		}

		private void validate_extended_color(SpecVersion spec_version, DynamicColor extended_color) {
			if (!(this.name == extended_color.name)) {
				GLib.error("IllegalArgumentException: Attempting to extend color %s with color %s of a different name for spec version %s.".printf(this.name, extended_color.name, spec_version.to_string()));
			}
			if (this.is_background != extended_color.is_background) {
				GLib.error("IllegalArgumentException: Attempting to extend color %s as a %s with color %s as a %s for spec version %s".printf(this.name, (this.is_background ? "background" : "foreground"), extended_color.name, (extended_color.is_background ? "background" : "foreground"), spec_version.to_string()));
			}
		}
	}
}
