var util = require('util');
var eos = require('end-of-stream');
var headers = require('./headers');

var Readable = require('readable-stream').Readable;
var Writable = require('readable-stream').Writable;
var PassThrough = require('readable-stream').PassThrough;

var END_OF_TAR = new Buffer(1024);
END_OF_TAR.fill(0);

var noop = function() {};

var overflow = function(self, size) {
	size &= 511;
	if (size) self.push(END_OF_TAR.slice(0, 512 - size));
};

var Sink = function(to) {
	Writable.call(this);
	this.written = 0;
	this._to = to;
	this._destroyed = false;
};

util.inherits(Sink, Writable);

Sink.prototype._write = function(data, enc, cb) {
	this.written += data.length;
	if (this._to.push(data)) return cb();
	this._to._drain = cb;
};

Sink.prototype.destroy = function() {
	if (this._destroyed) return;
	this._destroyed = true;
	this.emit('close');
};

var Void = function() {
	Writable.call(this)
	this._destroyed = false;
};

util.inherits(Void, Writable);

Void.prototype._write = function(data, enc, cb) {
	cb(new Error('No body allowed for this entry'))
};

Void.prototype.destroy = function() {
	if (this._destroyed) return;
	this._destroyed = true;
	this.emit('close')
}

var Pack = function(opts) {
	if (!(this instanceof Pack)) return new Pack(opts);
	Readable.call(this, opts);

	this._drain = noop;
	this._finalized = false;
	this._finalizing = false;
	this._destroyed = false;
	this._stream = null;
};

util.inherits(Pack, Readable);

Pack.prototype.entry = function(header, buffer, callback) {
	if (this._stream) throw new Error('already piping an entry');
	if (this._finalized || this._destroyed) return;

	if (typeof buffer === 'function') {
		callback = buffer;
		buffer = null;
	}

	if (!callback) callback = noop;

	var self = this;

	if (!header.size)  header.size = 0;
	if (!header.type)  header.type = 'file';
	if (!header.mode)  header.mode = header.type === 'directory' ? 0755 : 0644;
	if (!header.uid)   header.uid = 0;
	if (!header.gid)   header.gid = 0;
	if (!header.mtime) header.mtime = new Date();

	if (typeof buffer === 'string') buffer = new Buffer(buffer);
	if (Buffer.isBuffer(buffer)) {
		header.size = buffer.length;
		this._encode(header);
		this.push(buffer);
		overflow(self, header.size);
		process.nextTick(callback);
		return new Void();
	}
	if (header.type !== 'file' && header.type !== 'contigious-file') {
		this._encode(header);
		process.nextTick(callback);
		return new Void();
	}

	var sink = new Sink(this);

	this._encode(header);
	this._stream = sink;

	eos(sink, function(err) {
		self._stream = null;

		if (err) { // stream was closed
			self.destroy();
			return callback(err);
		}

		if (sink.written !== header.size) { // corrupting tar
			self.destroy();
			return callback(new Error('size mismatch'));
		}

		overflow(self, header.size);
		if (self._finalizing) self.finalize();
		callback();
	});

	return sink;
};

Pack.prototype.finalize = function() {
	if (this._stream) {
		this._finalizing = true;
		return;
	}

	if (this._finalized) return;
	this._finalized = true;
	this.push(END_OF_TAR);
	this.push(null);
};

Pack.prototype.destroy = function(err) {
	if (this._destroyed) return;
	this._destroyed = true;

	if (err) this.emit('error', err);
	this.emit('close');
	if (this._stream && this._stream.destroy) this._stream.destroy();
};

Pack.prototype._encode = function(header) {
	var buf = headers.encode(header);
	if (buf) this.push(buf);
	else this._encodePax(header);
};

Pack.prototype._encodePax = function(header) {
	var paxHeader = headers.encodePax({
		name: header.name,
		linkname: header.linkname
	});

	var newHeader = {
		name: 'PaxHeader',
		mode: header.mode,
		uid: header.uid,
		gid: header.gid,
		size: paxHeader.length,
		mtime: header.mtime,
		type: 'pax-header',
		linkname: header.linkname && 'PaxHeader',
		uname: header.uname,
		gname: header.gname,
		devmajor: header.devmajor,
		devminor: header.devminor
	};

	this.push(headers.encode(newHeader));
	this.push(paxHeader);
	overflow(this, paxHeader.length);

	newHeader.size = header.size;
	newHeader.type = header.type;
	this.push(headers.encode(newHeader));
};

Pack.prototype._read = function(n) {
	var drain = this._drain;
	this._drain = noop;
	drain();
};

module.exports = Pack;
