/*  Zutils - Utilities dealing with compressed files
    Copyright (C) 2009-2015 Antonio Diaz Diaz.

    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 2 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 <http://www.gnu.org/licenses/>.
*/

#define _FILE_OFFSET_BITS 64

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/wait.h>

#include "arg_parser.h"
#include "rc.h"


const char * invocation_name = 0;
const char * program_name = 0;
int verbosity = 0;

namespace {

const char * const config_file_name = "zutilsrc";
const char * const     program_year = "2015";

std::string compressor_names[num_formats] =
  { "bzip2", "gzip", "lzip", "xz" };		// default compressor names

// args to compressors, maybe empty
std::vector< std::string > compressor_args[num_formats];

// vector of enabled formats plus [num_formats] for uncompressed.
// empty means all enabled.
std::vector< bool > enabled_formats;

struct { const char * from; const char * to; int format_index; } const
  known_extensions[] = {
  { ".bz2",  "",     fmt_bz2 },
  { ".tbz",  ".tar", fmt_bz2 },
  { ".tbz2", ".tar", fmt_bz2 },
  { ".gz",   "",     fmt_gz },
  { ".tgz",  ".tar", fmt_gz },
  { ".lz",   "",     fmt_lz },
  { ".tlz",  ".tar", fmt_lz },
  { ".xz",   "",     fmt_xz },
  { ".txz",  ".tar", fmt_xz },
  { 0,       0,      -1 } };


int my_fgetc( FILE * const f )
  {
  int ch;
  bool comment = false;

  do {
    ch = std::fgetc( f );
    if( ch == '#' ) comment = true;
    else if( ch == '\n' || ch == EOF ) comment = false;
    else if( ch == '\\' && comment )
      {
      const int c = std::fgetc( f );
      if( c == '\n' ) { std::ungetc( c, f ); comment = false; }
      }
    }
  while( comment );
  return ch;
  }


// Returns the parity of escapes (backslashes) at the end of a string.
bool trailing_escape( const std::string & s )
  {
  unsigned len = s.size();
  bool odd_escape = false;
  while( len > 0 && s[--len] == '\\' ) odd_escape = !odd_escape;
  return odd_escape;
  }


// Read a line discarding comments, leading whitespace and blank lines.
// Escaped newlines are discarded.
// Returns the empty string if at EOF.
//
const std::string & my_fgets( FILE * const f, int & linenum )
  {
  static std::string s;
  bool strip = true;			// strip leading whitespace
  s.clear();

  while( true )
    {
    int ch = my_fgetc( f );
    if( strip )
      {
      strip = false;
      while( std::isspace( ch ) )
        { if( ch == '\n' ) { ++linenum; } ch = my_fgetc( f ); }
      }
    if( ch == EOF ) { if( s.size() ) { ++linenum; } break; }
    else if( ch == '\n' )
      {
      ++linenum; strip = true;
      if( trailing_escape( s ) ) s.erase( s.size() - 1 );
      else if( s.size() ) break;
      }
    else s += ch;
    }
  return s;
  }


bool parse_compressor_command( const std::string & s, int i,
                               const int format_index )
  {
  const int len = s.size();
  while( i < len && std::isspace( s[i] ) ) ++i;		// strip spaces
  int l = i;
  while( i < len && !std::isspace( s[i] ) ) ++i;
  if( l >= i || s[l] == '-' ) return false;
  compressor_names[format_index].assign( s, l, i - l );

  compressor_args[format_index].clear();
  while( i < len )
    {
    while( i < len && std::isspace( s[i] ) ) ++i;	// strip spaces
    l = i;
    while( i < len && !std::isspace( s[i] ) ) ++i;
    if( l < i )
      compressor_args[format_index].push_back( std::string( s, l, i - l ) );
    }
  return true;
  }


bool parse_rc_line( const std::string & line,
                    const char * const filename, const int linenum )
  {
  const int len = line.size();
  int i = 0;
  while( i < len && std::isspace( line[i] ) ) ++i;	// strip spaces
  int l = i;
  while( i < len && line[i] != '=' && !std::isspace( line[i] ) ) ++i;
  if( l >= i )
    { if( verbosity >= 0 )
        std::fprintf( stderr, "%s %d: missing format name.\n", filename, linenum );
      return false; }
  const std::string name( line, l, i - l );
  int format_index = -1;
  for( int j = 0; j < num_formats; ++j )
    if( name == format_names[j] ) { format_index = j; break; }
  if( format_index < 0 )
    { if( verbosity >= 0 )
        std::fprintf( stderr, "%s %d: bad format name '%s'\n",
                      filename, linenum, name.c_str() );
      return false; }

  while( i < len && std::isspace( line[i] ) ) ++i;	// strip spaces
  if( i <= 0 || i >= len || line[i] != '=' )
    { if( verbosity >= 0 )
        std::fprintf( stderr, "%s %d: missing '='.\n", filename, linenum );
      return false; }
  ++i;							// skip the '='
  if( !parse_compressor_command( line, i, format_index ) )
    {
    if( verbosity >= 0 )
      std::fprintf( stderr, "%s %d: missing compressor name.\n", filename, linenum );
    return false;
    }
  return true;
  }


    // Returns 0 for success, 1 for file not found, 2 for syntax error.
int process_rcfile( const std::string & name )
  {
  FILE * const f = std::fopen( name.c_str(), "r" );
  if( !f ) return 1;

  int linenum = 0;
  int retval = 0;

  while( true )
    {
    const std::string & line = my_fgets( f, linenum );
    if( line.empty() ) break;				// EOF
    if( !parse_rc_line( line, name.c_str(), linenum ) )
      { retval = 2; break; }
    }
  std::fclose( f );
  return retval;
  }

} // end namespace


bool enabled_format( const int format_index )
  {
  if( enabled_formats.size() <= num_formats ) return true;
  if( format_index < 0 ) return enabled_formats[num_formats];
  return enabled_formats[format_index];
  }


void parse_format_list( const std::string & arg )
  {
  const std::string un( "uncompressed" );
  bool error = arg.empty();
  enabled_formats.assign( num_formats + 1, false );

  for( unsigned l = 0, r; l < arg.size(); l = r + 1 )
    {
    r = std::min( arg.find( ',', l ), arg.size() );
    if( l >= r ) { error = true; break; }		// empty format
    int format_index = num_formats;
    const std::string s( arg, l, r - l );
    for( int i = 0; i < num_formats; ++i )
      if( s == format_names[i] )
        { format_index = i; break; }
    if( format_index == num_formats && un.find( s ) != 0 )
      { error = true; break; }
    enabled_formats[format_index] = true;
    }
  if( error )
    { show_error( "Bad argument for '--format' option." ); std::exit( 1 ); }
  }


int parse_format_type( const std::string & arg )
  {
  for( int i = 0; i < num_formats; ++i )
    if( arg == format_names[i] )
      return i;
  show_error( "Bad argument for '--force-format' option." );
  std::exit( 1 );
  }


int extension_index( const std::string & name )
  {
  for( int i = 0; known_extensions[i].from; ++i )
    {
    const std::string ext( known_extensions[i].from );
    if( name.size() > ext.size() &&
        name.compare( name.size() - ext.size(), ext.size(), ext ) == 0 )
      return i;
    }
  return -1;
  }

int extension_format( const int eindex )
  { return ( eindex >= 0 ) ? known_extensions[eindex].format_index : -1; }

const char * extension_from( const int eindex )
  { return known_extensions[eindex].from; }

const char * extension_to( const int eindex )
  { return known_extensions[eindex].to; }


void maybe_process_config_file( const Arg_parser & parser )
  {
  for( int i = 0; i < parser.arguments(); ++i )
    if( parser.code( i ) == 'N' ) return;
  std::string name;
  const char * p = std::getenv( "HOME" ); if( p ) name = p;
  if( name.size() )
    {
    name += "/."; name += config_file_name;
    const int retval = process_rcfile( name );
    if( retval == 0 ) return;
    if( retval == 2 ) std::exit( 2 );
    }
  name = SYSCONFDIR; name += '/'; name += config_file_name;
  const int retval = process_rcfile( name );
  if( retval == 2 ) std::exit( 2 );
  }


void parse_compressor( const std::string & arg, const int format_index,
                       const int eretval )
  {
  if( !parse_compressor_command( arg, 0, format_index ) )
    { show_error( "Missing compressor name." ); std::exit( eretval ); }
  }


const char * get_compressor_name( const int format_index )
  {
  if( format_index >= 0 && format_index < num_formats &&
      compressor_names[format_index].size() )
    return compressor_names[format_index].c_str();
  return 0;
  }


const std::vector< std::string > & get_compressor_args( const int format_index )
  {
  return compressor_args[format_index];
  }


void show_help_addr()
  {
  std::printf( "\nReport bugs to zutils-bug@nongnu.org\n"
               "Zutils home page: http://www.nongnu.org/zutils/zutils.html\n" );
  }


void show_version()
  {
  std::printf( "%s (zutils) %s\n", program_name, PROGVERSION );
  std::printf( "Copyright (C) %s Antonio Diaz Diaz.\n", program_year );
  std::printf( "License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html>\n"
               "This is free software: you are free to change and redistribute it.\n"
               "There is NO WARRANTY, to the extent permitted by law.\n" );
  }


void show_error( const char * const msg, const int errcode, const bool help )
  {
  if( verbosity >= 0 )
    {
    if( msg && msg[0] )
      {
      std::fprintf( stderr, "%s: %s", program_name, msg );
      if( errcode > 0 )
        std::fprintf( stderr, ": %s.", std::strerror( errcode ) );
      std::fprintf( stderr, "\n" );
      }
    if( help )
      std::fprintf( stderr, "Try '%s --help' for more information.\n",
                    invocation_name );
    }
  }


void show_error2( const char * const msg, const char * const name )
  {
  if( verbosity >= 0 )
    std::fprintf( stderr, "%s: %s '%s': %s.\n",
                  program_name, msg, name, std::strerror( errno ) );
  }


void internal_error( const char * const msg )
  {
  if( verbosity >= 0 )
    std::fprintf( stderr, "%s: internal error: %s\n", program_name, msg );
  std::exit( 3 );
  }


void show_close_error( const char * const prog_name )
  {
  if( verbosity >= 0 )
    std::fprintf( stderr, "%s: Can't close output of %s: %s.\n",
                  program_name, prog_name, std::strerror( errno ) );
  }


void show_exec_error( const char * const prog_name )
  {
  if( verbosity >= 0 )
    std::fprintf( stderr, "%s: Can't exec '%s': %s.\n",
                  program_name, prog_name, std::strerror( errno ) );
  }


void show_fork_error( const char * const prog_name )
  {
  if( verbosity >= 0 )
    std::fprintf( stderr, "%s: Can't fork '%s': %s.\n",
                  program_name, prog_name, std::strerror( errno ) );
  }


int wait_for_child( const pid_t pid, const char * const name,
                    const int eretval, const bool isgzxz )
  {
  int status;
  while( waitpid( pid, &status, 0 ) == -1 )
    {
    if( errno != EINTR )
      {
      if( verbosity >= 0 )
        std::fprintf( stderr, "%s: Error waiting termination of '%s': %s.\n",
                      program_name, name, std::strerror( errno ) );
      _exit( eretval );
      }
    }
  if( WIFEXITED( status ) )
    {
    const int tmp = WEXITSTATUS( status );
    if( isgzxz && eretval == 1 && tmp == 1 ) return 2;		// for ztest
    return tmp;
    }
  return eretval;
  }
