// Copyright 2015 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.syntax;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintableValue;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.Formattable;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.MissingFormatWidthException;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * (Pretty) Printing of Skylark values
 */
public final class Printer {

  private static final char SKYLARK_QUOTATION_MARK = '"';

  /*
   * Suggested maximum number of list elements that should be printed via printList().
   * By default, this setting is not considered and no limitation takes place.
   */
  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT = 4;

  /*
   * Suggested limit for printList() to shorten the values of list elements when their combined
   * string length reaches this value.
   * By default, this setting is not considered and no limitation takes place.
   */
  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH = 32;

  private Printer() {
  }

  /**
   * Get an informal representation of object x.
   * Currently only differs from repr in the behavior for strings and labels at top-level,
   * that are returned as is rather than quoted.
   * @param quotationMark The quotation mark to be used (' or ")
   * @return the representation.
   */
  public static String str(Object x, char quotationMark) {
    return print(new StringBuilder(), x, quotationMark).toString();
  }

  public static String str(Object x) {
    return str(x, SKYLARK_QUOTATION_MARK);
  }

  /**
   * Get an official representation of object x.
   * For regular data structures, the value should be parsable back into an equal data structure.
   * @param quotationMark The quotation mark to be used (' or ")
   * @return the representation.
   */
  public static String repr(Object x, char quotationMark) {
    return write(new StringBuilder(), x, quotationMark).toString();
  }

  public static String repr(Object x) {
    return repr(x, SKYLARK_QUOTATION_MARK);
  }

  // In absence of a Python naming tradition, the write() vs print() function names
  // follow the Lisp tradition: print() displays the informal representation (as in Python str)
  // whereas write() displays a readable representation (as in Python repr).
  /**
   * Print an informal representation of object x.
   * Currently only differs from repr in the behavior for strings and labels at top-level,
   * that are returned as is rather than quoted.
   * @param buffer the Appendable to which to print the representation
   * @param o the object
   * @param quotationMark The quotation mark to be used (' or ")
   * @return the buffer, in fluent style
   */
  private static Appendable print(Appendable buffer, Object o, char quotationMark) {
    if (o instanceof SkylarkPrintableValue) {
      ((SkylarkPrintableValue) o).print(buffer, quotationMark);
      return buffer;
    }

    if (o instanceof String) {
      return append(buffer, (String) o);
    }
    return write(buffer, o, quotationMark);
  }

  private static Appendable print(Appendable buffer, Object o) {
    return print(buffer, o, SKYLARK_QUOTATION_MARK);
  }

  /**
   * Print an official representation of object x.
   * For regular data structures, the value should be parsable back into an equal data structure.
   * @param buffer the Appendable to write to.
   * @param o the string a representation of which to write.
   * @param quotationMark The quotation mark to be used (' or ")
   * @return the Appendable, in fluent style.
   */
  public static Appendable write(Appendable buffer, Object o, char quotationMark) {
    if (o == null) {
      throw new NullPointerException(); // Java null is not a build language value.

    } else if (o instanceof SkylarkValue) {
      ((SkylarkValue) o).write(buffer, quotationMark);

    } else if (o instanceof String) {
      writeString(buffer, (String) o, quotationMark);

    } else if (o instanceof Integer || o instanceof Double) {
      append(buffer, o.toString());

    } else if (o == Boolean.TRUE) {
      append(buffer, "True");

    } else if (o == Boolean.FALSE) {
      append(buffer, "False");

    } else if (o instanceof List<?>) {
      List<?> seq = (List<?>) o;
      printList(buffer, seq, false, quotationMark);

    } else if (o instanceof Map<?, ?>) {
      Map<?, ?> dict = (Map<?, ?>) o;
      printList(buffer, getSortedEntrySet(dict), "{", ", ", "}", null, quotationMark);

    } else if (o instanceof Map.Entry<?, ?>) {
      Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
      write(buffer, entry.getKey(), quotationMark);
      append(buffer, ": ");
      write(buffer, entry.getValue(), quotationMark);

    } else if (o instanceof PathFragment) {
      append(buffer, ((PathFragment) o).getPathString());

    } else if (o instanceof Class<?>) {
      append(buffer, EvalUtils.getDataTypeNameFromClass((Class<?>) o));

    } else {
      append(buffer, o.toString());
    }

    return buffer;
  }

  /**
   * Returns the sorted entry set of the given map
   */
  private static <K, V> Set<Map.Entry<K, V>> getSortedEntrySet(Map<K, V> dict) {
    if (!(dict instanceof SortedMap<?, ?>)) {
      Map<K, V> tmp = new TreeMap<>(EvalUtils.SKYLARK_COMPARATOR);
      tmp.putAll(dict);
      dict = tmp;
    }

    return dict.entrySet();
  }

  public static Appendable write(Appendable buffer, Object o) {
    return write(buffer, o, SKYLARK_QUOTATION_MARK);
  }

  // Throughout this file, we transform IOException into AssertionError.
  // During normal operations, we only use in-memory Appendable-s that
  // cannot cause an IOException.
  public static Appendable append(Appendable buffer, char c) {
    try {
      return buffer.append(c);
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  public static Appendable append(Appendable buffer, CharSequence s) {
    try {
      return buffer.append(s);
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  private static Appendable append(Appendable buffer, CharSequence s, int start, int end) {
    try {
      return buffer.append(s, start, end);
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  private static Appendable backslashChar(Appendable buffer, char c) {
    return append(append(buffer, '\\'), c);
  }

  private static Appendable escapeCharacter(Appendable buffer, char c, char quote) {
    if (c == quote) {
      return backslashChar(buffer, c);
    }
    switch (c) {
      case '\\':
        return backslashChar(buffer, '\\');
      case '\r':
        return backslashChar(buffer, 'r');
      case '\n':
        return backslashChar(buffer, 'n');
      case '\t':
        return backslashChar(buffer, 't');
      default:
        if (c < 32) {
          return append(buffer, String.format("\\x%02x", (int) c));
        }
        return append(buffer, c); // no need to support UTF-8
    } // endswitch
  }

  /**
   * Write a properly escaped Skylark representation of a string to a buffer.
   *
   * @param buffer the Appendable to write to.
   * @param s the string a representation of which to write.
   * @param quote the quote character to use, '"' or '\''.
   * @return the Appendable, in fluent style.
   */
  private static Appendable writeString(Appendable buffer, String s, char quote) {
    append(buffer, quote);
    int len = s.length();
    for (int i = 0; i < len; i++) {
      char c = s.charAt(i);
      escapeCharacter(buffer, c, quote);
    }
    return append(buffer, quote);
  }

  /**
   * Print a list of object representations
   * @param buffer an appendable buffer onto which to write the list.
   * @param list the list of objects to write (each as with repr)
   * @param before a string to print before the list
   * @param separator a separator to print between each object
   * @param after a string to print after the list
   * @param singletonTerminator null or a string to print after the list if it is a singleton
   * The singleton case is notably relied upon in python syntax to distinguish
   * a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
   * @param quotationMark The quotation mark to be used (' or ")
   * @return the Appendable, in fluent style.
   */
  public static Appendable printList(
      Appendable buffer,
      Iterable<?> list,
      String before,
      String separator,
      String after,
      String singletonTerminator,
      char quotationMark) {
    return printList(
        buffer, list, before, separator, after, singletonTerminator, quotationMark, -1, -1);
  }

  /**
   * Print a list of object representations.
   *
   * <p>The length of the output will be limited when both {@code maxItemsToPrint} and {@code
   * criticalItemsStringLength} have values greater than zero.
   *
   * @param buffer an appendable buffer onto which to write the list.
   * @param list the list of objects to write (each as with repr)
   * @param before a string to print before the list
   * @param separator a separator to print between each object
   * @param after a string to print after the list
   * @param singletonTerminator null or a string to print after the list if it is a singleton
   * The singleton case is notably relied upon in python syntax to distinguish
   *    a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
   * @param quotationMark The quotation mark to be used (' or ")
   * @param maxItemsToPrint the maximum number of elements to be printed.
   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
   *    'Soft' means that this limit may be exceeded because of formatting.
   * @return the Appendable, in fluent style.
   */
  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
      String separator, String after, String singletonTerminator, char quotationMark,
      int maxItemsToPrint, int criticalItemsStringLength) {
    append(buffer, before);
    int len = 0;
    // Limits the total length of the string representation of the elements, if specified.
    if (maxItemsToPrint > 0 && criticalItemsStringLength > 0) {
      len = appendListElements(LengthLimitedAppendable.create(buffer, criticalItemsStringLength),
          list, separator, quotationMark, maxItemsToPrint);
    } else {
      len = appendListElements(buffer, list, separator, quotationMark);
    }
    if (singletonTerminator != null && len == 1) {
      append(buffer, singletonTerminator);
    }
    return append(buffer, after);
  }

  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
      String separator, String after, String singletonTerminator, int maxItemsToPrint,
      int criticalItemsStringLength) {
    return printList(buffer, list, before, separator, after, singletonTerminator,
        SKYLARK_QUOTATION_MARK, maxItemsToPrint, criticalItemsStringLength);
  }

  /**
   * Appends the given elements to the specified {@link Appendable} and returns the number of
   * elements.
   */
  private static int appendListElements(
      Appendable appendable, Iterable<?> list, String separator, char quotationMark) {
    boolean printSeparator = false; // don't print the separator before the first element
    int len = 0;
    for (Object o : list) {
      if (printSeparator) {
        append(appendable, separator);
      }
      write(appendable, o, quotationMark);
      printSeparator = true;
      len++;
    }
    return len;
  }

  /**
   * Tries to append the given elements to the specified {@link Appendable} until specific limits
   * are reached.
   * @return the number of appended elements.
   */
  private static int appendListElements(LengthLimitedAppendable appendable, Iterable<?> list,
      String separator, char quotationMark, int maxItemsToPrint) {
    boolean printSeparator = false; // don't print the separator before the first element
    boolean skipArgs = false;
    int items = Iterables.size(list);
    int len = 0;
    // We don't want to print "1 more arguments", hence we don't skip arguments if there is only one
    // above the limit.
    int itemsToPrint = (items - maxItemsToPrint == 1) ? items : maxItemsToPrint;
    appendable.enforceLimit();
    for (Object o : list) {
      // We don't want to print "1 more arguments", even if we hit the string limit.
      if (len == itemsToPrint || (appendable.hasHitLimit() && len < items - 1)) {
        skipArgs = true;
        break;
      }
      if (printSeparator) {
        append(appendable, separator);
      }
      write(appendable, o, quotationMark);
      printSeparator = true;
      len++;
    }
    appendable.ignoreLimit();
    if (skipArgs) {
      append(appendable, separator);
      append(appendable, String.format("<%d more arguments>", items - len));
    }
    return len;
  }

  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
      String separator, String after, String singletonTerminator) {
    return printList(
        buffer, list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
  }

  /**
   * Print a Skylark list or tuple of object representations
   * @param buffer an appendable buffer onto which to write the list.
   * @param list the contents of the list or tuple
   * @param isTuple is it a tuple or a list?
   * @param quotationMark The quotation mark to be used (' or ")
   * @param maxItemsToPrint the maximum number of elements to be printed.
   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
   * 'Soft' means that this limit may be exceeded because of formatting.
   * @return the Appendable, in fluent style.
   */
  public static Appendable printList(Appendable buffer, Iterable<?> list, boolean isTuple,
      char quotationMark, int maxItemsToPrint, int criticalItemsStringLength) {
    if (isTuple) {
      return printList(buffer, list, "(", ", ", ")", ",", quotationMark, maxItemsToPrint,
          criticalItemsStringLength);
    } else {
      return printList(buffer, list, "[", ", ", "]", null, quotationMark, maxItemsToPrint,
          criticalItemsStringLength);
    }
  }

  public static Appendable printList(
      Appendable buffer, Iterable<?> list, boolean isTuple, char quotationMark) {
    return printList(buffer, list, isTuple, quotationMark, -1, -1);
  }

  /**
   * Print a list of object representations
   * @param list the list of objects to write (each as with repr)
   * @param before a string to print before the list
   * @param separator a separator to print between each object
   * @param after a string to print after the list
   * @param singletonTerminator null or a string to print after the list if it is a singleton
   * The singleton case is notably relied upon in python syntax to distinguish
   * a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
   * @param quotationMark The quotation mark to be used (' or ")
   * @return a String, the representation.
   */
  public static String listString(Iterable<?> list, String before, String separator, String after,
      String singletonTerminator, char quotationMark) {
    return printList(new StringBuilder(), list, before, separator, after, singletonTerminator,
               quotationMark).toString();
  }

  public static String listString(
      Iterable<?> list, String before, String separator, String after, String singletonTerminator) {
    return listString(list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
  }

  /**
   * Perform Python-style string formatting, lazily.
   *
   * @param pattern a format string.
   * @param arguments positional arguments.
   * @return the formatted string.
   */
  public static Formattable formattable(final String pattern, Object... arguments) {
    final ImmutableList<Object> args = ImmutableList.copyOf(arguments);
    return new Formattable() {
        @Override
        public String toString() {
          return formatToString(pattern, args);
        }

        @Override
        public void formatTo(Formatter formatter, int flags, int width, int precision) {
          Printer.formatTo(formatter.out(), pattern, args);
        }
      };
  }

  /**
   * Perform Python-style string formatting.
   *
   * @param pattern a format string.
   * @param arguments a tuple containing positional arguments.
   * @return the formatted string.
   */
  public static String format(String pattern, Object... arguments) {
    return formatToString(pattern, ImmutableList.copyOf(arguments));
  }

  /**
   * Perform Python-style string formatting.
   *
   * @param pattern a format string.
   * @param arguments a tuple containing positional arguments.
   * @return the formatted string.
   */
  public static String formatToString(String pattern, List<?> arguments) {
    return formatTo(new StringBuilder(), pattern, arguments).toString();
  }

  /**
   * Perform Python-style string formatting, as per pattern % tuple
   * Limitations: only %d %s %r %% are supported.
   *
   * @param buffer an Appendable to output to.
   * @param pattern a format string.
   * @param arguments a list containing positional arguments.
   * @return the buffer, in fluent style.
   */
  // TODO(bazel-team): support formatting arguments, and more complex Python patterns.
  public static Appendable formatTo(Appendable buffer, String pattern, List<?> arguments) {
    // N.B. MissingFormatWidthException is the only kind of IllegalFormatException
    // whose constructor can take and display arbitrary error message, hence its use below.

    int length = pattern.length();
    int argLength = arguments.size();
    int i = 0; // index of next character in pattern
    int a = 0; // index of next argument in arguments

    while (i < length) {
      int p = pattern.indexOf('%', i);
      if (p == -1) {
        append(buffer, pattern, i, length);
        break;
      }
      if (p > i) {
        append(buffer, pattern, i, p);
      }
      if (p == length - 1) {
        throw new MissingFormatWidthException(
            "incomplete format pattern ends with %: " + repr(pattern));
      }
      char directive = pattern.charAt(p + 1);
      i = p + 2;
      switch (directive) {
        case '%':
          append(buffer, '%');
          continue;
        case 'd':
        case 'r':
        case 's':
          if (a >= argLength) {
            throw new MissingFormatWidthException("not enough arguments for format pattern "
                + repr(pattern) + ": "
                + repr(Tuple.copyOf(arguments)));
          }
          Object argument = arguments.get(a++);
          switch (directive) {
            case 'd':
              if (argument instanceof Integer) {
                append(buffer, argument.toString());
                continue;
              } else {
                throw new MissingFormatWidthException(
                    "invalid argument " + repr(argument) + " for format pattern %d");
              }
            case 'r':
              write(buffer, argument);
              continue;
            case 's':
              print(buffer, argument);
              continue;
          }
        default:
          throw new MissingFormatWidthException(
              "unsupported format character " + repr(String.valueOf(directive))
              + " at index " + (p + 1) + " in " + repr(pattern));
      }
    }
    if (a < argLength) {
      throw new MissingFormatWidthException(
          "not all arguments converted during string formatting");
    }
    return buffer;
  }

  /**
   * Helper class for {@code Appendable}s that want to limit the length of their input.
   *
   * <p>Instances of this class act as a proxy for one {@code Appendable} object and decide whether
   * the given input (or parts of it) can be written to the underlying {@code Appendable}, depending
   * on whether the specified maximum length has been met or not.
   */
  private static final class LengthLimitedAppendable implements Appendable {

    private static final ImmutableSet<Character> SPECIAL_CHARS =
        ImmutableSet.of(',', ' ', '"', '\'', ':', '(', ')', '[', ']', '{', '}');

    private static final Pattern ARGS_PATTERN = Pattern.compile("<\\d+ more arguments>");

    private final Appendable original;
    private int limit;
    private boolean ignoreLimit;
    private boolean previouslyShortened;
    
    private LengthLimitedAppendable(Appendable original, int limit) {
      this.original = original;
      this.limit = limit;
    }

    public static LengthLimitedAppendable create(Appendable original, int limit) {
      // We don't want to overwrite the limit if original is already an instance of this class.
      return (original instanceof LengthLimitedAppendable)
          ? (LengthLimitedAppendable) original : new LengthLimitedAppendable(original, limit);
    }

    @Override
    public Appendable append(CharSequence csq) throws IOException {
      if (ignoreLimit || hasOnlySpecialChars(csq)) {
        // Don't update limit.
        original.append(csq);
        previouslyShortened = false;
      } else {
        int length = csq.length();
        if (length <= limit) {
          limit -= length;
          original.append(csq);
        } else {
          original.append(csq, 0, limit);
          // We don't want to append multiple ellipses.
          if (!previouslyShortened) {
            original.append("...");
          }
          appendTrailingSpecialChars(csq, limit);
          previouslyShortened = true;
          limit = 0;
        }
      }
      return this;
    }

    /**
     * Appends any trailing "special characters" (e.g. brackets, quotation marks) in the given
     * sequence to the output buffer, regardless of the limit.
     *
     * <p>For example, let's look at foo(['too long']). Without this method, the shortened result
     * would be foo(['too...) instead of the prettier foo(['too...']).
     *
     * <p>If the input string was already shortened and contains "<x more arguments>", this part
     * will also be appended.
     */
    private void appendTrailingSpecialChars(CharSequence csq, int limit) throws IOException {
      int length = csq.length();
      Matcher matcher = ARGS_PATTERN.matcher(csq);
      // We assume that everything following the "x more arguments" part has to be copied, too.
      int start = matcher.find() ? matcher.start() : length;
      // Find the left-most non-arg char that has to be copied.
      for (int i = start - 1; i > limit; --i) {
        if (isSpecialChar(csq.charAt(i))) {
          start = i;
        } else {
          break;
        }
      }
      if (start < length) {
        original.append(csq, start, csq.length());
      }
    }

    /**
     * Returns whether the given sequence denotes characters that are not part of the value of an
     * argument.
     *
     * <p>Examples are brackets, braces and quotation marks.
     */
    private boolean hasOnlySpecialChars(CharSequence csq) {
      for (int i = 0; i < csq.length(); ++i) {
        if (!isSpecialChar(csq.charAt(i))) {
          return false;
        }
      }
      return true;
    }

    private boolean isSpecialChar(char c)    {
      return SPECIAL_CHARS.contains(c);
    }

    @Override
    public Appendable append(CharSequence csq, int start, int end) throws IOException {
      return append(csq.subSequence(start, end));
    }

    @Override
    public Appendable append(char c) throws IOException {
      return append(String.valueOf(c));
    }
    
    public boolean hasHitLimit()  {
      return limit <= 0;
    }

    public void enforceLimit()  {
      ignoreLimit = false;
    }
    
    public void ignoreLimit() {
      ignoreLimit = true;
    }

    @Override
    public String toString() {
      return original.toString();
    }
  }
}
