package jp.cssj.cti.helpers;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * ファイルシステムを使って結果を生成します。
 * 
 * @author <a href="mailto:miyabe at gnn.co.jp">MIYABE Tatsuhiko </a>
 * @version $Id: FileContentBuilder.java,v 1.6 2005/08/18 04:51:26 harumanx Exp $
 */
public class FileContentBuilder implements PositionalContentBuilder {
	private static final Logger LOGGER = Logger
			.getLogger(FileContentBuilder.class.getName());

	/**
	 * セグメントのサイズです。
	 */
	private static final int SEGMENT_SIZE = 8192;

	/**
	 * フラグメントバッファの最大サイズ、その合計の最大サイズ、クローズ時にディスクに落とすときの敷居値
	 */
	private final int fragmentBufferSize, totalBufferSize, threshold;

	private List frgs = new ArrayList();

	protected final OutputStream out;

	protected final RandomAccessFile raf;

	protected File file;

	protected Fragment first = null, last = null;

	protected long length = 0, onMemory = 0;

	protected int segment;

	protected class Fragment {
		public Fragment prev = null, next = null;

		private final int id;

		private int len = 0;

		private byte[] buffer = null;

		private IntList segments;

		private int segLen = 0;

		public Fragment(int id) {
			this.id = id;
		}

		public int getId() {
			return this.id;
		}

		public int getLength() {
			return this.len;
		}

		public void write(byte[] buff, int pos, int len) throws IOException {
			if (this.segments == null
					&& (this.len + len) < fragmentBufferSize
					&& (FileContentBuilder.this.onMemory + fragmentBufferSize) <= totalBufferSize) {
				if (this.buffer == null) {
					this.buffer = new byte[fragmentBufferSize];
					FileContentBuilder.this.onMemory += fragmentBufferSize;
				}
				System.arraycopy(buff, pos, this.buffer, this.len, len);
			} else {
				if (this.buffer != null) {
					this.rafWrite(this.buffer, 0, this.len);
					FileContentBuilder.this.onMemory -= fragmentBufferSize;
					this.buffer = null;
				}
				this.rafWrite(buff, pos, len);
			}
			this.len += len;
			FileContentBuilder.this.length += len;
		}

		private void rafWrite(byte[] buff, int off, int len) throws IOException {
			if (this.segments == null) {
				this.segments = new IntList(10);
				this.segments.add(FileContentBuilder.this.segment++);
			}
			while (len > 0) {
				if (this.segLen == FileContentBuilder.SEGMENT_SIZE) {
					this.segments.add(FileContentBuilder.this.segment++);
					this.segLen = 0;
				}
				int seg = this.segments.get(this.segments.size() - 1);
				int wlen = Math.min(len, FileContentBuilder.SEGMENT_SIZE
						- this.segLen);
				long wpos = (long) seg * (long) FileContentBuilder.SEGMENT_SIZE
						+ (long) this.segLen;
				FileContentBuilder.this.raf.seek(wpos);
				FileContentBuilder.this.raf.write(buff, off, wlen);
				this.segLen += wlen;
				off += wlen;
				len -= wlen;
			}
		}

		public void close() throws IOException {
			if (this.buffer != null) {
				if (this.len >= FileContentBuilder.this.threshold) {
					this.rafWrite(this.buffer, 0, this.len);
					FileContentBuilder.this.onMemory -= fragmentBufferSize;
					this.buffer = null;
				} else if (this.len < this.buffer.length) {
					byte[] temp = new byte[this.len];
					System.arraycopy(this.buffer, 0, temp, 0, temp.length);
					FileContentBuilder.this.onMemory -= (this.buffer.length - this.len);
					this.buffer = temp;
				}
			}
		}

		public void writeTo(OutputStream out, byte[] buff) throws IOException {
			if (this.segments == null) {
				if (this.buffer != null) {
					out.write(this.buffer, 0, this.len);
					this.buffer = null;
				}
			} else {
				assert buff.length == FileContentBuilder.SEGMENT_SIZE;
				for (int i = 0; i < this.segments.size() - 1; ++i) {
					int seg = this.segments.get(i);
					long rpos = (long) seg
							* (long) FileContentBuilder.SEGMENT_SIZE;
					FileContentBuilder.this.raf.seek(rpos);
					FileContentBuilder.this.raf.readFully(buff);
					out.write(buff);
				}
				int seg = this.segments.get(this.segments.size() - 1);
				long rpos = (long) seg * (long) FileContentBuilder.SEGMENT_SIZE;
				FileContentBuilder.this.raf.seek(rpos);
				FileContentBuilder.this.raf.readFully(buff, 0, this.segLen);
				out.write(buff, 0, this.segLen);
			}
		}
	}

	public FileContentBuilder(OutputStream out, int fragmentBufferSize,
			int totalBufferSize, int threshold) throws IOException {
		this.out = out;
		this.file = File.createTempFile("cssj-", ".frgs");
		this.file.deleteOnExit();
		this.raf = new RandomAccessFile(file, "rw");
		this.fragmentBufferSize = fragmentBufferSize;
		this.totalBufferSize = totalBufferSize;
		this.threshold = threshold;
	}

	public FileContentBuilder(OutputStream out) throws IOException {
		this(out, 8192, 1024 * 1024 * 2, 1024);
	}

	protected int nextId() {
		return this.frgs.size();
	}

	protected Fragment getFragment(int id) throws IOException {
		if (id < 0 || id >= this.frgs.size()) {
			String message = "断片番号" + id + "は存在しません。";
			throw new IOException(message);
		}
		return (Fragment) this.frgs.get(id);
	}

	protected void putFragment(int id, Fragment frg) {
		assert (id == this.frgs.size());
		this.frgs.add(frg);
	}

	public Info getPositionalInfo() {
		final long[] idToPosition = new long[this.frgs.size()];
		long position = 0;
		Fragment frg = this.first;
		while (frg != null) {
			idToPosition[frg.getId()] = position;
			position += frg.getLength();
			frg = frg.next;
		}
		return new Info() {
			public long getPosition(int id) {
				return idToPosition[id];
			}
		};
	}

	public void addFragment() throws IOException {
		int id = this.nextId();
		Fragment frg = new Fragment(id);
		if (this.first == null) {
			this.first = frg;
		} else {
			this.last.next = frg;
			frg.prev = this.last;
		}
		this.putFragment(id, frg);
		this.last = frg;
	}

	public void insertFragmentBefore(int anchorId) throws IOException {
		int id = this.nextId();
		Fragment anchor = this.getFragment(anchorId);
		Fragment frg = new Fragment(id);
		this.putFragment(id, frg);
		frg.prev = anchor.prev;
		frg.next = anchor;
		anchor.prev.next = frg;
		anchor.prev = frg;
		if (this.first == anchor) {
			this.first = frg;
		}
	}

	public void write(int id, byte[] b, int off, int len) throws IOException {
		Fragment frg = this.getFragment(id);
		frg.write(b, off, len);
	}

	public void close(int id) throws IOException {
		Fragment frg = this.getFragment(id);
		frg.close();
	}

	public long getLength() {
		return this.length;
	}

	public void finish() throws IOException {
		try {
			if (LOGGER.isLoggable(Level.FINE)) {
				int total = this.frgs.size();
				int onMemory = 0;
				for (int i = 0; i < total; ++i) {
					Fragment f = (Fragment) this.frgs.get(i);
					if (f.segments == null) {
						++onMemory;
					}
				}
				LOGGER.fine(total + "個のフラグメントが生成されました。");
				LOGGER.fine("うち" + onMemory + "個がオンメモリーで、" + (total - onMemory)
						+ "個がディスク上にあります。");
			}
			if (this.first == null) {
				// 空
				return;
			}

			Fragment frg = this.first;
			byte[] buff = new byte[SEGMENT_SIZE];
			while (frg != null) {
				frg.writeTo(this.out, buff);
				frg = frg.next;
			}
		} finally {
			this.finalize();
		}
	}

	protected void finalize() throws IOException {
		if (this.first != null) {
			this.first = null;
			this.last = null;
			this.frgs = null;
		}
		if (this.file != null) {
			try {
				this.out.close();
			} catch (Exception e) {
				//ignore
			}
			try {
				this.raf.close();
			} catch (Exception e) {
				//ignore
			}
			this.file.delete();
			this.file = null;
		}
	}
}