/*
 * 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.
 */

package com.google.ux.material.libmonet.score;

import com.google.ux.material.libmonet.hct.Hct;
import com.google.ux.material.libmonet.utils.MathUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
 * based on suitability.
 *
 * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
 * muddied, while curating the high cluster count to a much smaller number of appropriate choices.
 */
public final class Score {
  private static final double TARGET_CHROMA = 48.; // A1 Chroma
  private static final double WEIGHT_PROPORTION = 0.7;
  private static final double WEIGHT_CHROMA_ABOVE = 0.3;
  private static final double WEIGHT_CHROMA_BELOW = 0.1;
  private static final double CUTOFF_CHROMA = 5.;
  private static final double CUTOFF_EXCITED_PROPORTION = 0.01;

  private Score() {}

  public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) {
    // Fallback color is Google Blue.
    return score(colorsToPopulation, 4, 0xff4285f4, true);
  }

  public static List<Integer> score(Map<Integer, Integer> colorsToPopulation, int desired) {
    return score(colorsToPopulation, desired, 0xff4285f4, true);
  }

  public static List<Integer> score(
      Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb) {
    return score(colorsToPopulation, desired, fallbackColorArgb, true);
  }

  /**
   * Given a map with keys of colors and values of how often the color appears, rank the colors
   * based on suitability for being used for a UI theme.
   *
   * @param colorsToPopulation map with keys of colors and values of how often the color appears,
   *     usually from a source image.
   * @param desired max count of colors to be returned in the list.
   * @param fallbackColorArgb color to be returned if no other options available.
   * @param filter whether to filter out undesireable combinations.
   * @return Colors sorted by suitability for a UI theme. The most suitable color is the first item,
   *     the least suitable is the last. There will always be at least one color returned. If all
   *     the input colors were not suitable for a theme, a default fallback color will be provided,
   *     Google Blue.
   */
  public static List<Integer> score(
      Map<Integer, Integer> colorsToPopulation,
      int desired,
      int fallbackColorArgb,
      boolean filter) {

    // Get the HCT color for each Argb value, while finding the per hue count and
    // total count.
    List<Hct> colorsHct = new ArrayList<>();
    int[] huePopulation = new int[360];
    double populationSum = 0.;
    for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
      Hct hct = Hct.fromInt(entry.getKey());
      colorsHct.add(hct);
      int hue = (int) Math.floor(hct.getHue());
      huePopulation[hue] += entry.getValue();
      populationSum += entry.getValue();
    }

    // Hues with more usage in neighboring 30 degree slice get a larger number.
    double[] hueExcitedProportions = new double[360];
    for (int hue = 0; hue < 360; hue++) {
      double proportion = huePopulation[hue] / populationSum;
      for (int i = hue - 14; i < hue + 16; i++) {
        int neighborHue = MathUtils.sanitizeDegreesInt(i);
        hueExcitedProportions[neighborHue] += proportion;
      }
    }

    // Scores each HCT color based on usage and chroma, while optionally
    // filtering out values that do not have enough chroma or usage.
    List<ScoredHCT> scoredHcts = new ArrayList<>();
    for (Hct hct : colorsHct) {
      int hue = MathUtils.sanitizeDegreesInt((int) Math.round(hct.getHue()));
      double proportion = hueExcitedProportions[hue];
      if (filter && (hct.getChroma() < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
        continue;
      }

      double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
      double chromaWeight =
          hct.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
      double chromaScore = (hct.getChroma() - TARGET_CHROMA) * chromaWeight;
      double score = proportionScore + chromaScore;
      scoredHcts.add(new ScoredHCT(hct, score));
    }
    // Sorted so that colors with higher scores come first.
    Collections.sort(scoredHcts, new ScoredComparator());

    // Iterates through potential hue differences in degrees in order to select
    // the colors with the largest distribution of hues possible. Starting at
    // 90 degrees(maximum difference for 4 colors) then decreasing down to a
    // 15 degree minimum.
    List<Hct> chosenColors = new ArrayList<>();
    for (int differenceDegrees = 90; differenceDegrees >= 15; differenceDegrees--) {
      chosenColors.clear();
      for (ScoredHCT entry : scoredHcts) {
        Hct hct = entry.hct;
        boolean hasDuplicateHue = false;
        for (Hct chosenHct : chosenColors) {
          if (MathUtils.differenceDegrees(hct.getHue(), chosenHct.getHue()) < differenceDegrees) {
            hasDuplicateHue = true;
            break;
          }
        }
        if (!hasDuplicateHue) {
          chosenColors.add(hct);
        }
        if (chosenColors.size() >= desired) {
          break;
        }
      }
      if (chosenColors.size() >= desired) {
        break;
      }
    }
    List<Integer> colors = new ArrayList<>();
    if (chosenColors.isEmpty()) {
      colors.add(fallbackColorArgb);
    }
    for (Hct chosenHct : chosenColors) {
      colors.add(chosenHct.toInt());
    }
    return colors;
  }

  private static class ScoredHCT {
    public final Hct hct;
    public final double score;

    public ScoredHCT(Hct hct, double score) {
      this.hct = hct;
      this.score = score;
    }
  }

  private static class ScoredComparator implements Comparator<ScoredHCT> {
    public ScoredComparator() {}

    @Override
    public int compare(ScoredHCT entry1, ScoredHCT entry2) {
      return Double.compare(entry2.score, entry1.score);
    }
  }
}
