// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Provider;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

using Dbg = System.Management.Automation;

namespace Microsoft.PowerShell.Commands
{
    /// <summary>
    /// The content stream class for the file system provider. It implements both
    /// the IContentReader and IContentWriter interfaces.
    /// </summary>
    /// <remarks>
    /// Note, this class does no specific error handling. All errors are allowed to
    /// propagate to the caller so that they can be written to the error pipeline
    /// if necessary.
    /// </remarks>
    internal class FileSystemContentReaderWriter : IContentReader, IContentWriter
    {
        #region tracer

        /// <summary>
        /// An instance of the PSTraceSource class used for trace output
        /// using "FileSystemContentStream" as the category.
        /// </summary>
        [Dbg.TraceSourceAttribute(
            "FileSystemContentStream",
            "The provider content reader and writer for the file system")]
        private static Dbg.PSTraceSource s_tracer =
            Dbg.PSTraceSource.GetTracer("FileSystemContentStream",
            "The provider content reader and writer for the file system");

        #endregion tracer

        private string _path;
        private string _streamName;
        private FileMode _mode;
        private FileAccess _access;
        private FileShare _share;
        private Encoding _encoding;
        private CmdletProvider _provider;

        private FileStream _stream;
        private StreamReader _reader;
        private StreamWriter _writer;
        private bool _usingByteEncoding;

        private const char DefaultDelimiter = '\n';

        private string _delimiter = $"{DefaultDelimiter}";
        private int[] _offsetDictionary;
        private bool _usingDelimiter;
        private StringBuilder _currentLineContent;
        private bool _waitForChanges;
        private bool _isRawStream;
        private long _fileOffset;

        private FileAttributes _oldAttributes;
        private bool _haveOldAttributes;

        // The reader to read file content backward
        private FileStreamBackReader _backReader;
        private bool _alreadyDetectEncoding = false;

        // False to add a newline to the end of the output string, true if not.
        private bool _suppressNewline = false;

        /// <summary>
        /// Constructor for the content stream.
        /// </summary>
        /// <param name="path">
        /// The path to the file to get the content from.
        /// </param>
        /// <param name="mode">
        /// The file mode to open the file with.
        /// </param>
        /// <param name="access">
        /// The file access requested in the file.
        /// </param>
        /// <param name="share">
        /// The file share to open the file with
        /// </param>
        /// <param name="encoding">
        /// The encoding of the file to be read or written.
        /// </param>
        /// <param name="usingByteEncoding">
        /// If true, bytes will be read from the file. If false, the specified encoding
        /// will be used to read the file.
        /// </param>
        /// <param name="waitForChanges">
        /// If true, we will perform blocking reads on the file, waiting for new content to be appended
        /// </param>
        /// <param name="provider">
        /// The CmdletProvider invoking this stream
        /// </param>
        /// <param name="isRawStream">
        /// Indicates raw stream.
        /// </param>
        public FileSystemContentReaderWriter(
            string path, FileMode mode, FileAccess access,
            FileShare share, Encoding encoding, bool usingByteEncoding,
            bool waitForChanges, CmdletProvider provider, bool isRawStream) :
                this(path, null, mode, access, share, encoding, usingByteEncoding, waitForChanges, provider, isRawStream)
        {
        }

        /// <summary>
        /// Constructor for the content stream.
        /// </summary>
        /// <param name="path">
        /// The path to the file to get the content from.
        /// </param>
        /// <param name="streamName">
        /// The name of the Alternate Data Stream to get the content from. If null or empty, returns
        /// the file's primary content.
        /// </param>
        /// <param name="mode">
        /// The file mode to open the file with.
        /// </param>
        /// <param name="access">
        /// The file access requested in the file.
        /// </param>
        /// <param name="share">
        /// The file share to open the file with
        /// </param>
        /// <param name="encoding">
        /// The encoding of the file to be read or written.
        /// </param>
        /// <param name="usingByteEncoding">
        /// If true, bytes will be read from the file. If false, the specified encoding
        /// will be used to read the file.
        /// </param>
        /// <param name="waitForChanges">
        /// If true, we will perform blocking reads on the file, waiting for new content to be appended
        /// </param>
        /// <param name="provider">
        /// The CmdletProvider invoking this stream
        /// </param>
        /// <param name="isRawStream">
        /// Indicates raw stream.
        /// </param>
        public FileSystemContentReaderWriter(
            string path, string streamName, FileMode mode, FileAccess access, FileShare share,
            Encoding encoding, bool usingByteEncoding, bool waitForChanges, CmdletProvider provider,
            bool isRawStream)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw PSTraceSource.NewArgumentNullException(nameof(path));
            }

            if (s_tracer.IsEnabled)
            {
                s_tracer.WriteLine("path = {0}", path);
                s_tracer.WriteLine("mode = {0}", mode);
                s_tracer.WriteLine("access = {0}", access);
            }

            _path = path;
            _streamName = streamName;
            _mode = mode;
            _access = access;
            _share = share;
            _encoding = encoding;
            _usingByteEncoding = usingByteEncoding;
            _waitForChanges = waitForChanges;
            _provider = provider;
            _isRawStream = isRawStream;

            CreateStreams(path, streamName, mode, access, share, encoding);
        }

        /// <summary>
        /// Constructor for the content stream.
        /// </summary>
        /// <param name="path">
        /// The path to the file to get the content from.
        /// </param>
        /// <param name="streamName">
        /// The name of the Alternate Data Stream to get the content from. If null or empty, returns
        /// the file's primary content.
        /// </param>
        /// <param name="mode">
        /// The file mode to open the file with.
        /// </param>
        /// <param name="access">
        /// The file access requested in the file.
        /// </param>
        /// <param name="share">
        /// The file share to open the file with
        /// </param>
        /// <param name="encoding">
        /// The encoding of the file to be read or written.
        /// </param>
        /// <param name="usingByteEncoding">
        /// If true, bytes will be read from the file. If false, the specified encoding
        /// will be used to read the file.
        /// </param>
        /// <param name="waitForChanges">
        /// If true, we will perform blocking reads on the file, waiting for new content to be appended
        /// </param>
        /// <param name="provider">
        /// The CmdletProvider invoking this stream
        /// </param>
        /// <param name="isRawStream">
        /// Indicates raw stream.
        /// </param>
        /// <param name="suppressNewline">
        /// False to add a newline to the end of the output string, true if not.
        /// </param>
        public FileSystemContentReaderWriter(
            string path, string streamName, FileMode mode, FileAccess access, FileShare share,
            Encoding encoding, bool usingByteEncoding, bool waitForChanges, CmdletProvider provider,
            bool isRawStream, bool suppressNewline)
                : this(path, streamName, mode, access, share, encoding, usingByteEncoding, waitForChanges, provider, isRawStream)
        {
            _suppressNewline = suppressNewline;
        }

        /// <summary>
        /// Constructor for the content stream.
        /// </summary>
        /// <param name="path">
        /// The path to the file to get the content from.
        /// </param>
        /// <param name="streamName">
        /// The name of the Alternate Data Stream to get the content from. If null or empty, returns
        /// the file's primary content.
        /// </param>
        /// <param name="mode">
        /// The file mode to open the file with.
        /// </param>
        /// <param name="access">
        /// The file access requested in the file.
        /// </param>
        ///  <param name="share">
        ///    The file share to open the file with
        ///  </param>
        /// <param name="delimiter">
        /// The delimiter to use when reading strings. Each time read is called, all contents up to an including
        /// the delimiter is read.
        /// </param>
        /// <param name="encoding">
        /// The encoding of the file to be read or written.
        /// </param>
        /// <param name="waitForChanges">
        /// If true, we will perform blocking reads on the file, waiting for new content to be appended
        /// </param>
        /// <param name="provider">
        /// The CmdletProvider invoking this stream
        /// </param>
        /// <param name="isRawStream">
        /// Indicates raw stream.
        /// </param>
        public FileSystemContentReaderWriter(
            string path,
            string streamName,
            FileMode mode,
            FileAccess access,
            FileShare share,
            string delimiter,
            Encoding encoding,
            bool waitForChanges,
            CmdletProvider provider,
            bool isRawStream)
            : this(path, streamName, mode, access, share, encoding, false, waitForChanges, provider, isRawStream)
        {
            // If the delimiter is default ('\n') we'll use ReadLine() method.
            // Otherwise allocate temporary structures for ReadDelimited() method.
            if (!(delimiter.Length == 1 && delimiter[0] == DefaultDelimiter))
            {
                _delimiter = delimiter;
                _usingDelimiter = true;

                // We expect that we are parsing files where line lengths can be relatively long.
                const int DefaultLineLength = 256;
                _currentLineContent = new StringBuilder(DefaultLineLength);

                // For Boyer-Moore string search algorithm.
                // Populate the offset lookups.
                // These will tell us the maximum number of characters
                // we can read to generate another possible match (safe shift).
                // If we read more characters than this, we risk consuming
                // more of the stream than we need.
                //
                // Because an unicode character size is 2 byte we would to have use
                // very large array with 65535 size to keep this safe offsets.
                // One solution is to pack unicode character to byte.
                // The workaround is to use low byte from unicode character.
                // This allow us to use small array with size 256.
                // This workaround is the fastest and provides excellent results
                // in regular search scenarios when the file contains
                // mostly characters from the same alphabet.
                _offsetDictionary = new int[256];

                // If next char from file is not in search pattern safe shift is the search pattern length.
                for (var n = 0; n < _offsetDictionary.Length; n++)
                {
                    _offsetDictionary[n] = _delimiter.Length;
                }

                // If next char from file is in search pattern we should calculate a safe shift.
                char currentChar;
                byte lowByte;
                for (var i = 0; i < _delimiter.Length; i++)
                {
                    currentChar = _delimiter[i];
                    lowByte = Unsafe.As<char, byte>(ref currentChar);
                    _offsetDictionary[lowByte] = _delimiter.Length - i - 1;
                }
            }
        }

        /// <summary>
        /// Reads the specified number of characters or a lines from the file.
        /// </summary>
        /// <param name="readCount">
        /// If less than 1, then the entire file is read at once. If 1 or greater, then
        /// readCount is used to determine how many items (ie: lines, bytes, delimited tokens)
        /// to read per call.
        /// </param>
        /// <returns>
        /// An array of strings representing the character(s) or line(s) read from
        /// the file.
        /// </returns>
        public IList Read(long readCount)
        {
            if (_isRawStream && _waitForChanges)
            {
                throw PSTraceSource.NewInvalidOperationException(FileSystemProviderStrings.RawAndWaitCannotCoexist);
            }

            bool waitChanges = _waitForChanges;

            s_tracer.WriteLine("blocks requested = {0}", readCount);

            var blocks = new List<object>();
            bool readToEnd = (readCount <= 0);

            if (_alreadyDetectEncoding && _reader.BaseStream.Position == 0)
            {
                Encoding curEncoding = _reader.CurrentEncoding;
                // Close the stream, and reopen the stream to make the BOM correctly processed.
                // The reader has already detected encoding, so if we don't reopen the stream, the BOM (if there is any)
                // will be treated as a regular character.
                _stream.Dispose();
                CreateStreams(_path, null, _mode, _access, _share, curEncoding);
                _alreadyDetectEncoding = false;
            }

            try
            {
                for (long currentBlock = 0; (currentBlock < readCount) || (readToEnd); ++currentBlock)
                {
                    if (waitChanges && _provider.Stopping)
                        waitChanges = false;

                    if (_usingByteEncoding)
                    {
                        if (!ReadByteEncoded(waitChanges, blocks, readBackward: false))
                            break;
                    }
                    else
                    {
                        if (_usingDelimiter || _isRawStream)
                        {
                            if (!ReadDelimited(waitChanges, blocks, readBackward: false, _delimiter))
                                break;
                        }
                        else
                        {
                            if (!ReadByLine(waitChanges, blocks, readBackward: false))
                                break;
                        }
                    }
                }

                s_tracer.WriteLine("blocks read = {0}", blocks.Count);
            }
            catch (Exception e)
            {
                if ((e is IOException) ||
                    (e is ArgumentException) ||
                    (e is System.Security.SecurityException) ||
                    (e is UnauthorizedAccessException) ||
                    (e is ArgumentNullException))
                {
                    // Exception contains specific message about the error occured and so no need for errordetails.
                    _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, _path));
                    return null;
                }
                else
                    throw;
            }

            return blocks.ToArray();
        }

        /// <summary>
        /// Read the content regardless of the 'waitForChanges' flag.
        /// </summary>
        /// <param name="readCount"></param>
        /// <returns></returns>
        internal IList ReadWithoutWaitingChanges(long readCount)
        {
            bool oldWaitChanges = _waitForChanges;
            _waitForChanges = false;
            try
            {
                return Read(readCount);
            }
            finally
            {
                _waitForChanges = oldWaitChanges;
            }
        }

        /// <summary>
        /// Move the pointer of the stream to the position where there are 'backCount' number
        /// of items (depends on what we are using: delimiter? line? byts?) to the end of the file.
        /// </summary>
        /// <param name="backCount"></param>
        internal void SeekItemsBackward(int backCount)
        {
            if (backCount < 0)
            {
                // The caller needs to guarantee that 'backCount' is greater or equals to 0
                throw PSTraceSource.NewArgumentException(nameof(backCount));
            }

            if (_isRawStream && _waitForChanges)
            {
                throw PSTraceSource.NewInvalidOperationException(FileSystemProviderStrings.RawAndWaitCannotCoexist);
            }

            s_tracer.WriteLine("blocks seek backwards = {0}", backCount);

            var blocks = new List<object>();
            if (_reader != null)
            {
                // Make the reader automatically detect the encoding
                Seek(0, SeekOrigin.Begin);
                _reader.Peek();
                _alreadyDetectEncoding = true;
            }

            Seek(0, SeekOrigin.End);

            if (backCount == 0)
            {
                // If backCount is 0, we should move the position to the end of the file.
                // Maybe the "waitForChanges" is true in this case, which means that we are waiting for new inputs.
                return;
            }

            string actualDelimiter = string.Create(
                _delimiter.Length,
                _delimiter,
                (chars, buf) =>
                {
                    for (int i = 0, j = buf.Length - 1; i < chars.Length; i++, j--)
                    {
                        chars[i] = buf[j];
                    }
                });

            long currentBlock = 0;
            string lastDelimiterMatch = null;

            try
            {
                if (_isRawStream)
                {
                    // We always read to the end for the raw data.
                    // If it's indicated as RawStream, we move the pointer to the
                    // beginning of the file
                    Seek(0, SeekOrigin.Begin);
                    return;
                }

                for (; currentBlock < backCount; ++currentBlock)
                {
                    if (_usingByteEncoding)
                    {
                        if (!ReadByteEncoded(waitChanges: false, blocks, readBackward: true))
                            break;
                    }
                    else
                    {
                        if (_usingDelimiter)
                        {
                            if (!ReadDelimited(waitChanges: false, blocks, readBackward: true, actualDelimiter))
                                break;
                            // If the delimiter is at the end of the file, we need to read one more
                            // to get to the right position. For example:
                            //      ua123ua456ua -- -Tail 1
                            // If we read backward only once, we get 'ua', and cannot get to the right position
                            // So we read one more time, get 'ua456ua', and then we can get the right position
                            lastDelimiterMatch = (string)blocks[0];
                            if (currentBlock == 0 && lastDelimiterMatch.Equals(actualDelimiter, StringComparison.Ordinal))
                                backCount++;
                        }
                        else
                        {
                            if (!ReadByLine(waitChanges: false, blocks, readBackward: true))
                                break;
                        }
                    }

                    blocks.Clear();
                }

                // If usingByteEncoding is true, we don't create the reader and _backReader
                if (!_usingByteEncoding)
                {
                    long curStreamPosition = _backReader.GetCurrentPosition();
                    if (_usingDelimiter)
                    {
                        if (currentBlock == backCount)
                        {
                            Dbg.Diagnostics.Assert(lastDelimiterMatch != null, "lastDelimiterMatch should not be null when currentBlock == backCount");
                            if (lastDelimiterMatch.EndsWith(actualDelimiter, StringComparison.Ordinal))
                            {
                                curStreamPosition += _backReader.GetByteCount(_delimiter);
                            }
                        }
                    }

                    Seek(curStreamPosition, SeekOrigin.Begin);
                }

                s_tracer.WriteLine("blocks seek position = {0}", _stream.Position);
            }
            catch (Exception e)
            {
                if ((e is IOException) ||
                    (e is ArgumentException) ||
                    (e is System.Security.SecurityException) ||
                    (e is UnauthorizedAccessException) ||
                    (e is ArgumentNullException))
                {
                    // Exception contains specific message about the error occured and so no need for errordetails.
                    _provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, _path));
                }
                else
                    throw;
            }
        }

        private bool ReadByLine(bool waitChanges, List<object> blocks, bool readBackward)
        {
            // Reading lines as strings
            string line = readBackward ? _backReader.ReadLine() : _reader.ReadLine();

            if (line == null)
            {
                if (waitChanges)
                {
                    // We only wait for changes when read forwards. So here we use reader, instead of 'localReader'
                    do
                    {
                        WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
                        line = _reader.ReadLine();
                    }
                    while ((line == null) && (!_provider.Stopping));
                }
            }

            if (line != null)
            {
                blocks.Add(line);
            }

            int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
            if (peekResult == -1)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        private bool ReadDelimited(bool waitChanges, List<object> blocks, bool readBackward, string actualDelimiter)
        {
            if (_isRawStream)
            {
                // when -Raw is used we want to anyway read the whole thing
                // so avoiding the while loop by reading the entire content.
                string contentRead = _reader.ReadToEnd();
                if (contentRead.Length > 0)
                {
                    blocks.Add(contentRead);
                }

                // We already read whole file so return EOF.
                return false;
            }

            // Since the delimiter is a string, we're essentially
            // dealing with a "find the substring" algorithm, but with
            // the additional restriction that we cannot read past the
            // end of the delimiter. If we read past the end of the delimiter,
            // then we'll eat up bytes that we need from the filestream.
            // The solution is a modified Boyer-Moore string search algorithm.
            // This version retains the sub-linear search performance (via the
            // lookup tables).
            int numRead = 0;
            int currentOffset = actualDelimiter.Length;
            Span<char> readBuffer = stackalloc char[currentOffset];
            bool delimiterNotFound = true;
            _currentLineContent.Clear();

            do
            {
                // Read in the required batch of characters
                numRead = readBackward
                                ? _backReader.Read(readBuffer.Slice(0, currentOffset))
                                : _reader.Read(readBuffer.Slice(0, currentOffset));

                // If we want to wait for changes, then we'll keep on attempting to read
                // until we fill the buffer.
                if (numRead == 0)
                {
                    if (waitChanges)
                    {
                        // But stop reading if the provider is stopping
                        while ((numRead < currentOffset) && (!_provider.Stopping))
                        {
                            // Get the change, and try to read more characters
                            // We only wait for changes when read forwards, so here we don't need to check if 'readBackward' is
                            // true or false, we only use 'reader'. The member 'reader' will be updated by WaitForChanges.
                            WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
                            numRead += _reader.Read(readBuffer.Slice(0, currentOffset - numRead));
                        }
                    }
                }

                if (numRead > 0)
                {
                    _currentLineContent.Append(readBuffer.Slice(0, numRead));

                    // Look up the final character in our offset table.
                    // If the character doesn't exist in the lookup table, then it's not in
                    // our search key.  That means the match must happen strictly /after/ the
                    // current position.  Because of that, we can feel confident reading in the
                    // number of characters in the search key, without the risk of reading too many.
                    var currentChar = _currentLineContent[_currentLineContent.Length - 1];
                    currentOffset = _offsetDictionary[Unsafe.As<char, byte>(ref currentChar)];

                    // We want to keep reading if delimiter not found and we haven't hit the end of file
                    delimiterNotFound = true;

                    // If the final letters matched, then we will get an offset of "0".
                    // In that case, we'll either have a match (and break from the while loop,)
                    // or we need to move the scan forward one position.
                    if (currentOffset == 0)
                    {
                        currentOffset = 1;

                        if (actualDelimiter.Length <= _currentLineContent.Length)
                        {
                            delimiterNotFound = false;
                            int i = 0;
                            int j = _currentLineContent.Length - actualDelimiter.Length;
                            for (; i < actualDelimiter.Length; i++, j++)
                            {
                                if (actualDelimiter[i] != _currentLineContent[j])
                                {
                                    delimiterNotFound = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            while (delimiterNotFound && (numRead != 0));

            // We've reached the end of file or end of line.
            if (_currentLineContent.Length > 0)
            {
                // Add the block read to the ouptut array list, trimming a trailing delimiter, if present.
                // Note: If -Tail was specified, we get here in the course of 2 distinct passes:
                //  - Once while reading backward simply to determine the appropriate *start position* for later forward reading, ignoring the content of the blocks read (in reverse).
                //  - Then again during forward reading, for regular output processing; it is only then that trimming the delimiter is necessary.
                //    (Trimming it during backward reading would not only be unnecessary, but could interfere with determining the correct start position.)
                blocks.Add(
                    !readBackward && !delimiterNotFound
                        ? _currentLineContent.ToString(0, _currentLineContent.Length - actualDelimiter.Length)
                        : _currentLineContent.ToString());
            }

            int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
            if (peekResult != -1)
            {
                return true;
            }
            else
            {
                if (readBackward && _currentLineContent.Length > 0)
                {
                    return true;
                }

                return false;
            }
        }

        private bool ReadByteEncoded(bool waitChanges, List<object> blocks, bool readBackward)
        {
            if (_isRawStream)
            {
                // if RawSteam, read all bytes and return. When RawStream is used, we dont
                // support -first, -last
                byte[] bytes = new byte[_stream.Length];
                int numBytesToRead = (int)_stream.Length;
                int numBytesRead = 0;
                while (numBytesToRead > 0)
                {
                    // Read may return anything from 0 to numBytesToRead.
                    int n = _stream.Read(bytes, numBytesRead, numBytesToRead);

                    // Break when the end of the file is reached.
                    if (n == 0)
                    {
                        break;
                    }

                    numBytesRead += n;
                    numBytesToRead -= n;
                }

                if (numBytesRead == 0)
                {
                    return false;
                }
                else
                {
                    blocks.Add(bytes);
                    return true;
                }
            }

            if (readBackward)
            {
                if (_stream.Position == 0)
                {
                    return false;
                }

                _stream.Position--;
                blocks.Add((byte)_stream.ReadByte());
                _stream.Position--;
                return true;
            }

            // Reading bytes not strings
            int byteRead = _stream.ReadByte();

            // We've found the end of the file.
            if (byteRead == -1)
            {
                // If we want to tail the file, wait for
                // the changes
                if (waitChanges)
                {
                    WaitForChanges(_path, _mode, _access, _share, ClrFacade.GetDefaultEncoding());
                    byteRead = _stream.ReadByte();
                }
            }

            // Add the byte we read to the list of blocks
            if (byteRead != -1)
            {
                blocks.Add((byte)byteRead);
                return true;
            }
            else
            {
                return false;
            }
        }

        private void CreateStreams(string filePath, string streamName, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, Encoding fileEncoding)
        {
            // Try to mask off the ReadOnly, and Hidden attributes
            // if they've specified Force.
            if (File.Exists(filePath) && _provider.Force)
            {
                // Store the old attributes so that we can recover them
                // in the Close();
                _oldAttributes = File.GetAttributes(filePath);
                _haveOldAttributes = true;

                // Clear the hidden attribute, and if we're writing, also clear readonly.
                var attributesToClear = FileAttributes.Hidden;
                if ((fileAccess & (FileAccess.Write)) != 0)
                {
                    attributesToClear |= FileAttributes.ReadOnly;
                }

                File.SetAttributes(_path, (File.GetAttributes(filePath) & ~attributesToClear));
            }

            // If we want to write to the stream, attempt to open it for reading as well
            // so that we can determine the file encoding as we append to it
            FileAccess requestedAccess = fileAccess;
            if ((fileAccess & (FileAccess.Write)) != 0)
            {
                fileAccess = FileAccess.ReadWrite;
            }

            try
            {
#if !UNIX
                if (!string.IsNullOrEmpty(streamName))
                {
                    _stream = AlternateDataStreamUtilities.CreateFileStream(filePath, streamName, fileMode, fileAccess, fileShare);
                }
                else
#endif
                {
                    _stream = new FileStream(filePath, fileMode, fileAccess, fileShare);
                }
            }
            catch (IOException)
            {
#if !UNIX
                if (!string.IsNullOrEmpty(streamName))
                {
                    _stream = AlternateDataStreamUtilities.CreateFileStream(filePath, streamName, fileMode, requestedAccess, fileShare);
                }
                else
#endif
                {
                    _stream = new FileStream(filePath, fileMode, requestedAccess, fileShare);
                }
            }

            if (!_usingByteEncoding)
            {
                // Open the reader stream
                if ((fileAccess & (FileAccess.Read)) != 0)
                {
                    _reader = new StreamReader(_stream, fileEncoding);
                    _backReader = new FileStreamBackReader(_stream, fileEncoding);
                }

                // Open the writer stream
                if ((fileAccess & (FileAccess.Write)) != 0)
                {
                    // Ensure we are using the proper encoding
                    if ((_reader != null) &&
                        ((fileAccess & (FileAccess.Read)) != 0))
                    {
                        _reader.Peek();
                        fileEncoding = _reader.CurrentEncoding;
                    }

                    _writer = new StreamWriter(_stream, fileEncoding);
                }
            }
        }

        /// <summary>
        /// Waits for changes to the specified file.  To do this, it closes the file
        /// and then monitors for changes.  Once a change appears, it reopens the streams
        /// and seeks to the last read position.
        /// </summary>
        /// <param name="filePath">The path of the file to read / monitor.</param>
        /// <param name="fileMode">The FileMode of the file (ie: Open / Append).</param>
        /// <param name="fileAccess">The access properties of the file (ie: Read / Write).</param>
        /// <param name="fileShare">The sharing properties of the file (ie: Read / ReadWrite).</param>
        /// <param name="fileEncoding">The encoding of the file.</param>
        private void WaitForChanges(string filePath, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, Encoding fileEncoding)
        {
            // Close the old stream, and store our current position.
            if (_stream != null)
            {
                _fileOffset = _stream.Position;
                _stream.Dispose();
            }

            // Watch for changes, as a blocking call.
            FileInfo watchFile = new FileInfo(filePath);
            long originalLength = watchFile.Length;

            using (FileSystemWatcher watcher = new FileSystemWatcher(watchFile.DirectoryName, watchFile.Name))
            {
                ErrorEventArgs errorEventArgs = null;
                var tcs = new TaskCompletionSource<FileSystemEventArgs>();
                FileSystemEventHandler onChangedHandler = (object source, FileSystemEventArgs e) => { tcs.TrySetResult(e); };
                RenamedEventHandler onRenamedHandler = (object source, RenamedEventArgs e) => { tcs.TrySetResult(e); };
                ErrorEventHandler onErrorHandler = (object source, ErrorEventArgs e) =>
                {
                    errorEventArgs = e;
                    tcs.TrySetResult(new FileSystemEventArgs(WatcherChangeTypes.All, watchFile.DirectoryName, watchFile.Name));
                };

                // With WaitForChanged, we registered for all change types, so we do the same here.
                watcher.Changed += onChangedHandler;
                watcher.Created += onChangedHandler;
                watcher.Deleted += onChangedHandler;
                watcher.Renamed += onRenamedHandler;
                watcher.Error += onErrorHandler;

                try
                {
                    watcher.EnableRaisingEvents = true;

                    while (!_provider.Stopping)
                    {
                        bool isTaskCompleted = tcs.Task.Wait(500);

                        if (errorEventArgs != null)
                        {
                            throw errorEventArgs.GetException();
                        }

                        if (isTaskCompleted)
                            break;

                        // If a process is still writing, .NET doesn't generate a change notification.
                        // So do a simple comparison on file size
                        watchFile.Refresh();
                        if (originalLength != watchFile.Length)
                        {
                            break;
                        }
                    }
                }
                finally
                {
                    // Done here to guarantee that the handlers are removed prior
                    // to the call to ManualResetEvent's Dispose() method.
                    watcher.EnableRaisingEvents = false;
                    watcher.Changed -= onChangedHandler;
                    watcher.Created -= onChangedHandler;
                    watcher.Deleted -= onChangedHandler;
                    watcher.Renamed -= onRenamedHandler;
                    watcher.Error -= onErrorHandler;
                }
            }

            // Let the change complete.
            // This is a fairly arbitrary number.  Without it, though,
            // some of the filesystem streams cannot be reopened.
            System.Threading.Thread.Sleep(100);

            // Reopen the streams.
            CreateStreams(filePath, null, fileMode, fileAccess, fileShare, fileEncoding);

            // If the file has been shortened, restart from zero.
            // That will let us catch log roll-overs.
            if (_fileOffset > _stream.Length)
                _fileOffset = 0;

            // Seek to the place we last left off.
            _stream.Seek(_fileOffset, SeekOrigin.Begin);
            if (_reader != null) { _reader.DiscardBufferedData(); }

            if (_backReader != null) { _backReader.DiscardBufferedData(); }
        }

        /// <summary>
        /// Moves the current stream position in the file.
        /// </summary>
        /// <param name="offset">
        /// The offset from the origin to move the position to.
        /// </param>
        /// <param name="origin">
        /// The origin from which the offset is calculated.
        /// </param>
        public void Seek(long offset, SeekOrigin origin)
        {
            if (_writer != null) { _writer.Flush(); }

            _stream.Seek(offset, origin);

            if (_writer != null) { _writer.Flush(); }

            if (_reader != null) { _reader.DiscardBufferedData(); }

            if (_backReader != null) { _backReader.DiscardBufferedData(); }
        }

        /// <summary>
        /// Closes the file.
        /// </summary>
        public void Close()
        {
            bool streamClosed = false;

            if (_writer != null)
            {
                try
                {
                    _writer.Flush();
                    _writer.Dispose();
                }
                finally
                {
                    streamClosed = true;
                }
            }

            if (_reader != null)
            {
                _reader.Dispose();
                streamClosed = true;
            }

            if (_backReader != null)
            {
                _backReader.Dispose();
                streamClosed = true;
            }

            if (!streamClosed)
            {
                _stream.Flush();
                _stream.Dispose();
            }

            // Reset the attributes
            if (_haveOldAttributes && _provider.Force)
            {
                File.SetAttributes(_path, _oldAttributes);
            }
        }

        /// <summary>
        /// Writes the specified object to the file.
        /// </summary>
        /// <param name="content">
        /// The objects to write to the file
        /// </param>
        /// <returns>
        /// The objects written to the file.
        /// </returns>
        public IList Write(IList content)
        {
            foreach (object line in content)
            {
                object[] contentArray = line as object[];
                if (contentArray != null)
                {
                    foreach (object obj in contentArray)
                    {
                        WriteObject(obj);
                    }
                }
                else
                {
                    WriteObject(line);
                }
            }

            return content;
        }

        private void WriteObject(object content)
        {
            if (content == null)
            {
                return;
            }

            if (_usingByteEncoding)
            {
                try
                {
                    byte byteToWrite = (byte)content;

                    _stream.WriteByte(byteToWrite);
                }
                catch (InvalidCastException)
                {
                    throw PSTraceSource.NewArgumentException(nameof(content), FileSystemProviderStrings.ByteEncodingError);
                }
            }
            else
            {
                if (_suppressNewline)
                {
                    _writer.Write(content.ToString());
                }
                else
                {
                    _writer.WriteLine(content.ToString());
                }
            }
        }

        /// <summary>
        /// Closes the file stream.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        internal void Dispose(bool isDisposing)
        {
            if (isDisposing)
            {
                if (_stream != null)
                    _stream.Dispose();
                if (_reader != null)
                    _reader.Dispose();
                if (_backReader != null)
                    _backReader.Dispose();
                if (_writer != null)
                    _writer.Dispose();
            }
        }
    }

    internal sealed class FileStreamBackReader : StreamReader
    {
        internal FileStreamBackReader(FileStream fileStream, Encoding encoding)
            : base(fileStream, encoding)
        {
            _stream = fileStream;
            if (_stream.Length > 0)
            {
                long curPosition = _stream.Position;
                _stream.Seek(0, SeekOrigin.Begin);
                base.Peek();
                _stream.Position = curPosition;
                _currentEncoding = base.CurrentEncoding;
                _currentPosition = _stream.Position;

                // Get the oem encoding and system current ANSI code page
                _oemEncoding = EncodingConversion.Convert(null, EncodingConversion.OEM);
                _defaultAnsiEncoding = EncodingConversion.Convert(null, EncodingConversion.Default);
            }
        }

        private readonly FileStream _stream;
        private readonly Encoding _currentEncoding;
        private readonly Encoding _oemEncoding;
        private readonly Encoding _defaultAnsiEncoding;

        private const int BuffSize = 4096;

        private readonly byte[] _byteBuff = new byte[BuffSize];
        private readonly char[] _charBuff = new char[BuffSize];
        private int _byteCount = 0;
        private int _charCount = 0;
        private long _currentPosition = 0;
        private bool? _singleByteCharSet = null;

        private const byte BothTopBitsSet = 0xC0;
        private const byte TopBitUnset = 0x80;

        /// <summary>
        /// If the given encoding is OEM or Default, check to see if the code page
        /// is SBCS(single byte character set).
        /// </summary>
        /// <returns></returns>
        private bool IsSingleByteCharacterSet()
        {
            if (_singleByteCharSet != null)
                return (bool)_singleByteCharSet;

            // Porting note: only UTF-8 is supported on Linux, which is not an SBCS
            if ((_currentEncoding.Equals(_oemEncoding) ||
                 _currentEncoding.Equals(_defaultAnsiEncoding))
                && Platform.IsWindows)
            {
                NativeMethods.CPINFO cpInfo;
                if (NativeMethods.GetCPInfo((uint)_currentEncoding.CodePage, out cpInfo) &&
                    cpInfo.MaxCharSize == 1)
                {
                    _singleByteCharSet = true;
                    return true;
                }
            }

            _singleByteCharSet = false;
            return false;
        }

        /// <summary>
        /// We don't support this method because it is not used by the ReadBackward method in FileStreamContentReaderWriter.
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="index"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public override int ReadBlock(char[] buffer, int index, int count)
        {
            // This method is not supposed to be used
            throw PSTraceSource.NewNotSupportedException();
        }

        /// <summary>
        /// We don't support this method because it is not used by the ReadBackward method in FileStreamContentReaderWriter.
        /// </summary>
        /// <returns></returns>
        public override string ReadToEnd()
        {
            // This method is not supposed to be used
            throw PSTraceSource.NewNotSupportedException();
        }

        /// <summary>
        /// Reset the internal character buffer. Use it only when the position of the internal buffer and
        /// the base stream do not match. These positions can become mismatch when the user read the data
        /// into the buffer and then seek a new position in the underlying stream.
        /// </summary>
        internal new void DiscardBufferedData()
        {
            base.DiscardBufferedData();
            _currentPosition = _stream.Position;
            _charCount = 0;
            _byteCount = 0;
        }

        /// <summary>
        /// Return the current actual stream position.
        /// </summary>
        /// <returns></returns>
        internal long GetCurrentPosition()
        {
            if (_charCount == 0)
                return _currentPosition;

            // _charCount > 0
            int byteCount = _currentEncoding.GetByteCount(_charBuff, 0, _charCount);
            return (_currentPosition + byteCount);
        }

        /// <summary>
        /// Get the number of bytes the delimiter will
        /// be encoded to.
        /// </summary>
        /// <param name="delimiter"></param>
        /// <returns></returns>
        internal int GetByteCount(string delimiter)
        {
            char[] chars = delimiter.ToCharArray();
            return _currentEncoding.GetByteCount(chars, 0, chars.Length);
        }

        /// <summary>
        /// Peek the next character.
        /// </summary>
        /// <returns>Return -1 if we reach the head of the file.</returns>
        public override int Peek()
        {
            if (_charCount == 0)
            {
                if (RefillCharBuffer() == -1)
                {
                    return -1;
                }
            }

            // Return the next available character, but DONT consume it (don't advance the _charCount)
            return (int)_charBuff[_charCount - 1];
        }

        /// <summary>
        /// Read the next character.
        /// </summary>
        /// <returns>Return -1 if we reach the head of the file.</returns>
        public override int Read()
        {
            if (_charCount == 0)
            {
                if (RefillCharBuffer() == -1)
                {
                    return -1;
                }
            }

            _charCount--;
            return _charBuff[_charCount];
        }

        /// <summary>
        /// Read a specific maximum of characters from the current stream into a buffer.
        /// </summary>
        /// <param name="buffer">Output buffer.</param>
        /// <param name="index">Start position to write with.</param>
        /// <param name="count">Number of bytes to read.</param>
        /// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
        /// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
        public override int Read(char[] buffer, int index, int count)
        {
            return ReadSpan(new Span<char>(buffer, index, count));
        }

        /// <summary>
        /// Read characters from the current stream into a Span buffer.
        /// </summary>
        /// <param name="buffer">Output buffer.</param>
        /// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
        public override int Read(Span<char> buffer)
        {
            return ReadSpan(buffer);
        }

        private int ReadSpan(Span<char> buffer)
        {
            // deal with the argument validation
            int charRead = 0;
            int index = 0;
            int count = buffer.Length;

            do
            {
                if (_charCount == 0)
                {
                    if (RefillCharBuffer() == -1)
                    {
                        return charRead;
                    }
                }

                int toRead = _charCount > count ? count : _charCount;

                for (; toRead > 0; toRead--, count--, charRead++)
                {
                    buffer[index++] = _charBuff[--_charCount];
                }
            }
            while (count > 0);

            return charRead;
        }

        /// <summary>
        /// Read a line from the current stream.
        /// </summary>
        /// <returns>Return null if we reach the head of the file.</returns>
        public override string ReadLine()
        {
            if (_charCount == 0 && RefillCharBuffer() == -1)
            {
                return null;
            }

            int charsToRemove = 0;
            StringBuilder line = new StringBuilder();

            if (_charBuff[_charCount - 1] == '\r' ||
                _charBuff[_charCount - 1] == '\n')
            {
                charsToRemove++;
                line.Insert(0, _charBuff[--_charCount]);

                if (_charBuff[_charCount] == '\n')
                {
                    if (_charCount == 0 && RefillCharBuffer() == -1)
                    {
                        return string.Empty;
                    }

                    if (_charCount > 0 && _charBuff[_charCount - 1] == '\r')
                    {
                        charsToRemove++;
                        line.Insert(0, _charBuff[--_charCount]);
                    }
                }
            }

            while (true)
            {
                while (_charCount > 0)
                {
                    if (_charBuff[_charCount - 1] == '\r' ||
                        _charBuff[_charCount - 1] == '\n')
                    {
                        line.Remove(line.Length - charsToRemove, charsToRemove);
                        return line.ToString();
                    }
                    else
                    {
                        line.Insert(0, _charBuff[--_charCount]);
                    }
                }

                if (RefillCharBuffer() == -1)
                {
                    line.Remove(line.Length - charsToRemove, charsToRemove);
                    return line.ToString();
                }
            }
        }

        /// <summary>
        /// Refill the internal character buffer.
        /// </summary>
        /// <returns></returns>
        private int RefillCharBuffer()
        {
            if ((RefillByteBuff()) == -1)
            {
                return -1;
            }

            _charCount = _currentEncoding.GetChars(_byteBuff, 0, _byteCount, _charBuff, 0);
            return _charCount;
        }

        /// <summary>
        /// Refill the internal byte buffer.
        /// </summary>
        /// <returns></returns>
        private int RefillByteBuff()
        {
            long lengthLeft = _stream.Position;

            if (lengthLeft == 0)
            {
                return -1;
            }

            int toRead = lengthLeft > BuffSize ? BuffSize : (int)lengthLeft;
            _stream.Seek(-toRead, SeekOrigin.Current);

            if (_currentEncoding is UTF8Encoding)
            {
                // It's UTF-8, we need to detect the starting byte of a character
                do
                {
                    _currentPosition = _stream.Position;
                    byte curByte = (byte)_stream.ReadByte();
                    if ((curByte & BothTopBitsSet) == BothTopBitsSet ||
                        (curByte & TopBitUnset) == 0x00)
                    {
                        _byteBuff[0] = curByte;
                        _byteCount = 1;
                        break;
                    }
                } while (lengthLeft > _stream.Position);

                if (lengthLeft == _stream.Position)
                {
                    // Cannot find a starting byte. The file is NOT UTF-8 format. Read 'toRead' number of bytes
                    _stream.Seek(-toRead, SeekOrigin.Current);
                    _byteCount = 0;
                }

                _byteCount += _stream.Read(_byteBuff, _byteCount, (int)(lengthLeft - _stream.Position));
                _stream.Position = _currentPosition;
            }
            else if (_currentEncoding is UnicodeEncoding ||
                _currentEncoding is UTF32Encoding ||
                _currentEncoding is ASCIIEncoding ||
                IsSingleByteCharacterSet())
            {
                // Unicode -- two bytes per character
                // UTF-32 -- four bytes per character
                // ASCII -- one byte per character
                // The BufferSize will be a multiple of 4, so we can just read toRead number of bytes
                // if the current file is encoded by any of these formatting

                // If IsSingleByteCharacterSet() returns true, we are sure that the given encoding is OEM
                // or Default, and it is SBCS(single byte character set) code page -- one byte per character
                _currentPosition = _stream.Position;
                _byteCount = _stream.Read(_byteBuff, 0, toRead);
                _stream.Position = _currentPosition;
            }
            else
            {
                // OEM and ANSI code pages include multibyte CJK code pages. If the current code page
                // is MBCS(multibyte character set), we cannot detect a starting byte.
                // UTF-7 has some characters encoded into UTF-16 and then in Modified Base64,
                // the start of these characters is indicated by a '+' sign, and the end is
                // indicated by a character that is not in Modified Base64 set.
                // For these encodings, we cannot detect a starting byte with confidence when
                // reading bytes backward. Throw out exception in these cases.
                string errMsg = StringUtil.Format(
                    FileSystemProviderStrings.ReadBackward_Encoding_NotSupport,
                    _currentEncoding.EncodingName);
                throw new BackReaderEncodingNotSupportedException(errMsg, _currentEncoding.EncodingName);
            }

            return _byteCount;
        }

        private static class NativeMethods
        {
            // Default values
            private const int MAX_DEFAULTCHAR = 2;
            private const int MAX_LEADBYTES = 12;

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            internal struct CPINFO
            {
                [MarshalAs(UnmanagedType.U4)]
                internal int MaxCharSize;

                [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_DEFAULTCHAR)]
                public byte[] DefaultChar;

                [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_LEADBYTES)]
                public byte[] LeadBytes;
            };

            /// <summary>
            /// Get information on a named code page.
            /// </summary>
            /// <param name="codePage"></param>
            /// <param name="lpCpInfo"></param>
            /// <returns></returns>
            [DllImport(PinvokeDllNames.GetCPInfoDllName, CharSet = CharSet.Unicode, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool GetCPInfo(uint codePage, out CPINFO lpCpInfo);
        }
    }

    /// <summary>
    /// The exception that indicates the encoding is not supported when reading backward.
    /// </summary>
    [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "This exception is internal and never thrown by any public API")]
    [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "This exception is internal and never thrown by any public API")]
    internal sealed class BackReaderEncodingNotSupportedException : NotSupportedException
    {
        internal BackReaderEncodingNotSupportedException(string message, string encodingName)
            : base(message)
        {
            EncodingName = encodingName;
        }

        internal BackReaderEncodingNotSupportedException(string encodingName)
        {
            EncodingName = encodingName;
        }

        /// <summary>
        /// Get the encoding name.
        /// </summary>
        internal string EncodingName { get; }
    }
}
