// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.core.util;

import java.util.Collection;
import java.util.Map;
import java.util.SortedMap;

import org.chromium.sdk.JsArray;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.chromium.sdk.JsValue.Type;

/**
 * A converter of JsValues into human-readable strings used in various contexts.
 */
public class JsValueStringifier {

  /**
   * A configuration class
   */
  public static class Config {

    /**
     * The maximum length of the text to render. If the value length exceeds
     * the limit, an ellipsis will be used to truncate the value.
     * The default is 80 characters.
     */
    public int maxLength = 80;
  }

  private static final String ELLIPSIS = "..."; //$NON-NLS-1$

  private static final String UNKNOWN_VALUE = "<null>"; //$NON-NLS-1$

  private final Config config;


  /**
   * Constructs a visible string for the given {@code value} (without exposing
   * the value structure). Encloses JavaScript string values in double quotes.
   *
   * @param value to build a visible string for
   * @return {@code value.getValueString()} (enclosed in double quotes if
   *         {@code value.getType() == TYPE_STRING}), or {@code null} if
   *         {@code value==null}
   */
  public static String toVisibleString(JsValue value) {
    return possiblyQuoteValueString(value);
  }

  private static String possiblyQuoteValueString(JsValue value) {
    if (value == null) {
      return UNKNOWN_VALUE;
    }
    String valueString = value.getValueString();
    return value.getType() == JsValue.Type.TYPE_STRING
        ? "\"" + valueString + "\"" //$NON-NLS-1$ //$NON-NLS-2$
        : valueString;
  }

  /**
   * Use the default config values.
   */
  public JsValueStringifier() {
    this.config = new Config();
  }

  /**
   * Use the specified {@code config} data.
   * @param config to use when rendering values.
   */
  public JsValueStringifier(Config config) {
    this.config = config;
  }

  public String render(JsValue value) {
    if (value == null) {
      return UNKNOWN_VALUE;
    }
    StringBuilder output = new StringBuilder();
    renderInternal(value, config.maxLength, true, output);
    return output.toString();
  }

  /**
   * @param value to render
   * @param maxLength the maximum length of the {@code output}
   * @param descend whether to descend into the object contents
   * @param output to render into
   */
  private void renderInternal(JsValue value, int maxLength, boolean descend, StringBuilder output) {
    if (!descend) {
      renderPrimitive(value, maxLength, output);
      return;
    }
    Type type = value.getType();
    // TODO(apavlov): implement good stringification of other types?
    switch (type) {
      case TYPE_ARRAY:
        renderArray(value.asObject().asArray(), maxLength, output);
        break;
      case TYPE_OBJECT:
        renderObject(value.asObject(), maxLength, output);
        break;
      default:
        renderPrimitive(value, maxLength, output);
        break;
    }
  }

  private void renderPrimitive(JsValue value, int maxLength, StringBuilder output) {
    output.append(possiblyQuoteValueString(value));
    truncate(output, maxLength, ELLIPSIS);
  }

  private void truncate(StringBuilder valueBuilder, int maxLength, String suffix) {
    int length = valueBuilder.length();
    if (length > maxLength) {
      valueBuilder.setLength(maxLength);
      valueBuilder.replace(
          maxLength - suffix.length(), maxLength,  suffix);
    }
  }

  private StringBuilder renderArray(JsArray value, int maxLength, StringBuilder output) {
    output.append('[');
    SortedMap<Integer, ? extends JsVariable> indexToElement = value.toSparseArray();
    boolean isFirst = true;
    int maxLengthWithoutLastBracket = maxLength - 1;
    StringBuilder elementBuilder = new StringBuilder();
    int entriesWritten = 0;
    for (Map.Entry<Integer, ? extends JsVariable> entry : indexToElement.entrySet()) {
      Integer index = entry.getKey();
      JsVariable var = entry.getValue();
      if (!isFirst) {
        output.append(',');
      } else {
        isFirst = false;
      }
      elementBuilder.setLength(0);
      elementBuilder.append(index).append('=');
      renderInternal(var.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */,
          false, elementBuilder);
      if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) {
        // reached max length
        appendNMore(output, indexToElement.size() - entriesWritten);
        break;
      } else {
        output.append(elementBuilder.toString());
        entriesWritten++;
      }
    }
    return output.append(']');
  }

  private StringBuilder renderObject(JsObject value, int maxLength, StringBuilder output) {
    output.append('[');
    Collection<? extends JsVariable> properties = value.getProperties();
    boolean isFirst = true;
    int maxLengthWithoutLastBracket = maxLength - 1;
    StringBuilder elementBuilder = new StringBuilder();
    int entriesWritten = 0;
    for (JsVariable property : properties) {
      String name = property.getName();
      if (!isFirst) {
        output.append(',');
      } else {
        isFirst = false;
      }
      elementBuilder.setLength(0);
      elementBuilder.append(name).append('=');
      renderInternal(property.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */,
          false, elementBuilder);
      if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) {
        // reached max length
        appendNMore(output, properties.size() - entriesWritten);
        break;
      } else {
        output.append(elementBuilder.toString());
        entriesWritten++;
      }
    }
    return output.append(']');
  }

  private StringBuilder appendNMore(StringBuilder output, int n) {
    return output.append(" +").append(n).append(ELLIPSIS); //$NON-NLS-1$
  }
}