#!/usr/bin/perl
use 5.008 ; use strict ; use warnings ;  # Confirmed also for 5.010 
use Getopt::Std ; getopts '~2e:n:t:vQ', \my %o ;
use Text::CSV_XS ;  #  Not a core module.
use FindBin qw [ $Script ] ; 
use Term::ANSIColor qw[ :constants color ] ; $Term::ANSIColor::AUTORESET = 1 ; 
use Encode ;# Encode was first released with perl v5.7.3

$o{e} = decode_utf8 $o{e} if defined $o{e} ;
$o{e} //= qw[ \ ] ;  # エスケープしたい文字列につける文字
$o{t} = decode_utf8 $o{t} if defined $o{t} ;
$o{n} = decode_utf8 $o{n} if defined $o{n} ;

& rev if $o{'~'} ;
& main ; 
exit 0 ;

# 逆操作。 TSV -> CSV 
sub rev ( ) { 
  grep { $_ = quotemeta $_ if defined $_ } ( $o{e} , $o{n}, $o{t} ) ; 


  my $csv = Text::CSV_XS->new( { binary => 1 } ) ;  # if binary =0 then UTF-8 character cause trouble
  while (<>){ 
    chomp ; 
    s/\r$// ;
    my @F = split /\t/, $_ , -1 ; 

    # エスケープされた文字を考慮しつつ、-t と -n の指定に従って,タブ文字も改行文字も復元する。
    for (@F){
      if ( defined $o{t} ) { 
        s/(?<!$o{e})$o{t}/\t/g ;  # 否定的後読みは (?<!pattern) ; 肯定的後読みは (?<=pattern)
        s/$o{e}$o{t}/$o{t}/g ;
      }

      if ( defined $o{n} ) { 
        my $e = $o{e} ; my $n = $o{n} ;
        s/(?<!$o{e})$o{n}/\n/g ;  # 否定的後読みは (?<!pattern) ; 肯定的後読みは (?<=pattern)
        s/$o{e}$o{n}/$o{n}/g ; #print STDERR BLUE "$o{e}, $o{n}\n" ;
      }

      my $status = $csv->print(*STDOUT, [@F]);
      print STDERR BRIGHT_RED "Something wrong at line $.\n" unless $status ; 
      print "\n" ;
    #print join ',' , map {qq["$_"]} @F ;
    }
  }
  exit 0; 
}

sub main ( ) { 
  binmode * STDOUT , ":utf8" ; # Necessry because Text::CSV_XS decodes UTF8 input.
  & core ; 
}

sub core ( ) {
  my $lines = 1 ; # CSV で読み込んでいるので、$. は2以上増えることがある。読み取る度に、 $lines から $. 行目までと認識するため。
  my %cols ; # 何個の列を何行が持っていたかを表す。3列の行が120行存在した、などを表す。
  our $csv = Text::CSV_XS -> new ( { binary => 1 } );  # if binary => 0 then when "\n" is included in a cell it cause trouble.

  # 入力が 一定秒数以内に始まらない場合に、画面に注意を表示する。
  my $alarmF = 0 ; 
  if ( -t ) { 
    $alarmF = 1 ; 
    $SIG{ALRM} = sub { 
      print STDERR GREEN "Waiting CSV-formatted input from STDIN.. ($Script)\n" ;
      $SIG{ALRM} = sub { print STDERR GREEN "." ; alarm 1 } ; 
      alarm 1 ; 
    } ; 
    alarm 1 ; 
  }

  my @from ; # どの文字列をどう置きかえるか。
  my @leng ; # その長さ
  my @dest ; # 置換先
  do {push @from , "\t" ; push @leng ,1 ; push @dest , $o{t} } if defined $o{t} ; 
  do {push @from , "\n" ; push @leng ,1 ; push @dest , $o{n} } if defined $o{n} ; 

  my @warnstr ; # 警告対象の文字列。改行やタブ文字など
  my @escape ; # エスケープ対象の文字列
  unless ($o{Q}) { 
    push @warnstr , $o{t} if defined $o{t} ; 
    push @warnstr , $o{n} if defined $o{n} ; 
    @warnstr = grep { $_ ne '' } @warnstr ;
    @escape = map { quotemeta $_ } @warnstr ; # この時点で -vのものははいっていない
    #print STDERR BRIGHT_BLUE join ", " , @escape , "\n" ;
    push @warnstr , "\t" if $o{v} || ! defined $o{t} ; 
    push @warnstr , "\n" if $o{v} || ! defined $o{n} ; 
  }

  # 入力からの読取り。
  my $posV = 0 ; # 出力上の縦方向の位置を表す
  while ( my $x = $csv -> getline( *ARGV ) ) {   # *ARGVはOld(er) support と perldoc Text::CSV_XSに記載あり。将来サポートされないかも。
    do { $alarmF = 0 ; alarm 0 } if $alarmF ;
    $posV ++ ; 
    $cols{ @$x } ++ ; # この行は、列を何個持っていたかの数から,後で,何個の行が何個の列を持っていたか情報表示をするようにする。

    # 入力レコード中にタブ文字か改行文字が現れた場合に、カウントし、表示する。
    my $posH = 0 ; # 出力上のセルの水平位置を表す。 
    for ( @$x ) { 
      $posH ++ ;
      for my $seek ( @warnstr ) { 
  	    if ( index ($_ , $seek , 0)   >= 0 ) { 
  	  	  my $tgt = $seek ; #quotemeta $seek ; 
  	      $tgt =~ s/\n/\\n/g ; 
  	      $tgt =~ s/\t/\\t/g ;   	  	
  	  	  my $lstr = $lines == $. ? $lines : "$lines-$." ; 
  	  	  my $t = $_ ;
          #$t =~s/\r//gs ;
          $t =~s/\n/\\n/gs; 
          $t =~s/\\n/\e[44m\\n\e[40m/g; 
          $t =~s/\t/\\t/gs ;
          $t =~s/\\t/\e[44m\\t\e[40m/g; 
          #$t =~ s/\n/\e[41m\\N\e[40m/gs ; 
  	  	  my $sout = qq[[$Script] Warning: "$tgt" detected at "$ARGV":] ;
  	  	  $sout .= qq" input line $lstr; output cell ($posV,$posH): \e[0m\e[4m$t\n" ; 
  	      print STDERR BRIGHT_RED $sout ;	
  	    }
      }
    }
  
  # 置換対象の文字を置換する。
    for my $cell ( @$x ) { 
      $cell =~ s/$_/$o{e}$_/g for @escape ; # エスケープする /
      for my $i ( 0 .. $#from ) { 
        my $p = 0 ; 
        substr $cell, $p, $leng[$i], $dest[$i] while 1+($p=index$cell,$from[$i],$p);
      }
    }
    
  # 出力処理
    print join ( "\t", @$x ) . "\n" ;  
    print "\n" if $o{2} ; #   # 出力各行の間に空行を挿入する場合の処理
  
    $lines = $. + 1 ; # <- tricky!
  }
  $csv->eof; # <-- - 必要か?

  return if $o{Q} ; 
  my $out = qq[[$Script] "$ARGV": $. lines =>] ;
  my $tmp = join " + " , map { "${_}x$cols{$_}"} sort {$a<=>$b} keys %cols ;
  print STDERR CYAN qq[$out $tmp\n] ;

  # エラー処理 (Text::CSV_XS のエラー処理)  , このプログラムの変数の使い方が理由で、この位置にENDを置いた。
  END{ 
    exit if $o{'~'} ;
    exit if ! defined $csv ; 
    my @tmp = $csv -> error_diag () ; # ($cde, $str, $pos, $rec, $fld) = $csv->error_diag ();
    if ( $tmp[0] != 2012 ) {  # perldoc Text::CSV_XS で 2012 を参照。EOFを意味する。
      print STDERR BRIGHT_RED join (":",@tmp),"\n" ;
      exit 1 ; 
    }
  }
}


## ヘルプとバージョン情報
BEGIN {
  our $VERSION = 0.52 ;
  $Getopt::Std::STANDARD_HELP_VERSION = 1 ; 
  grep { m/--help/} @ARGV and *VERSION_MESSAGE = sub {} ; 
    # 最初は 0.21 を目安とする。
    # 1.00 以上とする必要条件は英語版のヘルプをきちんと出すこと。
    # 2.00 以上とする必要条件はテストコードが含むこと。
   # 0.22 : 英文マニュアルをPOD形式にする。
   # 0.23 : 英文マニュアルのPOD形式の部分をさらに増やした。
}  
sub HELP_MESSAGE {
    use FindBin qw[ $Script $Bin ] ;
    sub EnvJ ( ) { $ENV{LANG} =~ m/^ja_JP/ ? 1 : 0 } ; # # ja_JP.UTF-8 
    sub en( ) { grep ( /^en(g(i(sh?)?)?)?/i , @ARGV ) ? 1 : 0 } # English という文字列を先頭から2文字以上を含むか 
    sub ja( ) { grep ( /^jp$|^ja(p(a(n?)?)?)?/i , @ARGV ) ? 1 : 0 } # jp または japan という文字列を先頭から2文字以上を含むか 
    sub opt( ) { grep (/^opt(i(o(ns?)?)?)?$/i, @ARGV ) ? 1 : 0 } # options という文字列を先頭から3文字以上含むから
    sub noPOD ( ) { grep (/^no-?p(od?)?\b/i, @ARGV) ? 1 : 0 } # POD を使わないと言う指定がされているかどうか
    my $jd = "JapaneseManual" ;
    my $flagE = ! ja && ( en || ! EnvJ ) ; # 英語にするかどうかのフラグ
    exec "perldoc $0" if $flagE &&  ! opt ; #&& ! noPOD   ; 
    $ARGV[1] //= '' ;
    open my $FH , '<' , $0 ;
    while(<$FH>){
        s/\Q'=script='\E/$Script/gi ;
        s/\Q'=bin='\E/$Bin/gi ;
        if ( s/^=head1\b\s*// .. s/^=cut\b\s*// ) { 
            if ( s/^=begin\s+$jd\b\s*// .. s/^=end\s+$jd\b\s*// xor $flagE ) {
                print $_ if ! opt || m/^\s+\-/  ; 
            }
        } 
    }
    close $FH ;
    exit 0 ;
}

=encoding utf8 

=head1 NAME

csv2tsv

=head1 VERSION 

0.51

=head1 SYNOPSIS

csv2tsv [B<-t> str] [B<-n> str] [-v] [-Q] [-2] [B<-~>] file

=head1 DESCRIPTION 

Transforms CSV formatted data (cf. RFC4180) into TSV formated data.
Input is assumed to be UTF-8.
(The input line ends can be both CRLF or LF. The output line ends are LF.)
Warnings/erros would be properly printed on STDERR (as far as the author of
this program experienced).

=head1 EXAMPLE 

csv2tsv file.csv > file.tsv     

csv2tsv B<-n> '[\n]' file.csv > file.tsv       
  # "\n" in the CSV cell will be transfomed to [\n].

csv2tsv B<-t> TAB file.csv > file.tsv       
  # "\t" in the CSV cell will be transfomed to "TAB". UTF-8 characters can be specified.

B<for> i B<in> *.csv ; B<do> csv2tsv -n'"\n"' -t'"\t"' $i > ${i/csv/tsv} ; B<done>
  # BASH or ZSH is required to use this "for" statement. Useful for multiple CSV files.

For the safety, when '-t' or '-n' is set with string character specification,
a B<warning> is displayed every time a values in the input cells matches the specified string charatcter
unless B<-Q> is set.

csv2tsv < file.csv > file.tsv     
  # file name information cannot be passed to "csv2tsv". So the warning messages may lack a few information.

=head1 OPTION

=over 4

=item B<-e> str

Escape character(s) to be used to attach previous to the string matched to the string specified by -t or -n.

=item B<-t> str 

What the input TAB character will be replaced with is specified. 

=item B<-n> str 

What "\n" character in the input CSV cell will be replaced with is specified. 

=item -v 

Always tell the existence of "\t" or "\n" even if "-t str" or "-n str" is specified. 

=item -Q 

No warning even if "\t" or "\n" is included in the cell of input. 

=item -2 

Double space output, to find "\n" anormality by human eyes. 
(For a kind expediency when this program author was firstly making this program)

=item B<-~>

The opposite conversion of csv2tsv, i.e. B<TSV to CSV> conversion.
TABs and LINEENDs will be recovered if the intput was generated by this program "csv2tsv" with the
same specification of "-t", "-n" and "-e".

=item --help 

Shows this help.

=item --help ja 

Shows Japanese help.

=item --version

Shows the version information of this program. 

=back 

=head1 AUTHOR

Toshiyuki Shimono
  bin4tsv@gmail.com

=head1 HISTORY 

 2015-09-28 : Firstly created on a whim.    
 2016-07-06 : Some options are added such as -2.    
 2016-08-03 : Response to tab and enter characgers.     
 2018-06-24 : Once realeased on CPAN for the sake of Table::Hack.    
 2018-07-04 : Refinements to options. English manual is added. 

=head1 LICENSE AND COPYRIGHT

Copyright 2018 "Toshiyuki Shimono".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.

=begin JapaneseManual

  csv2tsv file.csv > file.tsv 
  csv2tsv < file.csv > file.tsv 

  CSV 形式(RFC 4180)のファイルを TSV形式(タブ文字区切り) に変換する。
  出力については、文字コード UTF-8 で改行コードは "\n" となる。

 オプション:

   -e st  : -t または -e で指定された文字列に一致する文字列の直前にエスケープを目的に入れる文字列。
   -t str : 入力のタブ文字を何に置き換えるかを文字列で指定する。空文字列が指定されない限り、エスケープも考慮される。
   -n str : 入力の改行文字を何に置き換えるかを文字列で指定する。空文字列が指定されない限り、エスケープも考慮される。
   -v    :  タブ文字と改行文字の存在を必ず指摘する。(-t や -n の指定があれば,通常、何も指摘の表示はしない。)
   -Q : 入力のレコード内に、タブ文字または改行文字があっても、警告を出さない。付けることで高速化はする。(no check)
   -2 : レコードの区切りを単一の \n ではなくて、2個続けた \n\n にする。CSVのセル内に改行文字がある場合に使うかもしれない。

   -~ : TSV形式からCSV形式に変換。 -t と -n と -e の指定でこのプログラムで変換済みと仮定して、タブも改行も復元。

   --help : この $0 のヘルプメッセージを出す。  perldoc -t $0 | cat でもほぼ同じ。
   --help opt : オプションのみのヘルプを出す。opt以外でも options と先頭が1文字以上一致すれば良い。
   --help en : 英文マニュアルを表示する
   --version : このプログラムのバージョン情報を表示する。
=cut