/*
 * 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.template.soy.data;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.template.soy.data.restricted.CollectionData;
import com.google.template.soy.data.restricted.StringData;

import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;

/**
 * A map data node in a Soy data tree.
 *
 * <p> Important: Even though this class is not marked 'final', do not extend this class.
 *
 */
public class SoyMapData extends CollectionData implements SoyDict {


  /** Underlying map. */
  private final Map<String, SoyData> map;


  public SoyMapData() {
    map = Maps.newLinkedHashMap();
  }


  /**
   * Constructor that initializes this SoyMapData from an existing map.
   * @param data The initial data in an existing map.
   */
  public SoyMapData(Map<String, ?> data) {
    map = new LinkedHashMap<>(data.size());

    for (Map.Entry<String, ?> entry : data.entrySet()) {
      String key;
      try {
        key = entry.getKey();
      } catch (ClassCastException cce) {
        throw new SoyDataException(
            "Attempting to convert a map with non-string key to Soy data (key type " +
            ((Map.Entry<?, ?>) entry).getKey().getClass().getName() + ").");
      }

      Object value = entry.getValue();

      try {
        map.put(key, SoyData.createFromExistingData(value));
      } catch (SoyDataException sde) {
        sde.prependKeyToDataPath(key);
        throw sde;
      }
    }
  }


  /**
   * Constructor that directly takes the keys/values as parameters.
   * @param data The initial data, with alternating keys/values.
   */
  public SoyMapData(Object... data) {
    this();
    put(data);
  }


  /**
   * Important: Please treat this method as superpackage-private. Do not call this method from
   * outside the 'tofu' and 'data' packages.
   *
   * Returns a view of this SoyMapData object as a Map.
   */
  public Map<String, SoyData> asMap() {
    return Collections.unmodifiableMap(map);
  }


  /**
   * Gets the keys in this map data.
   * @return A set containing the keys in this map data.
   */
  public Set<String> getKeys() {
    return Collections.unmodifiableSet(map.keySet());
  }


  /**
   * {@inheritDoc}
   *
   * <p> This method should only be used for debugging purposes.
   */
  @Override public String toString() {
    StringBuilder sb = new StringBuilder();
    try {
      render(sb);
    } catch (IOException e) {
      throw new RuntimeException(e);  // impossible
    }
    return sb.toString();
  }

  @Override public void render(Appendable appendable) throws IOException {
    appendable.append('{');
    Iterator<Map.Entry<String, SoyData>> iterator = map.entrySet().iterator();
    if (iterator.hasNext()) {
      Map.Entry<String, SoyData> entry = iterator.next();
      appendable.append(entry.getKey()).append(": ");
      entry.getValue().render(appendable);
      while (iterator.hasNext()) {
        appendable.append(", ");
        entry = iterator.next();
        appendable.append(entry.getKey()).append(": ");
        entry.getValue().render(appendable);
      }
    }
    appendable.append('}');
  }

  /**
   * {@inheritDoc}
   *
   * <p> A map is always truthy.
   */
  @Deprecated
  @Override public boolean toBoolean() {
    return true;
  }


  @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
  @Override public boolean equals(Object other) {
    return this == other;  // fall back to object equality
  }


  // -----------------------------------------------------------------------------------------------
  // Superpackage-private methods.


  /**
   * Important: Do not use outside of Soy code (treat as superpackage-private).
   *
   * Puts data into this data object at the specified key.
   * @param key An individual key.
   * @param value The data to put at the specified key.
   */
  @Override public void putSingle(String key, SoyData value) {
    map.put(key, value);
  }


  /**
   * Important: Do not use outside of Soy code (treat as superpackage-private).
   *
   * Removes the data at the specified key.
   * @param key An individual key.
   */
  @Override public void removeSingle(String key) {
    map.remove(key);
  }


  /**
   * Important: Do not use outside of Soy code (treat as superpackage-private).
   *
   * Gets the data at the specified key.
   * @param key An individual key.
   * @return The data at the specified key, or null if the key is not defined.
   */
  @Override public SoyData getSingle(String key) {
    return map.get(key);
  }


  // -----------------------------------------------------------------------------------------------
  // SoyDict.


  @Override @Nonnull public Map<String, ? extends SoyValueProvider> asJavaStringMap() {
    return asMap();
  }


  @Override @Nonnull public Map<String, ? extends SoyValue> asResolvedJavaStringMap() {
    return asMap();
  }


  // -----------------------------------------------------------------------------------------------
  // SoyRecord.


  @Override public boolean hasField(String name) {
    return getSingle(name) != null;
  }


  @Override public SoyValue getField(String name) {
    return getSingle(name);
  }


  @Override public SoyValueProvider getFieldProvider(String name) {
    return getSingle(name);
  }


  // -----------------------------------------------------------------------------------------------
  // SoyMap.


  @Override public int getItemCnt() {
    return getKeys().size();
  }


  @Override @Nonnull public Iterable<StringData> getItemKeys() {
    Set<String> internalKeys = getKeys();
    List<StringData> keys = Lists.newArrayListWithCapacity(internalKeys.size());
    for (String internalKey : internalKeys) {
      keys.add(StringData.forValue(internalKey));
    }
    return keys;
  }


  @Override public boolean hasItem(SoyValue key) {
    return getSingle(getStringKey(key)) != null;
  }


  @Override public SoyValue getItem(SoyValue key) {
    return getSingle(getStringKey(key));
  }


  @Override public SoyValueProvider getItemProvider(SoyValue key) {
    return getSingle(getStringKey(key));
  }


  /**
   * Gets the string key out of a SoyValue key, or throws SoyDataException if the key is not a
   * string.
   * @param key The SoyValue key.
   * @return The string key.
   */
  private String getStringKey(SoyValue key) {
    try {
      return key.stringValue();
    } catch (ClassCastException e) {
      throw new SoyDataException(
          "SoyDict accessed with non-string key (got key type " + key.getClass().getName() + ").");
    }
  }

}
