// ZipHelperStream.cs
//
// Copyright 2006, 2007 John Reilly
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.
// 
// As a special exception, the copyright holders of this library give you
// permission to link this library with independent modules to produce an
// executable, regardless of the license terms of these independent
// modules, and to copy and distribute the resulting executable under
// terms of your choice, provided that you also meet, for each linked
// independent module, the terms and conditions of the license of that
// module.  An independent module is a module which is not derived from
// or based on this library.  If you modify this library, you may extend
// this exception to your version of the library, but you are not
// obligated to do so.  If you do not wish to do so, delete this
// exception statement from your version.

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

namespace Externals.Compression.Zip
{

    /// <summary>
    /// Holds data pertinent to a data descriptor.
    /// </summary>
    internal 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; }
        }

#if !NET_1_0 && !NET_1_1 && !NETCF_1_0
        public override bool CanTimeout
        {
            get { return stream_.CanTimeout; }
        }
#endif

        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>
        override public void Close()
        {
            Stream toClose = stream_;
            stream_ = null;
            if (isOwner_ && (toClose != null))
            {
                isOwner_ = false;
                toClose.Close();
            }
        }
        #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.");
            }

            ZipExtraData 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("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
    }
}