### ----------------------------------------------------
### -- Text::CSV
### -- Licenses: Artistic-2.0
### -- Authors: H.Merijn Brand (Tux)
### -- File: lib/Text/CSV.rakumod
### ----------------------------------------------------
### -- Chunk 3 of 3
 #     \"str,ing"                       Capture
        #   generate
        #     [["a","b"][1,2],[3,4]]     AoA   Array
        #     [{a=>1,b=>2},{a=>3,b=>4}]  AoH   Array
        #     sub { ... }                      Routine
        #     { ... }                          Callable
        #     Supply.new                       Supply
        #     start { ...}                     Channel
        if $in ~~ Supplier {
            $in = $in.Supply;
            }
        given $in {
            when Str {
                $io-in = open $in, :r, :!chomp, :enc($encoding);
                }
            when IO::Handle {
                $io-in = $in;
                }
            when IO::Path {
                $io-in = open ($in);
                }
            when Array {
                $in.list.elems or return;
                given $in.list[0] {
                    when Str {  # AoS
                        $io-in = Text::IO::String.new ($in.list.join ($!eol // "\n"));
                        }
                    default {   # AoA, AoH
                        $fragment ~~ s:i{^ "row=" } = "" and
                            self.rowrange ($fragment);
                        my @i;
                        if $in.list[0] ~~ Hash {
                            my @hdr = $in.list[0].keys.sort;
                            @i = [ @hdr ], $in.map ({$[ $_{@hdr} ]}).Slip;
                            }
                        else {
                            @i = $in.list;
                            }
                        @in = $!rrange ?? @i[$!rrange.list (@i.elems)] !! @i;
                        }
                    }
                }
            when Capture {
                $io-in = Text::IO::String.new ($in.list.map ( -> \x --> Str { x.Str }).join ($!eol // "\n"));
                }
            when Supply {
                $fragment ~~ s:i{^ "row=" } = "" and self.rowrange ($fragment);
                my int $i = 0;
                $in.act (
                    -> \row {
                        !$!rrange || $!rrange.in ($i++) and @in.push:
                            row ~~ Str ?? $[ self.getline (row) ] !! row;
                        },
                    );
                $in.wait;
                @in;
                }
            when Channel {
                $fragment ~~ s:i{^ "row=" } = "" and self.rowrange ($fragment);
                my int $i = 0;
                react {
                    whenever $in -> \row {
                        !$!rrange || $!rrange.in ($i++) and @in.push:
                            row ~~ Str ?? $[ self.getline (row) ] !! row;
                        LAST { done; }
                        }
                    }
                }
            when Callable { # Sub, Routine, Callable, Block, Code
                $fragment ~~ s:i{^ "row=" } = "" and self.rowrange ($fragment);
                my int $i = 0;
                my @hdr;
                @in = gather while $in() -> $r {
                    if (!$!rrange || $!rrange.in ($i++)) {
                        if ($r ~~ Hash) {
                            unless (@hdr.elems) {
                                @hdr = @!cnames.elems ?? @!cnames !! $r.keys.sort;
                                take [ @hdr ];
                                }
                            take [ $r{@hdr} ];
                            }
                        else {
                            take $r;
                            }
                        }
                    };
                }
            when Iterator {
                $fragment ~~ s:i{^ "row=" } = "" and self.rowrange ($fragment);
                my int $i = 0;
                my @hdr;
                @in = gather while $in.pull-one () -> \r {
                    r =:= IterationEnd and last;
                    if (!$!rrange || $!rrange.in ($i++)) {
                        if (r ~~ Hash) {
                            unless (@hdr.elems) {
                                @hdr = @!cnames.elems ?? @!cnames !! r.keys.sort;
                                take [ @hdr ];
                                }
                            take [ r{@hdr} ];
                            }
                        else {
                            take r;
                            }
                        }
                    };
                }
            default {
                self!fail (5000);
                }
            }

        if (%hdrargs and $io-in) {
            if ($headers.defined) {
                %hdrargs<munge-column-names> = "none";
                %hdrargs<set-column-names>   = False;
                }
            self.header ($io-in, |%hdrargs);
            @!cnames.elems and $headers = @!cnames;
            }

        my Str        $tmpfn;
        my IO::Handle $io-out;
        LEAVE if $out ~~ Str { .close with $io-out }
        # out
        #   Array       - return AoA (default)
        #   Hash        - return AoH (headers required)
        #   Str         - return output as Str
        #   "file.csv"  - write to file
        #   $fh         - write to $fh
        #   Routine:D   - pass row(s) to Routine
        #   Supply:D    - emit to Supply
        #   Channel:D   - emit to Channel
        given $out {
            when Str {
                ($tmpfn, $io-out) = $out.defined
                    ?? (Str, open $out, :w, :!chomp)
                    !! tempfile;
                }
            when IO::Handle {
                $io-out = $out;
                }
            when Channel:U {
                $out = Channel.new;
                }
            when Supply:U {
                $out = Supplier::Preserving.new;
                }
            when Array:D {
                $out[0] ~~ Hash and $headers ||= "auto";
                }
            when Hash:D {
                # No specific action required here
                # Check if key is supplied?
                }
            when Array:U | Hash:U | Capture:U |
                 Supplier:D | Callable:D | Channel:D {
                # No specific action required here
                }
            when Nil {
                $io-out = Nil;
                }
            when Any {
                $in ~~ Array and $io-out = $*OUT;
                }
            default {
                self!fail (5001);
                }
            }

        # Find the correct spots to invoke $on-in and $before-out

        given $headers {
            when Array:D {
                self.column_names ($headers.list);
                }
            when Callable:D | Hash:D | Str:D {
                my $hdr;
                if ($io-in ~~ IO::Handle and $io-in.defined) {
                    $hdr = self.getline ($io-in, :!meta);
                    }
                elsif (@in.elems) {
                    $hdr = @in.shift;
                    }
                unless ($headers ~~ Str && $headers eq "skip") {
                    if ($hdr.defined) {
                           if $headers ~~ Callable { $_  = $headers($_)       for @$hdr }
                        elsif $headers ~~ Hash     { $_  = $headers{$_} // $_ for @$hdr }
                        elsif $headers eq "uc"     { $_ .= uc                 for @$hdr }
                        elsif $headers eq "lc"     { $_ .= lc                 for @$hdr }
                        self.column_names ($hdr.list);
                        $out //= Hash if $out === Any;
                        }
                    $io-out.defined and self.say ($io-out, @!cnames);
                    }
                }
            }

        if ($io-in ~~ IO::Handle and $io-in.defined) {
            @in = $fragment
                ?? self.fragment    ($io-in, :$meta, $fragment)
                !! self.getline_all ($io-in, :$meta);
            }

        my @h;
        $out ~~ Hash || $headers ~~ Array || ($headers ~~ Str && $headers eq "auto") and
            @h = @!cnames.elems ?? @!cnames !! @in.shift.list;
        @h.elems and @kh = @h;

      # unless (?$out || ?$tmpfn || ?$io-out) { # TODO: Fix for csv () in void context!
        unless ((?$out && $out !~~ Hash) || ?$tmpfn) {
            if ($out ~~ Hash or @h.elems) {
                # AOH
                @h or return [];
                if (my @dup = @h.repeated) {
                    self!fail (1013, @dup.Bag.map ({ "{$_.key}({$_.value+1})" }).join: ", ");
                    }
                @in.elems && @in[0] ~~ Hash or @in = @in.map (-> @r { $%( @h Z=> @r ) });
                if ($key) {
                    if ($key ~~ Str) {
                        @in[0]{$key }:exists or self!fail (4001);
                        if (?$out) {
                            for @in -> \h { $out{h{$key}} = h };
                            return $out;
                            }
                        return $%( @in.map ( -> \h --> Pair { h{$key} => h }) );
                        }
                    if ($key ~~ Array && $key.elems > 1) {
                        my @k   = @$key;
                        my $sep = @k.shift;
                        @k.map ({@in[0]{$_}:exists}).all or self!fail (4001);
                        if (?$out) {
                            for @in -> \h { $out{h{@k.map ({h{$_}}).join: $sep}} = h };
                            return $out;
                            }
                        return $%( @in.map ( -> \h --> Pair {
                            (@k.map ({h{$_}}).join: $sep) => h }));
                        }
                    self!fail (1501);
                    }
                return @in;
                }
            # AOA
            return @in;
            }

        my $eol = self.eol;
        $eol.defined or self.eol ("\r\n");
        for @in -> $row {
            $!csv-row.fields = $row[0] ~~ CSV::Field
                ?? $row
                !! $row.map ( -> \x --> CSV::Field { CSV::Field.new.add (x.Str); });
            my @row = $meta ?? self.fields !! self.strings;
            my $r = (@h.elems == 0 || @row[0] ~~ Hash)
                ?? @row
                !! $%( @h Z=> @row );
            given $out {
                when Callable {
                    $out($r);
                    }
                when Supplier {
                    $out.emit ($r);
                    }
                when Channel {
                    $out.send ($r);
                    }
                when Array:D {
                    $out.push: $r;
                    }
                default {
                    defined $io-out and $io-out.print (self.string);
                    }
                }
            }
        self.eol ($eol);

        given $out {
            when Supplier::Preserving {
                $out.done;
                return $out.Supply;
                }
            when Channel {
                $out.close;
                return $out;
                }
            when Array:D | Hash:D {
                return $out;
                }
            }

        if (?$tmpfn) {
            $io-out.close;
            my $fh = open $tmpfn, :r, :!chomp;
            return $fh.slurp-rest;
            }

        True;
        }

    method csv ( Any       :$in   is copy,
                 Any       :$out  is copy,
                 Str       :$encoding,
                 Str       :$fragment,
                 Bool      :$meta is copy,
                 Text::CSV :$csv  = self || Text::CSV.new,
                 *%args ) {

        # file can be alias for in or for out
        if (my $file = %args<file> :delete) {
            if ($in.defined) {
                $out //= $file;
                }
            else {
                $in    = $file;
                }
            }
        $meta //= $csv.keep_meta;

        $csv.CSV (:$in, :$out, :$encoding, :$fragment, :$meta, |%args);
        }
    }

BEGIN {
    sub alias (Str:D $m, *@aka) {
        my $r := Text::CSV.^find_method ($m);
        Text::CSV.^add_method ($_, $r) for @aka;
        }

    alias ("sep",                   < sep_char sep-char separator >);
    alias ("quo",                   < quote quote_char quote-char >);
    alias ("esc",                   < escape escape_char escape-char >);
    alias ("always_quote",          < always-quote quote_always quote-always >);
    alias ("quote_empty",           < quote-empty >);
    alias ("quote_space",           < quote-space >);
    alias ("escape_null",           < escape-null quote_null quote-null >);
    alias ("quote_binary",          < quote-binary >);
    alias ("allow_loose_quotes",    < allow-loose-quotes allow_loose_quote allow-loose-quote >);
    alias ("allow_loose_escapes",   < allow-loose-escapes allow_loose_escape allow-loose-escape >);
    alias ("allow_unquoted_escape", < allow-unquoted-escape allow_unquoted_escapes allow-unquoted-escapes >);
    alias ("allow_whitespace",      < allow-whitespace >);
    alias ("blank_is_undef",        < blank-is-undef >);
    alias ("empty_is_undef",        < empty-is-undef >);
    alias ("record_number",         < record-number >);
    alias ("auto_diag",             < auto-diag >);
    alias ("keep_meta",             < keep-meta meta>);
    alias ("diag_verbose",          < diag-verbose verbose_diag verbose-diag >);
    alias ("callbacks",             < hooks >);
    alias ("formula",               < formula-handling formula_handling >);
    alias ("undef_str",             < undef-str >);
    alias ("comment_str",           < comment-str >);
    alias ("skip_empty_rows",       < skip-empty-rows >);

    alias ("column_names",          < column-names >);
    alias ("error_diag",            < error-diag diag diag-error diag_error >);
    alias ("error_input",           < error-input >);
    alias ("getline_all",           < getline-all >);
    alias ("getline_hr_all",        < getline-hr-all >);
    alias ("getline_hr",            < getline-hr >);
    alias ("is_binary",             < is-binary >);
    alias ("is_missing",            < is-missing >);
    alias ("is_quoted",             < is-quoted >);
    alias ("is_utf8",               < is-utf8 >);
    alias ("set_diag",              < SetDiag set-diag >);
    }

sub csv (*%args) is export {
    %args<meta> //= False;
    Text::CSV.csv (|%args);
    }


