/*
 * Copyright 2014 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.gwt.dev.cfg;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.DefaultConfigurationProperty;
import com.google.gwt.thirdparty.guava.common.base.Splitter;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap.Builder;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * The compiler's representation of a set of configuration properties.
 * These are properties that don't vary by permutation.
 * They may be single-valued or multi-valued.
 */
public class ConfigurationProperties implements Serializable {

  public static final ConfigurationProperties EMPTY =
      new ConfigurationProperties(Collections.<ConfigurationProperty>emptyList());

  private static final Splitter SPLIT_ON_COMMAS =
      Splitter.on(',').omitEmptyStrings().trimResults();

  /**
   * Maps a configuration property's name to its values.
   *
   * <p>The value list may be empty, to represent a multi-valued property that has no values.
   * (This is distinct from a property that doesn't exist at all.)
   *
   * <p>The value list may contain nulls. By default, an undefined, single-valued
   * configuration property is represented as a list containing one null.
   *
   * <p>We don't have a way to distinguish a single-valued property from
   * a multi-valued property that happens to contain one entry. (It's available in
   * {@link ConfigurationProperty} but not preserved.)
   *
   * <p>(We can't use a Multimap due to the above requirements.)
   */
  private final ImmutableMap<String, List<String>> properties;

  /**
   * Takes a snapshot of some ConfigurationProperty instances.
   */
  public ConfigurationProperties(Iterable<ConfigurationProperty> properties) {
    Builder<String, List<String>> builder = ImmutableMap.builder();
    for (ConfigurationProperty property : properties) {
      builder.put(property.getName(), copyOf(property.getValues()));
      }
    this.properties = builder.build();
  }

  /**
   * Takes a snapshot of a module's configuration properties.
   */
  public ConfigurationProperties(ModuleDef def) {
    this(def.getProperties().getConfigurationProperties());
  }

  /**
   * Construct from a map (for testing).
   */
  public ConfigurationProperties(Map<String, List<String>> map) {
    Builder<String, List<String>> builder = ImmutableMap.builder();
    for (Entry<String, List<String>> entry : map.entrySet()) {
      builder.put(entry.getKey(), copyOf(entry.getValue()));
    }
    this.properties = builder.build();
  }

  /**
   * Returns a single-valued property as a boolean if possible.
   * If not set or not single-valued, returns the default value.
   */
  public boolean getBoolean(String key, boolean defaultValue) {
    List<String> values = getStrings(key);
    if (values.size() != 1 || values.get(0) == null) {
      return defaultValue;
    }
    return Boolean.parseBoolean(values.get(0));
  }

  /**
   * Returns a single-valued configuration property as an integer if possible.
   * If not set, not single-valued, or not an integer, returns the default value.
   */
  public int getInteger(String key, int defaultValue) {
    List<String> values = getStrings(key);
    if (values.size() != 1 || values.get(0) == null) {
      return defaultValue;
    }
    try {
      return Integer.parseInt(values.get(0));
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * Returns a single-valued property as a string if possible.
   * If not set or not single-valued, returns the default value.
   */
  public String getString(String key, String defaultValue) {
    List<String> values = getStrings(key);
    if (values.size() != 1 || values.get(0) == null) {
      return defaultValue;
    }
    return values.get(0);
  }

  /**
   * Returns all the values of a multi-valued configuration property, or an empty list
   * if not found.
   *
   * <p>A single-valued and unset configuration property will be returned as a list
   * containing one null.
   */
  public List<String> getStrings(String key) {
    if (!properties.containsKey(key)) {
      return Collections.emptyList();
    }
    return properties.get(key);
  }

  /**
   * Returns all the values of a multi-valued configuration property, or null
   * if not found.
   *
   * <p>A single-valued and unset configuration property will be returned as a list
   * containing one null.
   */
  public List<String> getStringsOrNull(String key) {
     return properties.get(key);
  }

  /**
   * Reads a configuration property as a comma-separated list of strings.
   * It may be a single-valued or multi-valued property. If multi-valued,
   * each value will be split on commas and all items concatenated.
   *
   * <p>Leading and trailing space is automatically trimmed, and nulls and
   * empty strings are automatically ignored.
   *
   * <p>Returns an empty list if the property doesn't exist.
   */
  public List<String> getCommaSeparatedStrings(String key) {
    List<String> result = Lists.newArrayList();
    for (String value : getStrings(key)) {
      if (value != null) {
        result.addAll(SPLIT_ON_COMMAS.splitToList(value));
      }
    }
    return result;
  }

  /**
   * Returns whether the property is multivalued or not. If the property is not defined then it is
   * considered single valued.
   */
  public boolean isMultiValued(String key) {
    return getStrings(key).size() > 1;
  }

  /**
   * Returns the ConfigurationProperty with the given key.
   * (API-compatible with {@link com.google.gwt.core.ext.PropertyOracle}.)
   */
  com.google.gwt.core.ext.ConfigurationProperty getConfigurationProperty(String key)
      throws BadPropertyValueException {
    if (!properties.containsKey(key)) {
      throw new BadPropertyValueException(key);
    }
    return new DefaultConfigurationProperty(key, properties.get(key));
  }

  /**
   * Returns an unmodifiable copy of a list that may contain nulls.
   */
  private static List<String> copyOf(List<String> values) {
    return Collections.unmodifiableList(Lists.newArrayList(values));
  }
}
