module CsvParser

grammar Csv
  include ParserExtensions

  rule records
    non_empty_records / empty_records
  end

  rule non_empty_records
    first_record
    other_records
    {
      def value
        arr = [first_record.value]
        rest = other_records.value
        if rest
          arr.push(*rest)
        end
        arr
      end
    }
  end

  rule empty_records
    ''
    {
      def value
        []
      end
    }
  end

  rule first_record
    '' &{ |s| @first_record = true; true }
    non_empty_record
    &{ |s| @first_record_length = @record_length; true }
    {
      def value
        non_empty_record.value
      end
    }
  end

  rule other_records
    '' &{ |s| @first_record = false; true }
    (
      (
        &{ |s| skip_empty_record? }
        (
          record_sep
          ( empty_record record_sep )*
          non_empty_record
        )*
        (
          &{ |s| skip_empty_record? }
          ( record_sep empty_record )+
        )?
        {
          def value
            val = elements[1].elements.collect { |elt| elt.non_empty_record.value }
            val.empty? ? nil : val
          end
        }
      )
      /
      (
        &{ |s| !skip_empty_record? }
        ( record_sep record )*
        {
          def value
            val = elements[1].elements.collect { |elt| elt.record.value }
            val.empty? ? nil : val
          end
        }
      )
    )
    {
      def value
        elements[2].value
      end
    }
  end

  rule record
    non_empty_record / empty_record
  end

  rule non_empty_record
    first:field
    &{ |s| @record_length = 1; @warning = nil; true }
    rest:(
      &{ |s|
        if @first_record || @record_length < @first_record_length
          true
        else
          if allow_uneven_records?
            @warning ||= [:extra_fields, input.line_of(index + 1), input.column_of(index + 1)]
            true
          else
            @failure_type = :extra_fields
            false
          end
        end
      }
      field_sep
      field
      &{ |s| @record_length += 1; true }
    )*
    &{ |s|
      if @first_record || @record_length >= @first_record_length
        if @warning
          warnings << @warning
        end
        true
      else
        if allow_uneven_records?
          warnings << [:missing_fields, input.line_of(index), input.column_of(index)]
          true
        else
          @failure_type = :missing_fields
          false
        end
      end
    }
    {
      def value
        arr = [first.value]
        rest.elements.each do |elt|
          arr << elt.field.value
        end
        arr
      end
    }
  end

  rule empty_record
    ''
    &{ |s|
      if allow_empty_record?
        true
      else
        @failure_type = :missing_fields
        false
      end
    }
    {
      def value
        []
      end
    }
  end

  rule field
    unquoted_text
    {
      def value
        elements.map(&:text_value).join
      end
    }
    /
    quoted_text
    {
      def value
        elements[1..-2].map(&:text_value).join
      end
    }
  end

  rule quoted_text
    quote
    ( !quote . )+
    (
      quote
      /
      '' !{ |s| @failure_type = :missing_quote; @failure_index = start_index; true }
    )
  end

  rule unquoted_text
    (
      !field_sep
      !record_sep
      (
        !quote
        /
        '' !{ |s| @failure_type = :stray_quote; true }
      )
      .
    )+
  end

  rule field_sep
    &{ |s| @field_sep_index = 0; true }
    (
      !record_sep
      !quote
      .
      &{ |s|
        if @field_sep_index < field_sep.length &&
           s[2].text_value == field_sep[@field_sep_index]
          @field_sep_index += 1
          true
        else
          false
        end
      }
    )+
    &{ |s| s.map(&:text_value).join == field_sep }
  end

  rule record_sep
    &{ |s| @record_sep_index = 0; true }
    (
      .
      &{ |s|
        if @record_sep_index < record_sep.length &&
           s[0].text_value == record_sep[@record_sep_index]
          @record_sep_index += 1
          true
        else
          false
        end
      }
    )+
    &{ |s| s.map(&:text_value).join == record_sep }
  end

  rule quote
    !"\\" . &{ |s| s[1].text_value[0] == quote_char[0] }
  end
end

end