using System;
using System.IO;
using System.Text;

namespace ICSharpCode.SharpZipLib.Zip
{
	/// <summary>
	/// Holds data pertinent to a data descriptor.
	/// </summary>
	public class DescriptorData
	{
		/// <summary>
		/// Get /set the compressed size of data.
		/// </summary>
		public long CompressedSize {
			get { return compressedSize; }
			set { compressedSize = value; }
		}

		/// <summary>
		/// Get / set the uncompressed size of data
		/// </summary>
		public long Size {
			get { return size; }
			set { size = value; }
		}

		/// <summary>
		/// Get /set the crc value.
		/// </summary>
		public long Crc {
			get { return crc; }
			set { crc = (value & 0xffffffff); }
		}

		#region Instance Fields
		long size;
		long compressedSize;
		long crc;
		#endregion
	}

	class EntryPatchData
	{
		public long SizePatchOffset {
			get { return sizePatchOffset_; }
			set { sizePatchOffset_ = value; }
		}

		public long CrcPatchOffset {
			get { return crcPatchOffset_; }
			set { crcPatchOffset_ = value; }
		}

		#region Instance Fields
		long sizePatchOffset_;
		long crcPatchOffset_;
		#endregion
	}

	/// <summary>
	/// This class assists with writing/reading from Zip files.
	/// </summary>
	internal class ZipHelperStream : Stream
	{
		#region Constructors
		/// <summary>
		/// Initialise an instance of this class.
		/// </summary>
		/// <param name="name">The name of the file to open.</param>
		public ZipHelperStream(string name)
		{
			stream_ = new FileStream(name, FileMode.Open, FileAccess.ReadWrite);
			isOwner_ = true;
		}

		/// <summary>
		/// Initialise a new instance of <see cref="ZipHelperStream"/>.
		/// </summary>
		/// <param name="stream">The stream to use.</param>
		public ZipHelperStream(Stream stream)
		{
			stream_ = stream;
		}
		#endregion

		/// <summary>
		/// Get / set a value indicating wether the the underlying stream is owned or not.
		/// </summary>
		/// <remarks>If the stream is owned it is closed when this instance is closed.</remarks>
		public bool IsStreamOwner {
			get { return isOwner_; }
			set { isOwner_ = value; }
		}

		#region Base Stream Methods
		public override bool CanRead {
			get { return stream_.CanRead; }
		}

		public override bool CanSeek {
			get { return stream_.CanSeek; }
		}

		public override bool CanTimeout {
			get { return stream_.CanTimeout; }
		}

		public override long Length {
			get { return stream_.Length; }
		}

		public override long Position {
			get { return stream_.Position; }
			set { stream_.Position = value; }
		}

		public override bool CanWrite {
			get { return stream_.CanWrite; }
		}

		public override void Flush()
		{
			stream_.Flush();
		}

		public override long Seek(long offset, SeekOrigin origin)
		{
			return stream_.Seek(offset, origin);
		}

		public override void SetLength(long value)
		{
			stream_.SetLength(value);
		}

		public override int Read(byte[] buffer, int offset, int count)
		{
			return stream_.Read(buffer, offset, count);
		}

		public override void Write(byte[] buffer, int offset, int count)
		{
			stream_.Write(buffer, offset, count);
		}

		/// <summary>
		/// Close the stream.
		/// </summary>
		/// <remarks>
		/// The underlying stream is closed only if <see cref="IsStreamOwner"/> is true.
		/// </remarks>
		protected override void Dispose(bool disposing)
		{
			Stream toClose = stream_;
			stream_ = null;
			if (isOwner_ && (toClose != null)) {
				isOwner_ = false;
				toClose.Dispose();
			}
		}

		#endregion

		// Write the local file header
		// TODO: ZipHelperStream.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage
		void WriteLocalHeader(ZipEntry entry, EntryPatchData patchData)
		{
			CompressionMethod method = entry.CompressionMethod;
			bool headerInfoAvailable = true; // How to get this?
			bool patchEntryHeader = false;

			WriteLEInt(ZipConstants.LocalHeaderSignature);

			WriteLEShort(entry.Version);
			WriteLEShort(entry.Flags);
			WriteLEShort((byte)method);
			WriteLEInt((int)entry.DosTime);

			if (headerInfoAvailable == true) {
				WriteLEInt((int)entry.Crc);
				if (entry.LocalHeaderRequiresZip64) {
					WriteLEInt(-1);
					WriteLEInt(-1);
				} else {
					WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize);
					WriteLEInt((int)entry.Size);
				}
			} else {
				if (patchData != null) {
					patchData.CrcPatchOffset = stream_.Position;
				}
				WriteLEInt(0);  // Crc

				if (patchData != null) {
					patchData.SizePatchOffset = stream_.Position;
				}

				// For local header both sizes appear in Zip64 Extended Information
				if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) {
					WriteLEInt(-1);
					WriteLEInt(-1);
				} else {
					WriteLEInt(0);  // Compressed size
					WriteLEInt(0);  // Uncompressed size
				}
			}

			byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name);

			if (name.Length > 0xFFFF) {
				throw new ZipException("Entry name too long.");
			}

			var ed = new ZipExtraData(entry.ExtraData);

			if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) {
				ed.StartNewEntry();
				if (headerInfoAvailable) {
					ed.AddLeLong(entry.Size);
					ed.AddLeLong(entry.CompressedSize);
				} else {
					ed.AddLeLong(-1);
					ed.AddLeLong(-1);
				}
				ed.AddNewEntry(1);

				if (!ed.Find(1)) {
					throw new ZipException("Internal error cant find extra data");
				}

				if (patchData != null) {
					patchData.SizePatchOffset = ed.CurrentReadIndex;
				}
			} else {
				ed.Delete(1);
			}

			byte[] extra = ed.GetEntryData();

			WriteLEShort(name.Length);
			WriteLEShort(extra.Length);

			if (name.Length > 0) {
				stream_.Write(name, 0, name.Length);
			}

			if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) {
				patchData.SizePatchOffset += stream_.Position;
			}

			if (extra.Length > 0) {
				stream_.Write(extra, 0, extra.Length);
			}
		}

		/// <summary>
		/// Locates a block with the desired <paramref name="signature"/>.
		/// </summary>
		/// <param name="signature">The signature to find.</param>
		/// <param name="endLocation">Location, marking the end of block.</param>
		/// <param name="minimumBlockSize">Minimum size of the block.</param>
		/// <param name="maximumVariableData">The maximum variable data.</param>
		/// <returns>Eeturns the offset of the first byte after the signature; -1 if not found</returns>
		public long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData)
		{
			long pos = endLocation - minimumBlockSize;
			if (pos < 0) {
				return -1;
			}

			long giveUpMarker = Math.Max(pos - maximumVariableData, 0);

			// TODO: This loop could be optimised for speed.
			do {
				if (pos < giveUpMarker) {
					return -1;
				}
				Seek(pos--, SeekOrigin.Begin);
			} while (ReadLEInt() != signature);

			return Position;
		}

		/// <summary>
		/// Write Zip64 end of central directory records (File header and locator).
		/// </summary>
		/// <param name="noOfEntries">The number of entries in the central directory.</param>
		/// <param name="sizeEntries">The size of entries in the central directory.</param>
		/// <param name="centralDirOffset">The offset of the dentral directory.</param>
		public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, long centralDirOffset)
		{
			long centralSignatureOffset = stream_.Position;
			WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature);
			WriteLELong(44);    // Size of this record (total size of remaining fields in header or full size - 12)
			WriteLEShort(ZipConstants.VersionMadeBy);   // Version made by
			WriteLEShort(ZipConstants.VersionZip64);   // Version to extract
			WriteLEInt(0);      // Number of this disk
			WriteLEInt(0);      // number of the disk with the start of the central directory
			WriteLELong(noOfEntries);       // No of entries on this disk
			WriteLELong(noOfEntries);       // Total No of entries in central directory
			WriteLELong(sizeEntries);       // Size of the central directory
			WriteLELong(centralDirOffset);  // offset of start of central directory
											// zip64 extensible data sector not catered for here (variable size)

			// Write the Zip64 end of central directory locator
			WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature);

			// no of the disk with the start of the zip64 end of central directory
			WriteLEInt(0);

			// relative offset of the zip64 end of central directory record
			WriteLELong(centralSignatureOffset);

			// total number of disks
			WriteLEInt(1);
		}

		/// <summary>
		/// Write the required records to end the central directory.
		/// </summary>
		/// <param name="noOfEntries">The number of entries in the directory.</param>
		/// <param name="sizeEntries">The size of the entries in the directory.</param>
		/// <param name="startOfCentralDirectory">The start of the central directory.</param>
		/// <param name="comment">The archive comment.  (This can be null).</param>
		public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries,
			long startOfCentralDirectory, byte[] comment)
		{

			if ((noOfEntries >= 0xffff) ||
				(startOfCentralDirectory >= 0xffffffff) ||
				(sizeEntries >= 0xffffffff)) {
				WriteZip64EndOfCentralDirectory(noOfEntries, sizeEntries, startOfCentralDirectory);
			}

			WriteLEInt(ZipConstants.EndOfCentralDirectorySignature);

			// TODO: ZipFile Multi disk handling not done
			WriteLEShort(0);                    // number of this disk
			WriteLEShort(0);                    // no of disk with start of central dir


			// Number of entries
			if (noOfEntries >= 0xffff) {
				WriteLEUshort(0xffff);  // Zip64 marker
				WriteLEUshort(0xffff);
			} else {
				WriteLEShort((short)noOfEntries);          // entries in central dir for this disk
				WriteLEShort((short)noOfEntries);          // total entries in central directory
			}

			// Size of the central directory
			if (sizeEntries >= 0xffffffff) {
				WriteLEUint(0xffffffff);    // Zip64 marker
			} else {
				WriteLEInt((int)sizeEntries);
			}


			// offset of start of central directory
			if (startOfCentralDirectory >= 0xffffffff) {
				WriteLEUint(0xffffffff);    // Zip64 marker
			} else {
				WriteLEInt((int)startOfCentralDirectory);
			}

			int commentLength = (comment != null) ? comment.Length : 0;

			if (commentLength > 0xffff) {
				throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength));
			}

			WriteLEShort(commentLength);

			if (commentLength > 0) {
				Write(comment, 0, comment.Length);
			}
		}

		#region LE value reading/writing
		/// <summary>
		/// Read an unsigned short in little endian byte order.
		/// </summary>
		/// <returns>Returns the value read.</returns>
		/// <exception cref="IOException">
		/// An i/o error occurs.
		/// </exception>
		/// <exception cref="EndOfStreamException">
		/// The file ends prematurely
		/// </exception>
		public int ReadLEShort()
		{
			int byteValue1 = stream_.ReadByte();

			if (byteValue1 < 0) {
				throw new EndOfStreamException();
			}

			int byteValue2 = stream_.ReadByte();
			if (byteValue2 < 0) {
				throw new EndOfStreamException();
			}

			return byteValue1 | (byteValue2 << 8);
		}

		/// <summary>
		/// Read an int in little endian byte order.
		/// </summary>
		/// <returns>Returns the value read.</returns>
		/// <exception cref="IOException">
		/// An i/o error occurs.
		/// </exception>
		/// <exception cref="System.IO.EndOfStreamException">
		/// The file ends prematurely
		/// </exception>
		public int ReadLEInt()
		{
			return ReadLEShort() | (ReadLEShort() << 16);
		}

		/// <summary>
		/// Read a long in little endian byte order.
		/// </summary>
		/// <returns>The value read.</returns>
		public long ReadLELong()
		{
			return (uint)ReadLEInt() | ((long)ReadLEInt() << 32);
		}

		/// <summary>
		/// Write an unsigned short in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLEShort(int value)
		{
			stream_.WriteByte((byte)(value & 0xff));
			stream_.WriteByte((byte)((value >> 8) & 0xff));
		}

		/// <summary>
		/// Write a ushort in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLEUshort(ushort value)
		{
			stream_.WriteByte((byte)(value & 0xff));
			stream_.WriteByte((byte)(value >> 8));
		}

		/// <summary>
		/// Write an int in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLEInt(int value)
		{
			WriteLEShort(value);
			WriteLEShort(value >> 16);
		}

		/// <summary>
		/// Write a uint in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLEUint(uint value)
		{
			WriteLEUshort((ushort)(value & 0xffff));
			WriteLEUshort((ushort)(value >> 16));
		}

		/// <summary>
		/// Write a long in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLELong(long value)
		{
			WriteLEInt((int)value);
			WriteLEInt((int)(value >> 32));
		}

		/// <summary>
		/// Write a ulong in little endian byte order.
		/// </summary>
		/// <param name="value">The value to write.</param>
		public void WriteLEUlong(ulong value)
		{
			WriteLEUint((uint)(value & 0xffffffff));
			WriteLEUint((uint)(value >> 32));
		}

		#endregion

		/// <summary>
		/// Write a data descriptor.
		/// </summary>
		/// <param name="entry">The entry to write a descriptor for.</param>
		/// <returns>Returns the number of descriptor bytes written.</returns>
		public int WriteDataDescriptor(ZipEntry entry)
		{
			if (entry == null) {
				throw new ArgumentNullException("nameof(entry)");
			}

			int result = 0;

			// Add data descriptor if flagged as required
			if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) {
				// The signature is not PKZIP originally but is now described as optional
				// in the PKZIP Appnote documenting trhe format.
				WriteLEInt(ZipConstants.DataDescriptorSignature);
				WriteLEInt(unchecked((int)(entry.Crc)));

				result += 8;

				if (entry.LocalHeaderRequiresZip64) {
					WriteLELong(entry.CompressedSize);
					WriteLELong(entry.Size);
					result += 16;
				} else {
					WriteLEInt((int)entry.CompressedSize);
					WriteLEInt((int)entry.Size);
					result += 8;
				}
			}

			return result;
		}

		/// <summary>
		/// Read data descriptor at the end of compressed data.
		/// </summary>
		/// <param name="zip64">if set to <c>true</c> [zip64].</param>
		/// <param name="data">The data to fill in.</param>
		/// <returns>Returns the number of bytes read in the descriptor.</returns>
		public void ReadDataDescriptor(bool zip64, DescriptorData data)
		{
			int intValue = ReadLEInt();

			// In theory this may not be a descriptor according to PKZIP appnote.
			// In practise its always there.
			if (intValue != ZipConstants.DataDescriptorSignature) {
				throw new ZipException("Data descriptor signature not found");
			}

			data.Crc = ReadLEInt();

			if (zip64) {
				data.CompressedSize = ReadLELong();
				data.Size = ReadLELong();
			} else {
				data.CompressedSize = ReadLEInt();
				data.Size = ReadLEInt();
			}
		}

		#region Instance Fields
		bool isOwner_;
		Stream stream_;
		#endregion
	}
}
