package de.duehl.basics.text.xml.generation;

import java.util.ArrayList;
import java.util.List;

import de.duehl.basics.collections.CollectionsHelper;

/*
 * Copyright 2019 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.text.Text;
import de.duehl.basics.text.xml.NamedXmlParameter;

/**
 * Diese Klasse stellt einen StringBuilder zum Erstellen eines XML-Dokuments dar.
 *
 * @version 1.01     2019-05-17
 * @author Christian Dühl
 */

public class XmlBuilder {

    /** Kopfzeile der XML-Datei. */
    public static final String XML_HEADER =
            "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";

    private static final int INDENTAION_DEPTH = 4;

    /** Intern verwendeter StringBuilder. */
    private final StringBuilder builder;

    /** Gibt das Level der Einrückung an. */
    private int indentationLevel;

    /** Merkt sich die Länge des Inhalts bei der letzten Erhöhung der Einrückung. */
    private int lengthAtLastIncrease;

    /** Konstruktor. */
    public XmlBuilder() {
        builder = new StringBuilder();
        indentationLevel = 0;
        lengthAtLastIncrease = 0; // bei 0 auch keine Einrückung, wenn h1 gleich zu Beginn kommt.
    }

    /** Gibt die Länge des bisherigen Texts im Builder zurück. */
    public int length() {
        return builder.length();
    }

    /** Erhöht das Level der Einrückung. */
    public XmlBuilder increaseIndentationLevel() {
        ++indentationLevel;
        lengthAtLastIncrease = length();
        return this;
    }

    /** Verringert das Level der Einrückung. */
    public XmlBuilder decreaseIndentationLevel() {
        --indentationLevel;
        return this;
    }

    /** Setzt das Level der Einrückung auf 0. */
    public XmlBuilder setIndentationLevelToZero() {
        setIndentationLevel(0);
        return this;
    }

    /** Setzt das Level der Einrückung. */
    protected XmlBuilder setIndentationLevel(int indentationLevel) {
        this.indentationLevel = indentationLevel;
        return this;
    }

    /** Gibt die Länge des Inhalts des Builders bei der letzten Erhöhung der Einrückung zurück. */
    protected int getLengthAtLastIncrease() {
        return lengthAtLastIncrease;
    }

    /** Fügt passenden Leerraum hinzu, abhängig von der aktuellen Einrücktiefe. */
    protected final XmlBuilder appendSpaces() {
        String space = Text.multipleString(" ", indentationLevel * INDENTAION_DEPTH);
        builder.append(space);
        return this;
    }

    /** Löscht den Inhalt des HtmlBuilders. */
    public XmlBuilder clear() {
        builder.setLength(0);
        indentationLevel = 0;
        return this;
    }

    /** Fügt beliebigen Text hinzu. */
    public XmlBuilder append(String text) {
        builder.append(text);
        return this;
    }

    /** Fügt beliebigen Text hinzu. Dieser wird passend eingerückt.*/
    public XmlBuilder appendIndented(String text) {
        appendSpaces();
        append(text);
        return this;
    }

    /**
     * Fügt die übergebenen Zeilen passend eingerückt hinzu. Dafür sollten die übergebenen Zeilen
     * in der kleinsten Einrückung die Einrücktiefe null aufweisen, damit alles richtig aussieht.
     */
    public XmlBuilder appendMultipleLines(String ... lines) {
        List<String> list = CollectionsHelper.stringArrayToList(lines);
        return appendMultipleLines(list);
    }

    /**
     * Fügt die übergebenen Zeilen passend eingerückt hinzu. Dafür sollten die übergebenen Zeilen
     * in der kleinsten Einrückung die Einrücktiefe null aufweisen, damit alles richtig aussieht.
     */
    public XmlBuilder appendMultipleLines(List<String> lines) {
        for (String line : lines) {
            appendLn(line);
        }
        return this;
    }

    /** Fügt eine Zahl hinzu. */
    public XmlBuilder append(int number) {
        builder.append(Integer.toString(number));
        return this;
    }

    /** Fügt eine Zahl hinzu. Diese wird passend eingerückt. */
    public XmlBuilder appendIndented(int number) {
        appendSpaces();
        append(number);
        return this;
    }

    /** Fügt beliebigen Text und einen Zeilenumbruch hinzu. Der Text wird passend eingerückt. */
    public XmlBuilder appendLn(String text) {
        if (!text.isEmpty()) {
            appendIndented(text);
        }
        appendLineBreak();
        return this;
    }

    /** Fügt einen Zeilenumbruch im Dokument hinzu. */
    public XmlBuilder appendLineBreak() {
        append(Text.LINE_BREAK);
        return this;
    }

    /** Fügt eine leere Zeile hinzu. */
    public XmlBuilder appendEmptyLine() {
        appendLn("");
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag> hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     */
    public XmlBuilder appendOpeningTag(String tag) {
        appendSpaces();
        appendPureOpeningTag(tag);
        appendLineBreak();
        increaseIndentationLevel();
        return this;
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag param1 param2 ... > hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param parameters
     *            Parameter die angefügt werden.
     */
    public XmlBuilder appendOpeningTagWithParameters(String tag, String ... params) {
        List<String> list = CollectionsHelper.stringArrayToList(params);
        return appendOpeningTagWithParameters(tag, list);
    }

    /**
     * Fügt einen öffnenden Tag der Form <tag param1 param2 ... > hinzu.
     *
     * @param tag
     *            Der zu öffnende Tag.
     * @param parameters
     *            Parameter die angefügt werden.
     */
    public XmlBuilder appendOpeningTagWithParameters(String tag, List<String> params) {
        appendSpaces();
        appendPureOpeningTagWithParameters(tag, params);
        appendLineBreak();
        increaseIndentationLevel();
        return this;
    }

    private void appendPureOpeningTagWithParameters(String tag, List<String> params) {
        append("<");
        append(tag);
        for (String param : params) {
            append(" ").append(param);
        }
        append(">");
    }

    public void appendPureOpeningTagWithParameters(String tag, NamedXmlParameter ... parameter) {
        List<String> params = namedXmlParametersToNormalParameters(parameter);
        appendPureOpeningTagWithParameters(tag, params);
    }


    /**
     * Fügt einen schließenden Tag der Form <tag> hinzu.
     *
     * @param tag
     *            Der zu schließende Tag.
     */
    public XmlBuilder appendClosingTag(String tag) {
        decreaseIndentationLevel();
        appendSpaces();
        appendPureClosingTag(tag);
        appendLineBreak();
        return this;
    }

    /**
     * Fügt einen öffnenden Tag, der allein in einer Zeile steht, hinzu.
     *
     * @param tag
     *            Name des Tags (ohne spitze Klammern).
     * @param parameter
     *            0 bis beliebig viele Parameter.
     */
    public XmlBuilder appendOpeningTagWithParameters(String tag, NamedXmlParameter ... parameter) {
        List<String> params = namedXmlParametersToNormalParameters(parameter);
        appendOpeningTagWithParameters(tag, params);
        return this;
    }

    private List<String> namedXmlParametersToNormalParameters(NamedXmlParameter... parameter) {
        List<String> params = new ArrayList<>();
        for (NamedXmlParameter param : parameter) {
            params.add(param.toString());
        }
        return params;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param number
     *            Nummer die als Text innerhalb des Tags kommt.
     */
    public XmlBuilder appendInTag(String tag, double number) {
        appendInTag(tag, Double.toString(number));
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param number
     *            Nummer die als Text innerhalb des Tags kommt.
     */
    public XmlBuilder appendInTag(String tag, int number) {
        appendInTag(tag, Integer.toString(number));
        return this;
    }

    /**
     * Fügt einen Text umgeben von einem Tag (öffnender Tag vorn, schließender Tag hinten) ein.
     *
     * @param tag
     *            Name des Tags.
     * @param text
     *            Text innerhalb des Tags.
     */
    public XmlBuilder appendInTag(String tag, String text) {
        appendSpaces();
        appendPureOpeningTag(tag);
        append(text);
        appendPureClosingTag(tag);
        appendLineBreak();
        return this;
    }

    private void appendPureOpeningTag(String tag) {
        append("<");
        append(tag);
        append(">");
    }

    private void appendPureClosingTag(String tag) {
        append("</");
        append(tag);
        append(">");
    }

    /**
     * Fügt einen kompletten Tag auf einer Zeile hinzu.
     *
     * @param tag
     *            Name des Tags (ohne spitze Klammern).
     * @param contents
     *            Inhalt des Tags.
     * @param parameter
     *            0 bis beliebig viele Parameter.
     */
    public XmlBuilder appendInTag(String tag, String contents, NamedXmlParameter... parameter) {
        appendSpaces();
        appendPureOpeningTagWithParameters(tag, parameter);
        append(contents);
        appendPureClosingTag(tag);
        appendLineBreak();
        return this;
    }

    /** Gibt an, ob der Text leer ist. */
    public boolean isEmpty() {
        return 0 == length();
    }

    /** Gibt den Inhalt des Builders als String zurück. */
    @Override
    public String toString() {
        return builder.toString();
    }


    /** Erzeugt einen einzeiligen Kommentar. */
    public XmlBuilder appendCommentLine(String comment) {
        appendSpaces();
        appendComment(comment);
        appendLineBreak();
        return this;
    }

    /** Erzeugt einen Kommentar. */
    public XmlBuilder appendComment(String comment) {
        appendCommentStart();
        append(comment);
        apendCommentEnd();
        return this;
    }

    private void appendCommentStart() {
        builder.append("<!-- ");
    }

    private void apendCommentEnd() {
        builder.append(" -->");
    }

    protected int indexOf(String search) {
        return builder.indexOf(search);
    }

    protected void insert(int index, String contents) {
        builder.insert(index, contents);
    }

    /* ################################# XML-Spezielles ####################################### */

    /** Fügt den XML-Header hinzu. */
    public XmlBuilder appendXmlHeader() {
        appendLn(XML_HEADER);
        appendEmptyLine();
        return this;
    }

}
