/*
 * Copyright 2008 Google Inc.
 *
 * 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.common.css;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;

/**
 * MinimalSubstitutionMap is a SubstitutionMap that renames CSS classes to the
 * shortest string possible.
 *
 * @author bolinfest@google.com (Michael Bolin)
 */
public class MinimalSubstitutionMap implements SubstitutionMap {

  /** Possible first chars in a CSS class name */
  private static final char[] START_CHARS = {
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
      'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
      'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  };

  /** Possible non-first chars in a CSS class name */
  private static final char[] CHARS = {
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
      'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
      'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  };

  /**
   * Last value used with toShortString().
   */
  private int lastIndex;

  /**
   * Characters that can be used at the start of a CSS class name.
   */
  private final char[] startChars;

  /**
   * Characters that can be used in a CSS class name (though not necessarily as
   * the first character).
   */
  private final char[] chars;

  /**
   * Number of startChars.
   */
  private final int startCharsRadix;

  /**
   * Number of chars.
   */
  private final int charsRadix;

  /**
   * Value equal to Math.log(charsRadix). Stored as a field so it does not need
   * to be recomputed each time toShortString() is invoked.
   */
  private final double logCharsRadix;

  /**
   * Map of CSS classes that were renamed. Keys are original class names and
   * values are their renamed equivalents.
   */
  private final Map<String, String> renamedCssClasses;

  /**
   * A set of CSS class names that may not be output from this substitution map.
   */
  private final Set<String> outputValueBlacklist;

  public MinimalSubstitutionMap() {
    this(ImmutableSet.<String>of());
  }

  /**
   * @param outputValueBlacklist A set of CSS class names that may not be
   *     returned as the output from a substitution lookup.
   */
  public MinimalSubstitutionMap(Set<String> outputValueBlacklist) {
    this(START_CHARS, CHARS, outputValueBlacklist);
  }

  /**
   * Creates a new MinimalSubstitutionMap that generates CSS class names from
   * the specified set of characters.
   * @param startChars Possible values for the first character of a CSS class
   *     name.
   * @param chars Possible values for the characters other than the first
   *     character in a CSS class name.
   */
  @VisibleForTesting
  MinimalSubstitutionMap(char[] startChars, char[] chars) {
    this(startChars, chars, ImmutableSet.<String>of());
  }

  /**
   * Creates a new MinimalSubstitutionMap that generates CSS class names from
   * the specified set of characters.
   * @param startChars Possible values for the first character of a CSS class
   *     name.
   * @param chars Possible values for the characters other than the first
   *     character in a CSS class name.
   * @param outputValueBlacklist A set of CSS class names that may not be
   *     returned as the output from a substitution lookup.
   */
  @VisibleForTesting
  MinimalSubstitutionMap(
      char[] startChars, char[] chars, Set<String> outputValueBlacklist) {
    this.lastIndex = 0;
    this.startChars = Arrays.copyOf(startChars, startChars.length);
    this.startCharsRadix = this.startChars.length;
    this.chars = Arrays.copyOf(chars, chars.length);
    this.charsRadix = this.chars.length;
    this.logCharsRadix = Math.log(charsRadix);
    this.renamedCssClasses = Maps.newHashMap();
    this.outputValueBlacklist =
        ImmutableSet.copyOf(Preconditions.checkNotNull(outputValueBlacklist));
  }

  /** {@inheritDoc} */
  @Override
  public String get(String key) {
    String value = renamedCssClasses.get(key);
    if (value == null) {
      do {
        value = toShortString(lastIndex++);
      } while (this.outputValueBlacklist.contains(value));

      renamedCssClasses.put(key, value);
    }
    return value;
  }

  /**
   * Converts a 32-bit integer to a unique short string whose first character
   * is in {@link #START_CHARS} and whose subsequent characters, if any, are
   * in {@link #CHARS}. The result is 1-6 characters in length.
   * @param index The index into the enumeration of possible CSS class names
   *     given the set of valid CSS characters in this class.
   * @return The CSS class name that corresponds to the index of the
   *     enumeration.
   */
  @VisibleForTesting
  String toShortString(int index) {
    // Given the number of non-start characters, C, then for each start
    // character, S, there will be:
    //   1 one-letter CSS class name that starts with S
    //   C two-letter CSS class names that start with S
    //   C^2 three-letter CSS class names that start with S
    //   C^3 four-letter CSS class names that start with S
    //   and so on...
    //
    // That means that the number of non-start characters, n, in terms of i is
    // defined as the greatest value of n that satisfies the following:
    //
    // 1 + C + C^2 + ... + C^(n - 1) <= i
    //
    // Substituting (C^n - 1) / (C - 1) for the geometric series, we get:
    //
    // (C^n - 1) / (C - 1) <= i
    // (C^n - 1) <= i * (C - 1)
    // C^n <= i * (C - 1) + 1
    // log C^n <= log (i * (C - 1) + 1)
    // n log C <= log (i * (C - 1) + 1)
    // n <= log (i * (C - 1) + 1) / log C
    //
    // Because we are looking for the largest value of n that satisfies the
    // inequality and we require n to be an integer, n can be expressed as:
    //
    // n = [[ log (i * (C - 1) + 1) / log C ]]
    //
    // where [[ x ]] is the greatest integer not exceeding x.
    //
    // Once n is known, the standard modulo-then-divide approach can be used to
    // determine each character that should be appended to s.
    int i = index / startCharsRadix;
    final int n = (int) (Math.log(i * (charsRadix - 1) + 1) / logCharsRadix);

    // The array is 1 more than the number of secondary chars to account for the
    // first char.
    char[] cssNameChars = new char[n + 1];
    cssNameChars[0] = startChars[index % startCharsRadix];

    for (int k = 1; k <= n; ++k) {
      cssNameChars[k] = chars[i % charsRadix];
      i /= charsRadix;
    }

    return new String(cssNameChars);
  }
}
