/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.mapreduce.util;

import java.text.ParseException;
import java.util.List;

import com.google.common.collect.Lists;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.counters.AbstractCounters;
import org.apache.hadoop.mapreduce.Counter;
import org.apache.hadoop.mapreduce.counters.CounterGroupBase;
import org.apache.hadoop.util.StringUtils;

/**
 * String conversion utilities for counters.
 * Candidate for deprecation since we start to use JSON in 0.21+
 */
@InterfaceAudience.Private
public class CountersStrings {
  private static final char GROUP_OPEN = '{';
  private static final char GROUP_CLOSE = '}';
  private static final char COUNTER_OPEN = '[';
  private static final char COUNTER_CLOSE = ']';
  private static final char UNIT_OPEN = '(';
  private static final char UNIT_CLOSE = ')';
  private static char[] charsToEscape =  {GROUP_OPEN, GROUP_CLOSE,
                                          COUNTER_OPEN, COUNTER_CLOSE,
                                          UNIT_OPEN, UNIT_CLOSE};
  /**
   * Make the pre 0.21 counter string (for e.g. old job history files)
   * [(actual-name)(display-name)(value)]
   * @param counter to stringify
   * @return the stringified result
   */
  public static String toEscapedCompactString(Counter counter) {

    // First up, obtain the strings that need escaping. This will help us
    // determine the buffer length apriori.
    String escapedName, escapedDispName;
    long currentValue;
    synchronized(counter) {
      escapedName = escape(counter.getName());
      escapedDispName = escape(counter.getDisplayName());
      currentValue = counter.getValue();
    }
    int length = escapedName.length() + escapedDispName.length() + 4;


    length += 8; // For the following delimiting characters
    StringBuilder builder = new StringBuilder(length);
    builder.append(COUNTER_OPEN);

    // Add the counter name
    builder.append(UNIT_OPEN);
    builder.append(escapedName);
    builder.append(UNIT_CLOSE);

    // Add the display name
    builder.append(UNIT_OPEN);
    builder.append(escapedDispName);
    builder.append(UNIT_CLOSE);

    // Add the value
    builder.append(UNIT_OPEN);
    builder.append(currentValue);
    builder.append(UNIT_CLOSE);

    builder.append(COUNTER_CLOSE);

    return builder.toString();
  }

  /**
   * Make the 0.21 counter group string.
   * format: {(actual-name)(display-name)(value)[][][]}
   * where [] are compact strings for the counters within.
   * @param <G> type of the group
   * @param group to stringify
   * @return the stringified result
   */
  public static <G extends CounterGroupBase<?>>
  String toEscapedCompactString(G group) {
    List<String> escapedStrs = Lists.newArrayList();
    int length;
    String escapedName, escapedDispName;
    synchronized(group) {
      // First up, obtain the strings that need escaping. This will help us
      // determine the buffer length apriori.
      escapedName = escape(group.getName());
      escapedDispName = escape(group.getDisplayName());
      int i = 0;
      length = escapedName.length() + escapedDispName.length();
      for (Counter counter : group) {
        String escapedStr = toEscapedCompactString(counter);
        escapedStrs.add(escapedStr);
        length += escapedStr.length();
      }
    }
    length += 6; // for all the delimiting characters below
    StringBuilder builder = new StringBuilder(length);
    builder.append(GROUP_OPEN); // group start

    // Add the group name
    builder.append(UNIT_OPEN);
    builder.append(escapedName);
    builder.append(UNIT_CLOSE);

    // Add the display name
    builder.append(UNIT_OPEN);
    builder.append(escapedDispName);
    builder.append(UNIT_CLOSE);

    // write the value
    for(String escaped : escapedStrs) {
      builder.append(escaped);
    }

    builder.append(GROUP_CLOSE); // group end
    return builder.toString();
  }

  /**
   * Make the pre 0.21 counters string
   * @param <C> type of the counter
   * @param <G> type of the counter group
   * @param <T> type of the counters object
   * @param counters the object to stringify
   * @return the string in the following format
   * {(groupName)(group-displayName)[(counterName)(displayName)(value)]*}*
   */
  public static <C extends Counter, G extends CounterGroupBase<C>,
                 T extends AbstractCounters<C, G>>
  String toEscapedCompactString(T counters) {
    String[] groupsArray;
    int length = 0;
    synchronized(counters) {
      groupsArray = new String[counters.countCounters()];
      int i = 0;
      // First up, obtain the escaped string for each group so that we can
      // determine the buffer length apriori.
      for (G group : counters) {
        String escapedString = toEscapedCompactString(group);
        groupsArray[i++] = escapedString;
        length += escapedString.length();
      }
    }

    // Now construct the buffer
    StringBuilder builder = new StringBuilder(length);
    for (String group : groupsArray) {
      builder.append(group);
    }
    return builder.toString();
  }

  // Escapes all the delimiters for counters i.e {,[,(,),],}
  private static String escape(String string) {
    return StringUtils.escapeString(string, StringUtils.ESCAPE_CHAR,
                                    charsToEscape);
  }

  // Unescapes all the delimiters for counters i.e {,[,(,),],}
  private static String unescape(String string) {
    return StringUtils.unEscapeString(string, StringUtils.ESCAPE_CHAR,
                                      charsToEscape);
  }

  // Extracts a block (data enclosed within delimeters) ignoring escape
  // sequences. Throws ParseException if an incomplete block is found else
  // returns null.
  private static String getBlock(String str, char open, char close,
                                IntWritable index) throws ParseException {
    StringBuilder split = new StringBuilder();
    int next = StringUtils.findNext(str, open, StringUtils.ESCAPE_CHAR,
                                    index.get(), split);
    split.setLength(0); // clear the buffer
    if (next >= 0) {
      ++next; // move over '('

      next = StringUtils.findNext(str, close, StringUtils.ESCAPE_CHAR,
                                  next, split);
      if (next >= 0) {
        ++next; // move over ')'
        index.set(next);
        return split.toString(); // found a block
      } else {
        throw new ParseException("Unexpected end of block", next);
      }
    }
    return null; // found nothing
  }

  /**
   * Parse a pre 0.21 counters string into a counter object.
   * @param <C> type of the counter
   * @param <G> type of the counter group
   * @param <T> type of the counters object
   * @param compactString to parse
   * @param counters an empty counters object to hold the result
   * @return the counters object holding the result
   * @throws ParseException
   */
  @SuppressWarnings("deprecation")
  public static <C extends Counter, G extends CounterGroupBase<C>,
                 T extends AbstractCounters<C, G>>
  T parseEscapedCompactString(String compactString, T counters)
      throws ParseException {
    IntWritable index = new IntWritable(0);

    // Get the group to work on
    String groupString =
      getBlock(compactString, GROUP_OPEN, GROUP_CLOSE, index);

    while (groupString != null) {
      IntWritable groupIndex = new IntWritable(0);

      // Get the actual name
      String groupName =
        getBlock(groupString, UNIT_OPEN, UNIT_CLOSE, groupIndex);
      groupName = unescape(groupName);

      // Get the display name
      String groupDisplayName =
        getBlock(groupString, UNIT_OPEN, UNIT_CLOSE, groupIndex);
      groupDisplayName = unescape(groupDisplayName);

      // Get the counters
      G group = counters.getGroup(groupName);
      group.setDisplayName(groupDisplayName);

      String counterString =
        getBlock(groupString, COUNTER_OPEN, COUNTER_CLOSE, groupIndex);

      while (counterString != null) {
        IntWritable counterIndex = new IntWritable(0);

        // Get the actual name
        String counterName =
          getBlock(counterString, UNIT_OPEN, UNIT_CLOSE, counterIndex);
        counterName = unescape(counterName);

        // Get the display name
        String counterDisplayName =
          getBlock(counterString, UNIT_OPEN, UNIT_CLOSE, counterIndex);
        counterDisplayName = unescape(counterDisplayName);

        // Get the value
        long value =
          Long.parseLong(getBlock(counterString, UNIT_OPEN, UNIT_CLOSE,
                                  counterIndex));

        // Add the counter
        Counter counter = group.findCounter(counterName);
        counter.setDisplayName(counterDisplayName);
        counter.increment(value);

        // Get the next counter
        counterString =
          getBlock(groupString, COUNTER_OPEN, COUNTER_CLOSE, groupIndex);
      }

      groupString = getBlock(compactString, GROUP_OPEN, GROUP_CLOSE, index);
    }
    return counters;
  }
}
