// 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.skyframe;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.Aspect;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.AspectDescriptor;
import com.google.devtools.build.lib.packages.AspectParameters;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.syntax.SkylarkImport;
import com.google.devtools.build.skyframe.SkyFunctionName;
import java.util.List;
import javax.annotation.Nullable;

/**
 * An aspect in the context of the Skyframe graph.
 */
public final class AspectValue extends ActionLookupValue {

  /**
   * A base class for keys that have AspectValue as a Sky value.
   */
  public abstract static class AspectValueKey extends ActionLookupKey {
    public abstract String getDescription();
  }

  /**
   * A base class for a key representing an aspect applied to a particular target.
   */
  public static final class AspectKey extends AspectValueKey {
    private final Label label;
    private final ImmutableList<AspectKey> baseKeys;
    private final BuildConfiguration aspectConfiguration;
    private final BuildConfiguration baseConfiguration;
    private final AspectDescriptor aspectDescriptor;

    private AspectKey(
        Label label,
        BuildConfiguration baseConfiguration,
        ImmutableList<AspectKey> baseKeys,
        AspectDescriptor aspectDescriptor,
        BuildConfiguration aspectConfiguration) {
      this.baseKeys = baseKeys;
      this.label = label;
      this.baseConfiguration = baseConfiguration;
      this.aspectConfiguration = aspectConfiguration;
      this.aspectDescriptor = aspectDescriptor;
    }

    @Override
    protected SkyFunctionName getType() {
      return SkyFunctions.ASPECT;
    }


    @Override
    public Label getLabel() {
      return label;
    }

    public AspectClass getAspectClass() {
      return aspectDescriptor.getAspectClass();
    }

    @Nullable
    public AspectParameters getParameters() {
      return aspectDescriptor.getParameters();
    }

    public AspectDescriptor getAspectDescriptor() {
      return aspectDescriptor;
    }

    @Nullable
    public ImmutableList<AspectKey> getBaseKeys() {
      return baseKeys;
    }

    @Override
    public String getDescription() {
      if (baseKeys.isEmpty()) {
        return String.format("%s of %s",
            aspectDescriptor.getAspectClass().getName(), getLabel());
      } else {
        return String.format("%s on top of %s",
            aspectDescriptor.getAspectClass().getName(), baseKeys.toString());
      }
    }

    /**
     * Returns the configuration to be used for the evaluation of the aspect itself.
     *
     * <p>In dynamic configuration mode, the aspect may require more fragments than the target on
     * which it is being evaluated; in addition to configuration fragments required by the target
     * and its dependencies, an aspect has configuration fragment requirements of its own, as well
     * as dependencies of its own with their own configuration fragment requirements.
     *
     * <p>The aspect configuration contains all of these fragments, and is used to create the
     * aspect's RuleContext and to retrieve the dependencies. Note that dependencies will have their
     * configurations trimmed from this one as normal.
     *
     * <p>Because of these properties, this configuration is always a superset of that returned by
     * {@link #getBaseConfiguration()}. In static configuration mode, this configuration will be
     * equivalent to that returned by {@link #getBaseConfiguration()}.
     *
     * @see #getBaseConfiguration()
     */
    public BuildConfiguration getAspectConfiguration() {
      return aspectConfiguration;
    }

    /**
     * Returns the configuration to be used for the base target.
     *
     * <p>In dynamic configuration mode, the configured target this aspect is attached to may have
     * a different configuration than the aspect itself (see the documentation for
     * {@link #getAspectConfiguration()} for an explanation why). The base configuration is the one
     * used to construct a key to look up the base configured target.
     *
     * @see #getAspectConfiguration()
     */
    public BuildConfiguration getBaseConfiguration() {
      return baseConfiguration;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(
          label,
          baseKeys,
          aspectConfiguration,
          baseConfiguration,
          aspectDescriptor);
    }

    @Override
    public boolean equals(Object other) {
      if (this == other) {
        return true;
      }

      if (!(other instanceof AspectKey)) {
        return false;
      }

      AspectKey that = (AspectKey) other;
      return Objects.equal(label, that.label)
          && Objects.equal(baseKeys, that.baseKeys)
          && Objects.equal(aspectConfiguration, that.aspectConfiguration)
          && Objects.equal(baseConfiguration, that.baseConfiguration)
          && Objects.equal(aspectDescriptor, that.aspectDescriptor);
    }

    public String prettyPrint() {
      if (label == null) {
        return "null";
      }

      String baseKeysString =
          baseKeys.isEmpty()
          ? ""
          : String.format(" (over %s)", baseKeys.toString());
      return String.format("%s with aspect %s%s%s",
          label.toString(),
          aspectDescriptor.getAspectClass().getName(),
          (aspectConfiguration != null && aspectConfiguration.isHostConfiguration())
              ? "(host) " : "",
          baseKeysString
      );
    }

    @Override
    public String toString() {
      return (baseKeys == null ? label : baseKeys.toString())
          + "#"
          + aspectDescriptor.getAspectClass().getName()
          + " "
          + (aspectConfiguration == null ? "null" : aspectConfiguration.checksum())
          + " "
          + (baseConfiguration == null ? "null" : baseConfiguration.checksum())
          + " "
          + aspectDescriptor.getParameters();
    }

    public AspectKey withLabel(Label label) {
      ImmutableList.Builder<AspectKey> newBaseKeys = ImmutableList.builder();
      for (AspectKey baseKey : baseKeys) {
        newBaseKeys.add(baseKey.withLabel(label));
      }

      return new AspectKey(
          label, baseConfiguration,
          newBaseKeys.build(), aspectDescriptor, aspectConfiguration);
    }
  }

  /**
   * The key for a skylark aspect.
   */
  public static class SkylarkAspectLoadingKey extends AspectValueKey {

    private final Label targetLabel;
    private final BuildConfiguration aspectConfiguration;
    private final BuildConfiguration targetConfiguration;
    private final SkylarkImport skylarkImport;
    private final String skylarkValueName;

    private SkylarkAspectLoadingKey(
        Label targetLabel,
        BuildConfiguration aspectConfiguration,
        BuildConfiguration targetConfiguration,
        SkylarkImport skylarkImport,
        String skylarkFunctionName) {
      this.targetLabel = targetLabel;
      this.aspectConfiguration = aspectConfiguration;
      this.targetConfiguration = targetConfiguration;

      this.skylarkImport = skylarkImport;
      this.skylarkValueName = skylarkFunctionName;
    }

    @Override
    protected SkyFunctionName getType() {
      return SkyFunctions.LOAD_SKYLARK_ASPECT;
    }

    public Label getTargetLabel() {
      return targetLabel;
    }

    public String getSkylarkValueName() {
      return skylarkValueName;
    }

    public SkylarkImport getSkylarkImport() {
      return skylarkImport;
    }

    /**
     * @see AspectKey#getAspectConfiguration()
     */
    public BuildConfiguration getAspectConfiguration() {
      return aspectConfiguration;
    }

    /**
     * @see AspectKey#getBaseConfiguration()
     */
    public BuildConfiguration getTargetConfiguration() {
      return targetConfiguration;
    }

    @Override
    public String getDescription() {
      // Skylark aspects are referred to on command line with <file>%<value ame>
      return String.format("%s%%%s of %s", skylarkImport.getImportString(),
          skylarkValueName, targetLabel);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(targetLabel,
          aspectConfiguration,
          targetConfiguration,
          skylarkImport,
          skylarkValueName);
    }

    @Override
    public boolean equals(Object o) {
      if (o == this) {
        return true;
      }

      if (!(o instanceof SkylarkAspectLoadingKey)) {
        return false;
      }
      SkylarkAspectLoadingKey that = (SkylarkAspectLoadingKey) o;
      return Objects.equal(targetLabel, that.targetLabel)
          && Objects.equal(aspectConfiguration, that.aspectConfiguration)
          && Objects.equal(targetConfiguration, that.targetConfiguration)
          && Objects.equal(skylarkImport, that.skylarkImport)
          && Objects.equal(skylarkValueName, that.skylarkValueName);

    }
  }


  // These variables are only non-final because they may be clear()ed to save memory. They are null
  // only after they are cleared.
  @Nullable private Label label;
  @Nullable private Aspect aspect;
  @Nullable private Location location;
  @Nullable private AspectKey key;
  @Nullable private ConfiguredAspect configuredAspect;
  @Nullable private NestedSet<Package> transitivePackages;

  public AspectValue(
      AspectKey key,
      Aspect aspect,
      Label label,
      Location location,
      ConfiguredAspect configuredAspect,
      List<ActionAnalysisMetadata> actions,
      NestedSet<Package> transitivePackages,
      boolean removeActionsAfterEvaluation) {
    super(actions, removeActionsAfterEvaluation);
    this.label = Preconditions.checkNotNull(label, actions);
    this.aspect = Preconditions.checkNotNull(aspect, label);
    this.location = Preconditions.checkNotNull(location, label);
    this.key = Preconditions.checkNotNull(key, label);
    this.configuredAspect = Preconditions.checkNotNull(configuredAspect, label);
    this.transitivePackages = Preconditions.checkNotNull(transitivePackages, label);
  }

  public ConfiguredAspect getConfiguredAspect() {
    return Preconditions.checkNotNull(configuredAspect);
  }

  public Label getLabel() {
    return Preconditions.checkNotNull(label);
  }

  public Location getLocation() {
    return Preconditions.checkNotNull(location);
  }

  public AspectKey getKey() {
    return Preconditions.checkNotNull(key);
  }

  public Aspect getAspect() {
    return Preconditions.checkNotNull(aspect);
  }

  void clear(boolean clearEverything) {
    Preconditions.checkNotNull(label, this);
    Preconditions.checkNotNull(aspect, this);
    Preconditions.checkNotNull(location, this);
    Preconditions.checkNotNull(key, this);
    Preconditions.checkNotNull(configuredAspect, this);
    Preconditions.checkNotNull(transitivePackages, this);
    if (clearEverything) {
      label = null;
      aspect = null;
      location = null;
      key = null;
      configuredAspect = null;
    }
    transitivePackages = null;
  }

  NestedSet<Package> getTransitivePackages() {
    return Preconditions.checkNotNull(transitivePackages);
  }

  // TODO(janakr): Add a nice toString after cl/150542180 is submitted.

  public static AspectKey createAspectKey(
      Label label,
      BuildConfiguration baseConfiguration,
      ImmutableList<AspectKey> baseKeys,
      AspectDescriptor aspectDescriptor,
      BuildConfiguration aspectConfiguration) {
    return new AspectKey(
        label, baseConfiguration,
        baseKeys,
        aspectDescriptor,
        aspectConfiguration
    );
  }


  public static AspectKey createAspectKey(
      Label label,
      BuildConfiguration baseConfiguration, AspectDescriptor aspectDescriptor,
      BuildConfiguration aspectConfiguration) {
    return new AspectKey(
        label, baseConfiguration, ImmutableList.<AspectKey>of(),
        aspectDescriptor, aspectConfiguration);
  }


  public static SkylarkAspectLoadingKey createSkylarkAspectKey(
      Label targetLabel,
      BuildConfiguration aspectConfiguration,
      BuildConfiguration targetConfiguration,
      SkylarkImport skylarkImport,
      String skylarkExportName) {
    return new SkylarkAspectLoadingKey(
        targetLabel, aspectConfiguration, targetConfiguration, skylarkImport, skylarkExportName);
  }
}
