/*
 * 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.maven.shared.utils.xml;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;

/**
 * XMLWriter with nice indentation.
 *
 * @author kama
 */
public class PrettyPrintXMLWriter implements XMLWriter {
    private static final char[] CLOSE_1 = "/>".toCharArray();

    private static final char[] CLOSE_2 = "</".toCharArray();

    private static final char[] DEFAULT_LINE_INDENT = new char[] {' ', ' '};

    private PrintWriter writer;

    private ArrayList<String> elementStack = new ArrayList<String>();

    private boolean processingElement = false;

    private boolean documentStarted = false;

    private boolean endOnSameLine = false;

    private int depth = 0;

    private char[] lineIndent;

    private char[] lineSeparator;

    private String encoding;

    private String docType;

    /**
     * @param writer not null
     * @param lineIndent can be null, but the normal way is some spaces
     */
    public PrettyPrintXMLWriter(PrintWriter writer, String lineIndent) {
        this(writer, lineIndent, null, null);
    }

    /**
     * @param writer not null
     * @param lineIndent can be null, but the normal way is some spaces.
     */
    public PrettyPrintXMLWriter(Writer writer, String lineIndent) {
        this(new PrintWriter(writer), lineIndent);
    }

    /**
     * @param writer not null
     */
    public PrettyPrintXMLWriter(PrintWriter writer) {
        this(writer, null, null);
    }

    /**
     * @param writer not null
     */
    public PrettyPrintXMLWriter(Writer writer) {
        this(new PrintWriter(writer));
    }

    /**
     * @param writer not null
     * @param lineIndent can be null, but the normal way is some spaces
     * @param encoding can be null or invalid
     * @param doctype can be null
     */
    public PrettyPrintXMLWriter(PrintWriter writer, String lineIndent, String encoding, String doctype) {
        this(writer, lineIndent.toCharArray(), "\n".toCharArray(), encoding, doctype);
    }

    /**
     * @param writer not null
     * @param lineIndent can be null, but the normal way is some spaces
     * @param encoding can be null or invalid
     * @param doctype can be null
     */
    public PrettyPrintXMLWriter(Writer writer, String lineIndent, String encoding, String doctype) {
        this(new PrintWriter(writer), lineIndent, encoding, doctype);
    }

    /**
     * @param writer not null
     * @param encoding can be null or invalid
     * @param doctype can be null
     */
    public PrettyPrintXMLWriter(PrintWriter writer, String encoding, String doctype) {
        this(writer, DEFAULT_LINE_INDENT, "\n".toCharArray(), encoding, doctype);
    }

    /**
     * @param writer not null
     * @param encoding can be null or invalid
     * @param doctype can be null
     */
    public PrettyPrintXMLWriter(Writer writer, String encoding, String doctype) {
        this(new PrintWriter(writer), encoding, doctype);
    }

    /**
     * @param writer not null
     * @param lineIndent can be null, but the normal way is some spaces.
     * @param lineSeparator can be null, but the normal way is valid line separator
     * @param encoding can be null or the encoding to use.
     * @param doctype can be null
     */
    public PrettyPrintXMLWriter(
            PrintWriter writer, String lineIndent, String lineSeparator, String encoding, String doctype) {
        this(writer, lineIndent.toCharArray(), lineSeparator.toCharArray(), encoding, doctype);
    }

    /**
     * @param writer        not null
     * @param lineIndent    can be null, but the normal way is some spaces
     * @param lineSeparator can be null, but the normal way is valid line separator
     * @param encoding      can be null or the encoding to use
     * @param doctype       can be null
     */
    private PrettyPrintXMLWriter(
            PrintWriter writer, char[] lineIndent, char[] lineSeparator, String encoding, String doctype) {
        super();
        this.writer = writer;
        this.lineIndent = lineIndent;
        this.lineSeparator = lineSeparator;
        this.encoding = encoding;
        this.docType = doctype;

        depth = 0;

        // Fail early with assertions enabled. Issue is in the calling code not having checked for any errors.
        assert !writer.checkError() : "Unexpected error state PrintWriter passed to PrettyPrintXMLWriter.";
    }

    /** {@inheritDoc} */
    public void addAttribute(String key, String value) throws IOException {
        if (!processingElement) {
            throw new IllegalStateException("currently processing no element");
        }

        writer.write(' ');
        writer.write(key);
        writer.write('=');
        XMLEncode.xmlEncodeTextAsPCDATA(value, true, '"', writer);
        if (writer.checkError()) {
            throw new IOException("Failure adding attribute '" + key + "' with value '" + value + "'");
        }
    }

    /** {@inheritDoc} */
    public void setEncoding(String encoding) {
        if (documentStarted) {
            throw new IllegalStateException("Document headers already written!");
        }

        this.encoding = encoding;
    }

    /** {@inheritDoc} */
    public void setDocType(String docType) {
        if (documentStarted) {
            throw new IllegalStateException("Document headers already written!");
        }

        this.docType = docType;
    }

    /**
     * @param lineSeparator the line separator to be output
     */
    public void setLineSeparator(String lineSeparator) {
        if (documentStarted) {
            throw new IllegalStateException("Document headers already written!");
        }

        this.lineSeparator = lineSeparator.toCharArray();
    }

    /**
     * @param lineIndentParameter the line indent parameter
     */
    public void setLineIndenter(String lineIndentParameter) {
        if (documentStarted) {
            throw new IllegalStateException("Document headers already written!");
        }

        this.lineIndent = lineIndentParameter.toCharArray();
    }

    /** {@inheritDoc} */
    public void startElement(String elementName) throws IOException {

        if (elementName.isEmpty()) {
            throw new IllegalArgumentException("Element name cannot be empty");
        }

        boolean firstLine = ensureDocumentStarted();

        completePreviouslyOpenedElement();

        if (!firstLine) {
            newLine();
        }

        writer.write('<');
        writer.write(elementName);
        if (writer.checkError()) {
            throw new IOException("Failure starting element '" + elementName + "'.");
        }

        processingElement = true;

        elementStack.add(depth++, elementName);
    }

    /** {@inheritDoc} */
    public void writeText(String text) throws IOException {
        ensureDocumentStarted();

        completePreviouslyOpenedElement();

        XMLEncode.xmlEncodeText(text, writer);

        endOnSameLine = true;

        if (writer.checkError()) {
            throw new IOException("Failure writing text.");
        }
    }

    /** {@inheritDoc} */
    public void writeMarkup(String markup) throws IOException {
        ensureDocumentStarted();

        completePreviouslyOpenedElement();

        writer.write(markup);

        if (writer.checkError()) {
            throw new IOException("Failure writing markup.");
        }
    }

    /** {@inheritDoc} */
    public void endElement() throws IOException {
        String chars = elementStack.get(--depth);
        if (processingElement) {
            // this means we don't have any content yet so we just add a />
            writer.write(CLOSE_1);

            processingElement = false;
        } else {
            if (!endOnSameLine) {
                newLine();
            }

            // otherwise we need a full closing tag for that element
            writer.write(CLOSE_2);
            writer.write(chars);
            writer.write('>');
        }

        endOnSameLine = false;

        if (writer.checkError()) {
            throw new IOException("Failure ending element.");
        }
    }

    /**
     * Write the document if not already done.
     *
     * @return <code>true</code> if the document headers have freshly been written.
     */
    private boolean ensureDocumentStarted() {
        if (!documentStarted) {
            if (docType != null || encoding != null) {
                writeDocumentHeader();
            }

            documentStarted = true;

            return true;
        }

        return false;
    }

    private void writeDocumentHeader() {
        writer.write("<?xml version=\"1.0\"");

        if (encoding != null) {
            writer.write(" encoding=\"");
            writer.write(encoding);
            writer.write('\"');
        }

        writer.write("?>");

        newLine();

        if (docType != null) {
            writer.write("<!DOCTYPE ");
            writer.write(docType);
            writer.write('>');
            newLine();
        }
    }

    private void newLine() {
        writer.write(lineSeparator);

        for (int i = 0; i < depth; i++) {
            writer.write(lineIndent);
        }
    }

    private void completePreviouslyOpenedElement() {
        if (processingElement) {
            writer.write('>');
            processingElement = false;
        }
    }
}
