var Writable = require('readable-stream/writable');
var Readable = require('readable-stream/readable');
var Duplex = require('readable-stream/duplex');

var DUMMY = new Buffer(0);
var noop = function() {};

var toFunction = function(fn) {
	if (typeof fn === 'function') return fn;
	return function(cb) {
		cb(null, fn);
	};
};

var onreadable = function(rs, init) {
	var reading = false;
	var destroyed = false;

	rs._read = function() {
		reading = true;
	};

	rs.destroy = function() {
		destroyed = true;
	};

	init(function(err, source) {
		if (err) return rs.emit('error', err);

		var fwd = function() {
			var data;
			while ((data = source.read()) !== null) {
				reading = false;
				rs.push(data);
			}
		};

		source.on('readable', function() {
			if (reading) fwd();
		});

		source.on('end', function() {
			fwd();
			rs.push(null);
		});

		source.on('error', function(err) {
			rs.emit('error', err);
		});

		source.on('close', function() {
			fwd();
			process.nextTick(function() {
				rs.emit('close');
			});
		});

		rs._read = function() {
			reading = true;
			fwd();
		};

		rs.destroy = function() {
			if (destroyed) return;
			destroyed = true;
			if (source.destroy) source.destroy();
		};

		if (destroyed) {
			destroyed = false;
			rs.destroy();
			return;
		}

		if (reading) fwd();
	});

	return rs;
};

var onwritable = function(ws, init) {
	var ready = noop;
	var destroyed = false;

	ws._write = function(data, enc, cb) {
		ready = cb;
	};

	ws.destroy = function() {
		destroyed = true;
	};

	ws.write(DUMMY);

	init(function(err, source) {
		if (err) return ws.emit('error', err);

		source.on('close', function() {
			ws.emit('close');
		});

		source.on('error', function(err) {
			ws.emit('error', err);
		});

		ws._write = function(data, enc, cb) {
			if (data === DUMMY) return cb();
			source.write(data, enc, cb);
		};

		var emit = ws.emit;

		source.on('finish', function() {
			emit.call(ws, 'finish');
		});

		ws.destroy = function() {
			if (destroyed) return;
			destroyed = true;
			if (source.destroy) source.destroy();
		};

		ws.emit = function(name) {
			if (name !== 'finish') return emit.apply(ws, arguments);
			source.end();
		};

		if (destroyed) {
			destroyed = false;
			ws.destroy();
			return;
		}

		ready();
	});

	return ws;
};

exports.readable = function(opts, init) {
	if (arguments.length === 1) return exports.readable(null, opts);
	if (!opts) opts = {};
	return onreadable(new Readable(opts), toFunction(init));
};

exports.writable = function(opts, init) {
	if (arguments.length === 1) return exports.writable(null, opts);
	if (!opts) opts = {};
	return onwritable(new Writable(opts), toFunction(init));
};

exports.duplex = function(opts, initWritable, initReadable) {
	if (arguments.length === 2) return exports.duplex(null, opts, initWritable);
	if (!opts) opts = {};
	var dupl = new Duplex(opts);
	onwritable(dupl, toFunction(initWritable));
	onreadable(dupl, toFunction(initReadable));
	return dupl;
};