using System;
using System.IO;

namespace Volpe.Cafe.IO.DSV
{
    /// <summary>
    /// Represents a writer that can write data to a delimiter-separated values (DSV) file.
    /// </summary>
    /// <inheritdoc cref="DsvFile" path="/remarks"/>
    [Serializable]
    public class DsvWriter : IDisposable
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DsvWriter"/> class for the specified file, using the default
        /// encoding and buffer size. If the file exists, it will be appended to if <paramref name="headers"/> is
        /// <c>null</c>; otherwise, it will be overwritten. If the file does not exist, this constructor creates a new
        /// file. When a new file is created, the specified <paramref name="headers"/> will be written to the file.
        /// When a file is opened in append mode, the headers will be read from the existing file.
        /// </summary>
        /// <param name="path">The complete file path to write to.</param>
        /// <param name="headers">
        ///     An array of column headers to write to the file, or null, if appending an existing file.
        /// </param>
        /// <param name="delimiter">A character used for delimiting the columns in the DSV file.</param>
        /// <exception cref="ArgumentNullException">headers array is null and file is not being appended.</exception>
        /// <exception cref="ArgumentException">headers array is empty and file is not being appended.</exception>
        /// <inheritdoc cref="StreamWriter(string, bool)" path="/exception"/>
        public DsvWriter(string path, string[] headers, char delimiter)
            : this(path, headers, delimiter, DsvFile.DefaultEscapeChar, DsvFile.DefaultNewLine) { }

        /// <inheritdoc cref="DsvWriter(string, string[], char)"/>
        /// <remarks>
        /// The <paramref name="escapeChar"/> is the value used for enclosing fields that include special characters.
        /// 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 write values to the output
        /// file without escaping. In this case, it will be implicitly assumed that values do not contain any special
        /// characters and will be written to the output stream as is.
        /// <br/><br/>
        /// The <paramref name="newLine"/> is the line separator string, which is "\n" by default. Other allowed values
        /// are "\r" and "\r\n". If an invalid value is specified, the <paramref name="newLine"/> will be reset to the
        /// <see cref="DsvFile.DefaultNewLine"/>.
        /// </remarks>
        protected DsvWriter(string path, string[] headers, char delimiter, char escapeChar, string newLine)
        {
            bool createNew = !File.Exists(path) || (headers != null);
            if (createNew) {
                ThrowHelper.ThrowIfHeadersNotValid(headers);
            }
            else {
                using (var sr = new DsvReader(path, delimiter)) {
                    this.Headers = sr.GetHeaders();
                }
            }

            this.Delimiter = delimiter;
            this.DelimiterString = delimiter.ToString();
            this.EscapeChar = escapeChar;
            this.NewLine = DsvFile.IsValidNewLine(newLine) ? newLine : DsvFile.DefaultNewLine;            
            this.Path = path;
            this.Stream = new StreamWriter(path, !createNew);

            if (createNew) {
                this.Headers = headers;
                this.WriteValuesInternal(headers, false, this.NewLineEOF);
            }
        }

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

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

        /// <summary>
        /// Gets the name of the file associated with the <see cref="DsvWriter"/>.
        /// </summary>
        public string FileName => System.IO.Path.GetFileName(this.Path);

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

        /// <summary>
        /// Gets a string representation of the <see cref="Delimiter"/> character.
        /// </summary>
        protected string DelimiterString { get; }

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

        /// <summary>
        /// Gets a new-line string used when writing DSV files.
        /// </summary>
        public string NewLine { get; }

        /// <summary>
        /// Gets whether to write the <see cref="NewLine"/> string for the last record in the DSV file.
        /// </summary>
        /// <remarks>
        /// This property is included to allow for greater flexibility when defining subclasses.<br/>
        /// For example, while the RFC 4180 specification for the CSV file format allows for the inclusion or exclusion
        /// of a line terminator for the last record, some applications may prefer the new-line string to be omitted in
        /// the last line when reading CSV files.
        /// </remarks>
        protected virtual bool NewLineEOF => true;

        /// <summary>
        /// Gets the underlying <see cref="StreamWriter"/> object to use when writing the file data.
        /// </summary>
        protected StreamWriter 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 used to initialize the DSV file.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The file is disposed.</exception>
        public string[] GetHeaders()
        {
            ThrowHelper.ThrowIfDisposed(this);
            return (string[])this.Headers.Clone();
        }

        /// <summary>
        /// Writes the specified array of values, followed by a line terminator, to the DSV file.
        /// </summary>
        /// <param name="values">An array of values to write to the DSV file.</param>
        /// <exception cref="ArgumentNullException">values array is null.</exception>
        /// <exception cref="ArgumentException">
        ///     values array is empty.
        ///         -or-
        ///     values and headers length differs.
        /// </exception>
        /// <inheritdoc cref="StreamWriter.Write(string)" path="/exception"/>
        public virtual void WriteValues(params object[] values)
        {
            ThrowHelper.ThrowIfDisposed(this);
            ThrowHelper.ThrowIfValuesNotValid(values, this.Headers.Length);
            this.WriteValuesInternal(values);
        }

        /// <summary>
        /// Writes any buffered data to the underlying stream, then clears all buffers for the current writer.
        /// </summary>
        /// <inheritdoc cref="StreamWriter.Flush()" path="/exception"/>
        public virtual void Flush()
        {
            ThrowHelper.ThrowIfDisposed(this);
            this.Stream.Flush();
        }

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

        /// <summary>
        /// Writes any buffered data to the underlying stream, then closes the current writer and releases any system
        /// resources associated with it.
        /// </summary>
        /// <exception cref="IOException">An I/O error has occurred.</exception>
        /// <inheritdoc cref="StreamWriter.Close()" path="/exception"/>
        public void Dispose()
        {
            if (this.IsDisposed) {
                return;
            }

            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="DsvWriter"/>, optionally releasing the managed
        /// resources.
        /// </summary>
        /// <inheritdoc cref="StreamWriter.Dispose(bool)" path="/param"/>
        /// <inheritdoc cref="StreamWriter.Dispose(bool)" path="/exception"/>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && this.Stream != null) {
                // use separate try/catch for flush and close
                // in case flush fails, we still want to try to close
                try { this.Flush(); } catch { }
                try { this.Stream.Close(); } catch { }
            }

            this.Stream = null;
            this.Headers = null;
            this.Path = null;
            this.IsDisposed = true;
        }

        /// <summary>
        /// Assembles the specified <paramref name="values"/> array into a single string by merging and delimiting each
        /// element with the <see cref="Delimiter"/> character.
        /// </summary>
        /// <param name="values">An array of values to write to the DSV file.</param>
        /// <returns>A merged and delimited string of values to write the DSV file.</returns>
        /// <remarks>
        /// After merging the <paramref name="values"/> array, the resulting string can then be written to the DSV
        /// file. Note, however, that the new-line character(s) will not be appended to the string and must be added
        /// (as required) by the caller.
        /// </remarks>
        protected string GetDelimitedValue(object[] values)
        {
            values = this.ProcessEscapeChar(values);
            return string.Join(this.DelimiterString, values);
        }

        /// <summary>
        /// If escaping is enabled (i.e., <see cref="EscapeChar"/> is not the <see cref="DsvFile.NullChar"/>), checks
        /// each element in the specified <paramref name="values"/> array for special characters, and encloses them
        /// with an <see cref="EscapeChar"/> if necessary. If escaping is disabled, the <paramref name="values"/> array
        /// will be returned without any changes.
        /// </summary>
        /// <param name="values">An array of values to write to the DSV file.</param>
        /// <returns>An array of values with escaping applied if necessary.</returns>
        protected object[] ProcessEscapeChar(object[] values)
        {
            if (this.EscapeChar != DsvFile.NullChar) {
                string esc = this.EscapeChar.ToString();
                string esc2 = esc + esc;

                for (int i = 0; i < values.Length; i++) {
                    string s = values[i]?.ToString();
                    if (s == null || s.Length == 0) {
                        continue;
                    }

                    if (s.Contains(esc)) {
                        values[i] = esc + s.Replace(esc, esc2) + esc;
                    }
                    else if (s.Contains(this.DelimiterString) || s.Contains("\r") || s.Contains("\n")) {
                        values[i] = esc + s + esc;
                    }
                }
            }

            return values;
        }

        /// <summary>
        /// Writes the specified array of values, followed by a line terminator, to the DSV file.
        /// </summary>
        /// <param name="values">An array of values to write to the DSV file.</param>
        /// <remarks>
        /// This internal method implicitly assumes that <paramref name="values"/> is not <c>null</c> and of the same
        /// length as the <see cref="Headers"/> array.
        /// </remarks>
        /// <inheritdoc cref="StreamWriter.Write(string)" path="/exception"/>
        protected virtual void WriteValuesInternal(object[] values)
        {
            this.WriteValuesInternal(values, !this.NewLineEOF, this.NewLineEOF);
        }

        // private helper method -- called when writing header (from ctor) -or- writing values
        private void WriteValuesInternal(object[] values, bool prependNewLine, bool appendNewLine)
        {
            if (prependNewLine) {
                this.Stream.Write(this.NewLine);
            }

            string value = this.GetDelimitedValue(values);
            this.Stream.Write(value);

            if (appendNewLine) {
                this.Stream.Write(this.NewLine);
            }
        }

    }
}
