/*
 * 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 {
	/** The platform on which this scheme is intended to be used. */
	public enum Platform {
		PHONE,
		WATCH
	}

	/**
	 * Provides important settings for creating colors dynamically, and 6 color palettes. Requires: 1. A
	 * color. (source color) 2. A theme. (Variant) 3. Whether or not its dark mode. 4. Contrast level.
	 * (-1 to 1, currently contrast ratio 3.0 and 7.0)
	 */
	public class DynamicScheme : Object {
		public const SpecVersion DEFAULT_SPEC_VERSION = SpecVersion.SPEC_2021;
		public const Platform DEFAULT_PLATFORM = Platform.PHONE;

		/** The source color of the scheme in ARGB format. */
		public int source_color_argb { get; private set; }

		/** The source color of the scheme in HCT format. */
		public Hct source_color_hct { get; private set; }

		/** The extra source colors of the theme as HCT colors. */
		public Hct[] source_color_hct_list;

		/** The variant of the scheme. */
		public Variant variant { get; private set; }

		/** Whether or not the scheme is dark mode. */
		public bool is_dark { get; private set; }

		/** The platform on which this scheme is intended to be used. */
		public Platform platform { get; private set; }

		/**
		 * Value from -1 to 1. -1 represents minimum contrast. 0 represents standard (i.e. the design as
		 * spec'd), and 1 represents maximum contrast.
		 */
		public double contrast_level { get; private set; }

		/** The spec version of the scheme. */
		public SpecVersion spec_version { get; private set; }

		public TonalPalette primary_palette { get; private set; }
		public TonalPalette secondary_palette { get; private set; }
		public TonalPalette tertiary_palette { get; private set; }
		public TonalPalette neutral_palette { get; private set; }
		public TonalPalette neutral_variant_palette { get; private set; }
		public TonalPalette? error_palette { get; private set; }

		public DynamicScheme(Hct source_color_hct,
			Hct[]? source_color_hct_list = null,
			Variant variant,
			bool is_dark,
			double contrast_level,
			Platform platform = Platform.PHONE,
			SpecVersion spec_version = SpecVersion.SPEC_2021,
			TonalPalette primary_palette,
			TonalPalette secondary_palette,
			TonalPalette tertiary_palette,
			TonalPalette neutral_palette,
			TonalPalette neutral_variant_palette,
			TonalPalette? error_palette = null) {
			if (source_color_hct_list != null && source_color_hct_list.length == 0) {
				GLib.error("IllegalArgumentException: source_color_hct_list cannot be empty");
			}

			this.source_color_hct_list = source_color_hct_list ?? new Hct[] { source_color_hct };

			this.source_color_hct = this.source_color_hct_list[0];
			this.source_color_argb = this.source_color_hct.to_int();
			this.variant = variant;
			this.is_dark = is_dark;
			this.contrast_level = contrast_level;
			this.platform = platform;
			this.spec_version = maybe_fallback_spec_version(spec_version, variant);
			this.primary_palette = primary_palette;
			this.secondary_palette = secondary_palette;
			this.tertiary_palette = tertiary_palette;
			this.neutral_palette = neutral_palette;
			this.neutral_variant_palette = neutral_variant_palette;
			this.error_palette = error_palette ?? TonalPalette.from_hue_and_chroma(25.0, 84.0);
		}

		public static DynamicScheme from(DynamicScheme other, bool is_dark, double? contrast_level = null) {
			return new DynamicScheme(
			                         other.source_color_hct,
			                         other.source_color_hct_list,
			                         other.variant,
			                         is_dark,
			                         contrast_level ?? other.contrast_level,
			                         other.platform,
			                         other.spec_version,
			                         other.primary_palette,
			                         other.secondary_palette,
			                         other.tertiary_palette,
			                         other.neutral_palette,
			                         other.neutral_variant_palette,
			                         other.error_palette
			);
		}

		/**
		 * Returns the spec version to use for the given variant. If the variant is not supported by the
		 * given spec version, the fallback spec version is returned.
		 */
		private static SpecVersion maybe_fallback_spec_version(SpecVersion spec_version, Variant variant) {
			switch (variant) {
			case Variant.CMF:
				return spec_version;
			case Variant.EXPRESSIVE:
			case Variant.VIBRANT:
			case Variant.TONAL_SPOT:
			case Variant.NEUTRAL:
				return spec_version == SpecVersion.SPEC_2026 ? SpecVersion.SPEC_2025 : spec_version;
			case Variant.MONOCHROME:
			case Variant.FIDELITY:
			case Variant.CONTENT:
			case Variant.RAINBOW:
			case Variant.FRUIT_SALAD:
				return SpecVersion.SPEC_2021;
			default:
				GLib.warning("Unknown variant %s, falling back to SPEC_2021.", variant.to_string());
				return SpecVersion.SPEC_2021;
			}
		}

		/**
		 * Returns a new hue based on a piecewise function and input color hue.
		 *
		 * <p>For example, for the following function:
		 *
		 * <pre>
		 * result = 26, if 0 <= hue < 101;
		 * result = 39, if 101 <= hue < 210;
		 * result = 28, if 210 <= hue < 360.
		 * </pre>
		 *
		 * <p>call the function as:
		 *
		 * <pre>
		 * double[] hueBreakpoints = {0, 101, 210, 360};
		 * double[] hues = {26, 39, 28};
		 * double result = scheme.piecewise(sourceColor, hueBreakpoints, hues);
		 * </pre>
		 *
		 * @param sourceColorHct The input value.
		 * @param hueBreakpoints The breakpoints, in sorted order. No default lower or upper bounds are
		 *     assumed.
		 * @param hues The hues that should be applied when source color's hue is >= the same index in
		 *     hueBreakpoints array, and < the hue at the next index in hueBreakpoints array. Otherwise,
		 *     the source color's hue is returned.
		 */
		public static double get_piecewise_value(Hct source_color_hct, double[] hue_breakpoints, double[] hues) {
			int size = (int) Math.fmin(hue_breakpoints.length - 1, hues.length);
			double source_hue = source_color_hct.hue;
			for (int i = 0; i < size; i++) {
				if (source_hue >= hue_breakpoints[i] && source_hue < hue_breakpoints[i + 1]) {
					return MathUtils.sanitize_degrees_double(hues[i]);
				}
			}
			// No condition matched, return the source value.
			return source_hue;
		}

		/**
		 * Returns a shifted hue based on a piecewise function and input color hue.
		 *
		 * <p>For example, for the following function:
		 *
		 * <pre>
		 * result = hue + 26, if 0 <= hue < 101;
		 * result = hue - 39, if 101 <= hue < 210;
		 * result = hue + 28, if 210 <= hue < 360.
		 * </pre>
		 *
		 * <p>call the function as:
		 *
		 * <pre>
		 * double[] hueBreakpoints = {0, 101, 210, 360};
		 * double[] rotations = {26, -39, 28};
		 * double result = scheme.getRotatedHue(sourceColor, hueBreakpoints, rotations);
		 *
		 * @param sourceColorHct the source color of the theme, in HCT.
		 * @param hueBreakpoints The "breakpoints", i.e. the hues at which a rotation should be apply. No
		 * default lower or upper bounds are assumed.
		 * @param rotations The rotation that should be applied when source color's hue is >= the same
		 *     index in hues array, and < the hue at the next index in hues array. Otherwise, the source
		 *     color's hue is returned.
		 */
		public static double get_rotated_hue(Hct source_color_hct, double[] hue_breakpoints, double[] rotations) {
			double rotation = get_piecewise_value(source_color_hct, hue_breakpoints, rotations);
			if (Math.fmin(hue_breakpoints.length - 1, rotations.length) <= 0) {
				// No condition matched, return the source hue.
				rotation = 0;
			}
			return MathUtils.sanitize_degrees_double(source_color_hct.hue + rotation);
		}

		public Hct get_hct(DynamicColor dynamic_color) {
			return dynamic_color.get_hct(this);
		}

		public int get_argb(DynamicColor dynamic_color) {
			return dynamic_color.get_argb(this);
		}

		public string format() {
			var str = new StringBuilder();

			str
			 .append("Scheme: ")
			 .append("variant=%s, ".printf(variant.to_string()))
			 .append("mode=%s, ".printf(is_dark ? "dark" : "light"))
			 .append("platform=%s, ".printf(platform.to_string().down()))
			 .append("contrast_level=%d, ".printf((int) contrast_level))
			 .append("seed=%s, ".printf(source_color_hct.format()));

			if (source_color_hct_list.length > 1) {
				string[] string_array = new string[source_color_hct_list.length];

				for (int i = 0; i < source_color_hct_list.length; i++) {
					string_array[i] = source_color_hct_list[i].format();
				}

				str.append("source_color_hct_list=[%s], ".printf(string.joinv(", ", string_array)));
			}

			return str
			        .append("spec_version=%s".printf(spec_version.to_string()))
			        .str;
		}

		public static uint hash(DynamicScheme scheme) {
			uint h = 17;
			h = h * 31 + (uint) scheme.source_color_argb;
			h = h * 31 + (uint) scheme.variant;
			h = h * 31 + (scheme.is_dark ? 1u : 0u);
			h = h * 31 + hash_double(scheme.contrast_level);
			h = h * 31 + (uint) scheme.platform;
			h = h * 31 + (uint) scheme.spec_version;
			h = h * 31 + hash_double(scheme.primary_palette.hue);
			h = h * 31 + hash_double(scheme.primary_palette.chroma);
			h = h * 31 + hash_double(scheme.secondary_palette.hue);
			h = h * 31 + hash_double(scheme.secondary_palette.chroma);
			h = h * 31 + hash_double(scheme.tertiary_palette.hue);
			h = h * 31 + hash_double(scheme.tertiary_palette.chroma);
			h = h * 31 + hash_double(scheme.neutral_palette.hue);
			h = h * 31 + hash_double(scheme.neutral_palette.chroma);
			h = h * 31 + hash_double(scheme.neutral_variant_palette.hue);
			h = h * 31 + hash_double(scheme.neutral_variant_palette.chroma);
			if (scheme.error_palette != null) {
				h = h * 31 + hash_double(scheme.error_palette.hue);
				h = h * 31 + hash_double(scheme.error_palette.chroma);
			}
			for (int i = 0; i < scheme.source_color_hct_list.length; i++) {
				h = h * 31 + (uint) scheme.source_color_hct_list[i].to_int();
			}
			return h;
		}

		public static bool equal(DynamicScheme a, DynamicScheme b) {
			if (a == b) return true;
			if (a.source_color_argb != b.source_color_argb) return false;
			if (a.variant != b.variant) return false;
			if (a.is_dark != b.is_dark) return false;
			if (a.contrast_level != b.contrast_level) return false;
			if (a.platform != b.platform) return false;
			if (a.spec_version != b.spec_version) return false;
			if (!palette_equal(a.primary_palette, b.primary_palette)) return false;
			if (!palette_equal(a.secondary_palette, b.secondary_palette)) return false;
			if (!palette_equal(a.tertiary_palette, b.tertiary_palette)) return false;
			if (!palette_equal(a.neutral_palette, b.neutral_palette)) return false;
			if (!palette_equal(a.neutral_variant_palette, b.neutral_variant_palette)) return false;
			if (!palette_equal(a.error_palette, b.error_palette)) return false;
			if (a.source_color_hct_list.length != b.source_color_hct_list.length) return false;
			for (int i = 0; i < a.source_color_hct_list.length; i++) {
				if (a.source_color_hct_list[i].to_int() != b.source_color_hct_list[i].to_int()) return false;
			}
			return true;
		}

		private static bool palette_equal(TonalPalette? a, TonalPalette? b) {
			if (a == b) return true;
			if (a == null || b == null) return false;
			return a.hue == b.hue && a.chroma == b.chroma;
		}

		private static uint hash_double(double d) {
			uint64 bits = 0;
			GLib.Memory.copy(&bits, &d, sizeof(uint64));
			return (uint) (bits ^ (bits >> 32));
		}

		public int primary_palette_key_color {
			get { return get_argb(MaterialDynamicColors.primary_palette_key_color()); }
		}

		public int secondary_palette_key_color {
			get { return get_argb(MaterialDynamicColors.secondary_palette_key_color()); }
		}

		public int tertiary_palette_key_color {
			get { return get_argb(MaterialDynamicColors.tertiary_palette_key_color()); }
		}

		public int neutral_palette_key_color {
			get { return get_argb(MaterialDynamicColors.neutral_palette_key_color()); }
		}

		public int neutral_variant_palette_key_color {
			get { return get_argb(MaterialDynamicColors.neutral_variant_palette_key_color()); }
		}

		public int background {
			get { return get_argb(MaterialDynamicColors.background()); }
		}

		public int on_background {
			get { return get_argb(MaterialDynamicColors.on_background()); }
		}

		public int surface {
			get { return get_argb(MaterialDynamicColors.surface()); }
		}

		public int surface_dim {
			get { return get_argb(MaterialDynamicColors.surface_dim()); }
		}

		public int surface_bright {
			get { return get_argb(MaterialDynamicColors.surface_bright()); }
		}

		public int surface_container_lowest {
			get { return get_argb(MaterialDynamicColors.surface_container_lowest()); }
		}

		public int surface_container_low {
			get { return get_argb(MaterialDynamicColors.surface_container_low()); }
		}

		public int surface_container {
			get { return get_argb(MaterialDynamicColors.surface_container()); }
		}

		public int surface_container_high {
			get { return get_argb(MaterialDynamicColors.surface_container_high()); }
		}

		public int surface_container_highest {
			get { return get_argb(MaterialDynamicColors.surface_container_highest()); }
		}

		public int on_surface {
			get { return get_argb(MaterialDynamicColors.on_surface()); }
		}

		public int surface_variant {
			get { return get_argb(MaterialDynamicColors.surface_variant()); }
		}

		public int on_surface_variant {
			get { return get_argb(MaterialDynamicColors.on_surface_variant()); }
		}

		public int inverse_surface {
			get { return get_argb(MaterialDynamicColors.inverse_surface()); }
		}

		public int inverse_on_surface {
			get { return get_argb(MaterialDynamicColors.inverse_on_surface()); }
		}

		public int outline {
			get { return get_argb(MaterialDynamicColors.outline()); }
		}

		public int outline_variant {
			get { return get_argb(MaterialDynamicColors.outline_variant()); }
		}

		public int shadow {
			get { return get_argb(MaterialDynamicColors.shadow()); }
		}

		public int scrim {
			get { return get_argb(MaterialDynamicColors.scrim()); }
		}

		public int surface_tint {
			get { return get_argb(MaterialDynamicColors.surface_tint()); }
		}

		public int primary {
			get { return get_argb(MaterialDynamicColors.primary()); }
		}

		public int on_primary {
			get { return get_argb(MaterialDynamicColors.on_primary()); }
		}

		public int primary_container {
			get { return get_argb(MaterialDynamicColors.primary_container()); }
		}

		public int on_primary_container {
			get { return get_argb(MaterialDynamicColors.on_primary_container()); }
		}

		public int inverse_primary {
			get { return get_argb(MaterialDynamicColors.inverse_primary()); }
		}

		public int secondary {
			get { return get_argb(MaterialDynamicColors.secondary()); }
		}

		public int on_secondary {
			get { return get_argb(MaterialDynamicColors.on_secondary()); }
		}

		public int secondary_container {
			get { return get_argb(MaterialDynamicColors.secondary_container()); }
		}

		public int on_secondary_container {
			get { return get_argb(MaterialDynamicColors.on_secondary_container()); }
		}

		public int tertiary {
			get { return get_argb(MaterialDynamicColors.tertiary()); }
		}

		public int on_tertiary {
			get { return get_argb(MaterialDynamicColors.on_tertiary()); }
		}

		public int tertiary_container {
			get { return get_argb(MaterialDynamicColors.tertiary_container()); }
		}

		public int on_tertiary_container {
			get { return get_argb(MaterialDynamicColors.on_tertiary_container()); }
		}

		public int error {
			get { return get_argb(MaterialDynamicColors.error()); }
		}

		public int on_error {
			get { return get_argb(MaterialDynamicColors.on_error()); }
		}

		public int error_container {
			get { return get_argb(MaterialDynamicColors.error_container()); }
		}

		public int on_error_container {
			get { return get_argb(MaterialDynamicColors.on_error_container()); }
		}

		public int primary_fixed {
			get { return get_argb(MaterialDynamicColors.primary_fixed()); }
		}

		public int primary_fixed_dim {
			get { return get_argb(MaterialDynamicColors.primary_fixed_dim()); }
		}

		public int on_primary_fixed {
			get { return get_argb(MaterialDynamicColors.on_primary_fixed()); }
		}

		public int on_primary_fixed_variant {
			get { return get_argb(MaterialDynamicColors.on_primary_fixed_variant()); }
		}

		public int secondary_fixed {
			get { return get_argb(MaterialDynamicColors.secondary_fixed()); }
		}

		public int secondary_fixed_dim {
			get { return get_argb(MaterialDynamicColors.secondary_fixed_dim()); }
		}

		public int on_secondary_fixed {
			get { return get_argb(MaterialDynamicColors.on_secondary_fixed()); }
		}

		public int on_secondary_fixed_variant {
			get { return get_argb(MaterialDynamicColors.on_secondary_fixed_variant()); }
		}

		public int tertiary_fixed {
			get { return get_argb(MaterialDynamicColors.tertiary_fixed()); }
		}

		public int tertiary_fixed_dim {
			get { return get_argb(MaterialDynamicColors.tertiary_fixed_dim()); }
		}

		public int on_tertiary_fixed {
			get { return get_argb(MaterialDynamicColors.on_tertiary_fixed()); }
		}

		public int on_tertiary_fixed_variant {
			get { return get_argb(MaterialDynamicColors.on_tertiary_fixed_variant()); }
		}

		public int control_activated {
			get { return get_argb(MaterialDynamicColors.control_activated()); }
		}

		public int control_normal {
			get { return get_argb(MaterialDynamicColors.control_normal()); }
		}

		public int control_highlight {
			get { return get_argb(MaterialDynamicColors.control_highlight()); }
		}

		public int text_primary_inverse {
			get { return get_argb(MaterialDynamicColors.text_primary_inverse()); }
		}

		public int text_secondary_and_tertiary_inverse {
			get { return get_argb(MaterialDynamicColors.text_secondary_and_tertiary_inverse()); }
		}

		public int text_primary_inverse_disable_only {
			get { return get_argb(MaterialDynamicColors.text_primary_inverse_disable_only()); }
		}

		public int text_secondary_and_tertiary_inverse_disabled {
			get { return get_argb(MaterialDynamicColors.text_secondary_and_tertiary_inverse_disabled()); }
		}

		public int text_hint_inverse {
			get { return get_argb(MaterialDynamicColors.text_hint_inverse()); }
		}
	}
}
