/*
 * 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.log4j.pattern;

import org.apache.log4j.helpers.Loader;
import org.apache.log4j.helpers.LogLog;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

// Contributors:   Nelson Minar <(nelson@monkey.org>
//                 Igor E. Poteryaev <jah@mail.ru>
//                 Reinhard Deschler <reinhard.deschler@web.de>

/**
 * Most of the work of the {@link org.apache.log4j.EnhancedPatternLayout} class
 * is delegated to the ExtrasPatternParser class.
 * <p>It is this class that parses conversion patterns and creates
 * a chained list of {@link PatternConverter PatternConverters}.
 *
 * @author James P. Cakalic
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 * @author Paul Smith
 * @author Curt Arnold
 *
*/
public final class ExtrasPatternParser {
  /**
   * Escape character for format specifier.
   */
  private static final char ESCAPE_CHAR = '%';

  /**
   * Literal state.
   */
  private static final int LITERAL_STATE = 0;

  /**
   * In converter name state.
   */
  private static final int CONVERTER_STATE = 1;

  /**
   * Dot state.
   */
  private static final int DOT_STATE = 3;

  /**
   * Min state.
   */
  private static final int MIN_STATE = 4;

  /**
   * Max state.
   */
  private static final int MAX_STATE = 5;

  /**
   * Standard format specifiers for EnhancedPatternLayout.
   */
  private static final Map PATTERN_LAYOUT_RULES;

  /**
   * Standard format specifiers for rolling file appenders.
   */
  private static final Map FILENAME_PATTERN_RULES;

  static {
    // We set the global rules in the static initializer of ExtrasPatternParser class
    Map rules = new HashMap(17);
    rules.put("c", LoggerPatternConverter.class);
    rules.put("logger", LoggerPatternConverter.class);

    rules.put("C", ClassNamePatternConverter.class);
    rules.put("class", ClassNamePatternConverter.class);

    rules.put("d", DatePatternConverter.class);
    rules.put("date", DatePatternConverter.class);

    rules.put("F", FileLocationPatternConverter.class);
    rules.put("file", FileLocationPatternConverter.class);

    rules.put("l", FullLocationPatternConverter.class);

    rules.put("L", LineLocationPatternConverter.class);
    rules.put("line", LineLocationPatternConverter.class);

    rules.put("m", MessagePatternConverter.class);
    rules.put("message", MessagePatternConverter.class);

    rules.put("n", LineSeparatorPatternConverter.class);

    rules.put("M", MethodLocationPatternConverter.class);
    rules.put("method", MethodLocationPatternConverter.class);

    rules.put("p", LevelPatternConverter.class);
    rules.put("level", LevelPatternConverter.class);

    rules.put("r", RelativeTimePatternConverter.class);
    rules.put("relative", RelativeTimePatternConverter.class);

    rules.put("t", ThreadPatternConverter.class);
    rules.put("thread", ThreadPatternConverter.class);

    rules.put("x", NDCPatternConverter.class);
    rules.put("ndc", NDCPatternConverter.class);

    rules.put("X", PropertiesPatternConverter.class);
    rules.put("properties", PropertiesPatternConverter.class);

    rules.put("sn", SequenceNumberPatternConverter.class);
    rules.put("sequenceNumber", SequenceNumberPatternConverter.class);

    rules.put("throwable", ThrowableInformationPatternConverter.class);
    PATTERN_LAYOUT_RULES = new ReadOnlyMap(rules);

    Map fnameRules = new HashMap(4);
    fnameRules.put("d", FileDatePatternConverter.class);
    fnameRules.put("date", FileDatePatternConverter.class);
    fnameRules.put("i", IntegerPatternConverter.class);
    fnameRules.put("index", IntegerPatternConverter.class);

    FILENAME_PATTERN_RULES = new ReadOnlyMap(fnameRules);
  }

  /**
   * Private constructor.
   */
  private ExtrasPatternParser() {
  }

  /**
   * Get standard format specifiers for EnhancedPatternLayout.
   * @return read-only map of format converter classes keyed by format specifier strings.
   */
  public static Map getPatternLayoutRules() {
    return PATTERN_LAYOUT_RULES;
  }

  /**
   * Get standard format specifiers for rolling file appender file specification.
   * @return read-only map of format converter classes keyed by format specifier strings.
   */
  public static Map getFileNamePatternRules() {
    return FILENAME_PATTERN_RULES;
  }

  /** Extract the converter identifier found at position i.
   *
   * After this function returns, the variable i will point to the
   * first char after the end of the converter identifier.
   *
   * If i points to a char which is not a character acceptable at the
   * start of a unicode identifier, the value null is returned.
   *
   * @param lastChar last processed character.
   * @param pattern format string.
   * @param i current index into pattern format.
   * @param convBuf buffer to receive conversion specifier.
   * @param currentLiteral literal to be output in case format specifier in unrecognized.
   * @return position in pattern after converter.
   */
  private static int extractConverter(
    char lastChar, final String pattern, int i, final StringBuffer convBuf,
    final StringBuffer currentLiteral) {
    convBuf.setLength(0);

    // When this method is called, lastChar points to the first character of the
    // conversion word. For example:
    // For "%hello"     lastChar = 'h'
    // For "%-5hello"   lastChar = 'h'
    //System.out.println("lastchar is "+lastChar);
    if (!Character.isUnicodeIdentifierStart(lastChar)) {
      return i;
    }

    convBuf.append(lastChar);

    while (
      (i < pattern.length())
        && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
      convBuf.append(pattern.charAt(i));
      currentLiteral.append(pattern.charAt(i));

      //System.out.println("conv buffer is now ["+convBuf+"].");
      i++;
    }

    return i;
  }

  /**
   * Extract options.
   * @param pattern conversion pattern.
   * @param i start of options.
   * @param options array to receive extracted options
   * @return position in pattern after options.
   */
  private static int extractOptions(String pattern, int i, List options) {
    while ((i < pattern.length()) && (pattern.charAt(i) == '{')) {
      int end = pattern.indexOf('}', i);

      if (end == -1) {
        break;
      }

      String r = pattern.substring(i + 1, end);
      options.add(r);
      i = end + 1;
    }

    return i;
  }

  /**
   * Parse a format specifier.
   * @param pattern pattern to parse.
   * @param patternConverters list to receive pattern converters.
   * @param formattingInfos list to receive field specifiers corresponding to pattern converters.
   * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
   * @param rules map of stock pattern converters keyed by format specifier.
   */
  public static void parse(
    final String pattern, final List patternConverters,
    final List formattingInfos, final Map converterRegistry, final Map rules) {
    if (pattern == null) {
      throw new NullPointerException("pattern");
    }

    StringBuffer currentLiteral = new StringBuffer(32);

    int patternLength = pattern.length();
    int state = LITERAL_STATE;
    char c;
    int i = 0;
    ExtrasFormattingInfo formattingInfo = ExtrasFormattingInfo.getDefault();

    while (i < patternLength) {
      c = pattern.charAt(i++);

      switch (state) {
      case LITERAL_STATE:

        // In literal state, the last char is always a literal.
        if (i == patternLength) {
          currentLiteral.append(c);

          continue;
        }

        if (c == ESCAPE_CHAR) {
          // peek at the next char.
          switch (pattern.charAt(i)) {
          case ESCAPE_CHAR:
            currentLiteral.append(c);
            i++; // move pointer

            break;

          default:

            if (currentLiteral.length() != 0) {
              patternConverters.add(
                new LiteralPatternConverter(currentLiteral.toString()));
              formattingInfos.add(ExtrasFormattingInfo.getDefault());
            }

            currentLiteral.setLength(0);
            currentLiteral.append(c); // append %
            state = CONVERTER_STATE;
            formattingInfo = ExtrasFormattingInfo.getDefault();
          }
        } else {
          currentLiteral.append(c);
        }

        break;

      case CONVERTER_STATE:
        currentLiteral.append(c);

        switch (c) {
        case '-':
          formattingInfo =
            new ExtrasFormattingInfo(
              true, 
              formattingInfo.isRightTruncated(),
              formattingInfo.getMinLength(),
              formattingInfo.getMaxLength());
          break;

        case '!':
          formattingInfo =
            new ExtrasFormattingInfo(
              formattingInfo.isLeftAligned(), 
              true,
              formattingInfo.getMinLength(),
              formattingInfo.getMaxLength());
          break;


        case '.':
          state = DOT_STATE;

          break;

        default:

          if ((c >= '0') && (c <= '9')) {
            formattingInfo =
              new ExtrasFormattingInfo(
                formattingInfo.isLeftAligned(), 
                formattingInfo.isRightTruncated(),
                c - '0',
                formattingInfo.getMaxLength());
            state = MIN_STATE;
          } else {
            i = finalizeConverter(
                c, pattern, i, currentLiteral, formattingInfo,
                converterRegistry, rules, patternConverters, formattingInfos);

            // Next pattern is assumed to be a literal.
            state = LITERAL_STATE;
            formattingInfo = ExtrasFormattingInfo.getDefault();
            currentLiteral.setLength(0);
          }
        } // switch

        break;

      case MIN_STATE:
        currentLiteral.append(c);

        if ((c >= '0') && (c <= '9')) {
          formattingInfo =
            new ExtrasFormattingInfo(
              formattingInfo.isLeftAligned(),
              formattingInfo.isRightTruncated(),
              (formattingInfo.getMinLength() * 10) + (c - '0'),
              formattingInfo.getMaxLength());
        } else if (c == '.') {
          state = DOT_STATE;
        } else {
          i = finalizeConverter(
              c, pattern, i, currentLiteral, formattingInfo,
              converterRegistry, rules, patternConverters, formattingInfos);
          state = LITERAL_STATE;
          formattingInfo = ExtrasFormattingInfo.getDefault();
          currentLiteral.setLength(0);
        }

        break;

      case DOT_STATE:
        currentLiteral.append(c);

        if ((c >= '0') && (c <= '9')) {
          formattingInfo =
            new ExtrasFormattingInfo(
              formattingInfo.isLeftAligned(), 
              formattingInfo.isRightTruncated(),
              formattingInfo.getMinLength(),
              c - '0');
          state = MAX_STATE;
        } else {
            LogLog.error(
              "Error occured in position " + i
              + ".\n Was expecting digit, instead got char \"" + c + "\".");

          state = LITERAL_STATE;
        }

        break;

      case MAX_STATE:
        currentLiteral.append(c);

        if ((c >= '0') && (c <= '9')) {
          formattingInfo =
            new ExtrasFormattingInfo(
              formattingInfo.isLeftAligned(), 
              formattingInfo.isRightTruncated(),
              formattingInfo.getMinLength(),
              (formattingInfo.getMaxLength() * 10) + (c - '0'));
        } else {
          i = finalizeConverter(
              c, pattern, i, currentLiteral, formattingInfo,
              converterRegistry, rules, patternConverters, formattingInfos);
          state = LITERAL_STATE;
          formattingInfo = ExtrasFormattingInfo.getDefault();
          currentLiteral.setLength(0);
        }

        break;
      } // switch
    }

    // while
    if (currentLiteral.length() != 0) {
      patternConverters.add(
        new LiteralPatternConverter(currentLiteral.toString()));
      formattingInfos.add(ExtrasFormattingInfo.getDefault());
    }
  }

  /**
   * Creates a new PatternConverter.
   *
   *
   * @param converterId converterId.
   * @param currentLiteral literal to be used if converter is unrecognized or following converter
   *    if converterId contains extra characters.
   * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
   * @param rules map of stock pattern converters keyed by format specifier.
   * @param options converter options.
   * @return  converter or null.
   */
  private static PatternConverter createConverter(
    final String converterId, final StringBuffer currentLiteral,
    final Map converterRegistry, final Map rules, final List options) {
    String converterName = converterId;
    Object converterObj = null;

    for (int i = converterId.length(); (i > 0) && (converterObj == null);
        i--) {
      converterName = converterName.substring(0, i);

      if (converterRegistry != null) {
        converterObj = converterRegistry.get(converterName);
      }

      if ((converterObj == null) && (rules != null)) {
        converterObj = rules.get(converterName);
      }
    }

    if (converterObj == null) {
        LogLog.error("Unrecognized format specifier [" + converterId + "]");

      return null;
    }

    Class converterClass;

    if (converterObj instanceof Class) {
      converterClass = (Class) converterObj;
    } else {
      if (converterObj instanceof String) {
        try {
          converterClass = Loader.loadClass((String) converterObj);
        } catch (ClassNotFoundException ex) {
            LogLog.warn(
              "Class for conversion pattern %" + converterName + " not found",
              ex);

          return null;
        }
      } else {
          LogLog.warn(
            "Bad map entry for conversion pattern %" +  converterName + ".");

        return null;
      }
    }

    try {
      Method factory =
        converterClass.getMethod(
          "newInstance",
          new Class[] {
            Class.forName("[Ljava.lang.String;")
          });
      String[] optionsArray = new String[options.size()];
      optionsArray = (String[]) options.toArray(optionsArray);

      Object newObj =
        factory.invoke(null, new Object[] { optionsArray });

      if (newObj instanceof PatternConverter) {
        currentLiteral.delete(
          0,
          currentLiteral.length()
          - (converterId.length() - converterName.length()));

        return (PatternConverter) newObj;
      } else {
          LogLog.warn(
            "Class " + converterClass.getName()
            + " does not extend PatternConverter.");
      }
    } catch (Exception ex) {
        LogLog.error("Error creating converter for " + converterId, ex);

      try {
        //
        //  try default constructor
        PatternConverter pc = (PatternConverter) converterClass.newInstance();
        currentLiteral.delete(
          0,
          currentLiteral.length()
          - (converterId.length() - converterName.length()));

        return pc;
      } catch (Exception ex2) {
          LogLog.error("Error creating converter for " + converterId, ex2);
      }
    }

    return null;
  }

  /**
   * Processes a format specifier sequence.
   *
   * @param c initial character of format specifier.
   * @param pattern conversion pattern
   * @param i current position in conversion pattern.
   * @param currentLiteral current literal.
   * @param formattingInfo current field specifier.
   * @param converterRegistry map of user-provided pattern converters keyed by format specifier, may be null.
   * @param rules map of stock pattern converters keyed by format specifier.
   * @param patternConverters list to receive parsed pattern converter.
   * @param formattingInfos list to receive corresponding field specifier.
   * @return position after format specifier sequence.
   */
  private static int finalizeConverter(
    char c, String pattern, int i,
    final StringBuffer currentLiteral, final ExtrasFormattingInfo formattingInfo,
    final Map converterRegistry, final Map rules, final List patternConverters,
    final List formattingInfos) {
    StringBuffer convBuf = new StringBuffer();
    i = extractConverter(c, pattern, i, convBuf, currentLiteral);

    String converterId = convBuf.toString();

    List options = new ArrayList();
    i = extractOptions(pattern, i, options);

    PatternConverter pc =
      createConverter(
        converterId, currentLiteral, converterRegistry, rules, options);

    if (pc == null) {
      StringBuffer msg;

      if (converterId.length() == 0) {
        msg =
          new StringBuffer("Empty conversion specifier starting at position ");
      } else {
        msg = new StringBuffer("Unrecognized conversion specifier [");
        msg.append(converterId);
        msg.append("] starting at position ");
      }

      msg.append(Integer.toString(i));
      msg.append(" in conversion pattern.");

        LogLog.error(msg.toString());

      patternConverters.add(
        new LiteralPatternConverter(currentLiteral.toString()));
      formattingInfos.add(ExtrasFormattingInfo.getDefault());
    } else {
      patternConverters.add(pc);
      formattingInfos.add(formattingInfo);

      if (currentLiteral.length() > 0) {
        patternConverters.add(
          new LiteralPatternConverter(currentLiteral.toString()));
        formattingInfos.add(ExtrasFormattingInfo.getDefault());
      }
    }

    currentLiteral.setLength(0);

    return i;
  }

  /**
   * The class wraps another Map but throws exceptions on any attempt to modify the map.
   */
  private static class ReadOnlyMap implements Map {
    /**
     * Wrapped map.
     */
    private final Map map;

    /**
     * Constructor
     * @param src source map.
     */
    public ReadOnlyMap(Map src) {
      map = src;
    }

    /**
     * {@inheritDoc}
     */
    public void clear() {
      throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public boolean containsKey(Object key) {
      return map.containsKey(key);
    }

    /**
     * {@inheritDoc}
     */
    public boolean containsValue(Object value) {
      return map.containsValue(value);
    }

    /**
     * {@inheritDoc}
     */
    public Set entrySet() {
      return map.entrySet();
    }

    /**
     * {@inheritDoc}
     */
    public Object get(Object key) {
      return map.get(key);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isEmpty() {
      return map.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    public Set keySet() {
      return map.keySet();
    }

    /**
     * {@inheritDoc}
     */
    public Object put(Object key, Object value) {
      throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public void putAll(Map t) {
      throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public Object remove(Object key) {
      throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public int size() {
      return map.size();
    }

    /**
     * {@inheritDoc}
     */
    public Collection values() {
      return map.values();
    }
  }
}
