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

import com.google.common.collect.ImmutableSet;
import javax.annotation.Nullable;

/**
 * Simple string template expansion. String templates consist of text interspersed with
 * <code>$(variable)</code> or <code>$(function value)</code> references, which are replaced by
 * strings.
 */
public final class TemplateExpander {
  private final char[] buffer;
  private final int length;
  private int offset;

  private TemplateExpander(String expression) {
    buffer = expression.toCharArray();
    length = buffer.length;
    offset = 0;
  }

  /**
   * If the string contains a single variable, return the expansion of that variable. Otherwise,
   * return null.
   */
  @Nullable
  public static String expandSingleVariable(String expression, TemplateContext context)
      throws ExpansionException, InterruptedException {
    String var = new TemplateExpander(expression).getSingleVariable();
    return (var != null) ? context.lookupVariable(var) : null;
  }

  /**
   * Expands all references to template variables embedded within string "expr", using the provided
   * {@link TemplateContext} instance to expand individual variables.
   *
   * @param expression the string to expand.
   * @param context the context which defines the expansion of each individual variable
   * @return the expansion of "expr"
   * @throws ExpansionException if "expr" contained undefined or ill-formed variables references
   */
  public static Expansion expand(String expression, TemplateContext context)
      throws ExpansionException, InterruptedException {
    if (expression.indexOf('$') < 0) {
      return Expansion.create(expression, ImmutableSet.of());
    }
    return expand(expression, context, 0);
  }

  // Helper method for counting recursion depth.
  private static Expansion expand(String expression, TemplateContext context, int depth)
      throws ExpansionException, InterruptedException {
    if (depth > 10) { // plenty!
      throw new ExpansionException(
          String.format("potentially unbounded recursion during expansion of '%s'", expression));
    }
    return new TemplateExpander(expression).expand(context, depth);
  }

  private Expansion expand(TemplateContext context, int depth)
      throws ExpansionException, InterruptedException {
    StringBuilder result = new StringBuilder();
    ImmutableSet.Builder<String> lookedUpVariables = ImmutableSet.builder();
    while (offset < length) {
      char c = buffer[offset];
      if (c == '$') { // variable
        offset++;
        if (offset >= length) {
          throw new ExpansionException("unterminated $");
        }
        if (buffer[offset] == '$') {
          result.append('$');
        } else {
          String var = scanVariable();
          int spaceIndex = var.indexOf(' ');
          if (spaceIndex < 0) {
            String value = context.lookupVariable(var);
            lookedUpVariables.add(var);
            // To prevent infinite recursion for the ignored shell variables
            if (!value.equals(var)) {
              // recursively expand using Make's ":=" semantics:
              Expansion expansion = expand(value, context, depth + 1);
              value = expansion.expansion();
              lookedUpVariables.addAll(expansion.lookedUpVariables());
            }
            result.append(value);
          } else {
            String name = var.substring(0, spaceIndex);
            // Trim the string to remove leading and trailing whitespace.
            String param = var.substring(spaceIndex + 1).trim();
            String value = context.lookupFunction(name, param);
            lookedUpVariables.add(name);
            result.append(value);
          }
        }
      } else {
        result.append(c);
      }
      offset++;
    }
    return Expansion.create(result.toString(), lookedUpVariables.build());
  }

  /**
   * Starting at the current position, scans forward until the name of a template variable has been
   * consumed. Returns the variable name and advances the position. If the variable is a potential
   * shell variable returns the shell variable expression itself, so that we can let the shell
   * handle the expansion.
   *
   * @return the name of the variable found at the current point.
   * @throws ExpansionException if the variable reference was ill-formed.
   */
  private String scanVariable() throws ExpansionException {
    char c = buffer[offset];
    switch (c) {
      case '(' -> {
        // looks like $(SRCS)
        offset++;
        int start = offset;
        while (offset < length && buffer[offset] != ')') {
          offset++;
        }
        if (offset >= length) {
          throw new ExpansionException("unterminated variable reference");
        }
        return new String(buffer, start, offset - start);
        // We only parse ${variable} syntax to provide a better error message.
      }
      case '{' -> {
        // looks like ${SRCS}
        offset++;
        int start = offset;
        while (offset < length && buffer[offset] != '}') {
          offset++;
        }
        if (offset >= length) {
          throw new ExpansionException("unterminated variable reference");
        }
        String expr = new String(buffer, start, offset - start);
        throw new ExpansionException(
            "'${"
                + expr
                + "}' syntax is not supported; use '$("
                + expr
                + ")' instead for \"Make\" variables, or escape the '$' as "
                + "'$$' if you intended this for the shell");
      }
      case '@', '<', '^' -> {
        return String.valueOf(c);
      }
      default -> {
        int start = offset;
        while (offset + 1 < length && Character.isJavaIdentifierPart(buffer[offset + 1])) {
          offset++;
        }
        String expr = new String(buffer, start, offset + 1 - start);
        throw new ExpansionException(
            "'$"
                + expr
                + "' syntax is not supported; use '$("
                + expr
                + ")' instead for \"Make\" variables, or escape the '$' as "
                + "'$$' if you intended this for the shell");
      }
    }
  }

  /**
   * @return the variable name if the variable spans from offset to the end of the buffer, otherwise
   *     null
   * @throws ExpansionException if the variable reference was ill-formed
   */
  @Nullable
  private String getSingleVariable() throws ExpansionException {
    if (buffer[offset] == '$') {
      offset++;
      String result = scanVariable();
      if (offset + 1 == length) {
        return result;
      }
    }
    return null;
  }
}
