// 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 net.starlark.java.eval;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import javax.annotation.Nullable;
import net.starlark.java.syntax.StarlarkType;
import net.starlark.java.syntax.Types;

/**
 * An interface for Starlark values (such as Bazel structs) with fields that may be accessed using
 * Starlark's {@code x.field} notation and optionally updating using an {@code x.f=y} assignment.
 */
public interface Structure extends StarlarkValue {

  /**
   * Returns the value of the field with the given name, or null if the field does not exist. The
   * interpreter (Starlark code) calls the getValue below, which has access to StarlarkSemantics.
   *
   * <p>The set of names for which {@code getValue} returns non-null should match {@code
   * getFieldNames} if possible.
   *
   * @throws EvalException if a user-visible error occurs (other than non-existent field).
   */
  // TODO(adonovan): rename "getField".
  @Nullable
  Object getValue(String name) throws EvalException;

  /**
   * Returns the value of the field with the given name, or null if the field does not exist. The
   * interpreter (Starlark code) calls this getValue, but client code cannot be relied upon to do
   * so, so any checks done on the semantics are incompletely enforced.
   *
   * @param semantics the Starlark semantics, which determine the available fields
   * @param name the name of the field to retrieve
   * @throws EvalException if the field exists but could not be retrieved
   */
  @Nullable
  default Object getValue(StarlarkSemantics semantics, String name) throws EvalException {
    return this.getValue(name);
  }

  /**
   * Returns the names of this value's fields, in some undefined but stable order.
   *
   * <p>A call to {@code getValue} for each of these names should return non-null, though this is
   * not enforced.
   *
   * <p>The Starlark expression {@code dir(x)} reports the union of {@code getFieldNames()} and any
   * StarlarkMethod-annotated fields and methods of this value.
   */
  ImmutableCollection<String> getFieldNames();

  /**
   * Returns the error message to print for an attempt to access an undefined field.
   *
   * <p>May return null to use a default error message.
   */
  @Nullable
  String getErrorMessageForUnknownField(String field);

  /**
   * Updates the named field of this value as if by the Starlark statement {@code this.field =
   * value}.
   *
   * @throws EvalException if the update failed because this value is immutable, does not support
   *     field update, or update of that particular field, or because the value was inappropriate.
   */
  default void setField(String field, Object value) throws EvalException {
    throw Starlark.errorf("%s value does not support field assignment", Starlark.type(this));
  }

  /**
   * Returns the Starlark type of this struct. For efficiency, implementations should override this
   * method to return a memoized value.
   */
  @Override
  default Types.StructType getStarlarkType() {
    ImmutableMap.Builder<String, StarlarkType> fieldTypes =
        ImmutableMap.builderWithExpectedSize(getFieldNames().size());
    for (String fieldName : getFieldNames()) {
      try {
        fieldTypes.put(fieldName, Starlark.getStarlarkType(getValue(fieldName)));
      } catch (EvalException e) {
        // Ignore; if retrieving some internal-only field is an evaluation error, then retrieving it
        // should be a type checking error too.
      }
    }
    return Types.struct(fieldTypes.buildOrThrow());
  }
}
