// Copyright 2016 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.packages;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.syntax.BaseFunction;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.Printer;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.Preconditions;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;

/** A Skylark value that is a result of an 'aspect(..)' function call. */
@SkylarkModule(
  name = "Aspect",
  category = SkylarkModuleCategory.NONE,
  doc =
      "For more information about Aspects, please consult the <a href=\"globals.html#aspect\">"
          + "documentation of the aspect function</a> or the "
          + "<a href=\"../aspects.md\">introduction to Aspects</a>."
)
public class SkylarkAspect implements SkylarkExportable {
  private final BaseFunction implementation;
  private final ImmutableList<String> attributeAspects;
  private final ImmutableList<Attribute> attributes;
  private final ImmutableSet<String> paramAttributes;
  private final ImmutableSet<String> fragments;
  private final ImmutableSet<String> hostFragments;
  private final Environment funcallEnv;
  private SkylarkAspectClass aspectClass;

  public SkylarkAspect(
      BaseFunction implementation,
      ImmutableList<String> attributeAspects,
      ImmutableList<Attribute> attributes,
      ImmutableSet<String> paramAttributes,
      ImmutableSet<String> fragments,
      ImmutableSet<String> hostFragments,
      Environment funcallEnv) {
    this.implementation = implementation;
    this.attributeAspects = attributeAspects;
    this.attributes = attributes;
    this.paramAttributes = paramAttributes;
    this.fragments = fragments;
    this.hostFragments = hostFragments;
    this.funcallEnv = funcallEnv;
    ImmutableList.Builder<Pair<String, Attribute>> builder = ImmutableList.builder();
  }

  public BaseFunction getImplementation() {
    return implementation;
  }

  public ImmutableList<String> getAttributeAspects() {
    return attributeAspects;
  }

  public Environment getFuncallEnv() {
    return funcallEnv;
  }

  public ImmutableList<Attribute> getAttributes() {
    return attributes;
  }

  @Override
  public boolean isImmutable() {
    return implementation.isImmutable();
  }

  @Override
  public void write(Appendable buffer, char quotationMark) {
    Printer.append(buffer, "Aspect:");
    implementation.write(buffer, quotationMark);
  }

  public String getName() {
    return getAspectClass().getName();
  }

  public SkylarkAspectClass getAspectClass() {
    Preconditions.checkState(isExported());
    return aspectClass;
  }

  public ImmutableSet<String> getParamAttributes() {
    return paramAttributes;
  }

  @Override
  public void export(Label extensionLabel, String name) {
    Preconditions.checkArgument(!isExported());
    this.aspectClass = new SkylarkAspectClass(extensionLabel, name);
  }

  private static final List<String> allAttrAspects = Arrays.asList("*");

  public AspectDefinition getDefinition(AspectParameters aspectParams) {
    AspectDefinition.Builder builder = new AspectDefinition.Builder(aspectClass);
    if (allAttrAspects.equals(attributeAspects)) {
      builder.allAttributesAspect(aspectClass);
    } else {
      for (String attributeAspect : attributeAspects) {
        builder.attributeAspect(attributeAspect, aspectClass);
      }
    }
    
    for (Attribute attribute : attributes) {
      Attribute attr = attribute;  // Might be reassigned.
      if (!aspectParams.getAttribute(attr.getName()).isEmpty()) {
        String value = aspectParams.getOnlyValueOfAttribute(attr.getName());
        Preconditions.checkState(!Attribute.isImplicit(attr.getName()));
        Preconditions.checkState(attr.getType() == Type.STRING);
        Preconditions.checkArgument(aspectParams.getAttribute(attr.getName()).size() == 1,
            String.format("Aspect %s parameter %s has %d values (must have exactly 1).",
                          getName(),
                          attr.getName(),
                          aspectParams.getAttribute(attr.getName()).size()));
        attr = attr.cloneBuilder(Type.STRING).value(value).build(attr.getName());
      }
      builder.add(attr);
    }
    builder.requiresConfigurationFragmentsBySkylarkModuleName(fragments);
    builder.requiresHostConfigurationFragmentsBySkylarkModuleName(hostFragments);
    return builder.build();
  }

  @Override
  public boolean isExported() {
    return aspectClass != null;
  }

  public Function<Rule, AspectParameters> getDefaultParametersExtractor() {
    return new Function<Rule, AspectParameters>() {
      @Nullable
      @Override
      public AspectParameters apply(Rule rule) {
        AttributeMap ruleAttrs = RawAttributeMapper.of(rule);
        AspectParameters.Builder builder = new AspectParameters.Builder();
        for (Attribute aspectAttr : attributes) {
          if (!Attribute.isImplicit(aspectAttr.getName())) {
            String param = aspectAttr.getName();
            Attribute ruleAttr = ruleAttrs.getAttributeDefinition(param);
            if (paramAttributes.contains(aspectAttr.getName())) {
              // These are preconditions because if they are false, RuleFunction.call() should
              // already have generated an error.
              Preconditions.checkArgument(ruleAttr != null,
                  String.format("Cannot apply aspect %s to %s that does not define attribute '%s'.",
                                getName(),
                                rule.getTargetKind(),
                                param));
              Preconditions.checkArgument(ruleAttr.getType() == Type.STRING,
                  String.format("Cannot apply aspect %s to %s with non-string attribute '%s'.",
                                getName(),
                                rule.getTargetKind(),
                                param));
            }
            if (ruleAttr != null && ruleAttr.getType() == aspectAttr.getType()) {
              builder.addAttribute(param, (String) ruleAttrs.get(param, ruleAttr.getType()));
            }
          }
        }
        return builder.build();
      }
    };
  }
}
