/*
 * 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 PatternParser
 * 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 PatternParser {
    /**
     * 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 PatternParser 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 PatternParser() {
    }

    /**
     * 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.
     * <p>
     * After this function returns, the variable i will point to the first char after the end of the converter
     * identifier.
     * <p>
     * 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 StringBuilder convBuf,
            final StringBuilder 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");
        }

        StringBuilder currentLiteral = new StringBuilder(32);

        int patternLength = pattern.length();
        int state = LITERAL_STATE;
        char c;
        int i = 0;
        FormattingInfo formattingInfo = FormattingInfo.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(FormattingInfo.getDefault());
                        }

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

                break;

            case CONVERTER_STATE:
                currentLiteral.append(c);

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

                    break;

                case '.':
                    state = DOT_STATE;

                    break;

                default:

                    if ((c >= '0') && (c <= '9')) {
                        formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), 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 = FormattingInfo.getDefault();
                        currentLiteral.setLength(0);
                    }
                } // switch

                break;

            case MIN_STATE:
                currentLiteral.append(c);

                if ((c >= '0') && (c <= '9')) {
                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(),
                            (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 = FormattingInfo.getDefault();
                    currentLiteral.setLength(0);
                }

                break;

            case DOT_STATE:
                currentLiteral.append(c);

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

                break;
            } // switch
        }

        // while
        if (currentLiteral.length() != 0) {
            patternConverters.add(new LiteralPatternConverter(currentLiteral.toString()));
            formattingInfos.add(FormattingInfo.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 StringBuilder 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 = null;

        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 StringBuilder currentLiteral,
            final FormattingInfo formattingInfo, final Map converterRegistry, final Map rules,
            final List patternConverters, final List formattingInfos) {
        StringBuilder convBuf = new StringBuilder();
        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) {
            StringBuilder msg;

            if ((converterId == null) || (converterId.length() == 0)) {
                msg = new StringBuilder("Empty conversion specifier starting at position ");
            } else {
                msg = new StringBuilder("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(FormattingInfo.getDefault());
        } else {
            patternConverters.add(pc);
            formattingInfos.add(formattingInfo);

            if (currentLiteral.length() > 0) {
                patternConverters.add(new LiteralPatternConverter(currentLiteral.toString()));
                formattingInfos.add(FormattingInfo.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();
        }
    }
}
