/*  Zutils - Utilities dealing with compressed files
    Copyright (C) 2009, 2010, 2011 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 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 <http://www.gnu.org/licenses/>.
*/

#define _FILE_OFFSET_BITS 64

#include <cerrno>
#include <climits>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <list>
#include <string>
#include <vector>
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/stat.h>
#if defined(__MSVCRT__) || defined(__OS2__)
#include <io.h>
#endif

#include "arg_parser.h"
#include "zutils.h"

#if CHAR_BIT != 8
#error "Environments where CHAR_BIT != 8 are not supported."
#endif


namespace {

#ifdef O_BINARY
const int o_binary = O_BINARY;
#else
const int o_binary = 0;
#endif

enum Mode { m_none, m_zcat, m_zgrep, m_ztest };


void show_help() throw()
  {
  std::printf( "Zutils is a collection of utilities able to deal with any combination of\n" );
  std::printf( "compressed and non-compressed files transparently. If any given file,\n" );
  std::printf( "including standard input, is compressed, its uncompressed content is used.\n" );
  std::printf( "The supported compressors are bzip2, gzip, lzip and xz.\n" );
  std::printf( "\nUsage: %s <operation> [options] [files]\n", invocation_name );
  std::printf( "\nTry `%s <operation> --help' for more specific help.\n", invocation_name );
  std::printf( "\nOperations:\n" );
  std::printf( "  -h, --help                 display this help and exit\n" );
  std::printf( "  -V, --version              output version information and exit\n" );
  std::printf( "      --zcat                 zcat operation\n" );
  std::printf( "      --zgrep                zgrep operation\n" );
  std::printf( "      --ztest                ztest operation\n" );
  show_help_addr();
  }


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


int open_instream( std::string & input_filename, const Mode program_mode ) throw()
  {
  int infd = open( input_filename.c_str(), O_RDONLY | o_binary );
  if( infd < 0 )
    {
    if( ( program_mode == m_zcat || program_mode == m_zgrep )
        && simple_extension_index( input_filename ) < 0 )
      {
      for( int i = 0; simple_extensions[i]; ++i )
        {
        const std::string name( input_filename + simple_extensions[i] );
        infd = open( name.c_str(), O_RDONLY | o_binary );
        if( infd >= 0 ) { input_filename = name; break; }
        }
      }
    if( infd < 0 )
      show_error2( "Can't open input file", input_filename.c_str() );
    }
  return infd;
  }

#include "zcat.cc"
#include "zgrep.cc"
#include "ztest.cc"

} // end namespace


int main( const int argc, const char * const argv[] )
  {
  enum { help_opt = 256, verbose_opt, zcat_opt, zgrep_opt, ztest_opt };
  const Arg_parser::Option * options = 0;
  int infd = -1;
  Mode program_mode = m_none;
  bool recursive = false;
  std::string input_filename;
  std::list< std::string > filenames;
  Cat_options cat_options;
  std::vector< const char * > grep_args;	// args to grep, maybe empty
  std::vector< const char * > ztest_args;	// args to ztest, maybe empty
  invocation_name = argv[0];

  const Arg_parser::Option m_zcat_options[] =
    {
    { 'A', "show-all",         Arg_parser::no  },	// cat
    { 'b', "number-nonblank",  Arg_parser::no  },	// cat
    { 'c', "stdout",           Arg_parser::no  },	// gzip
    { 'd', "decompress",       Arg_parser::no  },	// gzip
    { 'e',  0,                 Arg_parser::no  },	// cat
    { 'E', "show-ends",        Arg_parser::no  },	// cat
    { 'f', "force",            Arg_parser::no  },	// gzip
    { 'h', "help",             Arg_parser::no  },
    { 'l', "list",             Arg_parser::no  },	// gzip
    { 'L', "license",          Arg_parser::no  },	// gzip
    { 'n', "number",           Arg_parser::no  },	// cat
    { 'q', "quiet",            Arg_parser::no  },
    { 'r', "recursive",        Arg_parser::no  },
    { 's', "squeeze-blank",    Arg_parser::no  },	// cat
    { 't',  0,                 Arg_parser::no  },	// cat
    { 'T', "show-tabs",        Arg_parser::no  },	// cat
    { 'v', "show-nonprinting", Arg_parser::no  },	// cat
    { 'V', "version",          Arg_parser::no  },
    { verbose_opt, "verbose",  Arg_parser::no  },
    { zcat_opt,    "zcat",     Arg_parser::no  },
    {  0 ,  0,                 Arg_parser::no  } };

  const Arg_parser::Option m_zgrep_options[] =
    {
    { 'a', "text",                  Arg_parser::no  },	// grep GNU
    { 'A', "after-context",         Arg_parser::yes },	// grep GNU
    { 'b', "byte-offset",           Arg_parser::no  },	// grep GNU
    { 'B', "before-context",        Arg_parser::yes },	// grep GNU
    { 'c', "count",                 Arg_parser::no  },	// grep
    { 'C', "context",               Arg_parser::yes },	// grep GNU
    { 'e', "regexp",                Arg_parser::yes },	// grep
    { 'E', "extended-regexp",       Arg_parser::no  },	// grep
    { 'f', "file ",                 Arg_parser::yes },	// grep
    { 'F', "fixed-strings",         Arg_parser::no  },	// grep
    { 'h', "no-filename",           Arg_parser::no  },	// grep GNU
    { 'H', "with-filename",         Arg_parser::no  },	// grep GNU
    { 'i', "ignore-case",           Arg_parser::no  },	// grep
    { 'I',  0,                      Arg_parser::no  },	// grep GNU
    { 'l', "files-with-matches",    Arg_parser::no  },	// grep
    { 'L', "files-without-match",   Arg_parser::no  },	// grep GNU
    { 'm', "max-count",             Arg_parser::yes },	// grep GNU
    { 'n', "line-number",           Arg_parser::no  },	// grep
    { 'o', "only-matching",         Arg_parser::no  },	// grep
    { 'q', "quiet",                 Arg_parser::no  },
    { 'r', "recursive",             Arg_parser::no  },
    { 's', "no-messages",           Arg_parser::no  },	// grep
    { 'v', "invert-match",          Arg_parser::no  },	// grep
    { 'V', "version",               Arg_parser::no  },
    { 'w', "word-regexp",           Arg_parser::no  },	// grep GNU
    { 'x', "line-regexp",           Arg_parser::no  },	// grep
    { help_opt,    "help",          Arg_parser::no  },
    { verbose_opt, "verbose",       Arg_parser::no  },
    { zgrep_opt,   "zgrep",         Arg_parser::no  },
    {  0 ,  0,                      Arg_parser::no  } };

  const Arg_parser::Option m_ztest_options[] =
    {
    { 'h', "help",        Arg_parser::no  },
    { 'q', "quiet",       Arg_parser::no  },
    { 'r', "recursive",   Arg_parser::no  },
    { 'v', "verbose",     Arg_parser::no  },
    { 'V', "version",     Arg_parser::no  },
    { ztest_opt, "ztest", Arg_parser::no  },
    {  0 ,  0,            Arg_parser::no  } };

  { // parse operation
  const Arg_parser::Option operations[] =
    {
    { 'h', "help",              Arg_parser::no  },
    { 'V', "version",           Arg_parser::no  },
    { zcat_opt,  "zcat",        Arg_parser::no  },
    { zgrep_opt, "zgrep",       Arg_parser::no  },
    { ztest_opt, "ztest",       Arg_parser::no  },
    {  0 ,  0,                  Arg_parser::no  } };

  const Arg_parser parser( argv[1], ( argc > 2 ) ? argv[2] : 0, operations );
  if( parser.error().size() )				// bad operation
    { show_error( parser.error().c_str(), 0, true ); return 1; }

  if( parser.arguments() > 0 )
    {
    switch( parser.code( 0 ) )
      {
      case  0 : break;
      case 'h': show_help(); return 0;
      case 'V': show_version(); return 0;
      case zcat_opt  : program_mode = m_zcat; options = m_zcat_options;
                       util_name = "zcat"; break;
      case zgrep_opt : program_mode = m_zgrep; options = m_zgrep_options;
                       util_name = "zgrep"; break;
      case ztest_opt : program_mode = m_ztest; options = m_ztest_options;
                       util_name = "ztest"; break;
      default : internal_error( "uncaught option" );
      }
    }

#if defined(__MSVCRT__) || defined(__OS2__)
  _setmode( STDIN_FILENO, O_BINARY );
  _setmode( STDOUT_FILENO, O_BINARY );
#endif

  if( program_mode == m_none )
    {
    show_error( "You must specify the operation to be performed.", 0, true );
    return 1;
    }
  } // end parse operation

  const Arg_parser parser( argc, argv, options );
  if( parser.error().size() )				// bad option
    { show_error( parser.error().c_str(), 0, true );
      return ( program_mode == m_zcat ) ? 1 : 2; }

  int argind = 0;
  bool grep_list = false;
  bool grep_show_name = true;
  bool grep_pattern_found = false;
  for( ; argind < parser.arguments(); ++argind )
    {
    const int code = parser.code( argind );
    const char * arg = parser.argument( argind ).c_str();
    if( !code )
      {
      if( program_mode == m_zgrep && !grep_pattern_found )
        { grep_args.push_back( arg ); grep_pattern_found = true; continue; }
      else break;					// no more options
      }
    switch( program_mode )
      {
      case m_none: internal_error( "invalid operation" );
      case m_zcat:
        switch( code )
          {
          case 'A': cat_options.show_ends = true;
                    cat_options.show_nonprinting = true;
                    cat_options.show_tabs = true; break;
          case 'b': cat_options.number_lines = 1; break;
          case 'c': break;
          case 'd': break;
          case 'e': cat_options.show_nonprinting = true;  // fall through
          case 'E': cat_options.show_ends = true; break;
          case 'f': break;
          case 'h': show_zcat_help(); return 0;
          case 'l': break;
          case 'L': break;
          case 'n': if( cat_options.number_lines == 0 )
                      { cat_options.number_lines = 2; } break;
          case 'q': verbosity = -1; break;
          case 'r': recursive = true; break;
          case 's': cat_options.squeeze_blank = true; break;
          case 't': cat_options.show_nonprinting = true;  // fall through
          case 'T': cat_options.show_tabs = true; break;
          case 'v': cat_options.show_nonprinting = true; break;
          case 'V': show_version( "Zcat" ); return 0;
          case verbose_opt : if( verbosity < 4 ) ++verbosity; break;
          case zcat_opt : break;
          default : internal_error( "uncaught option" );
          } break;
      case m_zgrep:
        switch( code )
          {
          case 'a': grep_args.push_back( "-a" ); break;
          case 'A': grep_args.push_back( "-A" ); grep_args.push_back( arg ); break;
          case 'b': grep_args.push_back( "-b" ); break;
          case 'B': grep_args.push_back( "-B" ); grep_args.push_back( arg ); break;
          case 'c': grep_args.push_back( "-c" ); break;
          case 'C': grep_args.push_back( "-C" ); grep_args.push_back( arg ); break;
          case 'e': grep_args.push_back( "-e" ); grep_args.push_back( arg );
                    grep_pattern_found = true; break;
          case 'E': grep_args.push_back( "-E" ); break;
          case 'f': grep_args.push_back( "-f" ); grep_args.push_back( arg );
                    grep_pattern_found = true; break;
          case 'F': grep_args.push_back( "-F" ); break;
          case 'h': grep_show_name = false; break;
          case 'H': grep_show_name = true; break;
          case 'i': grep_args.push_back( "-i" ); break;
          case 'I': grep_args.push_back( "-I" ); break;
          case 'l': grep_args.push_back( "-l" ); grep_list = true; break;
          case 'L': grep_args.push_back( "-L" ); grep_list = true; break;
          case 'm': grep_args.push_back( "-m" ); grep_args.push_back( arg ); break;
          case 'n': grep_args.push_back( "-n" ); break;
          case 'o': grep_args.push_back( "-o" ); break;
          case 'q': grep_args.push_back( "-q" ); verbosity = -1; break;
          case 'r': recursive = true; break;
          case 's': grep_args.push_back( "-s" ); verbosity = -1; break;
          case 'v': grep_args.push_back( "-v" ); break;
          case 'V': show_version( "Zgrep" ); return 0;
          case 'w': grep_args.push_back( "-w" ); break;
          case 'x': grep_args.push_back( "-x" ); break;
          case help_opt    : show_zgrep_help(); return 0;
          case verbose_opt : if( verbosity < 4 ) ++verbosity; break;
          case zgrep_opt   : break;
          default : internal_error( "uncaught option" );
          } break;
      case m_ztest:
        switch( code )
          {
          case 'h': show_ztest_help(); return 0;
          case 'q': verbosity = -1; ztest_args.push_back( "-q" ); break;
          case 'r': recursive = true; break;
          case 'v': if( verbosity < 4 ) ++verbosity;
                    ztest_args.push_back( "-v" ); break;
          case 'V': show_version( "Ztest" ); return 0;
          case ztest_opt : break;
          default : internal_error( "uncaught option" );
          } break;
      }
    } // end process options

  if( program_mode == m_zgrep && !grep_pattern_found )
    { show_error( "Pattern not found.", 0, true ); return 2; }

  bool filenames_given = false;
  for( ; argind < parser.arguments(); ++argind )
    {
    if( parser.argument( argind ) != "-" ) filenames_given = true;
    filenames.push_back( parser.argument( argind ) );
    }

  if( filenames.empty() ) filenames.push_back("-");

  int retval = ( ( program_mode == m_zgrep ) ? 1 : 0 );
  while( !filenames.empty() )
    {
    input_filename = filenames.front();
    filenames.pop_front();
    if( !input_filename.size() || input_filename == "-" )
      {
      input_filename.clear();
      infd = STDIN_FILENO;
      }
    else
      {
      if( recursive )
        {
        struct stat st;
        if( !stat( input_filename.c_str(), &st ) && S_ISDIR( st.st_mode ) )
          {
          DIR * const dirp = opendir( input_filename.c_str() );
          if( !dirp )
            {
            show_error2( "Can't open directory", input_filename.c_str() );
            if( retval < 1 ) retval = 1; continue;
            }
          std::list< std::string > tmp_list;
          while( true )
            {
            const struct dirent * const entryp = readdir( dirp );
            if( !entryp ) { closedir( dirp ); break; }
            std::string tmp_name( entryp->d_name );
            if( tmp_name != "." && tmp_name != ".." )
              tmp_list.push_back( input_filename + "/" + tmp_name );
            }
          filenames.splice( filenames.begin(), tmp_list );
          continue;
          }
        }
      infd = open_instream( input_filename, program_mode );
      if( infd < 0 ) { if( retval < 1 ) retval = 1; continue; }
      }

    int tmp = 0;
    switch( program_mode )
      {
      case m_none:
        break;
      case m_zcat:
        tmp = cat( infd, input_filename, cat_options );
        break;
      case m_zgrep:
        if( infd == STDIN_FILENO )
          tmp = zgrep_stdin( infd, grep_args );
        else tmp = zgrep_file( infd, input_filename, grep_args,
                               grep_list, grep_show_name );
        break;
      case m_ztest:
        if( infd == STDIN_FILENO )
          tmp = ztest_stdin( infd, ztest_args );
        else tmp = ztest_file( infd, input_filename, ztest_args );
        break;
      }
    if( program_mode == m_zgrep )
      { if( tmp == 0 || ( tmp == 2 && retval == 1 ) ) retval = tmp; }
    else if( tmp > retval ) retval = tmp;

    if( input_filename.size() )
      { close( infd ); infd = -1; }
    }

  if( std::fclose( stdout ) != 0 )
    {
    show_error( "Can't close stdout", errno );
    switch( program_mode )
      {
      case m_none: break;
      case m_zcat: retval = 1; break;
      case m_zgrep: if( retval != 0 || verbosity >= 0 ) retval = 2; break;
      case m_ztest: if( retval == 0 ) retval = 1; break;
      }
    }
  return retval;
  }
