/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.client.async;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;

import freenet.keys.ClientKeyBlock;
import freenet.keys.Key;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.io.BucketTools;

/**
 * Helper class to write FBlobs. Threadsafe, allows multiple getters to
 * write to the same BinaryBlobWriter.
 *
 * @author saces
 */
public final class BinaryBlobWriter {
    
    private static volatile boolean logMINOR;

    static {
        Logger.registerClass(BinaryBlobWriter.class);
    }
    
	private final HashSet<Key> _binaryBlobKeysAddedAlready;
	private final BucketFactory _bf;
	private final ArrayList<Bucket> _buckets;
	private final Bucket _out;
	private final boolean _isSingleBucket;

	private boolean _started = false;
	private boolean _finalized = false;

	private transient DataOutputStream _stream_cache = null;

	/**
	 * Persistent/'BigFile' constructor
	 * @param bf BucketFactory to generate internal buckets from
	 */
	public BinaryBlobWriter(BucketFactory bf) {
		_binaryBlobKeysAddedAlready = new HashSet<Key>();
		_buckets = new ArrayList<Bucket>();
		_bf = bf;
		_out = null;
		_isSingleBucket = false;
	}

	/**
	 * Transient constructor
	 * @param out Bucket to write the result to
	 */
	public BinaryBlobWriter(Bucket out) {
		_binaryBlobKeysAddedAlready = new HashSet<Key>();
		_buckets = null;
		_bf = null;
		assert out != null;
		_out = out;
		_isSingleBucket = true;
	}

	private DataOutputStream getOutputStream() throws IOException, BinaryBlobAlreadyClosedException {
		if (_finalized) {
			throw new BinaryBlobAlreadyClosedException("Already finalized (getting final data) on "+this);
		}
		if (_stream_cache==null) {
			if (_isSingleBucket) {
				_stream_cache = new DataOutputStream(_out.getOutputStream());
			} else {
				Bucket newBucket = _bf.makeBucket(-1);
				_buckets.add(newBucket);
				_stream_cache = new DataOutputStream(newBucket.getOutputStream());
			}
		}
		if (!_started) {
			BinaryBlob.writeBinaryBlobHeader(_stream_cache);
			_started = true;
		}
		return _stream_cache;
	}

	/**
	 * Add a block to the binary blob.
	 * @throws IOException
	 * @throws BinaryBlobAlreadyClosedException 
	 */
	public synchronized void addKey(ClientKeyBlock block, ClientContext context) throws IOException, BinaryBlobAlreadyClosedException {
		Key key = block.getKey();
		if(_binaryBlobKeysAddedAlready.contains(key)) return;
		BinaryBlob.writeKey(getOutputStream(), block.getBlock(), key);
		_binaryBlobKeysAddedAlready.add(key);
	}

	/**
	 * finalize the return bucket
	 * @throws IOException
	 * @throws BinaryBlobAlreadyClosedException 
	 */
	public void finalizeBucket() throws IOException, BinaryBlobAlreadyClosedException {
		if (_finalized) {
			throw new BinaryBlobAlreadyClosedException("Already finalized (closing blob).");
		}
		finalizeBucket(true);
	}

	private void finalizeBucket(boolean mark) throws IOException, BinaryBlobAlreadyClosedException {
		if (_finalized) throw new BinaryBlobAlreadyClosedException("Already finalized (closing blob - 2).");
		if(logMINOR) Logger.minor(this, "Finalizing binary blob "+this, new Exception("debug"));
		if (!_isSingleBucket) {
			if (!mark && (_buckets.size()==1)) {
				return;
			}
			Bucket out = _bf.makeBucket(-1);
			getSnapshot(out, mark);
			for (int i=0,n=_buckets.size(); i<n;i++) {
				_buckets.get(i).free();
			}
			if (mark) {
				out.setReadOnly();
			}
			_buckets.clear();
			_buckets.add(0, out);
		} else if (mark){
			DataOutputStream out = new DataOutputStream(getOutputStream());
			try {
			BinaryBlob.writeEndBlob(out);
			} finally {
			out.close();
			}
		}
		if (mark) {
			_finalized = true;
		}
	}

	public synchronized void getSnapshot(Bucket bucket) throws IOException, BinaryBlobAlreadyClosedException {
		if (_buckets.isEmpty()) return;
		if (_finalized) {
			BucketTools.copy(_buckets.get(0), bucket);
			return;
		}
		getSnapshot(bucket, true);
	}

	private void getSnapshot(Bucket bucket, boolean addEndmarker) throws IOException, BinaryBlobAlreadyClosedException {
		if (_buckets.isEmpty()) return;
		if (_finalized) {
			throw new BinaryBlobAlreadyClosedException("Already closed (getting final data snapshot)");
		}
		OutputStream out = bucket.getOutputStream();
		try {
		for (int i=0,n=_buckets.size(); i<n;i++) {
			BucketTools.copyTo(_buckets.get(i), out, -1);
		}
		if (addEndmarker) {
			DataOutputStream dout = new DataOutputStream(out);
			BinaryBlob.writeEndBlob(dout);
			dout.flush();
		}
		} finally {
			out.close();
		}
	}

	public synchronized Bucket getFinalBucket() {
		if (!_finalized) {
			throw new IllegalStateException("Not finalized!");
		}
		if (_isSingleBucket) {
			return _out;
		} else {
			return _buckets.get(0);
		}
	}

	public static class BinaryBlobAlreadyClosedException extends Exception {

		private static final long serialVersionUID = -1L;

		public BinaryBlobAlreadyClosedException(String message) {
			super(message);
		}
		
	}

	public boolean isFinalized() {
		return _finalized;
	}
}
