// 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
package com.google.devtools.build.skyframe;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * Encapsulation of data stored by {@link NodeEntry} when the value has finished building.
 *
 * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
 */
public abstract class ValueWithMetadata implements SkyValue {
  protected final SkyValue value;

  private static final NestedSet<TaggedEvents> NO_EVENTS =
      NestedSetBuilder.<TaggedEvents>emptySet(Order.STABLE_ORDER);

  private static final NestedSet<Postable> NO_POSTS =
      NestedSetBuilder.<Postable>emptySet(Order.STABLE_ORDER);

  private ValueWithMetadata(SkyValue value) {
    this.value = value;
  }

  /**
   * Builds a value entry value that has an error (and no value value).
   *
   * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
   */
  public static ValueWithMetadata error(
      ErrorInfo errorInfo,
      NestedSet<TaggedEvents> transitiveEvents,
      NestedSet<Postable> transitivePostables) {
    return (ValueWithMetadata) normal(null, errorInfo, transitiveEvents, transitivePostables);
  }

  /**
   * Builds a SkyValue that has a value, and possibly an error, and possibly events/postables. If it
   * has only a value, returns just the value in order to save memory.
   *
   * <p>This is public only for use in alternative {@code MemoizingEvaluator} implementations.
   */
  public static SkyValue normal(
      @Nullable SkyValue value,
      @Nullable ErrorInfo errorInfo,
      NestedSet<TaggedEvents> transitiveEvents,
      NestedSet<Postable> transitivePostables) {
    Preconditions.checkState(value != null || errorInfo != null,
        "Value and error cannot both be null");
    if (errorInfo == null) {
      return (transitiveEvents.isEmpty() && transitivePostables.isEmpty())
          ? value
          : ValueWithEvents.createValueWithEvents(value, transitiveEvents, transitivePostables);
    }
    return new ErrorInfoValue(errorInfo, value, transitiveEvents, transitivePostables);
  }

  @Nullable SkyValue getValue() {
    return value;
  }

  @Nullable
  abstract ErrorInfo getErrorInfo();

  public abstract NestedSet<TaggedEvents> getTransitiveEvents();

  public abstract NestedSet<Postable> getTransitivePostables();

  /** Implementation of {@link ValueWithMetadata} for the value case. */
  @VisibleForTesting
  public static class ValueWithEvents extends ValueWithMetadata {
    private final NestedSet<TaggedEvents> transitiveEvents;
    private final NestedSet<Postable> transitivePostables;

    private ValueWithEvents(
        SkyValue value,
        NestedSet<TaggedEvents> transitiveEvents,
        NestedSet<Postable> transitivePostables) {
      super(Preconditions.checkNotNull(value));
      this.transitiveEvents = Preconditions.checkNotNull(transitiveEvents);
      this.transitivePostables = Preconditions.checkNotNull(transitivePostables);
    }

    private static ValueWithEvents createValueWithEvents(
        SkyValue value,
        NestedSet<TaggedEvents> transitiveEvents,
        NestedSet<Postable> transitivePostables) {
      if (value instanceof NotComparableSkyValue) {
        return new NotComparableValueWithEvents(value, transitiveEvents, transitivePostables);
      } else {
        return new ValueWithEvents(value, transitiveEvents, transitivePostables);
      }
    }

    @Nullable
    @Override
    ErrorInfo getErrorInfo() { return null; }

    @Override
    public NestedSet<TaggedEvents> getTransitiveEvents() { return transitiveEvents; }

    @Override
    public NestedSet<Postable> getTransitivePostables() {
      return transitivePostables;
    }

    /**
     * We override equals so that if the same value is written to a {@link NodeEntry} twice, it can
     * verify that the two values are equal, and avoid incrementing its version.
     */
    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      ValueWithEvents that = (ValueWithEvents) o;

      // Shallow equals is a middle ground between using default equals, which might miss
      // nested sets with the same elements, and deep equality checking, which would be expensive.
      // All three choices are sound, since shallow equals and default equals are more
      // conservative than deep equals. Using shallow equals means that we may unnecessarily
      // consider some values unequal that are actually equal, but this is still a net win over
      // deep equals.
      return value.equals(that.value)
          && transitiveEvents.shallowEquals(that.transitiveEvents)
          && transitivePostables.shallowEquals(that.transitivePostables);
    }

    @Override
    public int hashCode() {
      return 31 * value.hashCode()
          + transitiveEvents.shallowHashCode()
          + 3 * transitivePostables.shallowHashCode();
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("value", value)
          .add("transitiveEvents size", transitiveEvents.toList().size())
          .add("transitivePostables size", transitivePostables.toList().size())
          .toString();
    }
  }

  private static final class NotComparableValueWithEvents extends ValueWithEvents
          implements NotComparableSkyValue {
    private NotComparableValueWithEvents(
        SkyValue value,
        NestedSet<TaggedEvents> transitiveEvents,
        NestedSet<Postable> transitivePostables) {
      super(value, transitiveEvents, transitivePostables);
    }
  }

  /**
   * Implementation of {@link ValueWithMetadata} for the error case.
   *
   * <p>Mark NotComparableSkyValue because it's unlikely that re-evaluation gives the same error.
   */
  private static final class ErrorInfoValue extends ValueWithMetadata
      implements NotComparableSkyValue {

    private final ErrorInfo errorInfo;
    private final NestedSet<TaggedEvents> transitiveEvents;
    private final NestedSet<Postable> transitivePostables;

    public ErrorInfoValue(
        ErrorInfo errorInfo,
        @Nullable SkyValue value,
        NestedSet<TaggedEvents> transitiveEvents,
        NestedSet<Postable> transitivePostables) {
      super(value);
      this.errorInfo = Preconditions.checkNotNull(errorInfo);
      this.transitiveEvents = Preconditions.checkNotNull(transitiveEvents);
      this.transitivePostables = Preconditions.checkNotNull(transitivePostables);
    }

    @Nullable
    @Override
    ErrorInfo getErrorInfo() { return errorInfo; }

    @Override
    public NestedSet<TaggedEvents> getTransitiveEvents() { return transitiveEvents; }

    @Override
    public NestedSet<Postable> getTransitivePostables() {
      return transitivePostables;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      ErrorInfoValue that = (ErrorInfoValue) o;

      // Shallow equals is a middle ground between using default equals, which might miss
      // nested sets with the same elements, and deep equality checking, which would be expensive.
      // All three choices are sound, since shallow equals and default equals are more
      // conservative than deep equals. Using shallow equals means that we may unnecessarily
      // consider some values unequal that are actually equal, but this is still a net win over
      // deep equals.
      return Objects.equals(this.value, that.value)
          && Objects.equals(this.errorInfo, that.errorInfo)
          && transitiveEvents.shallowEquals(that.transitiveEvents)
          && transitivePostables.shallowEquals(that.transitivePostables);
    }

    @Override
    public int hashCode() {
      return 31 * Objects.hash(value, errorInfo)
          + transitiveEvents.shallowHashCode()
          + 3 * transitivePostables.shallowHashCode();
    }

    @Override
    public String toString() {
      StringBuilder result = new StringBuilder();
      if (value != null) {
        result.append("Value: ").append(value);
      }
      if (errorInfo != null) {
        if (result.length() > 0) {
          result.append("; ");
        }
        result.append("Error: ").append(errorInfo);
      }
      return result.toString();
    }
  }

  @Nullable
  public static SkyValue justValue(SkyValue value) {
    if (value instanceof ValueWithMetadata) {
      return ((ValueWithMetadata) value).getValue();
    }
    return value;
  }

  public static ValueWithMetadata wrapWithMetadata(SkyValue value) {
    if (value instanceof ValueWithMetadata) {
      return (ValueWithMetadata) value;
    }
    return ValueWithEvents.createValueWithEvents(value, NO_EVENTS, NO_POSTS);
  }

  @Nullable
  public static ErrorInfo getMaybeErrorInfo(SkyValue value) {
    if (value.getClass() == ErrorInfoValue.class) {
      return ((ValueWithMetadata) value).getErrorInfo();
    }
    return null;
  }

  @VisibleForTesting
  public static NestedSet<TaggedEvents> getEvents(SkyValue value) {
    if (value instanceof ValueWithMetadata) {
      return ((ValueWithMetadata) value).getTransitiveEvents();
    }
    return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
  }

  static NestedSet<Postable> getPosts(SkyValue value) {
    if (value instanceof ValueWithMetadata) {
      return ((ValueWithMetadata) value).getTransitivePostables();
    }
    return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
  }
}
