/*
nochump.util.zip.ZipOutput
Copyright (C) 2007 David Chang (dchang@nochump.com)

This file is part of nochump.util.zip.

nochump.util.zip is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

nochump.util.zip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
*/
package nochump.util.zip {

	import flash.utils.Dictionary;
	import flash.utils.Endian;
	import flash.utils.ByteArray;
	
	public class ZipOutput {
		
		private var _entry:ZipEntry;
		private var _entries:Array = [];
		private var _names:Dictionary = new Dictionary();
		private var _def:Deflater = new Deflater();
		private var _crc:CRC32 = new CRC32();
		private var _buf:ByteArray = new ByteArray();
		private var _comment:String = "";
		
		public function ZipOutput() {
			_buf.endian = Endian.LITTLE_ENDIAN;
		}
		
		/**
		 * Returns the number of entries in this zip file.
		 */
		public function get size():uint {
			return _entries.length;
		}
		
		/**
		 * Returns the byte array of the finished zip.
		 */
		public function get byteArray():ByteArray {
			_buf.position = 0;
			return _buf;
		}
		
		/**
		 *
		 */
		public function set comment(value:String):void {
			_comment = value;
		}
		
		public function putNextEntry(e:ZipEntry):void {
			if(_entry != null) closeEntry();
			// TODO:
			if(e.dostime == 0) e.time = new Date().time;
			if (e.method == -1) e.method = ZipConstants.DEFLATED; // use default method
			switch(e.method) {
				case ZipConstants.DEFLATED:
					if (e.size == -1 || e.compressedSize == -1 || e.crc == 0) {
						// store size, compressed size, and crc-32 in data descriptor
						// immediately following the compressed entry data
						e.flag = 8;
					} else if (e.size != -1 && e.compressedSize != -1 && e.crc != 0) {
						// store size, compressed size, and crc-32 in LOC header
						e.flag = 0;
					} else {
						throw new ZipError("DEFLATED entry missing size, compressed size, or crc-32");
					}
					e.version = 20;
					break;
				case ZipConstants.STORED:
					// compressed size, uncompressed size, and crc-32 must all be
					// set for entries using STORED compression method
					if (e.size == -1) {
						e.size = e.compressedSize;
					} else if (e.compressedSize == -1) {
						e.compressedSize = e.size;
					} else if (e.size != e.compressedSize) {
						throw new ZipError("STORED entry where compressed != uncompressed size");
					}
					if (e.size == -1 || e.crc == 0) {
						throw new ZipError("STORED entry missing size, compressed size, or crc-32");
					}
					e.version = 10;
					e.flag = 0;
					break;
				default:
					throw new ZipError("unsupported compression method");
			}
			e.offset = _buf.position;
			if (_names[e.name] != null) {
				throw new ZipError("duplicate entry: " + e.name);
			} else {
				_names[e.name] = e;
			}
			writeLOC(e);
			_entries.push(e);
			_entry = e;
		}
		
		public function write(b:ByteArray):void {
			if (_entry == null) {
				throw new ZipError("no current ZIP entry");
			}
			//*
			switch (_entry.method) {
				case ZipConstants.DEFLATED:
					//super.write(b, off, len);
					var cb:ByteArray = new ByteArray();
					_def.setInput(b);
					_def.deflate(cb);
					_buf.writeBytes(cb);
					// TODO: test if Deflater can deflate to the end of _buf (saves from using variable cb and an extra copy)
					break;
				case ZipConstants.STORED:
					// TODO:
					//if (written - locoff > _entry.size) {
					//	throw new ZipError("attempt to write past end of STORED entry");
					//}
					//out.write(b, off, len);
					_buf.writeBytes(b);
					break;
				default:
					throw new Error("invalid compression method");
			}
			/**/
			_crc.update(b);
		}
		
		// check if this method is still necessary since we're not dealing with streams
		// seems crc and whether a data descriptor i necessary is determined here
		public function closeEntry():void {
			var e:ZipEntry = _entry;
			if(e != null) {
				switch (e.method) {
					case ZipConstants.DEFLATED:
						if ((e.flag & 8) == 0) {
							// verify size, compressed size, and crc-32 settings
							if (e.size != _def.getBytesRead()) {
								throw new ZipError("invalid entry size (expected " + e.size + " but got " + _def.getBytesRead() + " bytes)");
							}
							if (e.compressedSize != _def.getBytesWritten()) {
								throw new ZipError("invalid entry compressed size (expected " + e.compressedSize + " but got " + _def.getBytesWritten() + " bytes)");
							}
							if (e.crc != _crc.getValue()) {
								throw new ZipError( "invalid entry CRC-32 (expected 0x" + e.crc + " but got 0x" + _crc.getValue() + ")");
							}
						} else {
							e.size = _def.getBytesRead();
							e.compressedSize = _def.getBytesWritten();
							e.crc = _crc.getValue();
							writeEXT(e);
						}
						_def.reset();
						break;
					case ZipConstants.STORED:
						// TODO:
						break;
					default:
						throw new Error("invalid compression method");
				}
				_crc.reset();
				_entry = null;
			}
		}
		
		public function finish():void {
			if(_entry != null) closeEntry();
			if (_entries.length < 1) throw new ZipError("ZIP file must have at least one entry");
			var off:uint = _buf.position;
			// write central directory
			for(var i:uint = 0; i < _entries.length; i++) {
				writeCEN(_entries[i]);
			}
			writeEND(off, _buf.position - off);
		}
		
		private function writeLOC(e:ZipEntry):void {
			_buf.writeUnsignedInt(ZipConstants.LOCSIG);
			_buf.writeShort(e.version);
			_buf.writeShort(e.flag);
			_buf.writeShort(e.method);
			_buf.writeUnsignedInt(e.dostime); // dostime
			if ((e.flag & 8) == 8) {
				// store size, uncompressed size, and crc-32 in data descriptor
				// immediately following compressed entry data
				_buf.writeUnsignedInt(0);
				_buf.writeUnsignedInt(0);
				_buf.writeUnsignedInt(0);
			} else {
				_buf.writeUnsignedInt(e.crc); // crc-32
				_buf.writeUnsignedInt(e.compressedSize); // compressed size
				_buf.writeUnsignedInt(e.size); // uncompressed size
			}
			_buf.writeShort(e.name.length);
			_buf.writeShort(e.extra != null ? e.extra.length : 0);
			_buf.writeUTFBytes(e.name);
			if (e.extra != null) {
				_buf.writeBytes(e.extra);
			}
		}
		
		/*
		 * Writes extra data descriptor (EXT) for specified entry.
		 */
		private function writeEXT(e:ZipEntry):void {
			_buf.writeUnsignedInt(ZipConstants.EXTSIG); // EXT header signature
			_buf.writeUnsignedInt(e.crc); // crc-32
			_buf.writeUnsignedInt(e.compressedSize); // compressed size
			_buf.writeUnsignedInt(e.size); // uncompressed size
		}
		
		/*
		 * Write central directory (CEN) header for specified entry.
		 * REMIND: add support for file attributes
		 */
		private function writeCEN(e:ZipEntry):void {
			_buf.writeUnsignedInt(ZipConstants.CENSIG); // CEN header signature
			_buf.writeShort(e.version); // version made by
			_buf.writeShort(e.version); // version needed to extract
			_buf.writeShort(e.flag); // general purpose bit flag
			_buf.writeShort(e.method); // compression method
			_buf.writeUnsignedInt(e.dostime); // last modification time
			_buf.writeUnsignedInt(e.crc); // crc-32
			_buf.writeUnsignedInt(e.compressedSize); // compressed size
			_buf.writeUnsignedInt(e.size); // uncompressed size
			_buf.writeShort(e.name.length);
			_buf.writeShort(e.extra != null ? e.extra.length : 0);
			_buf.writeShort(e.comment != null ? e.comment.length : 0);
			_buf.writeShort(0); // starting disk number
			_buf.writeShort(0); // internal file attributes (unused)
			_buf.writeUnsignedInt(0); // external file attributes (unused)
			_buf.writeUnsignedInt(e.offset); // relative offset of local header
			_buf.writeUTFBytes(e.name);
			if (e.extra != null) {
				_buf.writeBytes(e.extra);
			}
			if (e.comment != null) {
				_buf.writeUTFBytes(e.comment);
			}
		}
		
		/*
		 * Writes end of central directory (END) header.
		 */
		private function writeEND(off:uint, len:uint):void {
			_buf.writeUnsignedInt(ZipConstants.ENDSIG); // END record signature
			_buf.writeShort(0); // number of this disk
			_buf.writeShort(0); // central directory start disk
			_buf.writeShort(_entries.length); // number of directory entries on disk
			_buf.writeShort(_entries.length); // total number of directory entries
			_buf.writeUnsignedInt(len); // length of central directory
			_buf.writeUnsignedInt(off); // offset of central directory
			_buf.writeUTF(_comment); // zip file comment
		}
		
	}
	
}