/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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.intellij.openapi.editor.colors;

import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.ui.ColorUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * Contains colors used in editor color scheme.
 */
public abstract class EditorColorPalette {
  protected final EditorColorsScheme myColorsScheme;
  private final MultiMap<Color, TextAttributesKey> myColors = MultiMap.createSmart();

  public final static Comparator<Color> ORDER_NONE = Comparator.comparingInt(EditorColorPalette::getDefaultOrder);
  public final static Comparator<Color> ORDER_BY_INTENSITY = Comparator.comparingInt(EditorColorPalette::getIntensity);

  public EditorColorPalette(EditorColorsScheme colorsScheme) {
    myColorsScheme = colorsScheme;
  }

  public EditorColorPalette withBackgroundColors() {
    return collectColors(attr -> attr.getBackgroundColor());
  }

  public EditorColorPalette withForegroundColors() {
    return collectColors(attr -> attr.getForegroundColor());
  }

  private Collection<Color> orderBy(@Nullable Comparator<? super Color> comparator) {
    List<Color> sorted = new ArrayList<>(myColors.keySet());
    Collections.sort(sorted, comparator);
    return sorted;
  }

  private static int getIntensity(@NotNull Color color) {
    return (color.getRed() + color.getGreen() + color.getBlue()) / 3;
  }

  @Contract(pure = true)
  private static int getDefaultOrder(@NotNull Color color) { return 0; }

  public Collection<Color> getColors(@NotNull Comparator<? super Color> comparator) {
    return comparator == ORDER_NONE ? myColors.keySet() : orderBy(comparator);
  }

  @NotNull
  public Set<Map.Entry<Color, Collection<TextAttributesKey>>> getEntries() {
    return myColors.entrySet();
  }

  /**
   * Collects colors from known color setup pages.
   *
   * @param attrColorReader the function to extract the color from attribute (ex. foreground or background)
   * @return the pallete with collected colors
   */
  public EditorColorPalette collectColors(@NotNull Function<? super TextAttributes, ? extends Color> attrColorReader) {
    return collectColorsWithFilter(attrColorReader, false);
  }

  /**
   * Collects colors from known color setup pages.
   *
   * @param attrColorReader          the function to extract the color from attribute (ex. foreground or background)
   * @param filterOutRainbowAttrKeys the flag to filter out the attributes that can be overwritten by semantic highlighting
   *                                 or not conflicting with semantic highlighting
   * @return the pallete with collected colors
   */
  public EditorColorPalette collectColorsWithFilter(@NotNull Function<? super TextAttributes, ? extends Color> attrColorReader,
                                                    boolean filterOutRainbowAttrKeys) {
    final MultiMap<Color, TextAttributesKey> colors = MultiMap.createSmart();
    for (TextAttributesKey key : getTextAttributeKeys(filterOutRainbowAttrKeys)) {
      TextAttributes attributes = myColorsScheme.getAttributes(key);
      if (attributes != null) {
        Color usedColor = attrColorReader.fun(attributes);
        if (usedColor != null) {
          colors.putValue(usedColor, key);
        }
      }
    }
    myColors.putAllValues(colors);
    return this;
  }

  /**
   * Search for a color which does not exist in the color palette by adjusting brightness of a given {@code sampleColor}.
   *
   * @param sampleColor The sample color to start with.
   * @return An adjusted color or the sample color if it doesn't conflict with the palette or {@code null} if non-conflicting
   * color can't be found with used algorithms of brightness adjustment.
   */
  @Nullable
  public Color getClosestNonConflictingColor(@NotNull Color sampleColor) {
    boolean searchBrighter = ColorUtil.isDark(sampleColor);
    Color foundColor = getClosestNonConflictingColor(sampleColor, getAdjuster(searchBrighter));
    if (foundColor == null) {
      foundColor = getClosestNonConflictingColor(sampleColor, getAdjuster(!searchBrighter));
    }
    return foundColor;
  }

  @Contract(pure = true)
  private static Function<Color, Color> getAdjuster(boolean searchBrighter) {
    return searchBrighter ? Color::brighter : Color::darker;
  }

  /**
   * Search for a color which does not exist in the color palette using a given algorithm of color adjustment.
   *
   * @param sampleColor   The sample color to start with.
   * @param colorAdjuster The color adjustment function, for example {@code Color:brighter}
   * @return An adjusted color or the sample color if it doesn't conflict with the palette or {@code null} if non-conflicting
   * color can't be found with used algorithms of brightness adjustment.
   */
  @Nullable
  public Color getClosestNonConflictingColor(@NotNull Color sampleColor, @NotNull Function<? super Color, ? extends Color> colorAdjuster) {
    if (myColors.containsKey(sampleColor)) {
      Color newColor = colorAdjuster.fun(sampleColor);
      return !sampleColor.equals(newColor) ? getClosestNonConflictingColor(newColor, colorAdjuster) : null;
    }
    return sampleColor;
  }

  protected abstract Collection<TextAttributesKey> getTextAttributeKeys(boolean filterRainbowAttrKeys);
}
