#!perl

our $DATE = '2016-04-14'; # DATE
our $VERSION = '0.05'; # VERSION

use 5.010001;
# IFUNBUILT
# use strict;
# use warnings;
# END IFUNBUILT

my %Opts = (
    #input_format => undef,
    output_format => "text",
);
my $Input; # envres
my $Input_Form; # hash/aos/aoaos/aohos
my $Input_Obj; # TableData::Object::* object
my $SubCmd;
my $Output; # envres

sub _get_table_spec_from_envres {
    my $envres = shift;
    my $ff = $envres->[3]{'table.fields'};
    return undef unless $ff;
    my $spec = {fields=>{}};
    my $i = 0;
    for (@$ff) {
        $spec->{fields}{$_} = {pos=>$i};
        $i++;
    }
    $spec;
}

sub parse_cmdline {
    require Getopt::Panjang;

    my $opt_help;
    my %common_opts = (
        'output-format|f=s' => sub {
            my %a = @_;
            $Opts{output_format} = $a{value};
        },
        'json' => sub {
            $Opts{output_format} = 'json';
        },
        'version|v' => sub {
            say "td version ", ($main::VERSION // '?');
            exit 0;
        },
        'help|h|?' => sub { $opt_help++ },
    );

    my $res = Getopt::Panjang::get_options(spec => \%common_opts);
    my $remaining_argv = $res->[3]{'func.remaining_argv'};

    $SubCmd = shift @{ $remaining_argv } // '';
    @ARGV = @$remaining_argv;

    $opt_help++ unless $SubCmd;
    if ($opt_help) {
        if ($SubCmd eq 'sum') {
            print <<'_';
Usage:
  td sum < INPUT
_
        } elsif ($SubCmd eq 'sum-row') {
            print <<'_';
Usage:
  td sum-row < INPUT
_
        } elsif ($SubCmd eq 'avg') {
            print <<'_';
Usage:
  td avg < INPUT
_
        } elsif ($SubCmd eq 'avg-row') {
            print <<'_';
Usage:
  td avg-row < INPUT
_
        } elsif ($SubCmd eq 'rowcount') {
            print <<'_';
Usage:
  td rowcount < INPUT
_
        } elsif ($SubCmd eq 'colcount') {
            print <<'_';
Usage:
  td colcount < INPUT
_
        } elsif ($SubCmd eq 'info') {
            print <<'_';
Usage:
  td info < INPUT
_
        } elsif ($SubCmd eq 'select') {
            print <<'_';
Usage:
  td select <COL,COL2...> [--sort <COL,COL2,...>] < INPUT
_
        } elsif ($SubCmd eq 'sort') {
            print <<'_';
Usage:
  td sort <COL,COL2...> < INPUT
_
        } elsif ($SubCmd ne '') {
            die "Unknown subcommand '$SubCmd'\n";
        } else {
            print <<'_';
Usage:
  td [COMMON_OPTS] <SUBCOMMAND> [SUBCMD_OPTS] < INPUT
  td --version (or -v)
  td --help (or -h, -?)
Common options:
  --output-format=s, -f

Consult manpage/documentation for more details.
_
        }
        exit 0;
    } elsif ($res->[0] != 200 && (
        $res->[3]{'func.val_invalid_opts'} ||
            $res->[3]{'func.val_missing_opts'} ||
            $res->[3]{'func.ambiguous_opts'} ||
            # XXX later when subcommands have options too, allow unknown options
            # and re-get_options() with subcommand's spec
            $res->[3]{'func.unknown_opts'}
        )) {
        die "$res->[1]\n";
    }
}

sub _decode_json {
    require Cpanel::JSON::XS;

    state $json = Cpanel::JSON::XS->new->allow_nonref;
    $json->decode(shift);
}

sub get_input {
    require Data::Check::Structure;

    {
        local $/;
        $Input = _decode_json(~~<STDIN>);
    }

    # give envelope if not enveloped
    unless (ref($Input) eq 'ARRAY' &&
                @$Input >= 2 && @$Input <= 4 &&
                $Input->[0] =~ /\A[2-5]\d\d\z/ &&
                !ref($Input->[1])
            ) {
        $Input = [200, "Envelope added by tabledata", $Input];
    }

    # detect table form
    if (ref($Input->[2]) eq 'HASH') {
        $Input_Form = 'hash';
        require TableData::Object::hash;
        $Input_Obj = TableData::Object::hash->new($Input->[2]);
    } elsif (Data::Check::Structure::is_aos($Input->[2])) {
        $Input_Form = 'aos';
        require TableData::Object::aos;
        $Input_Obj = TableData::Object::aos->new($Input->[2]);
    } elsif (Data::Check::Structure::is_aoaos($Input->[2])) {
        $Input_Form = 'aoaos';
        my $spec = _get_table_spec_from_envres($Input);
        require TableData::Object::aoaos;
        $Input_Obj = TableData::Object::aoaos->new($Input->[2], $spec);
    } elsif (Data::Check::Structure::is_aohos($Input->[2])) {
        $Input_Form = 'aohos';
        my $spec = _get_table_spec_from_envres($Input);
        require TableData::Object::aohos;
        $Input_Obj = TableData::Object::aohos->new($Input->[2], $spec);
    } else {
        # simply pass it through
        $Input_Form = '';
    }
}

sub process {
    if (!$Input_Form) {
        $Output = $Input;
    } elsif ($SubCmd eq 'info') {
        my $form = ref($Input_Obj); $form =~ s/^TableData::Object:://;
        my $info = {
            form => $form,
            rowcount => $Input_Obj->row_count,
            colcount => $Input_Obj->col_count,
            cols => join(", ", @{ $Input_Obj->cols_by_idx }),
        };
        $Output = [200, "OK", $info];
    } elsif ($SubCmd eq 'rowcount') {
        $Output = [200, "OK", $Input_Obj->row_count];
    } elsif ($SubCmd eq 'colcount') {
        $Output = [200, "OK", $Input_Obj->col_count];
    } elsif ($SubCmd =~ /\A(sum|sum-row|avg|avg-row)\z/) {
        require Scalar::Util;
        my $cols = $Input_Obj->cols_by_idx;
        my $rows = $Input_Obj->rows_as_aoaos;
        my $sum_row = [map {0} @$cols];
        for my $i (0..$#{$rows}) {
            my $row = $rows->[$i];
            for my $j (0..@$cols-1) {
                $sum_row->[$j] += $row->[$j]
                    if Scalar::Util::looks_like_number($row->[$j]);
            }
        }
        my $avg_row;
        if ($SubCmd =~ /avg/) {
            if (@$rows) {
                $avg_row = [map { $_ / @$rows } @$sum_row];
            } else {
                $avg_row = [map {0} @$cols];
            }
        }
        # XXX return aohos if input is aohos
        if ($SubCmd eq 'sum') {
            $Output = [200, "OK", [$sum_row],
                       {'table.fields' => $cols}];
        } elsif ($SubCmd eq 'sum-row') {
            $Output = [200, "OK", [@$rows, $sum_row],
                       {'table.fields' => $cols}];
        } elsif ($SubCmd eq 'avg') {
            $Output = [200, "OK", [$avg_row],
                       {'table.fields' => $cols}];
        } elsif ($SubCmd eq 'avg-row') {
            $Output = [200, "OK", [@$rows, $avg_row],
                       {'table.fields' => $cols}];
        }
    } elsif ($SubCmd =~ /\A(sort|select)\z/) {
        my $res;
        if ($SubCmd eq 'sort') {
            if ($Input_Form eq 'aohos') {
                $res = $Input_Obj->select_as_aohos(undef, undef, \@ARGV);
            } else {
                $res = $Input_Obj->select_as_aoaos(undef, undef, \@ARGV);
            }
        } elsif ($SubCmd eq 'select') {
            if ($Input_Form eq 'aohos') {
                $res = $Input_Obj->select_as_aohos(\@ARGV);
            } else {
                $res = $Input_Obj->select_as_aoaos(\@ARGV);
            }
        }

        my $ff = $res->{spec}{fields};
        my $tff = [];
        for (keys %$ff) {
            $tff->[$ff->{$_}{pos}] = $_;
        }

        $Output = [200, "OK", $res->{data},
                   {'table.fields'=>$tff}];
    } else {
        $Output = [400, "Unknown subcommand '$SubCmd'"];
    }
}

sub display_output {
    require Perinci::Result::Format::Lite;
    my $fres = Perinci::Result::Format::Lite::format(
        $Output, $Opts{output_format});

    # ux: prefix error message with program name
    if ($Output->[0] =~ /\A[45]/ && defined($Output->[1])) {
        $fres = "td: $fres";
    }

    print $fres;
}

# MAIN

parse_cmdline();
get_input();
process();
display_output();

1;
# ABSTRACT: Manipulate table data
# PODNAME: td

__END__

=pod

=encoding UTF-8

=head1 NAME

td - Manipulate table data

=head1 VERSION

This document describes version 0.05 of td (from Perl distribution App-td), released on 2016-04-14.

=head1 SYNOPSIS

Usage:

 % command-that-outputs-table-data-in-json | td <subcommand> [options ...]

Examples:

 # count number of rows (equivalent to "wc -l" Unix command)
 % osnames -l --json | td rowcount

 # count number of columns
 % osnames -l --json | td colcount

 # info about table (form, rowcount, colcount, column names, etc)
 % osnames -l --json | td info

 # select some columns
 % osnames -l --json | td select value description

 # sort by column(s) (add "-" prefix to for descending order)
 % osnames -l --json | td sort value tags
 % osnames -l --json | td sort -- -value

 # return sum of all numeric columns
 % list-files -l --json | td sum

 # append a sum row
 % list-files -l --json | td sum-row

 # return average of all numeric columns
 % list-files -l --json | td avg

 # append an average row
 % list-files -l --json | td avg

=head1 DESCRIPTION

B<EARLY RELEASE. MORE SUBCOMMANDS WILL BE ADDED.>

=head1 OPTIONS

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/App-td>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-App-td>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-td>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 SEE ALSO

L<TableDef>

L<TableData::Object>

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by perlancar@cpan.org.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
