using System;
using System.IO;
using Wayne.Lib.IO.UnitTest;

namespace Wayne.Lib.IO
{
    /// <summary>
    /// A file writing stream that ensures that the files remain consistent even when a writing operation is interrupted.
    /// The writing will be performed to a temporary file that will be exchanged with the target file when the writing has 
    /// completed. There is a static method, Cleanup that should be called when a program starts up that will clean up and 
    /// restore the files in the best possible state.
    /// </summary>
    public class SafeFileWritingStream : Stream
    {
        #region Fields

        private readonly IFileSupport fileSupport;
        private readonly SafeFileWritingInterruptPoint interruptAtStage = SafeFileWritingInterruptPoint.DontInterrupt;
        private readonly SafeFileInfo safeFileName;
        private readonly Stream targetFile;
        private readonly Stream tempOutputFile;
        private bool closed;

        #endregion

        #region Construction

        /// <summary>
        /// Creates a new instance of te SafeFileWriting Stream for the specified file.
        /// </summary>
        /// <param name="fileName"></param>
        /// <exception cref="SafeFileWritingIOException">If some of the files that should be not is accessible.</exception>
        [Obsolete("Use constructor taking IFilesupport parameter.")]
        public SafeFileWritingStream(string fileName)
            : this(FileSupport.fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt)
        {
        }

        /// <summary>
        /// Creates a new instance of te SafeFileWriting Stream for the specified file.
        /// </summary>
        /// <param name="fileSupport">The filesupport implementation to use.</param>
        /// <param name="fileName"></param>
        /// <exception cref="SafeFileWritingIOException">If some of the files that should be not is accessible.</exception>
        public SafeFileWritingStream(IFileSupport fileSupport, string fileName)
            : this(fileSupport, fileName, SafeFileWritingInterruptPoint.DontInterrupt)
        {
        }

        /// <summary>
        /// Private constuctor used for unit testing.
        /// </summary>
        /// <param name="fileSupport">The filesupport implementation to use.</param>
        /// <param name="fileName"></param>
        /// <param name="interruptAtStage"></param>
        private SafeFileWritingStream(IFileSupport fileSupport, string fileName, SafeFileWritingInterruptPoint interruptAtStage)
        {
            fileName = Paths.Parse(fileName);

            this.fileSupport = fileSupport;
            this.interruptAtStage = interruptAtStage;

            SetWritingStage(SafeFileWritingInterruptPoint.WritingNotBegun);

            safeFileName = SafeFileInfo.FromOriginalFileName(fileName);

            try
            {
                if (fileSupport.FileExists(fileName))
                    targetFile = fileSupport.Open(fileName, FileMode.Open, FileAccess.Write, FileShare.Read);
            }
            catch (IOException ioException)
            {
                throw new SafeFileWritingIOException("Error opening target file (" + fileName + ")", ioException);
            }

            //If there is an -old file try to delete it, otherwise it is ok for the File.Delete to throw.

            if (fileSupport.FileExists(safeFileName.OldFileName))
            {
                try
                {
                    fileSupport.Delete(safeFileName.OldFileName);
                }
                catch (IOException ioException)
                {
                    throw new SafeFileWritingIOException("Error deleting a -old file (" + safeFileName.OldFileName + ")", ioException);
                }
            }

            try
            {
                tempOutputFile = fileSupport.Open(safeFileName.TempFileName, FileMode.Create, FileAccess.Write, FileShare.None);
            }
            catch (IOException ioException)
            {
                throw new SafeFileWritingIOException("Error opening the temp file (" + safeFileName.TempFileName + ")", ioException);
            }

            SetWritingStage(SafeFileWritingInterruptPoint.WritingOngoing);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Always false, SafeFileStream is write-only
        /// </summary>
        public override bool CanRead
        {
            get { return false; }
        }

        /// <summary>
        /// Always false, SafeFileStream is write-only
        /// </summary>
        public override bool CanSeek
        {
            get { return false; }
        }

        /// <summary>
        /// Always true, SafeFileStream is write-only
        /// </summary>
        public override bool CanWrite
        {
            get { return true; }
        }

        /// <summary>
        /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. 
        /// </summary>
        /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
        /// <exception cref="IOException">An I/O error occurs.</exception>
        public override void Flush()
        {
            tempOutputFile.Flush();
        }

        /// <summary>
        /// Gets the length in bytes of the stream. 
        /// </summary>
        /// <exception cref="NotSupportedException">CanSeek for this stream is false.</exception>
        /// <exception cref="IOException">An I/O error occurs, such as the file being closed.</exception>
        public override long Length
        {
            get { return tempOutputFile.Length; }
        }

        /// <summary>
        /// Gets or sets the current position of this stream. 
        /// </summary>
        /// <exception cref="NotSupportedException">The stream does not support seeking.</exception>
        public override long Position
        {
            get
            {
                return tempOutputFile.Position;
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Not supported
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        /// <exception cref="NotSupportedException">Always</exception>
        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported. Stream is Write-only.
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="origin"></param>
        /// <returns></returns>
        /// <exception cref="NotSupportedException">Always.</exception>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported. Fast-forward writing only.
        /// </summary>
        /// <param name="value"></param>
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Writes a block of bytes to this stream using data from a buffer. 
        /// </summary>
        /// <param name="buffer">The buffer containing data to write to the stream.</param>
        /// <param name="offset">The zero-based byte offset in array at which to begin copying bytes to the current stream.</param>
        /// <param name="count">The maximum number of bytes to be written to the current stream.</param>
        /// <exception cref="ArgumentNullException">array is null.</exception>
        /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
        /// <exception cref="IOException">An I/O error occurs.</exception>
        /// <exception cref="ArgumentException">offset and count describe an invalid range in array.</exception>
        /// <exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
        public override void Write(byte[] buffer, int offset, int count)
        {
            tempOutputFile.Write(buffer, offset, count);
        }

        /// <summary>
        /// Closes the stream and overwrites the target file.
        /// </summary>
        public override void Close()
        {
            base.Close();
            if (!closed)
            {
                closed = true;

                //Close the temporary file.
                tempOutputFile.Close();

                //Unlock the original file.
                if (targetFile != null)
                {
                    targetFile.Close();

                    //Rename the original file to .old
                    fileSupport.Move(safeFileName.OriginalFileName, safeFileName.OldFileName, 100, 100);

                    SetWritingStage(SafeFileWritingInterruptPoint.WritingCompleteOriginalFileRenamedToOld);
                }

                //Change name on the temporary file to the target file name
                fileSupport.Move(safeFileName.TempFileName, safeFileName.OriginalFileName, 100, 100);

                SetWritingStage(SafeFileWritingInterruptPoint.WritingCompleteTempFileRenamedToTargetFile);

                if (targetFile != null)
                {
                    //Delete the old file             
                    fileSupport.Delete(safeFileName.OldFileName, 100, 100);
                    SetWritingStage(SafeFileWritingInterruptPoint.WritingComplete);
                }
            }
        }

        /// <summary>
        /// Private method that is used to enable interrupts in different phases of the writing 
        /// for the unit testing.
        /// </summary>
        /// <param name="writingStage"></param>
        private void SetWritingStage(SafeFileWritingInterruptPoint writingStage)
        {
            if (writingStage == interruptAtStage)
            {
                targetFile.Close();
                tempOutputFile.Close();
                throw new SafeFileWritingInterruptedException();
            }
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// All file types that is written with the SafeFileWritingStream should be cleaned at certain points
        /// to maintain the integrity. The typical place to place a call to this method is at the startup of
        /// a module. If the module wrote something and was interrupted by a program shutdown, it can rescue some data with this
        /// method.
        /// </summary>
        /// <param name="folderPath"></param>
        /// <param name="pattern"></param>
        /// <param name="temporaryFileFoundCallback">Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.</param>
        /// <param name="userToken">Token that is returned in the invokation of the temporaryFileFoundCallback.</param>
        /// <exception cref="ArgumentException">Folder does not exist</exception>
        [Obsolete("Use Cleanup method supplying file support.")]
        public static void Cleanup(string folderPath, string pattern, EventHandler<SafeFileWritingCleanupEventArgs> temporaryFileFoundCallback, object userToken)
        {
            Cleanup(FileSupport.fileSupport, folderPath, pattern, temporaryFileFoundCallback, userToken);
        }

        /// <summary>
        /// All file types that is written with the SafeFileWritingStream should be cleaned at certain points
        /// to maintain the integrity. The typical place to place a call to this method is at the startup of
        /// a module. If the module wrote something and was interrupted by a program shutdown, it can rescue some data with this
        /// method.
        /// </summary>
        /// <param name="fileSupport">The filesupport implementation to use.</param>
        /// <param name="folderPath"></param>
        /// <param name="pattern"></param>
        /// <param name="temporaryFileFoundCallback">Delegate that is called when a temp file is found and asks for action to take. This delegate may be invoked several times.</param>
        /// <param name="userToken">Token that is returned in the invokation of the temporaryFileFoundCallback.</param>
        public static void Cleanup(IFileSupport fileSupport, string folderPath, string pattern, EventHandler<SafeFileWritingCleanupEventArgs> temporaryFileFoundCallback, object userToken)
        {
            if (fileSupport.DirectoryExists(folderPath))
            {
                foreach (SafeFileInfo safeFile in SafeFileInfo.GetFiles(fileSupport, folderPath, pattern))
                {
                    //Stage 0 - No writing has been interrupted.
                    if (safeFile.OriginalFileExists && !safeFile.TempFileExists && !safeFile.OldFileExist)
                    {
                        //Do nothing
                    }

                    //Stage 1 - Writing has been done, the Temp file has been created, and a undefined percentage of the file has been written.
                    else if ((safeFile.OriginalFileExists && safeFile.TempFileExists) || //Overwrite scenario
                        (!safeFile.OriginalFileExists && !safeFile.OldFileExist && safeFile.TempFileExists)) //Create new file scenario
                    {
                        //Remove the Temp file and send event log with the content of the temp file.
                        fileSupport.Delete(safeFile.TempFileName);

                        SafeFileWritingCleanupEventArgs evArgs = new SafeFileWritingCleanupEventArgs(safeFile.TempFileName, userToken);

                        if (temporaryFileFoundCallback != null)
                            temporaryFileFoundCallback(null, evArgs);

                        if (evArgs.Action == SafeFileWritingCleanupAction.StoreInRestoredTempFileDir)
                        {
                            string restoredTempFilesDir = Paths.Combine("RestoredTemp");
                            fileSupport.EnsureDirectoryExists(restoredTempFilesDir);
                            string newFileName = string.Concat(Guid.NewGuid(), Path.GetFileName(safeFile.TempFileName));
                            fileSupport.Move(safeFile.TempFileName, Path.Combine(restoredTempFilesDir, newFileName), 100, 100);
                        }
                        else
                        {
                            fileSupport.Delete(safeFile.TempFileName);
                        }
                    }

                    //Stage 2- Writing has been completed, The original file has been renamed to old.
                    else if (safeFile.TempFileExists && safeFile.OldFileExist)
                    {
                        //Remove the Old file 
                        fileSupport.Delete(safeFile.OldFileName);

                        //...and make the temp file the real file.
                        fileSupport.Move(safeFile.TempFileName, safeFile.OriginalFileName, 100, 100);
                    }

                    //Stage 3 - Temp file has been sucessfully renamed but old file still remains.
                    else if (safeFile.OriginalFileExists && safeFile.OldFileExist)
                    {
                        fileSupport.Delete(safeFile.OldFileName);
                    }
                }
            }            
        }
        #endregion
    }
}