/**
 *   This file is part of Skript.
 *
 *  Skript is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Skript is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Güttinger and contributors
 */
package ch.njol.yggdrasil.xml;

import static ch.njol.yggdrasil.Tag.*;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.OutputStream;
import java.util.Locale;
import java.util.regex.Pattern;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.eclipse.jdt.annotation.Nullable;

import ch.njol.util.StringUtils;
import ch.njol.yggdrasil.Tag;
import ch.njol.yggdrasil.Yggdrasil;
import ch.njol.yggdrasil.YggdrasilException;
import ch.njol.yggdrasil.YggdrasilOutputStream;

/**
 * @deprecated XML has so many quirks that storing arbitrary data cannot be guaranteed.
 * @author Peter Güttinger
 */
@Deprecated
public final class YggXMLOutputStream extends YggdrasilOutputStream {
	
	private final OutputStream os;
	private final XMLStreamWriter out;
	
	private final short version;
	
	@SuppressWarnings("null")
	public YggXMLOutputStream(final Yggdrasil y, final OutputStream out) throws IOException, FactoryConfigurationError {
		super(y);
		version = y.version;
		try {
			os = out;
			this.out = XMLOutputFactory.newFactory().createXMLStreamWriter(out, "UTF-8");
			this.out.writeStartDocument("utf-8", "1.0");
			this.out.writeStartElement("yggdrasil");
			writeAttribute("version", "" + version);
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	// private
	
	@SuppressWarnings("null")
	private String getTypeName(Class<?> c) throws NotSerializableException {
		String a = "";
		while (c.isArray()) {
			a += "[]";
			c = c.getComponentType();
		}
		final String s;
		final Tag type = getType(c);
		switch (type) {
			case T_OBJECT:
			case T_ENUM:
				s = yggdrasil.getID(c);
				break;
			case T_BOOLEAN:
			case T_BOOLEAN_OBJ:
			case T_BYTE:
			case T_BYTE_OBJ:
			case T_CHAR:
			case T_CHAR_OBJ:
			case T_DOUBLE:
			case T_DOUBLE_OBJ:
			case T_FLOAT:
			case T_FLOAT_OBJ:
			case T_INT:
			case T_INT_OBJ:
			case T_LONG:
			case T_LONG_OBJ:
			case T_SHORT:
			case T_SHORT_OBJ:
			case T_CLASS:
			case T_STRING:
			default:
				s = type.name;
				break;
			case T_NULL:
			case T_REFERENCE:
			case T_ARRAY:
				throw new YggdrasilException("" + c.getCanonicalName());
		}
		return s + a;
	}
	
	@SuppressWarnings("null")
	private final static Pattern valid = Pattern.compile("[\\u0009 \\u000A \\u000D \\u0020-\\u007E \\u0085 \\u00A0-\\uD7FF \\uE000-\\uFFFD \\x{10000}–\\x{10FFFF}]*", Pattern.COMMENTS);
	
	private static void validateString(final String s) throws IOException {
		if (!valid.matcher(s).matches())
			throw new IOException("The string '" + s + "' contains characters illegal in XML 1.0: '" + toUnicodeEscapes("" + valid.matcher(s).replaceAll("")) + "'");
	}
	
	private static String toUnicodeEscapes(final String s) {
		final StringBuilder b = new StringBuilder();
		for (int i = 0; i < s.length(); i++) {
			b.append(String.format("\\u%04x", (int) s.charAt(i)));
		}
		return "" + b;
	}
	
	private void writeEndElement() throws IOException {
		try {
			out.writeEndElement();
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	private void writeAttribute(final String s, final String value) throws IOException {
		validateString(s);
		validateString(value);
		try {
			out.writeAttribute(s, value);
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	private void writeCharacters(final String s) throws IOException {
		validateString(s);
		try {
			out.writeCharacters(s);
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	// Tag
	
	@Override
	protected void writeTag(final Tag t) throws IOException {
		try {
			if (t == T_NULL)
				out.writeEmptyElement(t.name);
			else
				out.writeStartElement(t.name);
			writeID();
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	// Primitives
	
	@Override
	protected void writePrimitiveValue(final Object o) throws IOException {
		writeCharacters("" + o);
		writeEndElement();
	}
	
	@Override
	protected void writePrimitive_(final Object o) throws IOException {
		@SuppressWarnings("null")
		final Tag type = getPrimitiveFromWrapper(o.getClass());
		final int size;
		final long value;
		switch (type) {
			case T_BYTE:
				size = 1;
				value = 0xFFL & ((Byte) o);
				break;
			case T_SHORT:
				size = 2;
				value = 0xFFFFL & ((Short) o);
				break;
			case T_INT:
				size = 4;
				value = 0xFFFFFFFFL & ((Integer) o);
				break;
			case T_LONG:
				size = 8;
				value = (Long) o;
				break;
			case T_FLOAT:
				size = 4;
				value = 0xFFFFFFFFL & Float.floatToIntBits((Float) o);
				break;
			case T_DOUBLE:
				size = 8;
				value = Double.doubleToLongBits((Double) o);
				break;
			case T_CHAR:
				size = 2;
				value = 0xFFFFL & ((Character) o);
				break;
			case T_BOOLEAN:
				size = 0; // results in 1 character - 0 or 1
				value = ((Boolean) o) ? 1 : 0;
				break;
			//$CASES-OMITTED$
			default:
				throw new YggdrasilException("Invalid call to writePrimitive with argument " + o);
		}
		final String s = Long.toHexString(value).toUpperCase(Locale.ENGLISH);
		writeCharacters(StringUtils.multiply('0', Math.max(0, 2 * size - s.length())) + s);
	}
	
	// String
	
	@Override
	protected void writeStringValue(final String s) throws IOException {
		writeCharacters(s);
		writeEndElement();
	}
	
	// Array
	
	@Override
	protected void writeArrayComponentType(final Class<?> contentType) throws IOException {
		writeAttribute("componentType", getTypeName(contentType));
	}
	
	@Override
	protected void writeArrayLength(final int length) throws IOException {
		writeAttribute("length", "" + length);
	}
	
	@Override
	protected void writeArrayEnd() throws IOException {
		writeEndElement();
	}
	
	// Enum
	
	@Override
	protected void writeEnumType(final String type) throws IOException {
		writeAttribute("type", type);
	}
	
	@Override
	protected void writeEnumID(final String id) throws IOException {
		writeCharacters(id);
		writeEndElement();
	}
	
	// Class
	
	@Override
	protected void writeClassType(final Class<?> c) throws IOException {
		writeCharacters(getTypeName(c));
		writeEndElement();
	}
	
	// Reference
	
	@Override
	protected void writeReferenceID(final int ref) throws IOException {
		writeCharacters("" + ref);
		writeEndElement();
	}
	
	// generic Object
	
	@Override
	protected void writeObjectType(final String type) throws IOException {
		writeAttribute("type", type);
	}
	
	@Override
	protected void writeNumFields(final short numFields) throws IOException {
		writeAttribute("numFields", "" + numFields);
	}
	
	// name of the next field
	@Nullable
	private String id = null;
	
	private final void writeID() throws IOException {
		if (id != null) {
			writeAttribute("id", id);
			id = null;
		}
	}
	
	@Override
	protected void writeFieldID(final String id) throws IOException {
		this.id = id;
	}
	
	@Override
	protected void writeObjectEnd() throws IOException {
		writeEndElement();
	}
	
	// stream
	
	@Override
	public void flush() throws IOException {
		try {
			out.flush();
			os.flush();
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
	@Override
	public void close() throws IOException {
		try {
			out.writeEndElement(); // </yggdrasil>
			out.writeEndDocument();
			out.close();
			os.close();
		} catch (final XMLStreamException e) {
			throw new IOException(e);
		}
	}
	
}