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

import org.apache.log4j.chainsaw.messages.MessageCenter;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.pattern.*;
import org.apache.log4j.xml.Log4jEntityResolver;
import org.apache.log4j.xml.SAXErrorHandler;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class LogFilePatternLayoutBuilder {
    public static String getLogFormatFromPatternLayout(String patternLayout) {
        String input = OptionConverter.convertSpecialChars(patternLayout);
        List converters = new ArrayList();
        List fields = new ArrayList();
        Map converterRegistry = null;

        PatternParser.parse(input, converters, fields, converterRegistry, PatternParser.getPatternLayoutRules());
        return getFormatFromConverters(converters);
    }

    public static String getTimeStampFormat(String patternLayout) {
        int basicIndex = patternLayout.indexOf("%d");
        if (basicIndex < 0) {
            return null;
        }

        int index = patternLayout.indexOf("%d{");
        //%d - default
        if (index < 0) {
            return "yyyy-MM-dd HH:mm:ss,SSS";
        }

        int length = patternLayout.substring(index).indexOf("}");
        String timestampFormat = patternLayout.substring(index + "%d{".length(), index + length);
        if (timestampFormat.equals("ABSOLUTE")) {
            return "HH:mm:ss,SSS";
        }
        if (timestampFormat.equals("ISO8601")) {
            return "yyyy-MM-dd HH:mm:ss,SSS";
        }
        if (timestampFormat.equals("DATE")) {
            return "dd MMM yyyy HH:mm:ss,SSS";
        }
        return timestampFormat;
    }

    private static String getFormatFromConverters(List converters) {
        StringBuffer buffer = new StringBuffer();
        for (Object converter1 : converters) {
            LoggingEventPatternConverter converter = (LoggingEventPatternConverter) converter1;
            if (converter instanceof DatePatternConverter) {
                buffer.append("TIMESTAMP");
            } else if (converter instanceof MessagePatternConverter) {
                buffer.append("MESSAGE");
            } else if (converter instanceof LoggerPatternConverter) {
                buffer.append("LOGGER");
            } else if (converter instanceof ClassNamePatternConverter) {
                buffer.append("CLASS");
            } else if (converter instanceof RelativeTimePatternConverter) {
                buffer.append("PROP(RELATIVETIME)");
            } else if (converter instanceof ThreadPatternConverter) {
                buffer.append("THREAD");
            } else if (converter instanceof NDCPatternConverter) {
                buffer.append("NDC");
            } else if (converter instanceof LiteralPatternConverter) {
                LiteralPatternConverter literal = (LiteralPatternConverter) converter;
                //format shouldn't normally take a null, but we're getting a literal, so passing in the buffer will work
                literal.format(null, buffer);
            } else if (converter instanceof SequenceNumberPatternConverter) {
                buffer.append("PROP(log4jid)");
            } else if (converter instanceof LevelPatternConverter) {
                buffer.append("LEVEL");
            } else if (converter instanceof MethodLocationPatternConverter) {
                buffer.append("METHOD");
            } else if (converter instanceof FullLocationPatternConverter) {
                buffer.append("PROP(locationInfo)");
            } else if (converter instanceof LineLocationPatternConverter) {
                buffer.append("LINE");
            } else if (converter instanceof FileLocationPatternConverter) {
                buffer.append("FILE");
            } else if (converter instanceof PropertiesPatternConverter) {
//                PropertiesPatternConverter propertiesConverter = (PropertiesPatternConverter) converter;
//                String option = propertiesConverter.getOption();
//                if (option != null && option.length() > 0) {
//                    buffer.append("PROP(" + option + ")");
//                } else {
                buffer.append("PROP(PROPERTIES)");
//                }
            } else if (converter instanceof LineSeparatorPatternConverter) {
                //done
            }
        }
        return buffer.toString();
    }

    public static Map<String, Map<String, String>> getAppenderConfiguration(File file) {
        try {
            return getXMLFileAppenderConfiguration(file);
        } catch (IOException | SAXException | ParserConfigurationException e) {
            //ignore
        }
        try {
            return getPropertiesFileAppenderConfiguration(file);
        } catch (Exception e) {
            //ignore
        }
        //don't return null
        return new HashMap<>();
    }

    public static Map<String, Map<String, String>> getPropertiesFileAppenderConfiguration(File propertyFile) throws IOException {
        Map<String, Map<String, String>> result = new HashMap<>();
        String appenderPrefix = "log4j.appender";
        Properties props = new Properties();
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(propertyFile);
            props.load(inputStream);
            Enumeration propertyNames = props.propertyNames();
            Map<String, String> appenders = new HashMap<>();
            while (propertyNames.hasMoreElements()) {
                String propertyName = propertyNames.nextElement().toString();
                if (propertyName.startsWith(appenderPrefix)) {
                    String value = propertyName.substring(appenderPrefix.length() + 1);
                    if (!value.contains(".")) {
                        //no sub-values - this entry is the appender name & class
                        appenders.put(value, props.getProperty(propertyName).trim());
                    }
                }
            }
            for (Object o : appenders.entrySet()) {
                Map.Entry appenderEntry = (Map.Entry) o;
                String appenderName = appenderEntry.getKey().toString();
                String appenderClassName = appenderEntry.getValue().toString();
                if (appenderClassName.toLowerCase(Locale.ENGLISH).endsWith("fileappender")) {
                    String layout = props.getProperty(appenderPrefix + "." + appenderName + ".layout");
                    if (layout != null && layout.trim().equals("org.apache.log4j.PatternLayout")) {
                        String conversion = props.getProperty(appenderPrefix + "." + appenderName + ".layout.ConversionPattern");
                        String file = props.getProperty(appenderPrefix + "." + appenderName + ".File");
                        if (conversion != null && file != null) {
                            Map<String, String> entry = new HashMap<>();
                            entry.put("file", file.trim());
                            entry.put("conversion", conversion.trim());
                            result.put(appenderName, entry);
                        }
                    }
                }
            }
          /*
          example:
          log4j.appender.R=org.apache.log4j.RollingFileAppender
          log4j.appender.R.File=${catalina.base}/logs/tomcat.log
          log4j.appender.R.MaxFileSize=10MB
          log4j.appender.R.MaxBackupIndex=10
          log4j.appender.R.layout=org.apache.log4j.PatternLayout
          log4j.appender.R.layout.ConversionPattern=%d - %p %t %c - %m%n
           */
        } catch (IOException ioe) {
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
        //don't return null
        return result;
    }

    private static Map<String, Map<String, String>> getXMLFileAppenderConfiguration(File file) throws IOException, ParserConfigurationException, SAXException {
        Map<String, Map<String, String>> result = new HashMap<>();
        try (InputStream stream = file.toURI().toURL().openStream()) {
            InputSource src = new InputSource(stream);
            src.setSystemId(file.toURI().toURL().toString());
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = dbf.newDocumentBuilder();

            docBuilder.setErrorHandler(new SAXErrorHandler());
            docBuilder.setEntityResolver(new Log4jEntityResolver());
            Document doc = docBuilder.parse(src);
            NodeList appenders = doc.getElementsByTagName("appender");
            for (int i = 0; i < appenders.getLength(); i++) {
                Node appender = appenders.item(i);
                NamedNodeMap appenderAttributes = appender.getAttributes();
//        Class appenderClass = Class.forName(map.getNamedItem("class").getNodeValue());
                Node appenderClass = appenderAttributes.getNamedItem("class");
                if (appenderAttributes.getNamedItem("name") != null && appenderClass != null && appenderClass.getNodeValue() != null) {
                    //all log4j fileappenders end in fileappender..if a custom fileappender also ends in fileappender and uses the same dom nodes to be loaded,
                    //try to parse the nodes as well
                    if (appenderClass.getNodeValue().toLowerCase(Locale.ENGLISH).endsWith("fileappender")) {
                        String appenderName = appenderAttributes.getNamedItem("name").getNodeValue();
                        //subclass of FileAppender - add it
                        Map<String, String> entry = new HashMap<>();
                        NodeList appenderChildren = appender.getChildNodes();
                        for (int j = 0; j < appenderChildren.getLength(); j++) {
                            Node appenderChild = appenderChildren.item(j);
                            if (appenderChild.getNodeName().equals("param") && appenderChild.hasAttributes()) {
                                Node fileNameNode = appenderChild.getAttributes().getNamedItem("name");
                                if (fileNameNode != null && fileNameNode.getNodeValue().equalsIgnoreCase("file")) {
                                    Node fileValueNode = appenderChild.getAttributes().getNamedItem("value");
                                    if (fileValueNode != null) {
                                        entry.put("file", fileValueNode.getNodeValue());
                                    }
                                }
                            }
                            if (appenderChild.getNodeName().equalsIgnoreCase("layout") && appenderChild.hasAttributes()) {
                                NamedNodeMap layoutAttributes = appenderChild.getAttributes();
                                Node layoutNode = layoutAttributes.getNamedItem("class");
                                if (layoutNode != null && layoutNode.getNodeValue() != null && layoutNode.getNodeValue().equalsIgnoreCase("org.apache.log4j.PatternLayout")) {
                                    NodeList layoutChildren = appenderChild.getChildNodes();
                                    for (int k = 0; k < layoutChildren.getLength(); k++) {
                                        Node layoutChild = layoutChildren.item(k);
                                        if (layoutChild.getNodeName().equals("param") && layoutChild.hasAttributes()) {
                                            Node layoutName = layoutChild.getAttributes().getNamedItem("name");
                                            if (layoutName != null && layoutName.getNodeValue() != null && layoutName.getNodeValue().equalsIgnoreCase("conversionpattern")) {
                                                Node conversionValue = layoutChild.getAttributes().getNamedItem("value");
                                                if (conversionValue != null) {
                                                    entry.put("conversion", conversionValue.getNodeValue());
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        result.put(appenderName, entry);
                    }
                }
            }
        }
        MessageCenter.getInstance().getLogger().info("getXMLFileAppenderConfiguration for file: " + file + ", result: " + result);
        return result;
    }
}
