require "csv"

# A CSV Builder writes CSV to an `IO`.
#
# ```
# require "csv"
#
# result = CSV.build do |csv|
#   # A row can be written by specifying several values
#   csv.row "Hello", 1, 'a', "String with \"quotes\"", '"', :sym
#
#   # Or an enumerable
#   csv.row [1, 2, 3]
#
#   # Or using a block, and appending to the row
#   csv.row do |row|
#     # Appending a single value
#     row << 4
#
#     # Or multiple values
#     row.concat 5, 6
#
#     # Or an enumerable
#     row.concat [7, 8]
#   end
# end
# puts result
# ```
#
# Output:
#
# ```text
# Hello,1,a,"String with ""quotes""","""",sym
# 1,2,3
# 4,5,6,7,8
# ```
class CSV::Builder
  enum Quoting
    # No quotes
    NONE

    # Quotes according to RFC 4180 (default)
    RFC

    # Always quote
    ALL
  end

  # Creates a builder that will write to the given `IO`.
  def initialize(@io : IO, @separator : Char = DEFAULT_SEPARATOR, @quote_char : Char = DEFAULT_QUOTE_CHAR, @quoting : Quoting = Quoting::RFC)
    @first_cell_in_row = true
  end

  # Yields a `CSV::Builder::Row` to append a row. A newline is appended
  # to `IO` after the block exits.
  def row(&)
    yield Row.new(self, @separator, @quote_char, @quoting)
    @io << '\n'
    @first_cell_in_row = true
  end

  # Appends the given values as a single row, and then a newline.
  def row(values : Enumerable) : Nil
    row do |row|
      values.each do |value|
        row << value
      end
    end
  end

  # :ditto:
  def row(*values) : Nil
    row values
  end

  # :nodoc:
  def cell(&)
    append_cell do
      yield @io
    end
  end

  # :nodoc:
  def quote_cell(value : String) : Nil
    append_cell do
      @io << @quote_char
      value.each_char do |char|
        case char
        when @quote_char
          @io << @quote_char << @quote_char
        else
          @io << char
        end
      end
      @io << @quote_char
    end
  end

  private def append_cell(&)
    @io << @separator unless @first_cell_in_row
    yield
    @first_cell_in_row = false
  end

  # A CSV Row being built.
  struct Row
    @builder : Builder

    # :nodoc:
    def initialize(@builder, @separator : Char = DEFAULT_SEPARATOR, @quote_char : Char = DEFAULT_QUOTE_CHAR, @quoting : Quoting = Quoting::RFC)
    end

    # Appends the given value to this row.
    def <<(value : String) : Nil
      if needs_quotes?(value)
        @builder.quote_cell value
      else
        @builder.cell { |io| io << value }
      end
    end

    # :ditto:
    def <<(value : Nil | Bool | Number) : Nil
      case @quoting
      when .all?
        @builder.cell { |io|
          io << @quote_char
          io << value
          io << @quote_char
        }
      else
        @builder.cell { |io| io << value }
      end
    end

    # :ditto:
    def <<(value) : Nil
      self << value.to_s
    end

    # Appends the given values to this row.
    def concat(values : Enumerable) : Nil
      values.each do |value|
        self << value
      end
    end

    # :ditto:
    def concat(*values) : Nil
      concat values
    end

    # Appends a comma, thus skipping a cell.
    def skip_cell : Nil
      self << nil
    end

    private def needs_quotes?(value : String)
      case @quoting
      when .rfc?
        value.each_byte do |byte|
          case byte.unsafe_chr
          when @separator, @quote_char, '\n'
            return true
          else
            # keep scanning
          end
        end
        false
      when .all?
        true
      else
        false
      end
    end
  end
end
