package org.nem.core.serialization;

import org.nem.core.utils.StringEncoder;

import java.io.*;
import java.math.BigInteger;
import java.util.Collection;

/**
 * A binary serializer that supports forward-only serialization.
 */
public class BinarySerializer extends Serializer implements AutoCloseable {

	/**
	 * Sentinel value used to indicate that a serialized byte array should be deserialized as null.
	 */
	public static final int NULL_BYTES_SENTINEL_VALUE = 0xFFFFFFFF;

	private final ByteArrayOutputStream stream;

	/**
	 * Creates a new binary serializer.
	 */
	public BinarySerializer() {
		this(null);
	}

	/**
	 * Creates a new binary serializer.
	 *
	 * @param context The serialization context to use.
	 */
	public BinarySerializer(final SerializationContext context) {
		super(context);
		this.stream = new ByteArrayOutputStream();
	}

	@Override
	public void writeInt(final String label, final int i) {
		final byte[] bytes = {
				(byte)(i & 0xFF),
				(byte)((i >> 8) & 0xFF),
				(byte)((i >> 16) & 0xFF),
				(byte)((i >> 24) & 0xFF),
		};
		this.writeBytesInternal(bytes);
	}

	@Override
	public void writeLong(final String label, final long l) {
		this.writeInt(label, (int)l);
		this.writeInt(label, (int)(l >> 32));
	}

	@Override
	public void writeDouble(final String label, final double d) {
		this.writeLong(label, Double.doubleToLongBits(d));
	}

	@Override
	public void writeBigInteger(final String label, final BigInteger i) {
		this.writeBytes(label, null == i ? null : i.toByteArray());
	}

	@Override
	protected void writeBytesImpl(final String label, final byte[] bytes) {
		this.writeBytesUnchecked(label, bytes);
	}

	private void writeBytesUnchecked(final String label, final byte[] bytes) {
		if (null == bytes) {
			this.writeInt(label, NULL_BYTES_SENTINEL_VALUE);
		} else {
			this.writeInt(label, bytes.length);
			this.writeBytesInternal(bytes);
		}
	}

	@Override
	protected void writeStringImpl(final String label, final String s) {
		this.writeBytes(label, null == s ? null : StringEncoder.getBytes(s));
	}

	@Override
	public void writeObject(final String label, final SerializableEntity object) {
		this.writeBytesUnchecked(label, this.serializeObject(object));
	}

	@Override
	public void writeObjectArray(final String label, final Collection<? extends SerializableEntity> objects) {
		if (null == objects) {
			this.writeInt(label, NULL_BYTES_SENTINEL_VALUE);
			return;
		}

		this.writeInt(label, objects.size());
		for (final SerializableEntity object : objects) {
			this.writeBytesUnchecked(label, this.serializeObject(object));
		}
	}

	@Override
	public void close() throws IOException {
		this.stream.close();
	}

	private byte[] serializeObject(final SerializableEntity object) {
		if (null == object) {
			return new byte[0];
		}

		try {
			try (BinarySerializer serializer = new BinarySerializer(this.getContext())) {
				object.serialize(serializer);
				return serializer.getBytes();
			}
		} catch (final Exception ex) {
			throw new SerializationException(ex);
		}
	}

	/**
	 * Gets the underlying byte buffer.
	 *
	 * @return The underlying byte buffer.
	 */
	public byte[] getBytes() {
		return this.stream.toByteArray();
	}

	private void writeBytesInternal(final byte[] bytes) {
		this.stream.write(bytes, 0, bytes.length);
	}

	/**
	 * Helper function that serializes a SerializableEntity to a byte array.
	 *
	 * @param entity The entity to serialize.
	 * @return The resulting byte array.
	 */
	public static byte[] serializeToBytes(final SerializableEntity entity) {
		try {
			try (BinarySerializer binarySerializer = new BinarySerializer()) {
				entity.serialize(binarySerializer);
				return binarySerializer.getBytes();
			}
		} catch (final Exception e) {
			throw new SerializationException(e);
		}
	}
}