using System.Text;

namespace ZB.MOM.WW.OtOpcUa.Commons.Csv;

/// <summary>
///     A pure RFC 4180 CSV writer with no file/stream I/O of its own — it renders rows of fields to a
///     <see cref="string"/> or a <see cref="TextWriter"/>. The inverse of <see cref="CsvParser"/>:
///     <c>CsvParser.Parse(CsvWriter.WriteToString(rows))</c> reproduces <c>rows</c> exactly for any field
///     content.
/// </summary>
/// <remarks>
///     <para><b>Quote-on-demand.</b> A field is wrapped in double-quotes only when it must be — i.e. when
///     it contains the delimiter, a double-quote, CR, or LF — and internal double-quotes are doubled
///     (<c>"</c> → <c>""</c>). Fields that need no quoting are emitted verbatim, so ordinary values stay
///     human-readable. Pass <c>quoteAllFields: true</c> to force every field quoted.</para>
///     <para><b>Newline.</b> The record terminator defaults to CRLF (<c>\r\n</c>) per RFC 4180 and is
///     configurable. No terminator is written after the final row (matching the parser's
///     no-phantom-trailing-row contract, so a round-trip is exact).</para>
/// </remarks>
public static class CsvWriter
{
    private const char Quote = '"';
    private const char Cr = '\r';
    private const char Lf = '\n';

    /// <summary>The RFC 4180 record terminator, <c>"\r\n"</c>. The default <c>newline</c> for every write.</summary>
    public const string Crlf = "\r\n";

    /// <summary>
    ///     Renders <paramref name="rows"/> to a CSV string.
    /// </summary>
    /// <param name="rows">The rows to write; each inner sequence is one record's fields. A <c>null</c> field is written as empty.</param>
    /// <param name="delimiter">The field separator (default comma).</param>
    /// <param name="newline">The record terminator between rows (default CRLF). Not appended after the last row.</param>
    /// <param name="quoteAllFields">When <c>true</c>, every field is quoted regardless of content.</param>
    /// <returns>The CSV text. An empty <paramref name="rows"/> yields the empty string.</returns>
    /// <exception cref="System.ArgumentNullException"><paramref name="rows"/> is <c>null</c>.</exception>
    /// <exception cref="System.ArgumentException"><paramref name="delimiter"/> is a quote, CR, or LF.</exception>
    public static string WriteToString(
        IEnumerable<IEnumerable<string?>> rows,
        char delimiter = ',',
        string newline = Crlf,
        bool quoteAllFields = false)
    {
        var sb = new StringBuilder();
        using var writer = new StringWriter(sb);
        Write(writer, rows, delimiter, newline, quoteAllFields);
        return sb.ToString();
    }

    /// <summary>
    ///     Writes <paramref name="rows"/> to <paramref name="writer"/>. The writer is not disposed or
    ///     flushed by this method.
    /// </summary>
    /// <param name="writer">The destination.</param>
    /// <param name="rows">The rows to write; each inner sequence is one record's fields. A <c>null</c> field is written as empty.</param>
    /// <param name="delimiter">The field separator (default comma).</param>
    /// <param name="newline">The record terminator between rows (default CRLF). Not appended after the last row.</param>
    /// <param name="quoteAllFields">When <c>true</c>, every field is quoted regardless of content.</param>
    /// <exception cref="System.ArgumentNullException"><paramref name="writer"/> or <paramref name="rows"/> is <c>null</c>.</exception>
    /// <exception cref="System.ArgumentException"><paramref name="delimiter"/> is a quote, CR, or LF.</exception>
    public static void Write(
        TextWriter writer,
        IEnumerable<IEnumerable<string?>> rows,
        char delimiter = ',',
        string newline = Crlf,
        bool quoteAllFields = false)
    {
        ArgumentNullException.ThrowIfNull(writer);
        ArgumentNullException.ThrowIfNull(rows);
        if (delimiter is Quote or Cr or Lf)
        {
            throw new ArgumentException("Delimiter must not be a double-quote, CR, or LF.", nameof(delimiter));
        }

        var firstRow = true;
        foreach (var row in rows)
        {
            if (!firstRow)
            {
                writer.Write(newline);
            }

            firstRow = false;

            WriteRow(writer, row, delimiter, quoteAllFields);
        }
    }

    private static void WriteRow(TextWriter writer, IEnumerable<string?> row, char delimiter, bool quoteAllFields)
    {
        ArgumentNullException.ThrowIfNull(row);

        var firstField = true;
        foreach (var field in row)
        {
            if (!firstField)
            {
                writer.Write(delimiter);
            }

            firstField = false;

            WriteField(writer, field ?? string.Empty, delimiter, quoteAllFields);
        }
    }

    private static void WriteField(TextWriter writer, string field, char delimiter, bool quoteAllFields)
    {
        if (!quoteAllFields && !NeedsQuoting(field, delimiter))
        {
            writer.Write(field);
            return;
        }

        writer.Write(Quote);
        foreach (var ch in field)
        {
            if (ch == Quote)
            {
                writer.Write(Quote); // double an internal quote
            }

            writer.Write(ch);
        }

        writer.Write(Quote);
    }

    private static bool NeedsQuoting(string field, char delimiter)
    {
        foreach (var ch in field)
        {
            if (ch == delimiter || ch == Quote || ch == Cr || ch == Lf)
            {
                return true;
            }
        }

        return false;
    }
}
