package org.codehaus.plexus.util.xml;

/*
 * Copyright The Codehaus Foundation.
 *
 * Licensed 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.
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;

import org.codehaus.plexus.util.xml.pull.MXParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

/**
 * Common XML utilities methods.
 *
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 *
 * @since 1.5.7
 */
public class XmlUtil {
    /** The default line indenter size i.e. 2. */
    public static final int DEFAULT_INDENTATION_SIZE = 2;

    /** The default line separator ("\n" on UNIX) */
    public static final String DEFAULT_LINE_SEPARATOR = System.getProperty("line.separator");

    /**
     * Determines if a given File shall be handled as XML.
     *
     * @param f not null file
     * @return <code>true</code> if the given file has XML content, <code>false</code> otherwise.
     */
    public static boolean isXml(File f) {
        if (f == null) {
            throw new IllegalArgumentException("f could not be null.");
        }

        if (!f.isFile()) {
            throw new IllegalArgumentException("The file '" + f.getAbsolutePath() + "' is not a file.");
        }

        try (Reader reader = ReaderFactory.newXmlReader(f)) {
            XmlPullParser parser = new MXParser();
            parser.setInput(reader);
            parser.nextToken();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Pretty format the input reader. For instance, the following input:
     *
     * <pre>
     * &lt;div&gt;&lt;b&gt;content&lt;/b&gt;&lt;/div&gt;
     * </pre>
     *
     * becomes
     *
     * <pre>
     * &lt;div&gt;
     *   &lt;b&gt;content&lt;/b&gt;
     * &lt;/div&gt;
     * </pre>
     *
     * @param reader not null
     * @param writer not null
     * @throws IOException if any or invalid xml content
     * @see #prettyFormat(Reader, Writer, int, String)
     * @see ReaderFactory to read an xml content
     * @see WriterFactory to write an xml content
     */
    public static void prettyFormat(Reader reader, Writer writer) throws IOException {
        prettyFormat(reader, writer, DEFAULT_INDENTATION_SIZE, DEFAULT_LINE_SEPARATOR);
    }

    /**
     * Pretty format the input reader. For instance, the following input:
     *
     * <pre>
     * &lt;div&gt;&lt;b&gt;content&lt;/b&gt;&lt;/div&gt;
     * </pre>
     *
     * becomes
     *
     * <pre>
     * &lt;div&gt;
     *   &lt;b&gt;content&lt;/b&gt;
     * &lt;/div&gt;
     * </pre>
     *
     * @param reader not null
     * @param writer not null
     * @param indentSize positive number for the indentation
     * @param lineSeparator the wanted line separator
     * @throws IOException if any or invalid xml content
     * @see ReaderFactory to read an xml content
     * @see WriterFactory to write an xml content
     */
    public static void prettyFormat(Reader reader, Writer writer, int indentSize, String lineSeparator)
            throws IOException {
        if (reader == null) {
            throw new IllegalArgumentException("The reader is null");
        }
        if (writer == null) {
            throw new IllegalArgumentException("The writer is null");
        }
        if (indentSize < 0) {
            indentSize = 0;
        }

        PrettyPrintXMLWriter xmlWriter = new PrettyPrintXMLWriter(writer);
        xmlWriter.setLineIndenter(StringUtils.repeat(" ", indentSize));
        xmlWriter.setLineSeparator(lineSeparator);

        XmlPullParser parser = new MXParser();
        try {
            parser.setInput(reader);

            prettyFormatInternal(parser, xmlWriter);
        } catch (XmlPullParserException e) {
            throw new IOException("Unable to parse the XML: " + e.getMessage());
        }
    }

    /**
     * Pretty format the input stream. For instance, the following input:
     *
     * <pre>
     * &lt;div&gt;&lt;b&gt;content&lt;/b&gt;&lt;/div&gt;
     * </pre>
     *
     * becomes
     *
     * <pre>
     * &lt;div&gt;
     *   &lt;b&gt;content&lt;/b&gt;
     * &lt;/div&gt;
     * </pre>
     *
     * @param is not null
     * @param os not null
     * @throws IOException if any or invalid xml content
     * @see #prettyFormat(InputStream, OutputStream, int, String)
     */
    public static void prettyFormat(InputStream is, OutputStream os) throws IOException {
        prettyFormat(is, os, DEFAULT_INDENTATION_SIZE, DEFAULT_LINE_SEPARATOR);
    }

    /**
     * Pretty format the input stream. For instance, the following input:
     *
     * <pre>
     * &lt;div&gt;&lt;b&gt;content&lt;/b&gt;&lt;/div&gt;
     * </pre>
     *
     * becomes
     *
     * <pre>
     * &lt;div&gt;
     *   &lt;b&gt;content&lt;/b&gt;
     * &lt;/div&gt;
     * </pre>
     *
     * @param is not null
     * @param os not null
     * @param indentSize positive number for the indentation
     * @param lineSeparator the wanted line separator
     * @throws IOException if any or invalid xml content
     */
    public static void prettyFormat(InputStream is, OutputStream os, int indentSize, String lineSeparator)
            throws IOException {
        if (is == null) {
            throw new IllegalArgumentException("The is is null");
        }
        if (os == null) {
            throw new IllegalArgumentException("The os is null");
        }
        if (indentSize < 0) {
            indentSize = 0;
        }

        try (Reader reader = ReaderFactory.newXmlReader(is);
                Writer writer = new OutputStreamWriter(os)) {
            final PrettyPrintXMLWriter xmlWriter = new PrettyPrintXMLWriter(writer);
            xmlWriter.setLineIndenter(StringUtils.repeat(" ", indentSize));
            xmlWriter.setLineSeparator(lineSeparator);

            final XmlPullParser parser = new MXParser();
            parser.setInput(reader);

            prettyFormatInternal(parser, xmlWriter);
        } catch (XmlPullParserException e) {
            throw new IOException("Unable to parse the XML: " + e.getMessage());
        }
    }

    /**
     * @param parser not null
     * @param writer not null
     * @throws XmlPullParserException if any
     * @throws IOException if any
     */
    private static void prettyFormatInternal(XmlPullParser parser, PrettyPrintXMLWriter writer)
            throws XmlPullParserException, IOException {
        boolean hasTag = false;
        boolean hasComment = false;
        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG) {
                hasTag = true;
                if (hasComment) {
                    writer.writeText(writer.getLineIndenter());
                    hasComment = false;
                }
                writer.startElement(parser.getName());
                for (int i = 0; i < parser.getAttributeCount(); i++) {
                    String key = parser.getAttributeName(i);
                    String value = parser.getAttributeValue(i);
                    writer.addAttribute(key, value);
                }
            } else if (eventType == XmlPullParser.TEXT) {
                String text = parser.getText();
                if (!text.trim().equals("")) {
                    text = StringUtils.removeDuplicateWhitespace(text);
                    writer.writeText(text);
                }
            } else if (eventType == XmlPullParser.END_TAG) {
                hasTag = false;
                writer.endElement();
            } else if (eventType == XmlPullParser.COMMENT) {
                hasComment = true;
                if (!hasTag) {
                    writer.writeMarkup(writer.getLineSeparator());
                    for (int i = 0; i < writer.getDepth(); i++) {
                        writer.writeMarkup(writer.getLineIndenter());
                    }
                }
                writer.writeMarkup("<!--" + parser.getText().trim() + " -->");
                if (!hasTag) {
                    writer.writeMarkup(writer.getLineSeparator());
                    for (int i = 0; i < writer.getDepth() - 1; i++) {
                        writer.writeMarkup(writer.getLineIndenter());
                    }
                }
            } else if (eventType == XmlPullParser.DOCDECL) {
                writer.writeMarkup("<!DOCTYPE" + parser.getText() + ">");
                writer.endOfLine();
            } else if (eventType == XmlPullParser.PROCESSING_INSTRUCTION) {
                writer.writeMarkup("<?" + parser.getText() + "?>");
                writer.endOfLine();
            } else if (eventType == XmlPullParser.CDSECT) {
                writer.writeMarkup("<![CDATA[" + parser.getText() + "]]>");
            } else if (eventType == XmlPullParser.ENTITY_REF) {
                writer.writeMarkup("&" + parser.getName() + ";");
            }

            eventType = parser.nextToken();
        }
    }
}
