/*  GNU Moe - My Own Editor
    Copyright (C) 2005, 2006, 2007, 2008, 2009 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/>.
*/

#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#include "arg_parser.h"
#include "buffer.h"
#include "buffer_handle.h"
#include "menu.h"
#include "rc.h"
#include "regex.h"


namespace RC {

const Arg_parser * parserp;

// priority of _default_buffer_options by place of specification:
// ( 0 = lowest, 3 = highest )
// 0 default priority
// 1 priority of option specified in rc file as 'default file option'
// 2 priority of option specified in rc file as 'file name dependent option'
// 3 priority of option specified in command line as 'default file option'
//
struct Buffer_options_priorities
  {
  int lmargin, rmargin;
  int auto_indent, overwrite, read_only, word_wrap;

  Buffer_options_priorities() throw()
    : lmargin( 0 ), rmargin( 0 ),
    auto_indent( 0 ), overwrite( 0 ), read_only( 0 ), word_wrap( 0 ) {}

  void reset() throw() { *this = Buffer_options_priorities(); }
  };


struct Regex_options
  {
  std::vector< std::string > regex_vector;
  Buffer::Options buffer_options;
  };


Options _editor_options;
Buffer::Options _default_buffer_options;
Buffer_options_priorities priorities;
std::vector< Regex_options > regex_options_vector;


enum Optcode
  {
  auto_unmark = 'u', backup = 'b', /*beep = 'p',*/ exit_ask = 'e',
  ignore_case = 'i', indent_step = 'n', keep_lines = 'k', max_windows = 'm',
  orphan_extra= '1', rectangle_mode = 'x', search_wrap = 's', smart_home = 'H',
  auto_indent = 'a', lmargin = 'l', rmargin = 'r', overwrite = 'O',
  read_only = 'o', word_wrap = 'w',
  no_auto_unmark = 256, no_backup, /*no_beep,*/ no_exit_ask, no_ignore_case,
  no_rectangle_mode, no_search_wrap, no_smart_home, no_auto_indent,
  no_overwrite, no_read_only, no_word_wrap
  };

const Arg_parser::Option options[] =
  {
  { 'h',               "help",           Arg_parser::no  },
  { 'V',               "version",        Arg_parser::no  },
  { auto_unmark,       "auto-unmark",    Arg_parser::no  },
  { no_auto_unmark,    "no-auto-unmark", Arg_parser::no  },
  { backup,            "backup",         Arg_parser::no  },
  { no_backup,         "no-backup",      Arg_parser::no  },
//  { beep,              "beep",           Arg_parser::no  },
//  { no_beep,           "no-beep",        Arg_parser::no  },
  { exit_ask,          "exit-ask",       Arg_parser::no  },
  { no_exit_ask,       "no-exit-ask",    Arg_parser::no  },
  { ignore_case,       "ignore-case",    Arg_parser::no  },
  { no_ignore_case,    "no-ignore-case", Arg_parser::no  },
  { indent_step,       "indent-step",    Arg_parser::yes },
  { keep_lines,        "keep-lines",     Arg_parser::yes },
  { max_windows,       "max-windows",    Arg_parser::yes },
  { orphan_extra,      "orphan",         Arg_parser::no  },
  { rectangle_mode,    "rectangle",      Arg_parser::no  },
  { no_rectangle_mode, "no-rectangle",   Arg_parser::no  },
  { search_wrap,       "search-wrap",    Arg_parser::no  },
  { no_search_wrap,    "no-search-wrap", Arg_parser::no  },
  { smart_home,        "smart-home",     Arg_parser::no  },
  { no_smart_home,     "no-smart-home",  Arg_parser::no  },
  { auto_indent,       "auto-indent",    Arg_parser::no  },
  { no_auto_indent,    "no-auto-indent", Arg_parser::no  },
  { lmargin,           "lmargin",        Arg_parser::yes },
  { rmargin,           "rmargin",        Arg_parser::yes },
  { overwrite,         "overwrite",      Arg_parser::no  },
  { no_overwrite,      "no-overwrite",   Arg_parser::no  },
  { read_only,         "read-only",      Arg_parser::no  },
  { no_read_only,      "no-read-only",   Arg_parser::no  },
  { word_wrap,         "word-wrap",      Arg_parser::no  },
  { no_word_wrap,      "no-word-wrap",   Arg_parser::no  },
  { 0,                 0,                Arg_parser::no  } };


const char * optname( const int code ) throw()
  {
  static char buf[2] = "?";

  if( code != 0 )
    for( int i = 0; options[i].code; ++i )
      if( code == options[i].code )
        { if( options[i].name ) return options[i].name; else break; }
  if( code > 0 && code < 256 ) buf[0] = code; else buf[0] = '?';
  return buf;
  }


bool is_buffer_option( const int code ) throw()
  {
  switch( code )
    {
    case auto_indent   :
    case no_auto_indent:
    case lmargin       :
    case rmargin       :
    case overwrite     :
    case no_overwrite  :
    case read_only     :
    case no_read_only  :
    case word_wrap     :
    case no_word_wrap  : return true;
    }
  return false;
  }


bool is_editor_option( const int code ) throw()
  { return ( !is_buffer_option( code ) ); }


void set_bool_option( bool & option, bool value,
                      int & option_priority, int priority = 0 ) throw()
  {
  if( !priority || priority >= option_priority ) option = value;
  if( priority > option_priority ) option_priority = priority;
  }


bool set_int_option( int & option, const std::string & arg,
                     int & option_priority, int priority = 0 ) throw()
  {
  bool good = true;
  if( ( !priority || priority >= option_priority ) && arg.size() )
    good = parse_int( arg, option );
  if( priority > option_priority ) option_priority = priority;
  return good;
  }


bool set_option_from_code( const int code, const std::string & arg,
                           Options *edop, Buffer::Options *bufop,
                           int priority = 0, const char * msg = "" )
  {
  if( ( is_editor_option( code ) && !edop ) ||
      ( is_buffer_option( code ) && !bufop ) )
    { std::fprintf( stderr, "%serror: option `%s' not allowed here\n", msg, optname( code ) );
      return false; }

  int errcode = 0;
  switch( code )
    {
    case auto_unmark:       edop->auto_unmark = true; break;
    case no_auto_unmark:    edop->auto_unmark = false; break;
    case backup:            edop->backup = true; break;
    case no_backup:         edop->backup = false; break;
//    case beep:              edop->beep = true; break;
//    case no_beep:           edop->beep = false; break;
    case exit_ask:          edop->exit_ask = true; break;
    case no_exit_ask:       edop->exit_ask = false; break;
    case ignore_case:       edop->ignore_case = true; break;
    case no_ignore_case:    edop->ignore_case = false; break;
    case indent_step:       if( !parse_int( arg, edop->indent_step ) ) errcode = 1;
                            break;
    case keep_lines:        if( !parse_int( arg, edop->keep_lines ) ) errcode = 1;
                            break;
    case max_windows:       if( !parse_int( arg, edop->max_windows ) ) errcode = 1;
                            break;
    case orphan_extra:      edop->orphan_extra = true; break;
    case rectangle_mode:    edop->rectangle_mode = true; break;
    case no_rectangle_mode: edop->rectangle_mode = false; break;
    case smart_home:        edop->smart_home = true; break;
    case no_smart_home:     edop->smart_home = false; break;
    case search_wrap:       edop->search_wrap = true; break;
    case no_search_wrap:    edop->search_wrap = false; break;
    case auto_indent:
      set_bool_option( bufop->auto_indent, true, priorities.auto_indent, priority ); break;
    case no_auto_indent:
      set_bool_option( bufop->auto_indent, false, priorities.auto_indent, priority ); break;
    case lmargin:
      {
      int i;
      if( !set_int_option( i, arg, priorities.lmargin, priority ) ) errcode = 1;
      else if( !bufop->set_lmargin( i - 1 ) ) errcode = 2;
      } break;
    case rmargin:
      {
      int i;
      if( !set_int_option( i, arg, priorities.rmargin, priority ) ) errcode = 1;
      else if( !bufop->set_rmargin( i - 1 ) ) errcode = 2;
      } break;
    case overwrite:
      set_bool_option( bufop->overwrite, true, priorities.overwrite, priority ); break;
    case no_overwrite:
      set_bool_option( bufop->overwrite, false, priorities.overwrite, priority ); break;
    case read_only:
      set_bool_option( bufop->read_only, true, priorities.read_only, priority ); break;
    case no_read_only:
      set_bool_option( bufop->read_only, false, priorities.read_only, priority ); break;
    case word_wrap:
      set_bool_option( bufop->word_wrap, true, priorities.word_wrap, priority ); break;
    case no_word_wrap:
      set_bool_option( bufop->word_wrap, false, priorities.word_wrap, priority ); break;
    default: std::fprintf( stderr, "%sinternal error: uncaught option\n", msg );
      return false;
    }

  if( !errcode ) return true;
  if( errcode == 1 )
    std::fprintf( stderr, "%serror: bad argument for option `%s'\n", msg, optname( code ) );
  else if( errcode == 2 )
    std::fprintf( stderr, "%serror: value out of range for option `%s'\n", msg, optname( code ) );
  return false;
  }

} // end namespace RC


RC::Options & RC::editor_options() throw() { return _editor_options; }


const Buffer::Options & RC::default_buffer_options() throw()
  { return _default_buffer_options; }


void RC::apply_regex_options( Buffer & buffer ) throw()
  {
  for( unsigned int i = 0; i < regex_options_vector.size(); ++i )
    {
    const Regex_options & ro = regex_options_vector[i];
    unsigned int j = 0;
    for( ; j < ro.regex_vector.size(); ++j )
      if( Regex::match_filename( ro.regex_vector[j], Menu::my_basename( buffer.name() ) ) )
        break;
    if( j >= ro.regex_vector.size() ) continue;
    if( priorities.lmargin <= 2 )
      buffer.options.set_lmargin( ro.buffer_options.lmargin() );
    if( priorities.rmargin <= 2 )
      buffer.options.set_rmargin( ro.buffer_options.rmargin() );
    if( priorities.auto_indent <= 2 )
      buffer.options.auto_indent = ro.buffer_options.auto_indent;
    if( priorities.overwrite <= 2 )
      buffer.options.overwrite = ro.buffer_options.overwrite;
    if( priorities.read_only <= 2 )
      buffer.options.read_only |= ro.buffer_options.read_only;
    if( priorities.word_wrap <= 2 )
      buffer.options.word_wrap = ro.buffer_options.word_wrap;
    break;
    }
  }


bool RC::parse_int( const std::string & s, int & result ) throw()
  {
  if( s.size() == 0 ) return false;
  const char *str = s.c_str();
  char *tail;
  result = std::strtol( str, &tail, 0 );
  return ( tail != str );
  }


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

  std::fprintf( stderr, "Processing `%s'... ", name.c_str() );
  std::fflush( stderr );

  const char delimiters[] = " \t\r\n";
  int retval = 0;
  int regex_status = 0;		// 1 = got regex name, 2 got regex option
  const int bufsize = 1024;
  char buf[bufsize];
  for( int line = 1; ; ++line )
    {
    if( !std::fgets( buf, bufsize, f ) ) break;
    int len = 0;
    while( len < bufsize && buf[len] != '\n' ) ++len;
    if( len >= bufsize )
      {
      while( true )
        {
        const int ch = std::fgetc( f );
        if( ch == EOF || ch == '\n' ) break;
        }
      std::fprintf( stderr, "\n%s %d: error: line too long\n",
                    name.c_str(), line );
      retval = 2;
      }
    if( len == 0 || std::isspace( buf[0] ) ) continue;
    if( buf[0] != '-' )		// Create new file name dependent options
      {
      const char * regex = std::strtok( buf, delimiters );
      if( regex_status != 1 )
        regex_options_vector.push_back( Regex_options() );
      regex_options_vector.back().regex_vector.push_back( regex );
      regex_status = 1;
      }
    else			// Set an option
      {
      const char * opt = std::strtok( buf, delimiters );
      const char * arg = std::strtok( 0, delimiters );
      char msg[80];
      snprintf( msg, sizeof msg, "\n%s %d: ", name.c_str(), line );
      Arg_parser parser( opt, arg, options );
      if( parser.error().size() || !parser.arguments() || !parser.code( 0 ) )
        { std::fprintf( stderr, "%s%s\n", msg, parser.error().c_str() );
          retval = 2; }
      else if( regex_status == 0 )
        { if( !set_option_from_code( parser.code( 0 ), parser.argument( 0 ),
                                     &_editor_options, &_default_buffer_options,
                                     1, msg ) ) retval = 2; }
      else
        {
        if( !set_option_from_code( parser.code( 0 ), parser.argument( 0 ), 0,
                                   &regex_options_vector.back().buffer_options,
                                   0, msg ) ) retval = 2;
        regex_status = 2;
        }
      }
    }
  std::fclose( f );
  if( !retval ) std::fprintf( stderr, "done\n" );
  return retval;
  }


    // Returns 0 if success, 1 if invalid option, 2 if a file can't be read.
int RC::process_options()
  {
  std::fprintf( stderr, "Processing options... " );
  std::fflush( stderr );

  int argind, line = 0;
  bool first_post = true;
  for( argind = 0; argind < parserp->arguments(); ++argind )
    {						// Process "global" options
    const int code = parserp->code( argind );
    const std::string & arg = parserp->argument( argind );
    if( !code )
      {
      if( arg[0] != '+' ) break;			// filename
      if( parse_int( arg, line ) && --line >= 0 ) continue;
      show_error( "bad line number" ); return 1;
      }
    if( !set_option_from_code( code, arg, &_editor_options,
                               &_default_buffer_options, 3,
                               first_post ? "\n" : "" ) )
      { first_post = false; return 1; }
    }

  bool non_regular_found = false;
  bool read_error_found = false;
  while( argind < parserp->arguments() )  // Process files and file options
    {
    Buffer * bufferp = 0;
    std::string name( parserp->argument( argind++ ) );
    Menu::tilde_expansion( name );
    if( Menu::is_regular( name, true ) )
      {
      try {
        const int i = Bufhandle_vector::find_or_add_handle( _default_buffer_options, &name, -1, line );
        bufferp = &Bufhandle_vector::handle( i ).buffer();
        }
      catch( Buffer::Error e )
        {
        if( first_post ) { first_post = false; std::fprintf( stderr, "\n" ); }
        std::fprintf( stderr, "warning: `%s': %s\n", name.c_str(), e.s );
        read_error_found = true;
        }
      }
    else
      {
      if( first_post ) { first_post = false; std::fprintf( stderr, "\n" ); }
      std::fprintf( stderr, "warning: `%s' is not a regular file\n", name.c_str() );
      non_regular_found = true;
      }
    line = 0;
    for( ; argind < parserp->arguments(); ++argind )	// Process file options
      {
      const int code = parserp->code( argind );
      const std::string & arg = parserp->argument( argind );
      if( !code )
        {
        if( arg[0] != '+' ) break;			// filename
        if( parse_int( arg, line ) && --line >= 0 ) continue;
        if( first_post ) { first_post = false; std::fprintf( stderr, "\n" ); }
        show_error( "bad line number" ); return 1;
        }
      if( bufferp &&
          !set_option_from_code( code, parserp->argument( argind ),
                                 0, &bufferp->options, 0,
                                 first_post ? "\n" : "" ) )
        { first_post = false; return 1; }
      }
    }
  // Add empty buffer if no files given or stdin is not a tty
  try { Bufhandle_vector::add_handle_if_pending_input_or_empty( line ); }
  catch( Buffer::Error e )
    {
    if( first_post ) { first_post = false; std::fprintf( stderr, "\n" ); }
    show_error( e.s ); return 1;
    }

  std::fprintf( stderr, "done\n" );
  delete parserp;
  if( non_regular_found || read_error_found )
    {
    if( non_regular_found )
      std::fprintf( stderr, "Warning: non-regular files will be ignored.\n" );
    if( read_error_found )
      std::fprintf( stderr, "Warning: one or more files couldn't be read.\n" );
    return 2;
    }
  return 0;
  }


int RC::read_options( const int argc, const char * const argv[] )
  {
  parserp = new Arg_parser( argc, argv, options, true );
  if( parserp->error().size() )				// bad option
    { show_error( parserp->error().c_str(), 0, true ); return 1; }
  for( int i = 0; i < parserp->arguments(); ++i )
    if( parserp->code( i ) == 'h' || parserp->code( i ) == 'V' )
      return parserp->code( i );
  return 0;
  }


void RC::reset() throw()
  {
  _editor_options.reset();
  _default_buffer_options.reset();
  priorities.reset();
  regex_options_vector.clear();
  }
