package com.firefly.codec.spdy.frames.control;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import com.firefly.codec.spdy.frames.compression.CompressionDictionary;
import com.firefly.codec.spdy.frames.compression.CompressionFactory;

public class HeadersBlockGenerator {
	private final CompressionFactory.Compressor compressor;
	private boolean needsDictionary = true;

	public HeadersBlockGenerator(CompressionFactory.Compressor compressor) {
		this.compressor = compressor;
	}

	public ByteBuffer generate(Fields headers) {
		// TODO: ByteArrayOutputStream is quite inefficient, but grows on
		// demand; optimize using ByteBuffer ?
		final Charset iso1 = StandardCharsets.ISO_8859_1;
		ByteArrayOutputStream buffer = new ByteArrayOutputStream(headers.getSize() * 64);
		writeCount(buffer, headers.getSize());
		for (Fields.Field header : headers) {
			String name = header.getName().toLowerCase(Locale.ENGLISH);
			byte[] nameBytes = name.getBytes(iso1);
			writeNameLength(buffer, nameBytes.length);
			buffer.write(nameBytes, 0, nameBytes.length);

			// Most common path first
			String value = header.getValue();
			byte[] valueBytes = value.getBytes(iso1);
			if (header.hasMultipleValues()) {
				List<String> values = header.getValues();
				for (int i = 1; i < values.size(); ++i) {
					byte[] moreValueBytes = values.get(i).getBytes(iso1);
					byte[] newValueBytes = Arrays.copyOf(valueBytes, valueBytes.length + 1 + moreValueBytes.length);
					newValueBytes[valueBytes.length] = 0;
					System.arraycopy(moreValueBytes, 0, newValueBytes, valueBytes.length + 1, moreValueBytes.length);
					valueBytes = newValueBytes;
				}
			}

			writeValueLength(buffer, valueBytes.length);
			buffer.write(valueBytes, 0, valueBytes.length);
		}

		return compress(buffer.toByteArray());
	}

	private ByteBuffer compress(byte[] bytes) {
		ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);

		// The headers compression context is per-session, so we need to synchronize
		synchronized (compressor) {
			if (needsDictionary) {
				compressor.setDictionary(CompressionDictionary.DICTIONARY_V3);
				needsDictionary = false;
			}

			compressor.setInput(bytes);

			// Compressed bytes may be bigger than input bytes, so we need to loop and accumulate them
            // Beware that the minimum amount of bytes generated by the compressor is few bytes, so we
            // need to use an output buffer that is big enough to exit the compress loop
			buffer.reset();
			int compressed;
			byte[] output = new byte[Math.max(256, bytes.length)];
			while (true) {
				// SPDY uses the SYNC_FLUSH mode
				compressed = compressor.compress(output);
				buffer.write(output, 0, compressed);
				if (compressed < output.length)
					break;
			}
		}

		return ByteBuffer.wrap(buffer.toByteArray());
	}

	private void writeCount(ByteArrayOutputStream buffer, int value) {
		buffer.write((value & 0xFF_00_00_00) >>> 24);
		buffer.write((value & 0x00_FF_00_00) >>> 16);
		buffer.write((value & 0x00_00_FF_00) >>> 8);
		buffer.write(value & 0x00_00_00_FF);
	}

	private void writeNameLength(ByteArrayOutputStream buffer, int length) {
		writeCount(buffer, length);
	}

	private void writeValueLength(ByteArrayOutputStream buffer, int length) {
		writeCount(buffer, length);
	}
}

@@ -815,20 +815,23 @@
 Charset 
-iso1
+charset
  = Stand
@@ -846,18 +846,13 @@
 ets.
-ISO_8859_1
+UTF_8
 ;%0A%09%09
@@ -1109,28 +1109,31 @@
 me.getBytes(
-iso1
+charset
 );%0A%09%09%09writeN
@@ -1319,28 +1319,31 @@
 ue.getBytes(
-iso1
+charset
 );%0A%09%09%09if (he
@@ -1518,12 +1518,15 @@
 tes(
-iso1
+charset
 );%0A%09

modify SPDY header frame