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

namespace Volpe.Cafe.IO.DSV
{
    /// <summary>
    /// Represents a reader that can read data from a delimiter-separated values (DSV) file.
    /// </summary>
    /// <inheritdoc cref="DsvFile" path="/remarks"/>
    [Serializable]
    public class DsvReader : IDisposable
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DsvReader"/> class for the specified file.
        /// </summary>
        /// <param name="path">The complete file path to read.</param>
        /// <param name="delimiter">A character used for delimiting the columns in the DSV file.</param>
        /// <param name="escapeChar">
        ///     An escape character used for enclosing fields that include special characters.
        /// </param>
        /// <inheritdoc cref="StreamReader(string)" path="/exception"/>
        /// <remarks>
        /// The default escape character is the double quote ('"'). Other typical values are the single quote ('\'')
        /// and the backslash ('\'). Alternatively, specify the null character ('\0') to process the input file without
        /// escaping. In this case, the file will be assumed to not contain any special characters and will be parsed
        /// as is.
        /// </remarks>
        public DsvReader(string path, char delimiter, char escapeChar = DsvFile.DefaultEscapeChar)
        {
            this.Path = path;
            this.Delimiter = delimiter;
            this.EscapeChar = escapeChar;
            this.Stream = new StreamReader(path);
            this.Headers = this.ReadValuesInternal();

            // we don't want to skip any blank lines before reading the header
            // the header MUST BE the first line!!
            this.SkipBlankLines = true;
        }

        /// <summary>
        /// Gets whether the <see cref="DsvReader"/> is disposed.
        /// </summary>
        public bool IsDisposed { get; private set; }

        /// <summary>
        /// Gets the complete file path associated with the <see cref="DsvReader"/>.
        /// </summary>
        public string Path { get; private set; }

        /// <summary>
        /// Gets a character used for delimiting the columns in the DSV file.
        /// </summary>
        public char Delimiter { get; }

        /// <summary>
        /// Gets an escape character used to enclose fields that include special characters. If this value is the null
        /// character ('\0'), the input file will be processed as un-escaped.
        /// </summary>
        public char EscapeChar { get; }

        /// <summary>
        /// Gets or sets whether to skip blank lines when reading the DSV file. The default value is <c>true</c>.
        /// </summary>
        /// <remarks>
        /// If this value is <c>true</c>, blank lines (that are not part of an escaped field) will be skipped when
        /// reading data from the DSV file. If this value is <c>false</c>, blank lines will be parsed as a single
        /// element array containing an empty string; that is, return a value of: <c>string[] { string.Empty }</c>.
        /// </remarks>
        public bool SkipBlankLines { get; set; }

        /// <summary>
        /// Gets the underlying <see cref="StreamReader"/> object to use when reading the DSV file data.
        /// </summary>
        protected StreamReader Stream { get; private set; }

        /// <summary>
        /// Gets an array of column headers in the DSV file.
        /// </summary>
        protected string[] Headers { get; private set; }

        /// <summary>
        /// Returns a copy of column headers read from the DSV file.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The file is disposed.</exception>
        public string[] GetHeaders()
        {
            ThrowHelper.ThrowIfDisposed(this);
            return (string[])this.Headers.Clone();
        }

        /// <summary>
        /// Seeks to the beginning of the file stream.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The file is disposed.</exception>
        /// <exception cref="IOException">An I/O error has occurred.</exception>
        public void SeekToStart()
        {
            ThrowHelper.ThrowIfDisposed(this);

            this.Stream.BaseStream.Seek(0, SeekOrigin.Begin);
            this.Stream.DiscardBufferedData();

            // consume header line
            if (this.EscapeChar == DsvFile.NullChar) {
                this.Stream.ReadLine();
            }
            else {
                bool skipBlankLines = this.SkipBlankLines;
                this.ReadValuesEscaped();
                this.SkipBlankLines = skipBlankLines;
            }
        }

        /// <summary>
        /// Seeks to the beginning of the stream, then reads all lines of values from the DSV file.
        /// </summary>
        /// <returns>
        /// A tuple containing the column header of the DSV file and a list of values for all lines read.
        /// </returns>
        /// <exception cref="ObjectDisposedException">The file is disposed.</exception>
        /// <inheritdoc cref="StreamReader.ReadLine()" path="/exception"/>
        public (string[] Header, List<string[]> Lines) ReadAll()
        {
            this.SeekToStart();

            string[] header = this.GetHeaders();
            List<string[]> lines = new List<string[]>();

            string[] values;
            while ((values = this.ReadValuesInternal()) != null) {
                lines.Add(values);
            }

            return (header, lines);
        }

        /// <summary>
        /// Reads the next line of values from the DSV file.
        /// </summary>
        /// <returns>
        /// An array of values read from the DSV file, or null, if no more data is available.
        /// </returns>
        /// <exception cref="ObjectDisposedException">The file is disposed.</exception>
        /// <inheritdoc cref="StreamReader.ReadLine()" path="/exception"/>
        public string[] ReadValues()
        {
            ThrowHelper.ThrowIfDisposed(this);
            return this.ReadValuesInternal();
        }

        /// <inheritdoc cref="ReadValues()" path="/summary"/>
        /// <inheritdoc cref="ReadValues()" path="/returns"/>
        /// <remarks>
        /// This internal method does not perform a disposed check and implicitly assumes that input file has not been
        /// disposed.
        /// </remarks>
        protected virtual string[] ReadValuesInternal()
        {
            return (this.EscapeChar == DsvFile.NullChar)
                ? this.ReadValuesUnescaped()
                : this.ReadValuesEscaped();
        }

        private string[] ReadValuesUnescaped()
        {
            string line = this.Stream.ReadLine();
            if (this.SkipBlankLines) {
                while (line == string.Empty) {
                    line = this.Stream.ReadLine();
                }
            }

            string[] values = line?.Split(this.Delimiter);
            return values;
        }

        private string[] ReadValuesEscaped()
        {
            if (this.Stream.Peek() == -1) {
                return null;
            }

            if (this.SkipBlankLines) {
                while (this.Stream.Peek() == '\r' || this.Stream.Peek() == '\n') {
                    this.Stream.Read();
                }
            }

            List<string> values = new List<string>();
            StringBuilder buffer = new StringBuilder(32);

            bool escaped = false;
            int c, c2;

            while ((c = this.Stream.Read()) != -1) {
                c2 = this.Stream.Peek();

                if (escaped) {
                    if (c == this.EscapeChar && c2 != this.EscapeChar) {
                        // end of escaped field
                        escaped = false;
                    }
                    else {
                        buffer.Append((char)c);
                        if (c == this.EscapeChar && c2 == this.EscapeChar) {
                            // consume extra quote
                            this.Stream.Read();
                        }
                    }
                }
                else if (c == this.EscapeChar) {
                    // start of escaped field
                    escaped = true;
                }
                else if (c == this.Delimiter) {
                    BufferToField();
                }
                else if (IsNewLine()) {
                    break;
                }
                else {
                    buffer.Append((char)c);
                }
            }

            BufferToField();
            return values.ToArray();

            // local helper functions
            bool IsNewLine()
            {
                if (c == '\r') {
                    if (c2 == '\n') {
                        // consume the extra char
                        this.Stream.Read();
                    }
                    return true;
                }
                return (c == '\n');
            }

            void BufferToField()
            {
                string s = buffer.ToString();
                values.Add(s);
                buffer.Clear();
            }
        }

        /// <inheritdoc cref="Dispose()"/>
        public void Close()
        {
            this.Dispose();
        }

        /// <summary>
        /// Closes the current DSV file reader and releases any system resources associated with it.
        /// </summary>
        public void Dispose()
        {
            if (this.IsDisposed) {
                return;
            }

            try {
                this.Stream?.Close();
            }
            finally {
                this.Stream = null;
                this.Headers = null;
                this.Path = null;
                this.IsDisposed = true;
            }

            GC.SuppressFinalize(this);
        }

    }
}
