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

/**
 * MakeVariableExpander defines a utility method, <code>expand</code>, for
 * expanding references to "Make" variables embedded within a string.  The
 * caller provides a Context instance which defines the expansion of each
 * variable.
 *
 * <p>Note that neither <code>$(location x)</code> nor Make-isms are treated
 * specially in any way by this class.
 */
public class MakeVariableExpander {

  private final char[] buffer;
  private final int length;
  private int offset;

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

  /**
   * Interface to be implemented by callers of MakeVariableExpander which
   * defines the expansion of each "Make" variable.
   */
  public interface Context {

    /**
     * Returns the expansion of the specified "Make" variable.
     *
     * @param var the variable to expand.
     * @return the expansion of the variable.
     * @throws ExpansionException if the variable "var" was not defined or
     *     there was any other error while expanding "var".
     */
    String lookupMakeVariable(String var) throws ExpansionException;
  }

  /**
   * Exception thrown by MakeVariableExpander.Context.expandVariable when an
   * unknown variable is passed.
   */
  public static class ExpansionException extends Exception {
    public ExpansionException(String message) {
      super(message);
    }
  }

  /**
   * Expands all references to "Make" variables embedded within string "expr",
   * using the provided Context 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 String expand(String expression, Context context) throws ExpansionException {
    if (expression.indexOf('$') < 0) {
      return expression;
    }
    return expand(expression, context, 0);
  }

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

  // Helper method for counting recursion depth.
  private static String expand(String expression, Context context, int depth)
      throws ExpansionException {
    if (depth > 10) { // plenty!
      throw new ExpansionException("potentially unbounded recursion during "
                                   + "expansion of '" + expression + "'");
    }
    return new MakeVariableExpander(expression).expand(context, depth);
  }

  private String expand(Context context, int depth) throws ExpansionException {
    StringBuilder result = new StringBuilder();
    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();
          String value = context.lookupMakeVariable(var);
          // To prevent infinite recursion for the ignored shell variables
          if (!value.equals(var)) {
            // recursively expand using Make's ":=" semantics:
            value = expand(value, context, depth + 1);
          }
          result.append(value);
        }
      } else {
        result.append(c);
      }
      offset++;
    }
    return result.toString();
  }

  /**
   * Starting at the current position, scans forward until the name of a Make
   * 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 '(': { // $(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);
      }
      case '{': { // ${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 '@':
      case '<':
      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 return null.
   * @throws ExpansionException if the variable reference was ill-formed.
   */
  public String getSingleVariable() throws ExpansionException {
    if (buffer[offset] == '$') {
      offset++;
      String result = scanVariable();
      if (offset + 1 == length) {
        return result;
      }
    }
    return null;
  }
}
