content: // Copyright 2024 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'dart:math' as math;
  
  import 'package:meta/meta.dart';
  
  import '../../../util/number.dart';
  import '../../color.dart';
  
  /// Gamut mapping using the deltaEOK difference formula and the local-MINDE
  /// improvement.
  ///
  /// @nodoc
  @internal
  final class LocalMindeGamutMap extends GamutMapMethod {
    /// A constant from the gamut-mapping algorithm.
    static const _jnd = 0.02;
  
    /// A constant from the gamut-mapping algorithm.
    static const _epsilon = 0.0001;
  
    const LocalMindeGamutMap() : super("local-minde");
  
    SassColor map(SassColor color) {
      // Algorithm from https://www.w3.org/TR/2022/CRD-css-color-4-20221101/#css-gamut-mapping-algorithm
      var originOklch = color.toSpace(ColorSpace.oklch);
  
      // The channel equivalents to `current` in the Color 4 algorithm.
      var lightness = originOklch.channel0OrNull;
      var hue = originOklch.channel2OrNull;
      var alpha = originOklch.alphaOrNull;
  
      if (fuzzyGreaterThanOrEquals(lightness ?? 0, 1)) {
        return color.isLegacy
            ? SassColor.rgb(255, 255, 255, color.alphaOrNull).toSpace(color.space)
            : SassColor.forSpaceInternal(color.space, 1, 1, 1, color.alphaOrNull);
      } else if (fuzzyLessThanOrEquals(lightness ?? 0, 0)) {
        return SassColor.rgb(0, 0, 0, color.alphaOrNull).toSpace(color.space);
      }
  
      var clipped = color.toGamut(GamutMapMethod.clip);
      if (_deltaEOK(clipped, color) < _jnd) return clipped;
  
      var min = 0.0;
      var max = originOklch.channel1;
      var minInGamut = true;
      while (max - min > _epsilon) {
        var chroma = (min + max) / 2;
  
        // In the Color 4 algorithm `current` is in Oklch, but all its actual uses
        // other than modifying chroma convert it to `color.space` first so we
        // just store it in that space to begin with.
        var current = ColorSpace.oklch.convert(
          color.space,
          lightness,
          chroma,
          hue,
          alpha,
        );
  
        // Per [this comment], the intention of the algorithm is to fall through
        // this clause if `minInGamut = false` without checking
        // `current.isInGamut` at all, even though that's unclear from the
        // pseudocode. `minInGamut = false` *should* imply `current.isInGamut =
        // false`.
        //
        // [this comment]: https://github.com/w3c/csswg-drafts/issues/10226#issuecomment-2065534713
        if (minInGamut && current.isInGamut) {
          min = chroma;
          continue;
        }
  
        clipped = current.toGamut(GamutMapMethod.clip);
        var e = _deltaEOK(clipped, current);
        if (e < _jnd) {
          if (_jnd - e < _epsilon) return clipped;
          minInGamut = false;
          min = chroma;
        } else {
          max = chroma;
        }
      }
      return clipped;
    }
  
    /// Returns the ΔEOK measure between [color1] and [color2].
    double _deltaEOK(SassColor color1, SassColor color2) {
      // Algorithm from https://www.w3.org/TR/css-color-4/#color-difference-OK
      var lab1 = color1.toSpace(ColorSpace.oklab);
      var lab2 = color2.toSpace(ColorSpace.oklab);
  
      return math.sqrt(
        math.pow(lab1.channel0 - lab2.channel0, 2) +
            math.pow(lab1.channel1 - lab2.channel1, 2) +
            math.pow(lab1.channel2 - lab2.channel2, 2),
      );
    }
  }
  