// ZipSegmentedStream.cs
// ------------------------------------------------------------------
//
// Copyright (c) 2009-2011 Dino Chiesa.
// All rights reserved.
//
// This code module is part of DotNetZip, a zipfile class library.
//
// ------------------------------------------------------------------
//
// This code is licensed under the Microsoft Public License.
// See the file License.txt for the license details.
// More info on: http://dotnetzip.codeplex.com
//
// ------------------------------------------------------------------
//
// last saved (in emacs):
// Time-stamp: <2011-July-13 22:25:45>
//
// ------------------------------------------------------------------
//
// This module defines logic for zip streams that span disk files.
//
// ------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.IO;

namespace Ionic.Zip
{
    internal class ZipSegmentedStream : System.IO.Stream
    {
        enum RwMode
        {
            None = 0,
            ReadOnly = 1,
            Write = 2,
            //Update = 3
        }

        private RwMode rwMode;
        private bool _exceptionPending; // **see note below
        private string _baseName;
        private string _baseDir;
        //private bool _isDisposed;
        private string _currentName;
        private string _currentTempName;
        private uint _currentDiskNumber;
        private uint _maxDiskNumber;
        private int _maxSegmentSize;
        private System.IO.Stream _innerStream;

        // **Note regarding exceptions:
        //
        // When ZipSegmentedStream is employed within a using clause,
        // which is the typical scenario, and an exception is thrown
        // within the scope of the using, Dispose() is invoked
        // implicitly before processing the initial exception.  If that
        // happens, this class sets _exceptionPending to true, and then
        // within the Dispose(bool), takes special action as
        // appropriate. Need to be careful: any additional exceptions
        // will mask the original one.

        private ZipSegmentedStream() : base()
        {
            _exceptionPending = false;
        }

        public static ZipSegmentedStream ForReading(string name,
                                                    uint initialDiskNumber,
                                                    uint maxDiskNumber)
        {
            ZipSegmentedStream zss = new ZipSegmentedStream()
                {
                    rwMode = RwMode.ReadOnly,
                    CurrentSegment = initialDiskNumber,
                    _maxDiskNumber = maxDiskNumber,
                    _baseName = name,
                };

            // Console.WriteLine("ZSS: ForReading ({0})",
            //                    Path.GetFileName(zss.CurrentName));

            zss._SetReadStream();

            return zss;
        }


        public static ZipSegmentedStream ForWriting(string name, int maxSegmentSize)
        {
            ZipSegmentedStream zss = new ZipSegmentedStream()
                {
                    rwMode = RwMode.Write,
                    CurrentSegment = 0,
                    _baseName = name,
                    _maxSegmentSize = maxSegmentSize,
                    _baseDir = Path.GetDirectoryName(name)
                };

            // workitem 9522
            if (zss._baseDir=="") zss._baseDir=".";

            zss._SetWriteStream(0);

            // Console.WriteLine("ZSS: ForWriting ({0})",
            //                    Path.GetFileName(zss.CurrentName));

            return zss;
        }


        /// <summary>
        ///   Sort-of like a factory method, ForUpdate is used only when
        ///   the application needs to update the zip entry metadata for
        ///   a segmented zip file, when the starting segment is earlier
        ///   than the ending segment, for a particular entry.
        /// </summary>
        /// <remarks>
        ///   <para>
        ///     The update is always contiguous, never rolls over.  As a
        ///     result, this method doesn't need to return a ZSS; it can
        ///     simply return a FileStream.  That's why it's "sort of"
        ///     like a Factory method.
        ///   </para>
        ///   <para>
        ///     Caller must Close/Dispose the stream object returned by
        ///     this method.
        ///   </para>
        /// </remarks>
        public static Stream ForUpdate(string name, uint diskNumber)
        {
            if (diskNumber >= 99)
                throw new ArgumentOutOfRangeException("diskNumber");

            string fname =
                String.Format("{0}.z{1:D2}",
                                 Path.Combine(Path.GetDirectoryName(name),
                                              Path.GetFileNameWithoutExtension(name)),
                                 diskNumber + 1);

            // Console.WriteLine("ZSS: ForUpdate ({0})",
            //                   Path.GetFileName(fname));

            // This class assumes that the update will not expand the
            // size of the segment. Update is used only for an in-place
            // update of zip metadata. It never will try to write beyond
            // the end of a segment.

            return File.Open(fname,
                             FileMode.Open,
                             FileAccess.ReadWrite,
                             FileShare.None);
        }

        public bool ContiguousWrite
        {
            get;
            set;
        }


        public UInt32 CurrentSegment
        {
            get
            {
                return _currentDiskNumber;
            }
            private set
            {
                _currentDiskNumber = value;
                _currentName = null; // it will get updated next time referenced
            }
        }

        /// <summary>
        ///   Name of the filesystem file corresponding to the current segment.
        /// </summary>
        /// <remarks>
        ///   <para>
        ///     The name is not always the name currently being used in the
        ///     filesystem.  When rwMode is RwMode.Write, the filesystem file has a
        ///     temporary name until the stream is closed or until the next segment is
        ///     started.
        ///   </para>
        /// </remarks>
        public String CurrentName
        {
            get
            {
                if (_currentName==null)
                    _currentName = _NameForSegment(CurrentSegment);

                return _currentName;
            }
        }


        public String CurrentTempName
        {
            get
            {
                return _currentTempName;
            }
        }

        private string _NameForSegment(uint diskNumber)
        {
            if (diskNumber >= 99)
            {
                _exceptionPending = true;
                throw new OverflowException("The number of zip segments would exceed 99.");
            }

            return String.Format("{0}.z{1:D2}",
                                 Path.Combine(Path.GetDirectoryName(_baseName),
                                              Path.GetFileNameWithoutExtension(_baseName)),
                                 diskNumber + 1);
        }


        // Returns the segment that WILL be current if writing
        // a block of the given length.
        // This isn't exactly true. It could roll over beyond
        // this number.
        public UInt32 ComputeSegment(int length)
        {
            if (_innerStream.Position + length > _maxSegmentSize)
                // the block will go AT LEAST into the next segment
                return CurrentSegment + 1;

            // it will fit in the current segment
            return CurrentSegment;
        }


        public override String ToString()
        {
            return String.Format("{0}[{1}][{2}], pos=0x{3:X})",
                                 "ZipSegmentedStream", CurrentName,
                                 rwMode.ToString(),
                                 this.Position);
        }


        private void _SetReadStream()
        {
            if (_innerStream != null)
            {
#if NETCF
                _innerStream.Close();
#else
                _innerStream.Dispose();
#endif
            }

            if (CurrentSegment + 1 == _maxDiskNumber)
                _currentName = _baseName;

            // Console.WriteLine("ZSS: SRS ({0})",
            //                   Path.GetFileName(CurrentName));

            _innerStream = File.OpenRead(CurrentName);
        }


        /// <summary>
        /// Read from the stream
        /// </summary>
        /// <param name="buffer">the buffer to read</param>
        /// <param name="offset">the offset at which to start</param>
        /// <param name="count">the number of bytes to read</param>
        /// <returns>the number of bytes actually read</returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (rwMode != RwMode.ReadOnly)
            {
                _exceptionPending = true;
                throw new InvalidOperationException("Stream Error: Cannot Read.");
            }

            int r = _innerStream.Read(buffer, offset, count);
            int r1 = r;

            while (r1 != count)
            {
                if (_innerStream.Position != _innerStream.Length)
                {
                    _exceptionPending = true;
                    throw new ZipException(String.Format("Read error in file {0}", CurrentName));

                }

                if (CurrentSegment + 1 == _maxDiskNumber)
                    return r; // no more to read

                CurrentSegment++;
                _SetReadStream();
                offset += r1;
                count -= r1;
                r1 = _innerStream.Read(buffer, offset, count);
                r += r1;
            }
            return r;
        }



        private void _SetWriteStream(uint increment)
        {
            if (_innerStream != null)
            {
#if NETCF
                _innerStream.Close();
#else
                _innerStream.Dispose();
#endif
                if (File.Exists(CurrentName))
                    File.Delete(CurrentName);
                File.Move(_currentTempName, CurrentName);
                // Console.WriteLine("ZSS: SWS close ({0})",
                //                   Path.GetFileName(CurrentName));
            }

            if (increment > 0)
                CurrentSegment += increment;

            SharedUtilities.CreateAndOpenUniqueTempFile(_baseDir,
                                                        out _innerStream,
                                                        out _currentTempName);

            // Console.WriteLine("ZSS: SWS open ({0})",
            //                   Path.GetFileName(_currentTempName));

            if (CurrentSegment == 0)
                _innerStream.Write(BitConverter.GetBytes(ZipConstants.SplitArchiveSignature), 0, 4);
        }


        /// <summary>
        /// Write to the stream.
        /// </summary>
        /// <param name="buffer">the buffer from which to write</param>
        /// <param name="offset">the offset at which to start writing</param>
        /// <param name="count">the number of bytes to write</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (rwMode != RwMode.Write)
            {
                _exceptionPending = true;
                throw new InvalidOperationException("Stream Error: Cannot Write.");
            }


            if (ContiguousWrite)
            {
                // enough space for a contiguous write?
                if (_innerStream.Position + count > _maxSegmentSize)
                    _SetWriteStream(1);
            }
            else
            {
                while (_innerStream.Position + count > _maxSegmentSize)
                {
                    int c = unchecked(_maxSegmentSize - (int)_innerStream.Position);
                    _innerStream.Write(buffer, offset, c);
                    _SetWriteStream(1);
                    count -= c;
                    offset += c;
                }
            }

            _innerStream.Write(buffer, offset, count);
        }


        public long TruncateBackward(uint diskNumber, long offset)
        {
            // Console.WriteLine("***ZSS.Trunc to disk {0}", diskNumber);
            // Console.WriteLine("***ZSS.Trunc:  current disk {0}", CurrentSegment);
            if (diskNumber >= 99)
                throw new ArgumentOutOfRangeException("diskNumber");

            if (rwMode != RwMode.Write)
            {
                _exceptionPending = true;
                throw new ZipException("bad state.");
            }

            // Seek back in the segmented stream to a (maybe) prior segment.

            // Check if it is the same segment.  If it is, very simple.
            if (diskNumber == CurrentSegment)
            {
                var x =_innerStream.Seek(offset, SeekOrigin.Begin);
                // workitem 10178
                Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);
                return x;
            }

            // Seeking back to a prior segment.
            // The current segment and any intervening segments must be removed.
            // First, close the current segment, and then remove it.
            if (_innerStream != null)
            {
#if NETCF
                _innerStream.Close();
#else
                _innerStream.Dispose();
#endif
                if (File.Exists(_currentTempName))
                    File.Delete(_currentTempName);
            }

            // Now, remove intervening segments.
            for (uint j= CurrentSegment-1; j > diskNumber; j--)
            {
                string s = _NameForSegment(j);
                // Console.WriteLine("***ZSS.Trunc:  removing file {0}", s);
                if (File.Exists(s))
                    File.Delete(s);
            }

            // now, open the desired segment.  It must exist.
            CurrentSegment = diskNumber;

            // get a new temp file, try 3 times:
            for (int i = 0; i < 3; i++)
            {
                try
                {
                    _currentTempName = SharedUtilities.InternalGetTempFileName();
                    // move the .z0x file back to a temp name
                    File.Move(CurrentName, _currentTempName);
                    break; // workitem 12403
                }
                catch(IOException)
                {
                    if (i == 2) throw;
                }
            }

            // open it
            _innerStream = new FileStream(_currentTempName, FileMode.Open);

            var r =  _innerStream.Seek(offset, SeekOrigin.Begin);

            // workitem 10178
            Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);

            return r;
        }



        public override bool CanRead
        {
            get
            {
                return (rwMode == RwMode.ReadOnly &&
                        (_innerStream != null) &&
                        _innerStream.CanRead);
            }
        }


        public override bool CanSeek
        {
            get
            {
                return (_innerStream != null) &&
                        _innerStream.CanSeek;
            }
        }


        public override bool CanWrite
        {
            get
            {
                return (rwMode == RwMode.Write) &&
                        (_innerStream != null) &&
                        _innerStream.CanWrite;
            }
        }

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

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

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

        public override long Seek(long offset, System.IO.SeekOrigin origin)
        {
            var x = _innerStream.Seek(offset, origin);
            // workitem 10178
            Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream);
            return x;
        }

        public override void SetLength(long value)
        {
            if (rwMode != RwMode.Write)
            {
                _exceptionPending = true;
                throw new InvalidOperationException();
            }
            _innerStream.SetLength(value);
        }


        protected override void Dispose(bool disposing)
        {
            // this gets called by Stream.Close()

            // if (_isDisposed) return;
            // _isDisposed = true;
            //Console.WriteLine("Dispose (mode={0})\n", rwMode.ToString());

            try
            {
                if (_innerStream != null)
                {
#if NETCF
                    _innerStream.Close();
#else
                    _innerStream.Dispose();
#endif
                    //_innerStream = null;
                    if (rwMode == RwMode.Write)
                    {
                        if (_exceptionPending)
                        {
                            // possibly could try to clean up all the
                            // temp files created so far...
                        }
                        else
                        {
                            // // move the final temp file to the .zNN name
                            // if (File.Exists(CurrentName))
                            //     File.Delete(CurrentName);
                            // if (File.Exists(_currentTempName))
                            //     File.Move(_currentTempName, CurrentName);
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

    }

}