content: /*
   * Copyright 2022 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.
   */
  
  package com.google.material.color.temperature;
  
  import com.google.material.color.hct.Hct;
  import com.google.material.color.utils.ColorUtils;
  import com.google.material.color.utils.MathUtils;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Design utilities using color temperature theory.
   *
   * <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
   * calculations when needed.
   */
  public final class TemperatureCache {
    private final Hct input;
  
    private Hct precomputedComplement;
    private List<Hct> precomputedHctsByTemp;
    private List<Hct> precomputedHctsByHue;
    private Map<Hct, Double> precomputedTempsByHct;
  
    private TemperatureCache() {
      throw new UnsupportedOperationException();
    }
  
    /**
     * Create a cache that allows calculation of ex. complementary and analogous colors.
     *
     * @param input Color to find complement/analogous colors of. Any colors will have the same tone,
     *              and chroma as the input color, modulo any restrictions due to the other hues having lower
     *              limits on chroma.
     */
    public TemperatureCache(Hct input) {
      this.input = input;
    }
  
    /**
     * A color that complements the input color aesthetically.
     *
     * <p>In art, this is usually described as being across the color wheel. History of this shows
     * intent as a color that is just as cool-warm as the input color is warm-cool.
     */
    public Hct getComplement() {
      if (precomputedComplement != null) {
        return precomputedComplement;
      }
  
      double coldestHue = getColdest().getHue();
      double coldestTemp = getTempsByHct().get(getColdest());
  
      double warmestHue = getWarmest().getHue();
      double warmestTemp = getTempsByHct().get(getWarmest());
      double range = warmestTemp - coldestTemp;
      boolean startHueIsColdestToWarmest = isBetween(input.getHue(), coldestHue, warmestHue);
      double startHue = startHueIsColdestToWarmest ? warmestHue : coldestHue;
      double endHue = startHueIsColdestToWarmest ? coldestHue : warmestHue;
      double directionOfRotation = 1.;
      double smallestError = 1000.;
      Hct answer = getHctsByHue().get((int) Math.round(input.getHue()));
  
      double complementRelativeTemp = (1. - getRelativeTemperature(input));
      // Find the color in the other section, closest to the inverse percentile
      // of the input color. This is the complement.
      for (double hueAddend = 0.; hueAddend <= 360.; hueAddend += 1.) {
        double hue = MathUtils.sanitizeDegreesDouble(
                startHue + directionOfRotation * hueAddend);
        if (!isBetween(hue, startHue, endHue)) {
          continue;
        }
        Hct possibleAnswer = getHctsByHue().get((int) Math.round(hue));
        double relativeTemp =
                (getTempsByHct().get(possibleAnswer) - coldestTemp) / range;
        double error = Math.abs(complementRelativeTemp - relativeTemp);
        if (error < smallestError) {
          smallestError = error;
          answer = possibleAnswer;
        }
      }
      precomputedComplement = answer;
      return precomputedComplement;
    }
  
    /**
     * 5 colors that pair well with the input color.
     *
     * <p>The colors are equidistant in temperature and adjacent in hue.
     */
    public List<Hct> getAnalogousColors() {
      return getAnalogousColors(5, 12);
    }
  
    /**
     * A set of colors with differing hues, equidistant in temperature.
     *
     * <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12
     * sections. This method allows provision of either of those values.
     *
     * <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
     *
     * @param count     The number of colors to return, includes the input color.
     * @param divisions The number of divisions on the color wheel.
     */
    public List<Hct> getAnalogousColors(int count, int divisions) {
      // The starting hue is the hue of the input color.
      int startHue = (int) Math.round(input.getHue());
      Hct startHct = getHctsByHue().get(startHue);
      double lastTemp = getRelativeTemperature(startHct);
  
      List<Hct> allColors = new ArrayList<>();
      allColors.add(startHct);
  
      double absoluteTotalTempDelta = 0.f;
      for (int i = 0; i < 360; i++) {
        int hue = MathUtils.sanitizeDegreesInt(startHue + i);
        Hct hct = getHctsByHue().get(hue);
        double temp = getRelativeTemperature(hct);
        double tempDelta = Math.abs(temp - lastTemp);
        lastTemp = temp;
        absoluteTotalTempDelta += tempDelta;
      }
  
      int hueAddend = 1;
      double tempStep = absoluteTotalTempDelta / (double) divisions;
      double totalTempDelta = 0.0;
      lastTemp = getRelativeTemperature(startHct);
      while (allColors.size() < divisions) {
        int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend);
        Hct hct = getHctsByHue().get(hue);
        double temp = getRelativeTemperature(hct);
        double tempDelta = Math.abs(temp - lastTemp);
        totalTempDelta += tempDelta;
  
        double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep);
        boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
        int indexAddend = 1;
        // Keep adding this hue to the answers until its temperature is
        // insufficient. This ensures consistent behavior when there aren't
        // `divisions` discrete steps between 0 and 360 in hue with `tempStep`
        // delta in temperature between them.
        //
        // For example, white and black have no analogues: there are no other
        // colors at T100/T0. Therefore, they should just be added to the array
        // as answers.
        while (indexSatisfied && allColors.size() < divisions) {
          allColors.add(hct);
          desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep);
          indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
          indexAddend++;
        }
        lastTemp = temp;
        hueAddend++;
  
        if (hueAddend > 360) {
          while (allColors.size() < divisions) {
            allColors.add(hct);
          }
          break;
        }
      }
  
      List<Hct> answers = new ArrayList<>();
      answers.add(input);
  
      int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0);
      for (int i = 1; i < (ccwCount + 1); i++) {
        int index = -i;
        while (index < 0) {
          index = allColors.size() + index;
        }
        if (index >= allColors.size()) {
          index = index % allColors.size();
        }
        answers.add(0, allColors.get(index));
      }
  
      int cwCount = count - ccwCount - 1;
      for (int i = 1; i < (cwCount + 1); i++) {
        int index = i;
        while (index < 0) {
          index = allColors.size() + index;
        }
        if (index >= allColors.size()) {
          index = index % allColors.size();
        }
        answers.add(allColors.get(index));
      }
  
      return answers;
    }
  
    /**
     * Temperature relative to all colors with the same chroma and tone.
     *
     * @param hct HCT to find the relative temperature of.
     * @return Value on a scale from 0 to 1.
     */
    public double getRelativeTemperature(Hct hct) {
      double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest());
      double differenceFromColdest =
              getTempsByHct().get(hct) - getTempsByHct().get(getColdest());
      // Handle when there's no difference in temperature between warmest and
      // coldest: for example, at T100, only one color is available, white.
      if (range == 0.) {
        return 0.5;
      }
      return differenceFromColdest / range;
    }
  
    /**
     * Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
     * warm.
     *
     * <p>Color science has researched emotion and harmony, which art uses to select colors. Warm-cool
     * is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in
     * Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and 21.
     *
     * <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
     * Return value has these properties:<br>
     * - Values below 0 are cool, above 0 are warm.<br>
     * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br>
     * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
     */
    public static double rawTemperature(Hct color) {
      double[] lab = ColorUtils.labFromArgb(color.toInt());
      double hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(Math.atan2(lab[2], lab[1])));
      double chroma = Math.hypot(lab[1], lab[2]);
      return -0.5
              + 0.02
              * Math.pow(chroma, 1.07)
              * Math.cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.)));
    }
  
    /** Coldest color with same chroma and tone as input. */
    private Hct getColdest() {
      return getHctsByTemp().get(0);
    }
  
    /**
     * HCTs for all colors with the same chroma/tone as the input.
     *
     * <p>Sorted by hue, ex. index 0 is hue 0.
     */
    private List<Hct> getHctsByHue() {
      if (precomputedHctsByHue != null) {
        return precomputedHctsByHue;
      }
      List<Hct> hcts = new ArrayList<>();
      for (double hue = 0.; hue <= 360.; hue += 1.) {
        Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone());
        hcts.add(colorAtHue);
      }
      precomputedHctsByHue = Collections.unmodifiableList(hcts);
      return precomputedHctsByHue;
    }
  
    /**
     * HCTs for all colors with the same chroma/tone as the input.
     *
     * <p>Sorted from coldest first to warmest last.
     */
    // Prevent lint for Comparator not being available on Android before API level 24, 7.0, 2016.
    // "AndroidJdkLibsChecker" for one linter, "NewApi" for another.
    // A java_library Bazel rule with an Android constraint cannot skip these warnings without this
    // annotation; another solution would be to create an android_library rule and supply
    // AndroidManifest with an SDK set higher than 23.
    @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
    private List<Hct> getHctsByTemp() {
      if (precomputedHctsByTemp != null) {
        return precomputedHctsByTemp;
      }
  
      List<Hct> hcts = new ArrayList<>(getHctsByHue());
      hcts.add(input);
      Comparator<Hct> temperaturesComparator =
              Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo);
      Collections.sort(hcts, temperaturesComparator);
      precomputedHctsByTemp = hcts;
      return precomputedHctsByTemp;
    }
  
    /** Keys of HCTs in getHctsByTemp, values of raw temperature. */
    private Map<Hct, Double> getTempsByHct() {
      if (precomputedTempsByHct != null) {
        return precomputedTempsByHct;
      }
  
      List<Hct> allHcts = new ArrayList<>(getHctsByHue());
      allHcts.add(input);
  
      Map<Hct, Double> temperaturesByHct = new HashMap<>();
      for (Hct hct : allHcts) {
        temperaturesByHct.put(hct, rawTemperature(hct));
      }
  
      precomputedTempsByHct = temperaturesByHct;
      return precomputedTempsByHct;
    }
  
    /** Warmest color with same chroma and tone as input. */
    private Hct getWarmest() {
      return getHctsByTemp().get(getHctsByTemp().size() - 1);
    }
  
    /**
     * Determines if an angle is between two other angles, rotating clockwise.
     */
    private static boolean isBetween(double angle, double a, double b) {
      if (a < b) {
        return a <= angle && angle <= b;
      }
      return a <= angle || angle <= b;
    }
  }
  