// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.syntax;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.lib.util.StringCanonicalizer;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import javax.annotation.Nullable;

/**
 *  <p>Root of Type symbol hierarchy for values in the build language.</p>
 *
 *  <p>Type symbols are primarily used for their <code>convert</code> method,
 *  which is a kind of cast operator enabling conversion from untyped (Object)
 *  references to values in the build language, to typed references.</p>
 *
 *  <p>For example, this code type-converts a value <code>x</code> returned by
 *  the evaluator, to a list of strings:</p>
 *
 *  <pre>
 *  Object x = expr.eval(env);
 *  List&lt;String&gt; s = Type.STRING_LIST.convert(x);
 *  </pre>
 */
public abstract class Type<T> {

  protected Type() {}

  /**
   * Converts untyped Object x resulting from the evaluation of an expression in the build language,
   * into a typed object of type T.
   *
   * <p>x must be *directly* convertible to this type. This therefore disqualifies "selector
   * expressions" of the form "{ config1: 'value1_of_orig_type', config2: 'value2_of_orig_type; }"
   * (which support configurable attributes). To handle those expressions, see
   * {@link com.google.devtools.build.lib.packages.BuildType#selectableConvert}.
   *
   * @param x the build-interpreter value to convert.
   * @param what an object having a toString describing what x is for; should be included in
   *    any exception thrown.  Grammatically, must produce a string describe a syntactic
   *    construct, e.g. "attribute 'srcs' of rule foo".
   * @param context the label of the current BUILD rule; must be non-null if resolution of
   *    package-relative label strings is required
   * @throws ConversionException if there was a problem performing the type conversion
   */
  public abstract T convert(Object x, Object what, @Nullable Object context)
      throws ConversionException;
  // TODO(bazel-team): Check external calls (e.g. in PackageFactory), verify they always want
  // this over selectableConvert.

  /**
   * Equivalent to {@link #convert(Object, String, Object)} where the label is {@code null}.
   * Useful for converting values to types that do not involve the type {@code LABEL}
   * and hence do not require the label of the current package.
   */
  public final T convert(Object x, Object what) throws ConversionException {
    return convert(x, what, null);
  }

  /**
   * Like {@link #convert(Object, String, Object)}, but converts skylark {@code None}
   * to given {@code defaultValue}.
   */
  @Nullable public final T convertOptional(Object x,
      String what, @Nullable Object context, T defaultValue)
      throws ConversionException {
    if (EvalUtils.isNullOrNone(x)) {
      return defaultValue;
    }
    return convert(x, what, context);
  }

  /**
   * Like {@link #convert(Object, String, Object)}, but converts skylark {@code None}
   * to java {@code null}.
   */
  @Nullable public final T convertOptional(Object x, String what, @Nullable Object context)
      throws ConversionException {
    return convertOptional(x, what, context, null);
  }

  /**
   * Like {@link #convert(Object, String)}, but converts skylark {@code NONE} to java {@code null}.
   */
  @Nullable public final T convertOptional(Object x, String what) throws ConversionException {
    return convertOptional(x, what, null);
  }

  public abstract T cast(Object value);

  @Override
  public abstract String toString();

  /**
   * Returns the default value for this type; may return null iff no default is defined for this
   * type.
   */
  public abstract T getDefaultValue();

  /** Function accepting a (potentially null) object value. See {@link #visitLabels}. */
  public static interface LabelVisitor {
    void visit(@Nullable Label label) throws InterruptedException;
  }

  /**
   * Extracts all labels associated with the instance of the type to visitor.
   *
   * <p>This is used to support reliable label visitation in
   * {@link com.google.devtools.build.lib.packages.AbstractAttributeMapper#visitLabels}. To preserve
   * that reliability, every type should faithfully define its own instance of this method. In other
   * words, be careful about defining default instances in base types that get auto-inherited by
   * their children. Keep all definitions as explicit as possible.
   */
  public abstract void visitLabels(LabelVisitor visitor, Object value) throws InterruptedException;

  /**
   * Implementation of concatenation for this type (e.g. "val1 + val2"). Returns null to
   * indicate concatenation isn't supported.
   */
  public T concat(@SuppressWarnings("unused") Iterable<T> elements) {
    return null;
  }

  /**
   * Converts an initialized Type object into a tag set representation.
   * This operation is only valid for certain sub-Types which are guaranteed
   * to be properly initialized.
   *
   * @param value the actual value
   * @throws UnsupportedOperationException if the concrete type does not support
   * tag conversion or if a convertible type has no initialized value.
   */
  public Set<String> toTagSet(Object value, String name) {
    String msg = "Attribute " + name + " does not support tag conversion.";
    throw new UnsupportedOperationException(msg);
  }

  /**
   * The type of an integer.
   */
  public static final Type<Integer> INTEGER = new IntegerType();

  /**
   * The type of a string.
   */
  public static final Type<String> STRING = new StringType();

  /**
   * The type of a boolean.
   */
  public static final Type<Boolean> BOOLEAN = new BooleanType();

  /**
   *  The type of a list of not-yet-typed objects.
   */
  public static final ObjectListType OBJECT_LIST = new ObjectListType();

  /**
   *  The type of a list of {@linkplain #STRING strings}.
   */
  public static final ListType<String> STRING_LIST = ListType.create(STRING);

  /**
   *  The type of a list of {@linkplain #INTEGER strings}.
   */
  public static final ListType<Integer> INTEGER_LIST = ListType.create(INTEGER);

  /**
   *  The type of a dictionary of {@linkplain #STRING strings}.
   */
  public static final DictType<String, String> STRING_DICT = DictType.create(STRING, STRING);

  /**
   * The type of a dictionary of {@linkplain #STRING_LIST label lists}.
   */
  public static final DictType<String, List<String>> STRING_LIST_DICT =
      DictType.create(STRING, STRING_LIST);

  /**
   * The type of a dictionary of {@linkplain #STRING strings}, where each entry
   * maps to a single string value.
   */
  public static final DictType<String, String> STRING_DICT_UNARY = DictType.create(STRING, STRING);

  /**
   *  For ListType objects, returns the type of the elements of the list; for
   *  all other types, returns null.  (This non-obvious implementation strategy
   *  is necessitated by the wildcard capture rules of the Java type system,
   *  which disallow conversion from Type{List{ELEM}} to Type{List{?}}.)
   */
  public Type<?> getListElementType() {
    return null;
  }

  /**
   *  ConversionException is thrown when a type-conversion fails; it contains
   *  an explanatory error message.
   */
  public static class ConversionException extends EvalException {
    private static String message(Type<?> type, Object value, @Nullable Object what) {
      StringBuilder builder = new StringBuilder();
      builder.append("expected value of type '").append(type).append("'");
      if (what != null) {
        builder.append(" for ").append(what);
      }
      builder.append(", but got ");
      Printer.write(builder, value);
      builder.append(" (").append(EvalUtils.getDataTypeName(value)).append(")");
      return builder.toString();
    }

    public ConversionException(Type<?> type, Object value, @Nullable Object what) {
      super(null, message(type, value, what));
    }

    public ConversionException(String message) {
      super(null, message);
    }
  }

  /********************************************************************
   *                                                                  *
   *                            Subclasses                            *
   *                                                                  *
   ********************************************************************/

  private static class ObjectType extends Type<Object> {
    @Override
    public Object cast(Object value) {
      return value;
    }

    @Override
    public String getDefaultValue() {
      throw new UnsupportedOperationException(
          "ObjectType has no default value");
    }

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) {
    }

    @Override
    public String toString() {
      return "object";
    }

    @Override
    public Object convert(Object x, Object what, Object context) {
      return x;
    }
  }

  private static class IntegerType extends Type<Integer> {
    @Override
    public Integer cast(Object value) {
      return (Integer) value;
    }

    @Override
    public Integer getDefaultValue() {
      return 0;
    }

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) {
    }

    @Override
    public String toString() {
      return "int";
    }

    @Override
    public Integer convert(Object x, Object what, Object context)
        throws ConversionException {
      if (!(x instanceof Integer)) {
        throw new ConversionException(this, x, what);
      }
      return (Integer) x;
    }

    @Override
    public Integer concat(Iterable<Integer> elements) {
      int ans = 0;
      for (Integer elem : elements) {
        ans += elem;
      }
      return Integer.valueOf(ans);
    }
  }

  private static class BooleanType extends Type<Boolean> {
    @Override
    public Boolean cast(Object value) {
      return (Boolean) value;
    }

    @Override
    public Boolean getDefaultValue() {
      return false;
    }

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) {
    }

    @Override
    public String toString() {
      return "boolean";
    }

    // Conversion to boolean must also tolerate integers of 0 and 1 only.
    @Override
    public Boolean convert(Object x, Object what, Object context)
        throws ConversionException {
      if (x instanceof Boolean) {
        return (Boolean) x;
      }
      Integer xAsInteger = INTEGER.convert(x, what, context);
      if (xAsInteger == 0) {
        return false;
      } else if (xAsInteger == 1) {
        return true;
      }
      throw new ConversionException("boolean is not one of [0, 1]");
    }

    /**
     * Booleans attributes are converted to tags based on their names.
     */
    @Override
    public Set<String> toTagSet(Object value, String name) {
      if (value == null) {
        String msg = "Illegal tag conversion from null on Attribute " + name  + ".";
        throw new IllegalStateException(msg);
      }
      String tag = (Boolean) value ? name : "no" + name;
      return ImmutableSet.of(tag);
    }
  }

  private static class StringType extends Type<String> {
    @Override
    public String cast(Object value) {
      return (String) value;
    }

    @Override
    public String getDefaultValue() {
      return "";
    }

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) {
    }

    @Override
    public String toString() {
      return "string";
    }

    @Override
    public String convert(Object x, Object what, Object context)
        throws ConversionException {
      if (!(x instanceof String)) {
        throw new ConversionException(this, x, what);
      }
      return StringCanonicalizer.intern((String) x);
    }

    @Override
    public String concat(Iterable<String> elements) {
      return Joiner.on("").join(elements);
    }

    /**
     * A String is representable as a set containing its value.
     */
    @Override
    public Set<String> toTagSet(Object value, String name) {
      if (value == null) {
        String msg = "Illegal tag conversion from null on Attribute " + name + ".";
        throw new IllegalStateException(msg);
      }
      return ImmutableSet.of((String) value);
    }
  }

  /**
   * A type to support dictionary attributes.
   */
  public static class DictType<KeyT, ValueT> extends Type<Map<KeyT, ValueT>> {

    private final Type<KeyT> keyType;
    private final Type<ValueT> valueType;

    private final Map<KeyT, ValueT> empty = ImmutableMap.of();

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) throws InterruptedException {
      for (Entry<KeyT, ValueT> entry : cast(value).entrySet()) {
        keyType.visitLabels(visitor, entry.getKey());
        valueType.visitLabels(visitor, entry.getValue());
      }
    }

    public static <KEY, VALUE> DictType<KEY, VALUE> create(
        Type<KEY> keyType, Type<VALUE> valueType) {
      return new DictType<>(keyType, valueType);
    }

    private DictType(Type<KeyT> keyType, Type<ValueT> valueType) {
      this.keyType = keyType;
      this.valueType = valueType;
    }

    public Type<KeyT> getKeyType() {
      return keyType;
    }

    public Type<ValueT> getValueType() {
      return valueType;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Map<KeyT, ValueT> cast(Object value) {
      return (Map<KeyT, ValueT>) value;
    }

    @Override
    public String toString() {
      return "dict(" + keyType + ", " + valueType + ")";
    }

    @Override
    public Map<KeyT, ValueT> convert(Object x, Object what, Object context)
        throws ConversionException {
      if (!(x instanceof Map<?, ?>)) {
        throw new ConversionException(String.format(
            "Expected a map for dictionary but got a %s", x.getClass().getName()));
      }
      // Order the keys so the return value will be independent of insertion order.
      Map<KeyT, ValueT> result = new TreeMap<>();
      Map<?, ?> o = (Map<?, ?>) x;
      for (Entry<?, ?> elem : o.entrySet()) {
        result.put(
            keyType.convert(elem.getKey(), "dict key element", context),
            valueType.convert(elem.getValue(), "dict value element", context));
      }
      return ImmutableMap.copyOf(result);
    }

    @Override
    public Map<KeyT, ValueT> getDefaultValue() {
      return empty;
    }
  }

  /** A type for lists of a given element type */
  public static class ListType<ElemT> extends Type<List<ElemT>> {

    private final Type<ElemT> elemType;

    private final List<ElemT> empty = ImmutableList.of();

    public static <ELEM> ListType<ELEM> create(Type<ELEM> elemType) {
      return new ListType<>(elemType);
    }

    private ListType(Type<ElemT> elemType) {
      this.elemType = elemType;
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<ElemT> cast(Object value) {
      return (List<ElemT>) value;
    }

    @Override
    public Type<ElemT> getListElementType() {
      return elemType;
    }

    @Override
    public List<ElemT> getDefaultValue() {
      return empty;
    }

    @Override
    public void visitLabels(LabelVisitor visitor, Object value) throws InterruptedException {
      for (ElemT elem : cast(value)) {
        elemType.visitLabels(visitor, elem);
      }
    }

    @Override
    public String toString() {
      return "list(" + elemType + ")";
    }

    @Override
    public List<ElemT> convert(Object x, Object what, Object context)
        throws ConversionException {
      Iterable<?> iterable;
      try {
        iterable = EvalUtils.toIterableStrict(x, null);
      } catch (EvalException ex) {
        throw new ConversionException(this, x, what);
      }
      int index = 0;
      List<ElemT> result = new ArrayList<>(Iterables.size(iterable));
      ListConversionContext conversionContext = new ListConversionContext(what);
      for (Object elem : iterable) {
        conversionContext.update(index);
        ElemT converted = elemType.convert(elem, conversionContext, context);
        if (converted != null) {
          result.add(converted);
        } else {
          // shouldn't happen but it does, rarely
          String message = "Converting a list with a null element: "
              + "element " + index + " of " + what + " in " + context;
          LoggingUtil.logToRemote(Level.WARNING, message,
              new ConversionException(message));
        }
        ++index;
      }
      // We preserve GlobList-s so they can make it to attributes;
      // some external code relies on attributes preserving this information.
      // TODO(bazel-team): somehow make Skylark extensible enough that
      // GlobList support can be wholly moved out of Skylark into an extension.
      if (x instanceof GlobList<?>) {
        return new GlobList<>(((GlobList<?>) x).getCriteria(), result);
      }
      if (x instanceof MutableList) {
        GlobList<?> globList = ((MutableList) x).getGlobList();
        if (globList != null) {
          return new GlobList<>(globList.getCriteria(), result);
        }
      }
      return result;
    }

    @Override
    public List<ElemT> concat(Iterable<List<ElemT>> elements) {
      ImmutableList.Builder<ElemT> builder = ImmutableList.builder();
      for (List<ElemT> list : elements) {
        builder.addAll(list);
      }
      return builder.build();
    }

    /**
     * A list is representable as a tag set as the contents of itself expressed
     * as Strings. So a {@code List<String>} is effectively converted to a {@code Set<String>}.
     */
    @Override
    public Set<String> toTagSet(Object items, String name) {
      if (items == null) {
        String msg = "Illegal tag conversion from null on Attribute" + name + ".";
        throw new IllegalStateException(msg);
      }
      Set<String> tags = new LinkedHashSet<>();
      @SuppressWarnings("unchecked")
      List<ElemT> itemsAsListofElem = (List<ElemT>) items;
      for (ElemT element : itemsAsListofElem) {
        tags.add(element.toString());
      }
      return tags;
    }

    /**
     * Provides a {@link #toString()} description of the context of the value in a list being
     * converted. This is preferred over a raw string to avoid uselessly constructing strings which
     * are never used. This class is mutable (the index is updated).
     */
    private static class ListConversionContext {
      private final Object what;
      private int index = 0;

      ListConversionContext(Object what) {
        this.what = what;
      }

      void update(int index) {
        this.index = index;
      }

      @Override
      public String toString() {
        return "element " + index + " of " + what;
      }

    }
  }

  /** Type for lists of arbitrary objects */
  public static class ObjectListType extends ListType<Object> {

    private static final Type<Object> elemType = new ObjectType();

    private ObjectListType() {
      super(elemType);
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Object> convert(Object x, Object what, Object context)
        throws ConversionException {
      if (x instanceof SkylarkList) {
        return ((SkylarkList) x).getImmutableList();
      } else if (x instanceof List) {
        return (List<Object>) x;
      } else if (x instanceof Iterable) {
        // Do not remove <Object>: workaround for Java 7 type inference.
        return ImmutableList.<Object>copyOf((Iterable<?>) x);
      } else {
        throw new ConversionException(this, x, what);
      }
    }
  }

  /**
   * The type of a general list.
   */
  public static final ListType<Object> LIST = new ListType<>(new ObjectType());
}
