// 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.analysis.actions;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.build.lib.util.StringUtilities;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;

/**
 * Action to expand a template and write the expanded content to a file.
 */
@Immutable // if all substitutions are immutable
public final class TemplateExpansionAction extends AbstractFileWriteAction {

  private static final String GUID = "786c1fe0-dca8-407a-b108-e1ecd6d1bc7f";

  /**
   * A pair of a string to be substituted and a string to substitute it with.
   * For simplicity, these are called key and value. All implementations must
   * be immutable, and always return the identical key. The returned values
   * must be the same, though they need not be the same object.
   *
   * <p>It should be assumed that the {@link #getKey} invocation is cheap, and
   * that the {@link #getValue} invocation is expensive.
   */
  @Immutable // if the keys and values in the passed in lists and maps are all immutable
  public abstract static class Substitution {
    private Substitution() {
    }

    public abstract String getKey();
    public abstract String getValue();

    /**
     * Returns an immutable Substitution instance for the given key and value.
     */
    public static Substitution of(final String key, final String value) {
      return new Substitution() {
        @Override
        public String getKey() {
          return key;
        }

        @Override
        public String getValue() {
          return value;
        }
      };
    }

    /**
     * Returns an immutable Substitution instance for the key and list of values. The
     * values will be joined by spaces before substitution.
     */
    public static Substitution ofSpaceSeparatedList(
        final String key, final ImmutableList<?> value) {
      return new Substitution() {
        @Override
        public String getKey() {
          return key;
        }

        @Override
        public String getValue() {
          return Joiner.on(" ").join(value);
        }
      };
    }

    /**
     * Returns an immutable Substitution instance for the key and map of values.  Corresponding
     * values in the map will be joined with "=", and pairs will be joined by spaces before
     * substitution.
     *
     * <p>For example, the map <(a,1), (b,2), (c,3)> will become "a=1 b=2 c=3".
     */
    public static Substitution ofSpaceSeparatedMap(
        final String key, final ImmutableMap<?, ?> value) {
      return new Substitution() {
        @Override
        public String getKey() {
          return key;
        }

        @Override
        public String getValue() {
          return Joiner.on(" ").withKeyValueSeparator("=").join(value);
        }
      };
    }

    @Override
    public boolean equals(Object object) {
      if (this == object) {
        return true;
      }
      if (object instanceof Substitution) {
        Substitution substitution = (Substitution) object;
        return substitution.getKey().equals(this.getKey())
            && substitution.getValue().equals(this.getValue());
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(getKey(), getValue());
    }

    @Override
    public String toString() {
      return "Substitution(" + getKey() + " -> " + getValue() + ")";
    }
  }

  /**
   * A substitution with a fixed key, and a computed value. The computed value
   * must not change over the lifetime of an instance, though the {@link
   * #getValue} method may return different String objects.
   *
   * <p>It should be assumed that the {@link #getKey} invocation is cheap, and
   * that the {@link #getValue} invocation is expensive.
   */
  public abstract static class ComputedSubstitution extends Substitution {
    private final String key;

    public ComputedSubstitution(String key) {
      this.key = key;
    }

    @Override
    public String getKey() {
      return key;
    }
  }

  /**
   * A template that contains text content, or alternatively throws an {@link
   * IOException}.
   */
  @Immutable // all subclasses are immutable
  public abstract static class Template {

    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    /**
     * We only allow subclasses in this file.
     */
    private Template() {
    }

    /**
     * Returns the text content of the template.
     */
    protected abstract String getContent() throws IOException;

    /**
     * Returns a string that is used for the action key. This must change if
     * the getContent method returns something different, but is not allowed to
     * throw an exception.
     */
    protected abstract String getKey();

    /**
     * Loads a template from the given resource. The resource is looked up
     * relative to the given class. If the resource cannot be loaded, the returned
     * template throws an {@link IOException} when {@link #getContent} is
     * called. This makes it safe to use this method in a constant initializer.
     */
    public static Template forResource(final Class<?> relativeToClass, final String templateName) {
      try {
        String content = ResourceFileLoader.loadResource(relativeToClass, templateName);
        return forString(content);
      } catch (final IOException e) {
        return new Template() {
          @Override
          protected String getContent() throws IOException {
            throw new IOException("failed to load resource file '" + templateName
                + "' due to I/O error: " + e.getMessage(), e);
          }

          @Override
          protected String getKey() {
            return "ERROR: " + e.getMessage();
          }
        };
      }
    }

    /**
     * Returns a template for the given text string.
     */
    public static Template forString(final String templateText) {
      return new Template() {
        @Override
        protected String getContent() {
          return templateText;
        }

        @Override
        protected String getKey() {
          return templateText;
        }
      };
    }

    /**
     * Returns a template that loads the given artifact. It is important that
     * the artifact is also an input for the action, or this won't work.
     * Therefore this method is private, and you should use the corresponding
     * {@link TemplateExpansionAction} constructor.
     */
    private static Template forArtifact(final Artifact templateArtifact) {
      return new Template() {
        @Override
        protected String getContent() throws IOException {
          Path templatePath = templateArtifact.getPath();
          try {
            return FileSystemUtils.readContent(templatePath, DEFAULT_CHARSET);
          } catch (IOException e) {
            throw new IOException("failed to load template file '" + templatePath.getPathString()
                + "' due to I/O error: " + e.getMessage(), e);
          }
        }

        @Override
        protected String getKey() {
          // This isn't strictly necessary, because the action inputs are automatically considered.
          return "ARTIFACT: " + templateArtifact.getExecPathString();
        }
      };
    }
  }

  private final Template template;
  private final ImmutableList<Substitution> substitutions;

  /**
   * Creates a new TemplateExpansionAction instance.
   *
   * @param owner the action owner.
   * @param inputs the Artifacts that this Action depends on
   * @param output the Artifact that will be created by executing this Action.
   * @param template the template that will be expanded by this Action.
   * @param substitutions the substitutions that will be applied to the
   *   template. All substitutions will be applied in order.
   * @param makeExecutable iff true will change the output file to be
   *   executable.
   */
  private TemplateExpansionAction(ActionOwner owner,
                                  Collection<Artifact> inputs,
                                  Artifact output,
                                  Template template,
                                  List<Substitution> substitutions,
                                  boolean makeExecutable) {
    super(owner, inputs, output, makeExecutable);
    this.template = template;
    this.substitutions = ImmutableList.copyOf(substitutions);
  }

  /**
   * Creates a new TemplateExpansionAction instance for an artifact template.
   *
   * @param owner the action owner.
   * @param templateArtifact the Artifact that will be read as the text template
   *   file
   * @param output the Artifact that will be created by executing this Action.
   * @param substitutions the substitutions that will be applied to the
   *   template. All substitutions will be applied in order.
   * @param makeExecutable iff true will change the output file to be
   *   executable.
   */
  public TemplateExpansionAction(ActionOwner owner,
                                 Artifact templateArtifact,
                                 Artifact output,
                                 List<Substitution> substitutions,
                                 boolean makeExecutable) {
    this(owner, ImmutableList.of(templateArtifact), output, Template.forArtifact(templateArtifact),
        substitutions, makeExecutable);
  }

  /**
   * Creates a new TemplateExpansionAction instance without inputs.
   *
   * @param owner the action owner.
   * @param output the Artifact that will be created by executing this Action.
   * @param template the template
   * @param substitutions the substitutions that will be applied to the
   *   template. All substitutions will be applied in order.
   * @param makeExecutable iff true will change the output file to be
   *   executable.
   */
  public TemplateExpansionAction(ActionOwner owner,
                                 Artifact output,
                                 Template template,
                                 List<Substitution> substitutions,
                                 boolean makeExecutable) {
    this(owner, Artifact.NO_ARTIFACTS, output, template, substitutions, makeExecutable);
  }

  /**
   * Expands the template by applying all substitutions.
   * @param template
   * @return the expanded text.
   */
  private String expandTemplate(String template) {
    for (Substitution entry : substitutions) {
      template = StringUtilities.replaceAllLiteral(template, entry.getKey(), entry.getValue());
    }
    return template;
  }

  @VisibleForTesting
  public String getFileContents() throws IOException {
    return expandTemplate(template.getContent());
  }

  @Override
  public String getSkylarkContent() throws IOException {
    return getFileContents();
  }

  @Override
  public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) throws IOException {
    final byte[] bytes = getFileContents().getBytes(Template.DEFAULT_CHARSET);
    return new DeterministicWriter() {
      @Override
      public void writeOutputFile(OutputStream out) throws IOException {
        out.write(bytes);
      }
    };
  }

  @Override
  protected String computeKey() {
    Fingerprint f = new Fingerprint();
    f.addString(GUID);
    f.addString(String.valueOf(makeExecutable));
    f.addString(template.getKey());
    f.addInt(substitutions.size());
    for (Substitution entry : substitutions) {
      f.addString(entry.getKey());
      f.addString(entry.getValue());
    }
    return f.hexDigestAndReset();
  }

  @Override
  public String getMnemonic() {
    return "TemplateExpand";
  }

  @Override
  protected String getRawProgressMessage() {
    return "Expanding template " + Iterables.getOnlyElement(getOutputs()).prettyPrint();
  }

  public List<Substitution> getSubstitutions() {
    return substitutions;
  }

  @Override
  public SkylarkDict<String, String> getSkylarkSubstitutions() {
    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
    for (Substitution entry : substitutions) {
      builder.put(entry.getKey(), entry.getValue());
    }
    return SkylarkDict.copyOf(null, builder.build());
  }
}
