### ----------------------------------------------------
### -- Text::CSV
### -- Licenses: Artistic-2.0
### -- Authors: H.Merijn Brand (Tux)
### -- File: lib/Text/CSV.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
#!raku

use Slang::Tuxic;
use File::Temp;
use Text::IO::String;

my $VERSION = "0.022";

my constant $opt_v = %*ENV<RAKU_VERBOSE> // 1;

my constant %errors =
    # Success
       0 => "",

    # Generic errors
    1000 => "INI - constructor failed",
    1001 => "INI - separator is equal to quote- or escape sequence",
    1002 => "INI - allow_whitespace with escape or quoter SP or TAB",
    1003 => "INI - \r or \n in main attr not allowed",
    1004 => "INI - callbacks should be Hash or undefined",
#   1005 => "INI - EOL too long",
#   1006 => "INI - SEP too long",
#   1007 => "INI - QUOTE too long",
    1008 => "INI - SEP undefined",

    1010 => "INI - the header is empty",
    1011 => "INI - the header contains more than one valid separator",
    1012 => "INI - the header contains an empty field",
    1013 => "INI - the header contains nun-unique fields",
#   1014 => "INI - header called on undefined stream",

    # Syntax errors
    1500 => "PRM - Invalid/unsupported argument(s)",
    # For perl5 compatability. Due to strict typing, these do not apply
    1501 => "PRM - The key attribute is passed as an unsupported type",

    # Parse errors
    2010 => "ECR - QUO char inside quotes followed by CR not part of EOL", # 5
    2011 => "ECR - Characters after end of quoted field",
    2012 => "EOF - End of data in parsing input stream",
    2013 => "ESP - Specification error for fragments RFC7111",
    2014 => "ENF - Inconsistent number of fields",
    2015 => "ERW - Empty row",

    #  EIQ - Error Inside Quotes
    2021 => "EIQ - NL or EOL inside quotes, binary off",
    2022 => "EIQ - CR char inside quotes, binary off",
    2023 => "EIQ - QUO sequence not allowed",
    2024 => "EIQ - EOF cannot be escaped, not even inside quotes",
    2025 => "EIQ - Loose unescaped escape",
    2026 => "EIQ - Binary character inside quoted field, binary off",
    2027 => "EIQ - Quoted field not terminated",

    # EIF - Error Inside Field
#   2030 => "EIF - NL char inside unquoted verbatim, binary off",
    2031 => "EIF - CR char is first char of field, not part of EOL",
    2032 => "EIF - CR char inside unquoted, not part of EOL",
    2034 => "EIF - Loose unescaped quote",
    2035 => "EIF - Escaped EOF in unquoted field",
    2036 => "EIF - ESC error",
    2037 => "EIF - Binary character in unquoted field, binary off",

    # Combine errors
    2110 => "ECB - Binary character in Combine, binary off",

    # IO errors
    2200 => "EIO - print to IO failed. See errno",

    # Hash-Ref errors
    3001 => "EHR - Unsupported syntax for column_names ()",
    3002 => "EHR - getline_hr () called before column_names ()",
    3003 => "EHR - bind_columns () and column_names () fields count mismatch",
    3004 => "EHR - bind_columns () only accepts refs to scalars",
    3006 => "EHR - bind_columns () did not pass enough refs for parsed fields",
    3007 => "EHR - bind_columns needs refs to writable scalars",
    3008 => "EHR - unexpected error in bound fields",
    3009 => "EHR - print (Hash) called before column_names ()",
    3010 => "EHR - print (Hash) called with invalid arguments",

    3100 => "EHK - Unsupported callback",

    # Content errors
    4001 => "PRM - The key does not exist as field in the data",

    # csv CI
    5000 => "CSV - Unsupported type for in",
    5001 => "CSV - Unsupported type for out",
    ;

sub progress (*@y) {
    my Str $x;
    my @x = @y.map ( -> \x --> Str { x.Str } );
    my $line = callframe (1).annotations<line>;
    for (@x) {
        s{^(\d+)$}   = sprintf "%3d -", $_;
        s:g{"True,"} = "True, ";
        s:g{"new("}  = "new (";
        $x ~= $_ ~ " ";
        }
    $x.say;
    } # progress



class RangeSet {

    has Pair @!ranges;

    method new (**@ranges) {
        self.bless (:@ranges);
        }

    multi method add (Pair:D $p)              { @!ranges.push: $p;                }
    multi method add (Range:D $r)             { @!ranges.push: $r.min => $r.max;  }
    multi method add (Int:D $from)            { @!ranges.push: $from  => $from;   }
    multi method add (Int:D $from, Num:D $to) { @!ranges.push: $from  => $to;     }
    multi method add (Int:D $from, Any:D $to) { @!ranges.push: $from  => $to.Num; }

    method min () { @!ranges>>.key.min   }
    method max () { @!ranges>>.value.max }

    method in (Int:D $i) returns Bool {
        for @!ranges -> $r {
            $i >= $r.key && $i <= $r.value and return True;
            }
        False;
        }

    method to_list () {
        gather {
            my Int $max = -1;
            for sort @!ranges -> $r {
                my $from = ($r.key, $max + 1).max.Int;
                my $to   = $r.value == Inf ?? 65535 !! $r.value;
                take (($from .. $to).Slip);
                $r.value == Inf and last;
                $max = ($max, $r.value).max.Int;
                }
            }
        }

    multi method list (Int $last) {
        my Int @x;
        my Int $max = -1;
        for sort @!ranges -> $r {
            my $from = ($r.key,   $max  + 1).max.Int;
            my $to   = ($r.value, $last - 1).min.Int;
            $from > $to and next;
            @x.append: $from .. $to;
            $to >= $last || $r.value == Inf and last;
            $max = ($max, $r.value).max.Int;
            }
        @x;
        }

    multi method list () {
        my @x;
        my Int $max = -1;
        for sort @!ranges -> $r {
            my $from = ($r.key, $max + 1).max.Int;
            $from > $r.value and next;
            @x.plan: $from .. $r.value;
            $r.value == Inf and last;
            $max = ($max, $r.value).max.Int;
            }
        @x.elems ?? @x !! ($.min .. $.max).grep (self);
        }
    }

class CellSet {

    class CellRange {

        has RangeSet $.row;
        has RangeSet $.col;

        method in (Int $row, Int $col) {
            $!row && $!col && $!row.in ($row) && $!col.in ($col);
            }

        method show {
            say "Row: ", $.row.to_list.raku;
            say "Col: ", $.col.to_list.raku;
            }
        }

    has CellRange @!cr;

    method add (Int:D $tlr,            Int:D $tlc,
                Num   $brr = $tlr.Num, Num   $brc = $tlc.Num) {
        my $row = RangeSet.new; $row.add ($tlr, $brr);
        my $col = RangeSet.new; $col.add ($tlc, $brc);
        @!cr.push: CellRange.new (:$row, :$col);
        }

    method in (Int:D $row, Int:D $col) returns Bool {
        #return @!cr.any (*.in ($row, $col));
        for @!cr -> $c {
            $c.in ($row, $col) and return True;
            }
        False;
        }

    method show {
        say "This CellSet contains there ranges:";
        .show for @!cr;
        say "-----------------------------------";
        }
    }

class CSV::Field {

    has Bool $.is_quoted  is rw is default(False);
    has Str  $.text       is rw;

    has Bool $!is_binary  is default(False);
    has Bool $!is_utf8    is default(False);
    has Bool $!is_missing is default(False);
    has Bool $!is_formula is default(False);
    has Bool $!analysed   is default(False);

    multi method new (Str(Cool) $str) {
        $str.defined ?? self.bless.add ($str) !! self.bless;
        }

    method Bool {
        $!text.defined && $!text ne "0" && $!text ne "";
        }

    method Str {
        # ?$!text ?? $!text !! $Text::CSV::undef_str
        $!text;
        }

    method chars {
        $!text.chars;
        }

    method Buf {
        Buf.new ($!text.encode ("utf8-c8").list);
        }

    method Numeric {
        $!text.defined
            ?? $!text ~~ m{^ <[0..9]> } ?? +$!text
            !!                              $!text.unival.Int
            !! Num;
        }

    method Real {
        $!text.defined
            ?? $!text ~~ m{^ <[0..9]> } ?? +$!text
            !!                              $!text.unival.Int
            !! Real;
        }

    method gist {
        $!text.defined or return "<undef>";
        $!analysed or self.analyse;
        my $s  = $!is_quoted  ?? "Q" !! "q";
           $s ~= $!is_binary  ?? "B" !! "b";
           $s ~= $!is_utf8    ?? "8" !! "7";
           $s ~= $!is_missing ?? "M" !! "m";
           $s ~= $!is_formula ?? "=" !! "-";
        $s ~ ":" ~ $!text.raku;
        }

    method add (Str $chunk) {
        $!text ~= $chunk;
        self;
        }

    method set_quoted () {
        $!is_quoted = True;
        $!text      = "";
        }

    method undefined () {
        !$!text.defined;
        }

    method analyse (Bool $force = !$!analysed) {
        $force or return;

        $!analysed = True;

        !$!text.defined || $!text eq "" and
            return; # Default is False for the rest

        $!text.starts-with ("=") and
            $!is_formula = True;

        $!text ~~ m:m{^ <[ \x09, \x20 .. \x7E ]>+ $} or
            $!is_binary  = True;

        $!text ~~ m:m{^ <[ \x00       .. \x7F ]>+ $} or
            $!is_utf8    = True;
        }

    method is_binary () returns Bool {
        $!analysed or self.analyse;
        $!is_binary;
        }

    method is_utf8 () returns Bool {
        $!analysed or self.analyse;
        $!is_utf8;
        }

    method is_missing () returns Bool {
        $!analysed or self.analyse;
        $!is_missing;
        }

    method is_formula () returns Bool {
        $!analysed or self.analyse;
        $!is_formula;
        }

    method is-quoted ()  returns Bool { $.is_quoted; }
    method is-binary ()  returns Bool { self.is_binary; }
    method is-utf8 ()    returns Bool { self.is_utf8; }
    method is-missing () returns Bool { self.is_missing; }
    method is-formula () returns Bool { self.is_formula; }

    method set-quoted ()              { self.set_quoted; }
    } # CSV::Field

class Text::CSV { ... }

class CSV::Row does Iterable does Positional does Associative {
    has Text::CSV  $.csv;
    has CSV::Field @.fields;

    multi method new (@f)  { @!fields = @f.map ( -> \x --> CSV::Field { CSV::Field.new (x) }); }

    method Str ()          { $!csv ?? $!csv.string (@!fields) !! Str; }
    method iterator ()     { @.fields.iterator; }
    method hash ()         { hash $!csv.column_names Z=> @!fields».Str; }
    method of ()           { CSV::Field; }
    method AT-KEY (Str $k) { %($!csv.column_names Z=> @!fields){$k}; }
    method strings ()      { @!fields».Str; }
    method AT-POS (int $i) { @!fields.AT-POS ($i); }
    method elems           { @!fields.elems; }
    method splice (*@args) { @!fields.splice (@args); }
    method keys ()         { $!csv.column_names; }

    method ASSIGN-POS (int $i, $v) {
        @!fields.AT-POS ($i).text = $v.Str;
        }
    method ASSIGN-KEY (Str:D $k, $v) {
        $!csv.column_names and
            %($!csv.column_names Z=> @!fields){$k}.text = $v.Str;
        }

    multi method push (CSV::Field $f) { @!fields.push: $f; }
    multi method push (Cool       $f) { @!fields.push: CSV::Field.new ($f); }
    multi method push (CSV::Row   $r) { @!fields.append: $r.fields; }

    method pop returns CSV::Field { @!fields.pop; }
    }

class Text::CSV {

    # Defaults are set in BUILD!
    has Str  $!eol;
    has Str  $!sep;
    has Str  $!quo;
    has Str  $!esc;

    has Bool $!binary;
    has Bool $!strict;
    has Bool $!decode_utf8;
    has Bool $!auto_diag;
    has Int  $!diag_verbose;
    has Bool $!keep_meta;
    has Int  $!skip_empty_rows;
    has Any  $!skip_empty_rows_cb;

    has Bool $!blank_is_undef;
    has Bool $!empty_is_undef;
    has Bool $!allow_whitespace;
    has Bool $!allow_loose_quotes;
    has Bool $!allow_loose_escapes;
    has Bool $!allow_unquoted_escape;

    has Bool $!always_quote;
    has Bool $!quote_space;
    has Bool $!escape_null;
    has Bool $!quote_empty;
    has Bool $!quote_binary;

    has Bool $!build;
    has Int  $!record_number;
    has Str  $!formula;
    has Str  $!undef_str;
    has Str  $!comment_str;

    has CSV::Row   $!csv-row;
    has Str        @!ahead;
    has Str        @!cnames;
    has IO::Handle $!io;
    has Bool       $!eof;
    has Int        @!types;
    has Int        @!crange;
    has RangeSet   $!rrange;
    has Int        $!strict_n;
    has            %!callbacks;

    has Int  $!errno;
    has Int  $!error_pos;
    has Int  $!error_field;
    has Str  $!error_input;
    has Str  $!error_message;

    class CSV::Diag does Iterable does Positional is Exception is export {
        has Int $.error   is readonly = 0;
        has Str $.message is readonly = %errors{$!error};
        has Int $.pos     is readonly;
        has Int $.field   is readonly;
        has Int $.record  is readonly;
        has Str $.buffer  is readonly;

        method sink {
            # See also src/core/Exception.pm - role X::Comp  method gist
            my $p = ($!pos    // "Unknown").Str;
            my $f = ($!field  // "Unknown").Str;
            my $r = ($!record // "Unknown").Str;
            my $m =
                 "\e[34m" ~ $!message
               ~ "\e[0m"  ~ " : error $!error @ record $r, field $f, position $p\n";
            $!buffer.defined && $!buffer.chars  && $!pos.defined &&
                $!pos >= 0 && $!pos < $!buffer.chars and $m ~=
                 "\e[32m" ~ substr ($!buffer, 0, $!pos - 1)
               ~ "\e[33m" ~ "\x[23CF]"
               ~ "\e[31m" ~ substr ($!buffer,    $!pos - 1)
               ~ "\e[0m\n";
            # I do not want the "in method sink at ..." here, but there
            # is no way yet to suppress that, so print instead of warn for now
            print $m;
            }
        method Numeric  { $!error; }
        method Str      { $!message; }
        method iterator { $[ $!error, $!message, $!pos, $!field, $!record, $!buffer ].iterator; }
        method hash     { { errno  => $!error,
                            error  => $!message,
                            pos    => $!pos,
                            field  => $!field,
                            recno  => $!record,
                            buffer => $!buffer,
                          }; }
#       method AT-KEY (Str $k) { } NYI
        method AT-POS (int $i) {
               $i == 0 ?? $!error
            !! $i == 1 ?? $!message
            !! $i == 2 ?? $!pos
            !! $i == 3 ?? $!field
            !! $i == 4 ?? $!record
            !! $i == 5 ?? $!buffer
            !! Any;
            }
        }

    # We need this to support aliasses and to catch unsupported attributes
    submethod BUILD (*%init) {
        # Defaults are disabled when BUILD is defined!

        $!sep                   = ',';
        $!quo                   = '"';
        $!esc                   = '"';

        $!binary                = True;
        $!decode_utf8           = True;
        $!auto_diag             = False;
        $!strict                = False;
        $!diag_verbose          = 0;
        $!keep_meta             = False;
        $!skip_empty_rows       = 0;
        $!skip_empty_rows_cb    = Any;

        $!blank_is_undef        = False;
        $!empty_is_undef        = False;
        $!allow_whitespace      = False;
        $!allow_loose_quotes    = False;
        $!allow_loose_escapes   = False;
        $!allow_unquoted_escape = False;

        $!always_quote          = False;
        $!quote_empty           = False;
        $!quote_space           = True;
        $!escape_null           = False;
        $!quote_binary          = True;
        $!formula               = "none";

        $!errno                 = 0;
        $!error_pos             = 0;
        $!error_field           = 0;
        $!error_input           = "";
        $!error_message         = "";
        $!record_number         = 0;

        $!io                    = IO::Handle;
        $!eof                   = False;

        $!csv-row               = CSV::Row.new (csv => self);

        self!set-attributes (%init);
        }

    method !set-attributes (%init) {
        $!build = True;
        for keys %init -> $attr {
            my @can = self.can (lc $attr) or self!fail (1000, "Unknown attribute '$attr'");
            .(self, %init{$attr}) for @can;
            }
        $!build = False;

        self!check_sanity;
        }

    method !fail (Int:D $errno, Int :$field, Str :$input, *@s) {
        $!errno          = $errno;
        $!error_pos      = 0;
        $!error_message  = %errors{$errno};
        $!error_message ~= (":", @s).join (" ") if @s.elems;
        $!error_field    = $field // ($!csv-row.fields.elems + 1);
        $!error_input    = $input;
        $!auto_diag and self.error_diag;    # Void context
        die self.error_diag;                # Exception object
        }

    method set_diag (Int:D $errno, Int :$pos, Int :$fieldno, Int :$recno) {
        $!errno         = $errno;
        $!error_message = %errors{$errno} // "";
        $!error_input   = "";

        $pos.defined     and $!error_pos     = $pos;
        $fieldno.defined and $!error_field   = $fieldno;
        $recno.defined   and $!record_number = $recno;
        }

    method !check_sanity () {
        $!build and return;

        #say "Sanity check: S:"~$!sep~" Q:"~($!quo//"<undef>")~" E:"~($!esc//"<undef>")~" WS:"~$!allow_whitespace;
        $!sep.defined                            or  self!fail (1008);
        $!sep eq ""                              and self!fail (1008);
        $!quo.defined and $!quo eq $!sep         and self!fail (1001);
        $!esc.defined and $!esc eq $!sep         and self!fail (1001);

                          $!sep ~~ m{<[\r\n]>}   and self!fail (1003);
        $!quo.defined and $!quo ~~ m{<[\r\n]>}   and self!fail (1003);
        $!esc.defined and $!esc ~~ m{<[\r\n]>}   and self!fail (1003);

        $!allow_whitespace or return;

                          $!sep ~~ m{ <[\ \t]> } and self!fail (1002);
        $!quo.defined and $!quo ~~ m{ <[\ \t]> } and self!fail (1002);
        $!esc.defined and $!esc ~~ m{ <[\ \t]> } and self!fail (1002);

        $!esc.defined or $!escape_null = False;
        }

    # String attributes
    method !a_str ($attr is rw, *@s) returns Str {
        if (@s.elems == 1) {
            my $x = @s[0];
            $x.defined && $x ~~ Str && $x eq "" and $x = Str;
            $attr = $x;
            self!check_sanity;
            }
        $attr;
        }
    method sep (*@s) returns Str { self!a_str ($!sep, @s); }
    method quo (*@s) returns Str { self!a_str ($!quo, @s); }
    method esc (*@s) returns Str { self!a_str ($!esc, @s); }
    method eol (*@s) returns Str {
        @s[0] ~~ Array and @s = |@s[0]; # Allow *OUT.nl-out
        self!a_str ($!eol, @s);
        }

    # Boolean attributes
    method !a_bool ($attr is rw, *@s) returns Bool {
        if (@s.elems == 1) {
            $attr = ?@s[0];
            self!check_sanity;
            }
        $attr;
        }
    method binary                (*@s) returns Bool { self!a_bool ($!binary,                @s); }
    method always_quote          (*@s) returns Bool { self!a_bool ($!always_quote,          @s); }
    method quote_empty           (*@s) returns Bool { self!a_bool ($!quote_empty,           @s); }
    method quote_space           (*@s) returns Bool { self!a_bool ($!quote_space,           @s); }
    method escape_null           (*@s) returns Bool { self!a_bool ($!escape_null,            @s); }
    method quote_binary          (*@s) returns Bool { self!a_bool ($!quote_binary,          @s); }
    method allow_loose_quotes    (*@s) returns Bool { self!a_bool ($!allow_loose_quotes,    @s); }
    method allow_loose_escapes   (*@s) returns Bool { self!a_bool ($!allow_loose_escapes,   @s); }
    method allow_unquoted_escape (*@s) returns Bool { self!a_bool ($!allow_unquoted_escape, @s); }
    method allow_whitespace      (*@s) returns Bool { self!a_bool ($!allow_whitespace,      @s); }
    method blank_is_undef        (*@s) returns Bool { self!a_bool ($!blank_is_undef,        @s); }
    method empty_is_undef        (*@s) returns Bool { self!a_bool ($!empty_is_undef,        @s); }
    method keep_meta             (*@s) returns Bool { self!a_bool ($!keep_meta,             @s); }
    method auto_diag             (*@s) returns Bool { self!a_bool ($!auto_diag,             @s); }
    method strict                (*@s) returns Bool { self!a_bool ($!strict,                @s); }
    method eof                   ()    returns Bool { $!eof || $!errno == 2012; }

    # Numeric attributes
    method !a_num ($attr is rw, *@s) returns Int {
        @s.elems == 1 and $attr = +@s[0];
        $attr;
        }
    method record_number (*@s) returns Int { self!a_num ($!record_number, @s); }

    # Numeric attributes, boolean allowed
    method !a_bool_int ($attr is rw, *@s) returns Int {
        if (@s.elems == 1) {
            my $v = @s[0];
            $attr = $v ~~ Bool ?? +$v !! $v.defined ?? $v eq "" ?? 0 !! +$v !! 0;
            }
        $attr;
        }
    method diag_verbose (*@s) returns Int { self!a_bool_int ($!diag_verbose, @s); }

    method header (IO::Handle:D $fh,
                   Array       :$sep-set            = [< , ; >],
                   Any         :$munge-column-names = "fc",
                   Bool        :$set-column-names   = True,
                   Bool        :$detect-bom         = True) { # unused for now
        my Str $hdr = $fh.get           or  self!fail (1010);

        # Determine separator conflicts
        my %sep = $hdr.comb.Bag{$sep-set.list}:kv;
        %sep.elems > 1 and self!fail (1011);

        self.sep (%sep.keys);

        given $munge-column-names {
            when Callable | "none" {             }
            when "lc"              { $hdr .= lc; }
            when "uc"              { $hdr .= uc; }
            default                { $hdr .= fc; }
            }

        self.getline ($hdr)             or  self!fail ($!errno);
        my @row = self.strings          or  self!fail (1010);
        $munge-column-names ~~ Callable and @row = @row.map ($munge-column-names);
        @row.grep ("")                  and self!fail (1012);
        if (my @dup = @row.repeated) {      self!fail (1013, @dup.Bag.map ({ "{$_.key}({$_.value+1})" }).join: ", "); }
        $set-column-names               and self.column-names: @row;
        self;
        }

    multi method formula ()         returns Str { $!formula;           };
    multi method formula (Str:U)    returns Str { $!formula = "undef"; };
    multi method formula (Str:D $x) returns Str {
        my Str $f = $x.lc;
        $f eq "" and $f = "empty";
        $f ~~ "none" | "die" | "croak" | "diag" | "empty" | "undef" or self!fail (1500);
        $!formula = $f;
        };

    multi method skip_empty_rows ()          returns Int { $!skip_empty_rows;              };
    multi method skip_empty_rows (Any:U)     returns Int { self.skip_empty_rows ("false"); };
    multi method skip_empty_rows (Bool  $x)  returns Int { self.skip_empty_rows (lc $x);   };
    multi method skip_empty_rows (Int:D $x
                          where 0 <= * <= 5) returns Int { self.skip_empty_rows (~$x);       };
    multi method skip_empty_rows (Callable:D $x) returns Int { $!skip_empty_rows_cb = $x; return $!skip_empty_rows = 6; }
    multi method skip_empty_rows (Routine:D  $x) returns Int { $!skip_empty_rows_cb = $x; return $!skip_empty_rows = 6; }
    multi method skip_empty_rows (Str:D  $x) returns Int {
        my Str $f = $x.lc;
        $!skip_empty_rows_cb = Any;
        $f ~~ "0" | ""     | "false" and return $!skip_empty_rows = 0;
        $f ~~ "1" | "skip" | "true"  and return $!skip_empty_rows = 1;
        $f ~~ "2" | "stop" | "eof"   and return $!skip_empty_rows = 2;
        $f ~~ "3" | "die"            and return $!skip_empty_rows = 3;
        $f ~~ "4" | "croak"          and return $!skip_empty_rows = 4;
        $f ~~ "5" | "error"          and return $!skip_empty_rows = 5;
        self!fail (1500);
        };

    multi method undef_str ()           returns Str { $!undef_str;         };
    multi method undef_str (Any:U)      returns Str { $!undef_str = Str;   };
    multi method undef_str (Str:D $x)   returns Str { $!undef_str = $x     };

    multi method comment_str ()         returns Str { $!comment_str;       };
    multi method comment_str (Any:U)    returns Str { $!comment_str = Str; };
    multi method comment_str (Str:D $x) returns Str { $!comment_str = $x   };

    multi method column_names (Bool:D $ where *.not) returns Array[Str] { @!cnames = (); }
    multi method column_names (Any:U)                returns Array[Str] { @!cnames = (); }
    multi method column_names (0)                    returns Array[Str] { @!cnames = (); }
    multi method column_names (*@c)                  returns Array[Str] {
        @c.elems and @!cnames = @c.map (*.Str);
        @!cnames;
        }

    my %predef-hooks =
        not_blank => { $^row.elems > 1 or $^row[0].defined && $^row[0] ne "" or $^row[0].is-quoted },
        not_empty => { $^row.first: { .defined && $_ ne ""   }},
        filled    => { $^row.first: { .defined && $_ ~~ /\S/ }};
    %predef-hooks<not-blank> = %predef-hooks<not_blank>;
    %predef-hooks<not-empty> = %predef-hooks<not_empty>;

    method callbacks (*@cb is copy) {
        if (@cb.elems == 1) {
            my $b = @cb[0];
            if ($b ~~ Hash || $b ~~ Pair) {
                @cb = $b.kv;
                }
            else {
                !$b.defined or
                   ($b ~~ Bool || $b ~~ Int and !?$b) or
                   ($b ~~ Str  && $b ~~ m:i/^( reset | clear | none | 0 )$/) or
                    self!fail (1004);
                %!callbacks = %();
                @cb = ();
                }
            }
        if (@cb.elems % 2) {
            self!fail (1004);
            }
        if (@cb.elems) {
            my %hooks;
            for @cb -> $name, $callback {
                my $hook = $callback;
                $hook.defined && $hook ~~ Str && %predef-hooks{$hook}.defined and
                    $hook = %predef-hooks{$hook};

                $name.defined &&  $name ~~ Str     &&
                $hook.defined && ($hook ~~ Routine || $hook ~~ Callable) or
                    self!fail (1004);

                my Str $cb_name = $name;
                $cb_name ~~ s{"-"} = "_";
                $cb_name ~~ /^ after_parse
                             | before_print
                             | filter
                             | error
                             $/ or self!fail (3100, $name);
                %hooks{$cb_name} = $hook;
                }
            %!callbacks = %hooks;
            }
        %!callbacks;
        }

    method !rfc7111ranges (Str:D $spec) returns RangeSet {
        $spec eq "" and self!fail (2013);
        my RangeSet $range = RangeSet.new;
        for $spec.split (";") -> $r {
            $r ~~ /^ (<[0..9]>+)["-"[(<[0..9]>+)||("*")]]? $/ or self!fail (2013);
            my Int $from = +$0;
                   $from ==  0  and self!fail (2013);
            my Str $tos  = ($1 // $from).Str;
                   $tos  eq "0" and self!fail (2013);
            my Num $to   = $tos eq "*" ?? Inf !! (+$tos - 1).Num;
            --$from <= $to or self!fail (2013);
            $range.add ($from, $to);
            }
        $range;
        }

    multi method colrange (@cr) {
        if (@cr.elems) {
            my RangeSet $range = RangeSet.new;
            $range.add ($_) for @cr;
            @!crange = $range.to_list;
            }
        @!crange;
        }

    multi method colrange (Str $range) {
        @!crange = ();
        $range.defined and @!crange = self!rfc7111ranges ($range).to_list.flat;
        @!crange;
        }

    multi method colrange () {
        @!crange;
        }

    multi method rowrange (Str $range) returns RangeSet {
        $!rrange = RangeSet;
        $range.defined and $!rrange = self!rfc71