package jp.cssj.driver.ctip.v2;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;
import java.util.List;

import jp.cssj.cti2.helpers.URIHelper;
import jp.cssj.driver.ctip.common.TcpUtils;

/**
 * @author <a href="mailto:miyabe at gnn.co.jp">MIYABE Tatsuhiko </a>
 * @version $Id: V2ContentProducer.java 549 2011-02-24 08:23:55Z miyabe $
 */
public class V2ContentProducer {
	protected final String charset;

	protected final URI serverURI;

	protected ReadableByteChannel channel;

	public V2ContentProducer(URI uri, String encoding) throws IOException {
		this.charset = encoding;
		this.serverURI = uri;
	}

	/**
	 * サーバーに接続し、リクエストを開始します。
	 * 
	 * @param request
	 * @throws IOException
	 */
	public V2RequestConsumer connect() throws IOException {
		String host = this.serverURI.getHost();
		int port = this.serverURI.getPort();
		if (port == -1) {
			port = 8099;
		}

		InetSocketAddress address = new InetSocketAddress(host, port);
		SocketChannel socketChannel = SelectorProvider.provider()
		.openSocketChannel();
		ByteChannel byteChanel;
		if (this.serverURI.getScheme().equals("ctips")) {
			SSLSocketChannel channel = new SSLSocketChannel(socketChannel);
			channel.connect(address);
			channel.configureBlocking(true);
			byteChanel = channel;
		} else {
			socketChannel.connect(address);
			socketChannel.configureBlocking(true);
			byteChanel = socketChannel;
		}
		this.channel = byteChanel;

		byte[] header = ("CTIP/2.0 " + this.charset + "\n")
				.getBytes("ISO-8859-1");
		TcpUtils.writeAll(byteChanel, ByteBuffer.wrap(header));
		return new V2RequestConsumer(byteChanel, this.charset);
	}

	private byte type, mode;

	private int blockId, anchorId;

	private long length;

	private short code;

	private URI uri;

	private String mimeType;

	private String message;

	private String encoding;

	private List args = new ArrayList();

	private ByteBuffer data;

	private ByteBuffer destLong = ByteBuffer.allocate(8);

	private ByteBuffer destInt = ByteBuffer.allocate(4);

	private ByteBuffer destShort = ByteBuffer.allocate(2);

	private ByteBuffer destByte = ByteBuffer.allocate(1);

	protected void close() throws IOException {
		if (this.channel != null) {
			this.channel.close();
			this.channel = null;
		}
	}

	/**
	 * 次のパケットにカーソルを移します。
	 * 
	 * @throws IOException
	 */
	public void next() throws IOException {
		int payload = TcpUtils.readInt(this.channel, this.destInt);
		this.type = TcpUtils.readByte(this.channel, this.destByte);
		// System.err.println(Integer.toHexString(this.type));
		switch (this.type) {
		case V2ServerPackets.START_DATA:
			try {
				this.uri = URIHelper.create(this.charset, TcpUtils.readString(
						this.channel, this.destShort, this.charset));
			} catch (URISyntaxException e) {
				throw new IOException(e.getMessage());
			}
			this.mimeType = TcpUtils.readString(this.channel, this.destShort,
					this.charset);
			this.encoding = TcpUtils.readString(this.channel, this.destShort,
					this.charset);
			this.length = TcpUtils.readLong(this.channel, this.destLong);
			break;

		case V2ServerPackets.BLOCK_DATA:
			this.blockId = TcpUtils.readInt(this.channel, this.destInt);
			payload -= 1 + 4;
			this.data = ByteBuffer.allocate(payload);
			TcpUtils.readAll(this.channel, this.data);
			this.data.position(0);
			break;

		case V2ServerPackets.ADD_BLOCK:
			break;

		case V2ServerPackets.INSERT_BLOCK:
		case V2ServerPackets.CLOSE_BLOCK:
			this.anchorId = TcpUtils.readInt(this.channel, this.destInt);
			break;

		case V2ServerPackets.MESSAGE:
			this.code = TcpUtils.readShort(this.channel, this.destShort);
			payload -= 1 + 2;
			{
				short len = TcpUtils.readShort(this.channel, this.destShort);
				byte[] buff = new byte[len];
				ByteBuffer dest = ByteBuffer.wrap(buff);
				TcpUtils.readAll(this.channel, dest);
				this.message = new String(buff, this.charset);
				payload -= 2 + len;
			}
			this.args.clear();
			while (payload > 0) {
				short len = TcpUtils.readShort(this.channel, this.destShort);
				byte[] buff = new byte[len];
				ByteBuffer dest = ByteBuffer.wrap(buff);
				TcpUtils.readAll(this.channel, dest);
				String arg = new String(buff, this.charset);
				this.args.add(arg);
				payload -= 2 + len;
			}

			break;
		case V2ServerPackets.ABORT:
			this.mode = TcpUtils.readByte(this.channel, this.destByte);
			this.code = TcpUtils.readShort(this.channel, this.destShort);
			payload -= 1 + 3;
			{
				short len = TcpUtils.readShort(this.channel, this.destShort);
				byte[] buff = new byte[len];
				ByteBuffer dest = ByteBuffer.wrap(buff);
				TcpUtils.readAll(this.channel, dest);
				this.message = new String(buff, this.charset);
				payload -= 2 + len;
			}
			while (payload > 0) {
				short len = TcpUtils.readShort(this.channel, this.destShort);
				byte[] buff = new byte[len];
				ByteBuffer dest = ByteBuffer.wrap(buff);
				TcpUtils.readAll(this.channel, dest);
				String arg = new String(buff, this.charset);
				this.args.add(arg);
				payload -= 2 + len;
			}

			break;

		case V2ServerPackets.MAIN_LENGTH:
		case V2ServerPackets.MAIN_READ:
			this.length = TcpUtils.readLong(this.channel, this.destLong);
			break;

		case V2ServerPackets.DATA:
			payload -= 1;
			this.data = ByteBuffer.allocate(payload);
			TcpUtils.readAll(this.channel, this.data);
			this.data.position(0);
			break;

		case V2ServerPackets.RESOURCE_REQUEST:
			try {
				this.uri = URIHelper.create(this.charset, TcpUtils.readString(
						this.channel, this.destShort, this.charset));
			} catch (URISyntaxException e) {
				throw new IOException(e.getMessage());
			}
			break;

		case V2ServerPackets.EOF:
		case V2ServerPackets.NEXT:
			break;

		default:
			throw new IOException("Bad response: type "
					+ Integer.toHexString(this.type));
		}
	}

	/**
	 * 断片のIDを返します。
	 * 
	 * @return　断片のID。
	 * @throws IOException
	 */
	public int getBlockId() throws IOException {
		return this.blockId;
	}

	/**
	 * アンカーとなる断片のIDを返します。
	 * 
	 * @return　断片のID。
	 * @throws IOException
	 */
	public int getAnchorId() throws IOException {
		return this.anchorId;
	}

	/**
	 * 現在のパケットのデータのタイプを返します。
	 * 
	 * @return パケットのタイプ。
	 * @throws IOException
	 */
	public byte getType() throws IOException {
		return this.type;
	}

	/**
	 * 進行状況を返します。
	 * 
	 * @return バイト数。
	 * @throws IOException
	 */
	public long getLength() throws IOException {
		return this.length;
	}

	/**
	 * メッセージを返します。
	 * 
	 * @return メッセージの文字列。
	 * @throws IOException
	 */
	public String getMessage() throws IOException {
		return this.message;
	}

	/**
	 * メッセージの引数返します。
	 * 
	 * @return メッセージの引数。
	 * @throws IOException
	 */
	public String[] getArgs() throws IOException {
		return (String[]) this.args.toArray(new String[this.args.size()]);
	}

	/**
	 * データのURIを返します。
	 * 
	 * @return データのURI。
	 * @throws IOException
	 */
	public URI getURI() throws IOException {
		return this.uri;
	}

	/**
	 * データのMIME型を返します。
	 * 
	 * @return データのMIME型。
	 * @throws IOException
	 */
	public String getMimeType() throws IOException {
		return this.mimeType;
	}

	/**
	 * データのエンコーディングを返します。
	 * 
	 * @return データのエンコーディング。
	 * @throws IOException
	 */
	public String getEncoding() throws IOException {
		return this.encoding;
	}

	/**
	 * メッセージコードを返します。
	 * 
	 * @return メッセージコード。
	 * @throws IOException
	 */
	public short getCode() throws IOException {
		return this.code;
	}

	/**
	 * 中断処理のモードを返します。
	 * 
	 * @return 中断処理のモード。
	 * @throws IOException
	 */
	public byte getMode() throws IOException {
		return this.mode;
	}

	/**
	 * データを取得します。
	 * 
	 * @param b
	 *            データが格納されるバッファ。
	 * @param off
	 *            バッファの開始位置。
	 * @param len
	 *            バッファに格納可能なバイト数。
	 * @return 取得されたデータの長さ。データがない場合は-1。
	 * @throws IOException
	 */
	public int read(byte[] b, int off, int len) throws IOException {
		if (this.data.remaining() <= 0) {
			return -1;
		}
		len = Math.min(len, this.data.remaining());
		this.data.get(b, off, len);
		return len;
	}
}