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

import org.apache.log4j.Layout;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.LocationInfo;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Arrays;

// 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.PatternLayout} class is delegated to the PatternParser class.
 *
 * <p>
 * It is this class that parses conversion patterns and creates a chained list of
 * {@link OptionConverter OptionConverters}.
 *
 * @author James P. Cakalic
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 * @since 0.8.2
 */
public class PatternParser {

    private static final char LEFT_BRACKET = '{';
    private static final char RIGHT_BRACKET = '}';
    private static final char N_CHAR = 'n';
    private static final char DOT_CHAR = '.';
    private static final char DASH_CHAR = '-';
    private static final char ESCAPE_CHAR = '%';

    private static final int LITERAL_STATE = 0;
    private static final int CONVERTER_STATE = 1;
    private static final int DOT_STATE = 3;
    private static final int MIN_STATE = 4;
    private static final int MAX_STATE = 5;

    static final int FULL_LOCATION_CONVERTER = 1000;
    static final int METHOD_LOCATION_CONVERTER = 1001;
    static final int CLASS_LOCATION_CONVERTER = 1002;
    static final int LINE_LOCATION_CONVERTER = 1003;
    static final int FILE_LOCATION_CONVERTER = 1004;

    static final int RELATIVE_TIME_CONVERTER = 2000;
    static final int THREAD_CONVERTER = 2001;
    static final int LEVEL_CONVERTER = 2002;
    static final int NDC_CONVERTER = 2003;
    static final int MESSAGE_CONVERTER = 2004;

    int state;
    protected StringBuffer currentLiteral = new StringBuffer(32);
    protected int patternLength;
    protected int i;
    PatternConverter head;
    PatternConverter tail;
    protected FormattingInfo formattingInfo = new FormattingInfo();
    protected String pattern;

    public PatternParser(String pattern) {
        this.pattern = pattern;
        patternLength = pattern.length();
        state = LITERAL_STATE;
    }

    private void addToList(PatternConverter pc) {
        if (head == null) {
            head = tail = pc;
        } else {
            tail.next = pc;
            tail = pc;
        }
    }

    protected String extractOption() {
        if ((i < patternLength) && (pattern.charAt(i) == LEFT_BRACKET)) {
            int end = pattern.indexOf(RIGHT_BRACKET, i);
            if (end > i) {
                String r = pattern.substring(i + 1, end);
                i = end + 1;
                return r;
            }
        }
        return null;
    }

    /**
     * The option is expected to be in decimal and positive. In case of error, zero is returned.
     */
    protected int extractPrecisionOption() {
        String opt = extractOption();
        int r = 0;
        if (opt != null) {
            try {
                r = Integer.parseInt(opt);
                if (r <= 0) {
                    LogLog.error("Precision option (" + opt + ") isn't a positive integer.");
                    r = 0;
                }
            } catch (NumberFormatException e) {
                LogLog.error("Category option \"" + opt + "\" not a decimal integer.", e);
            }
        }
        return r;
    }

    public PatternConverter parse() {
        char c;
        i = 0;
        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;
                    case N_CHAR:
                        currentLiteral.append(Layout.LINE_SEP);
                        i++; // move pointer
                        break;
                    default:
                        if (currentLiteral.length() != 0) {
                            addToList(new LiteralPatternConverter(currentLiteral.toString()));
                            // LogLog.debug("Parsed LITERAL converter: \""
                            // +currentLiteral+"\".");
                        }
                        currentLiteral.setLength(0);
                        currentLiteral.append(c); // append %
                        state = CONVERTER_STATE;
                        formattingInfo.reset();
                    }
                } else {
                    currentLiteral.append(c);
                }
                break;
            case CONVERTER_STATE:
                currentLiteral.append(c);
                switch (c) {
                case DASH_CHAR:
                    formattingInfo.leftAlign = true;
                    break;
                case DOT_CHAR:
                    state = DOT_STATE;
                    break;
                default:
                    if (c >= '0' && c <= '9') {
                        formattingInfo.min = c - '0';
                        state = MIN_STATE;
                    } else
                        finalizeConverter(c);
                } // switch
                break;
            case MIN_STATE:
                currentLiteral.append(c);
                if (c >= '0' && c <= '9')
                    formattingInfo.min = formattingInfo.min * 10 + (c - '0');
                else if (c == '.')
                    state = DOT_STATE;
                else {
                    finalizeConverter(c);
                }
                break;
            case DOT_STATE:
                currentLiteral.append(c);
                if (c >= '0' && c <= '9') {
                    formattingInfo.max = 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.max = formattingInfo.max * 10 + (c - '0');
                else {
                    finalizeConverter(c);
                    state = LITERAL_STATE;
                }
                break;
            } // switch
        } // while
        if (currentLiteral.length() != 0) {
            addToList(new LiteralPatternConverter(currentLiteral.toString()));
            // LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\".");
        }
        return head;
    }

    protected void finalizeConverter(char c) {
        PatternConverter pc = null;
        switch (c) {
        case 'c':
            pc = new CategoryPatternConverter(formattingInfo, extractPrecisionOption());
            // LogLog.debug("CATEGORY converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'C':
            pc = new ClassNamePatternConverter(formattingInfo, extractPrecisionOption());
            // LogLog.debug("CLASS_NAME converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'd':
            String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT;
            DateFormat df;
            String dOpt = extractOption();
            if (dOpt != null)
                dateFormatStr = dOpt;

            if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT))
                df = new ISO8601DateFormat();
            else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT))
                df = new AbsoluteTimeDateFormat();
            else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT))
                df = new DateTimeDateFormat();
            else {
                try {
                    df = new SimpleDateFormat(dateFormatStr);
                } catch (IllegalArgumentException e) {
                    LogLog.error("Could not instantiate SimpleDateFormat with " + dateFormatStr, e);
                    df = (DateFormat) OptionConverter.instantiateByClassName(
                            "org.apache.log4j.helpers.ISO8601DateFormat", DateFormat.class, null);
                }
            }
            pc = new DatePatternConverter(formattingInfo, df);
            // LogLog.debug("DATE converter {"+dateFormatStr+"}.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'F':
            pc = new LocationPatternConverter(formattingInfo, FILE_LOCATION_CONVERTER);
            // LogLog.debug("File name converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'l':
            pc = new LocationPatternConverter(formattingInfo, FULL_LOCATION_CONVERTER);
            // LogLog.debug("Location converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'L':
            pc = new LocationPatternConverter(formattingInfo, LINE_LOCATION_CONVERTER);
            // LogLog.debug("LINE NUMBER converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'm':
            pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER);
            // LogLog.debug("MESSAGE converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'M':
            pc = new LocationPatternConverter(formattingInfo, METHOD_LOCATION_CONVERTER);
            // LogLog.debug("METHOD converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'p':
            pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER);
            // LogLog.debug("LEVEL converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 'r':
            pc = new BasicPatternConverter(formattingInfo, RELATIVE_TIME_CONVERTER);
            // LogLog.debug("RELATIVE time converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        case 't':
            pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER);
            // LogLog.debug("THREAD converter.");
            // formattingInfo.dump();
            currentLiteral.setLength(0);
            break;
        /*
         * case 'u': if(i < patternLength) { char cNext = pattern.charAt(i); if(cNext >=
         * '0' && cNext <= '9') { pc = new UserFieldPatternConverter(formattingInfo,
         * cNext - '0'); LogLog.debug("USER converter ["+cNext+"].");
         * formattingInfo.dump(); currentLiteral.setLength(0); i++; } else
         * LogLog.error("Unexpected char" +cNext+" at position "+i); } break;
         */
        case 'x':
            pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER);
            // LogLog.debug("NDC converter.");
            currentLiteral.setLength(0);
            break;
        case 'X':
            String xOpt = extractOption();
            pc = new MDCPatternConverter(formattingInfo, xOpt);
            currentLiteral.setLength(0);
            break;
        default:
            LogLog.error("Unexpected char [" + c + "] at position " + i + " in conversion patterrn.");
            pc = new LiteralPatternConverter(currentLiteral.toString());
            currentLiteral.setLength(0);
        }

        addConverter(pc);
    }

    protected void addConverter(PatternConverter pc) {
        currentLiteral.setLength(0);
        // Add the pattern converter to the list.
        addToList(pc);
        // Next pattern is assumed to be a literal.
        state = LITERAL_STATE;
        // Reset formatting info
        formattingInfo.reset();
    }

    // ---------------------------------------------------------------------
    // PatternConverters
    // ---------------------------------------------------------------------

    private static class BasicPatternConverter extends PatternConverter {
        int type;

        BasicPatternConverter(FormattingInfo formattingInfo, int type) {
            super(formattingInfo);
            this.type = type;
        }

        public String convert(LoggingEvent event) {
            switch (type) {
            case RELATIVE_TIME_CONVERTER:
                return (Long.toString(event.timeStamp - LoggingEvent.getStartTime()));
            case THREAD_CONVERTER:
                return event.getThreadName();
            case LEVEL_CONVERTER:
                return event.getLevel().toString();
            case NDC_CONVERTER:
                return event.getNDC();
            case MESSAGE_CONVERTER: {
                return event.getRenderedMessage();
            }
            default:
                return null;
            }
        }
    }

    private static class LiteralPatternConverter extends PatternConverter {
        private String literal;

        LiteralPatternConverter(String value) {
            literal = value;
        }

        public final void format(StringBuffer sbuf, LoggingEvent event) {
            sbuf.append(literal);
        }

        public String convert(LoggingEvent event) {
            return literal;
        }
    }

    private static class DatePatternConverter extends PatternConverter {
        private DateFormat df;
        private Date date;

        DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) {
            super(formattingInfo);
            date = new Date();
            this.df = df;
        }

        public String convert(LoggingEvent event) {
            date.setTime(event.timeStamp);
            String converted = null;
            try {
                converted = df.format(date);
            } catch (Exception ex) {
                LogLog.error("Error occured while converting date.", ex);
            }
            return converted;
        }
    }

    private static class MDCPatternConverter extends PatternConverter {
        private String key;

        MDCPatternConverter(FormattingInfo formattingInfo, String key) {
            super(formattingInfo);
            this.key = key;
        }

        public String convert(LoggingEvent event) {
            if (key == null) {
                StringBuilder buf = new StringBuilder("{");
                Map properties = event.getProperties();
                if (properties.size() > 0) {
                    Object[] keys = properties.keySet().toArray();
                    Arrays.sort(keys);
                    for (int i = 0; i < keys.length; i++) {
                        buf.append('{');
                        buf.append(keys[i]);
                        buf.append(',');
                        buf.append(properties.get(keys[i]));
                        buf.append('}');
                    }
                }
                buf.append('}');
                return buf.toString();
            } else {
                Object val = event.getMDC(key);
                if (val == null) {
                    return null;
                } else {
                    return val.toString();
                }
            }
        }
    }

    private class LocationPatternConverter extends PatternConverter {
        int type;

        LocationPatternConverter(FormattingInfo formattingInfo, int type) {
            super(formattingInfo);
            this.type = type;
        }

        public String convert(LoggingEvent event) {
            LocationInfo locationInfo = event.getLocationInformation();
            switch (type) {
            case FULL_LOCATION_CONVERTER:
                return locationInfo.fullInfo;
            case METHOD_LOCATION_CONVERTER:
                return locationInfo.getMethodName();
            case LINE_LOCATION_CONVERTER:
                return locationInfo.getLineNumber();
            case FILE_LOCATION_CONVERTER:
                return locationInfo.getFileName();
            default:
                return null;
            }
        }
    }

    private static abstract class NamedPatternConverter extends PatternConverter {
        int precision;

        NamedPatternConverter(FormattingInfo formattingInfo, int precision) {
            super(formattingInfo);
            this.precision = precision;
        }

        abstract String getFullyQualifiedName(LoggingEvent event);

        public String convert(LoggingEvent event) {
            String n = getFullyQualifiedName(event);
            if (precision <= 0)
                return n;
            else {
                int len = n.length();

                // We substract 1 from 'len' when assigning to 'end' to avoid out of
                // bounds exception in return r.substring(end+1, len). This can happen if
                // precision is 1 and the category name ends with a dot.
                int end = len - 1;
                for (int i = precision; i > 0; i--) {
                    end = n.lastIndexOf('.', end - 1);
                    if (end == -1)
                        return n;
                }
                return n.substring(end + 1, len);
            }
        }
    }

    private class ClassNamePatternConverter extends NamedPatternConverter {

        ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) {
            super(formattingInfo, precision);
        }

        String getFullyQualifiedName(LoggingEvent event) {
            return event.getLocationInformation().getClassName();
        }
    }

    private class CategoryPatternConverter extends NamedPatternConverter {

        CategoryPatternConverter(FormattingInfo formattingInfo, int precision) {
            super(formattingInfo, precision);
        }

        String getFullyQualifiedName(LoggingEvent event) {
            return event.getLoggerName();
        }
    }
}
