package ru.yandex.webmaster.common.util.xml;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

/**
 * @author aherman
 */
public class SimpleXmlBuilder {
    private static final DateTimeFormatter JODA_DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeNoMillis();

    private static final ThreadLocal<SimpleDateFormat> DATE_TIME_FORMAT = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
        }
    };

    private final boolean prettyPrint;
    private final StringBuilder sb;
    private final boolean canClose;
    private final Deque<String> elementContext = new LinkedList<String>();

    private XmlState state = XmlState.XML_BEGIN;


    private SimpleXmlBuilder(StringBuilder sb, boolean prettyPrint, boolean canClose) {
        this.sb = sb;
        this.prettyPrint = prettyPrint;
        this.canClose = canClose;
    }

    public SimpleXmlBuilder(StringBuilder sb, boolean prettyPrint) {
        this(sb, prettyPrint, false);
    }

    public SimpleXmlBuilder(StringBuilder sb) {
        this(sb, false, false);
    }


    public SimpleXmlBuilder(boolean prettyPrint) {
        this(new StringBuilder(64), prettyPrint, true);
    }

    public SimpleXmlBuilder() {
        this(false);
    }

    public SimpleXmlBuilder open(String name) {
        validateIsTrue(state != XmlState.XML_END, "Xml already closed: state=%s", state);
        validateIsTrue(state != XmlState.TEXT, "Element start after text: state=%s", state);
        validateIsTrue(name.indexOf(' ') < 0, "Element name contains space symbol: name=%s", name);

        if (state == XmlState.ELEMENT_START_OPEN) {
            closeElementStart();
            if (prettyPrint) {
                sb.append('\n');
            }
        }

        sb.append('<');
        sb.append(name);

        elementContext.push(name);
        state = XmlState.ELEMENT_START_OPEN;
        return this;
    }

    public SimpleXmlBuilder attribute(String name, String value) {
        validateAttributePreconditions(name);
        validateIsTrue(value.indexOf('"') < 0, "Attribute value contains quotation mark: attribute=%s", name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(cleanAndEscapeCharacters(value)).append('"');
        return this;
    }

    public SimpleXmlBuilder attribute(String name, int value) {
        validateAttributePreconditions(name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(value).append('"');
        return this;
    }

    public SimpleXmlBuilder attribute(String name, long value) {
        validateAttributePreconditions(name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(value).append('"');
        return this;
    }

    public SimpleXmlBuilder attribute(String name, boolean value) {
        validateAttributePreconditions(name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(value).append('"');
        return this;
    }

    public SimpleXmlBuilder attribute(String name, Number value) {
        validateAttributePreconditions(name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(value).append('"');
        return this;
    }

    public SimpleXmlBuilder attribute(String name, Enum<?> value) {
        validateAttributePreconditions(name);
        sb.append(' ').append(cleanAndEscapeCharacters(name)).append('=').append('"').append(value.name()).append('"');
        return this;
    }

    private void validateAttributePreconditions(String name) {
        validateIsTrue(state == XmlState.ELEMENT_START_OPEN, "Element already closed start: attribute=%s", name);
        validateIsTrue(name.indexOf('"') < 0, "Attribute name contains quotation mark: attribute=%s", name);
        validateIsTrue(name.indexOf(' ') < 0, "Attribute name contains space symbol: attribute=%s", name);
    }

    public SimpleXmlBuilder element(String name, String text) {
        if (text != null) {
            this.open(name).text(text).close();
        }
        return this;
    }

    public SimpleXmlBuilder element(String name, int value) {
        this.open(name).text(value).close();
        return this;
    }

    public SimpleXmlBuilder element(String name, long value) {
        this.open(name).text(value).close();
        return this;
    }

    public SimpleXmlBuilder element(String name, Boolean value) {
        if(value != null) {
            element(name, value.booleanValue());
        }
        return this;
    }

    public SimpleXmlBuilder element(String name, boolean value) {
        this.open(name).text(value).close();
        return this;
    }

    public SimpleXmlBuilder element(String name, Number value) {
        if (value != null) {
            this.open(name).text(value).close();
        }
        return this;
    }

    public SimpleXmlBuilder element(String name, Enum<?> value) {
        if (value != null) {
            this.open(name).text(value).close();
        }
        return this;
    }

    public SimpleXmlBuilder element(String name, Date value) {
        if (value != null) {
            this.open(name).text(DATE_TIME_FORMAT.get().format(value)).close();
        }
        return this;
    }

    public SimpleXmlBuilder element(String name, DateTime value) {
        if (value != null) {
            this.open(name).text(JODA_DATE_TIME_FORMATTER.print(value)).close();
        }
        return this;
    }

    private void prepareForText() {
        validateIsTrue(state == XmlState.ELEMENT_START_OPEN || state == XmlState.TEXT, "Text merges with elements");
        if (state == XmlState.ELEMENT_START_OPEN) {
            closeElementStart();
        }
    }

    public SimpleXmlBuilder text(String text) {
        if (text == null) {
            return this;
        }
        prepareForText();

        sb.append(cleanAndEscapeCharacters(text));
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder text(int value) {
        prepareForText();

        sb.append(value);
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder text(long value) {
        prepareForText();

        sb.append(value);
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder text(boolean value) {
        prepareForText();

        sb.append(value);
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder text(Number value) {
        prepareForText();

        sb.append(value);
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder text(Enum<?> value) {
        if (value == null) {
            return this;
        }
        prepareForText();

        sb.append(value.name());
        state = XmlState.TEXT;
        return this;
    }

    public SimpleXmlBuilder cdataText(String value) {
        if (value == null) {
            return this;
        }
        prepareForText();
        if (value.contains("]]>")) {
            value = value.replaceAll("\\]\\]>", "]]]]><![CDATA[>");
        }
        sb.append("<![CDATA[").append(value).append("]]>");
        state = XmlState.TEXT;
        return this;
    }

    private void closeElementStart() {
        sb.append('>');
        state = XmlState.ELEMENT_START_CLOSED;
    }

    public void close() {
        close(false);
    }

    public void close(boolean forceCloseTag) {
        String name = elementContext.pop();
        if (state == XmlState.ELEMENT_START_OPEN) {
            if (forceCloseTag) {
                sb.append('>').append('<').append('/').append(name).append('>');
            } else {
                sb.append('/').append('>');
            }
        } else {
            sb.append('<').append('/').append(name).append('>');
        }

        if (prettyPrint) {
            sb.append('\n');
        }

        if (elementContext.isEmpty()) {
            state = XmlState.XML_END;
        } else {
            state = XmlState.ELEMENT_START_CLOSED;
        }
    }

    private static String cleanAndEscapeCharacters(String text) {
        boolean shouldBeEscaped = false;
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            if ((ch >= 0x00 && ch <= 0x08)
                    || ch == 0x0B
                    || ch == 0x0C
                    || (ch >= 0x0E && ch <= 0x1F)
                    || (ch >= 0x7F && ch <= 0x84)
                    || (ch >= 0x86 && ch <= 0x9F)
                    || (ch >= 0xFDD0 && ch <= 0xFDDF)
                    || ch == 0xFFFE
                    || ch == 0xFFFF
                    || ch == '&'
                    || ch == '<'
                    || ch == '>'
                    || ch == '"'
                    || ch == '\'')
            {
                shouldBeEscaped = true;
            }
        }
        if (!shouldBeEscaped) {
            return text;
        }

        StringBuilder sb = new StringBuilder((int) (text.length() * 1.2));
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            if (ch == 0x00
                    || ch == 0xFFFE
                    || ch == 0xFFFF)
            {
                continue;
            }

            if ((ch >= 0x01 && ch <= 0x08)
                    || ch == 0x0B
                    || ch == 0x0C
                    || (ch >= 0x0E && ch <= 0x1F)
                    || (ch >= 0x7F && ch <= 0x84)
                    || (ch >= 0x86 && ch <= 0x9F)
                    || (ch >= 0xFDD0 && ch <= 0xFDDF))
            {
                sb.append("&#x").append(Integer.toHexString(ch)).append(';');
            } else if(ch == '&') {
                sb.append("&amp;");
            } else if (ch == '<') {
                sb.append("&lt;");
            } else if (ch == '>') {
                sb.append("&gt;");
            } else if (ch == '"') {
                sb.append("&quot;");
            } else if (ch == '\'') {
                sb.append("&apos;");
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();
    }

    private String getContextXPath() {
        StringBuilder sb = new StringBuilder(16);

        Iterator<String> it = elementContext.descendingIterator();

        while (it.hasNext()) {
            String elementName = it.next();
            sb.append('/').append(elementName);
        }

        return sb.toString();
    }

    private void validateIsTrue(boolean value, String message, Object... args) {
        if (!value) {
            throw new IllegalArgumentException(String.format(message, args) + " path=" + getContextXPath());
        }
    }

    public void flush() {
        while(!elementContext.isEmpty()) {
            close();
        }
    }

    public String finish() {
        if (!canClose) {
            throw new IllegalStateException("External string builder provided");
        }
        return sb.toString();
    }

    private static enum XmlState {
        XML_BEGIN,

        ELEMENT_START_OPEN,
        ELEMENT_START_CLOSED,

        TEXT,

        XML_END
    }
}
