/*  GNU moe - My Own Editor
    Copyright (C) 2005-2017 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/>.
*/

#include <algorithm>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <list>
#include <string>
#include <vector>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>

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


// order of application of file options by place of specification:
// 1 options specified in rc file as 'default file options'.
// 2 options specified in rc file as 'file name dependent options'.
// 3 options specified in command line as 'default file options'.
// 4 options specified in command line as 'file options'.
// (4 is not applied to files opened during the editing sesion).

namespace RC {

const Arg_parser * parserp;

class File_options
  {
  struct Record
    {
    int code;
    int argument;
    Record( const int c, const int arg ) : code( c ), argument( arg ) {}
    };
  std::vector< Record > data;

public:
  File_options() {}
  void add_option( const int c, const int arg = 0 )
    { data.push_back( Record( c, arg ) ); }
  void reset() { data.clear(); }
  int size() const { return data.size(); }
  int code( const int i ) const { return data[i].code; }
  int argument( const int i ) const { return data[i].argument; }
  };


struct Regex_options
  {
  std::vector< std::string > regex_vector;
  File_options file_options;
  };


Options editor_options_;
File_options default_file_options;
File_options moerc_file_options;
std::vector< Regex_options > regex_options_vector;


enum Optcode
  {
  auto_unmark = 'u', backup = 'b', no_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_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 )
  {
  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_editor_option( const int code )
  {
  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 false;
    }
  return true;
  }


bool set_option_from_code( const int code, const std::string & arg,
                           File_options & file_options,
                           const char * const msg,
                           const bool allow_editor_options = false )
  {
  if( !allow_editor_options && is_editor_option( code ) )
    { std::fprintf( stderr, "%serror: Option '%s' not allowed here.\n",
                    msg, optname( code ) ); return false; }

  int errcode = 0, n;
  switch( code )
    {
    case auto_unmark:       editor_options_.auto_unmark = true; break;
    case no_auto_unmark:    editor_options_.auto_unmark = false; break;
    case backup:            editor_options_.backup = true; break;
    case no_backup:         editor_options_.backup = false; break;
//    case beep:              editor_options_.beep = true; break;
//    case no_beep:           editor_options_.beep = false; break;
    case exit_ask:          editor_options_.exit_ask = true; break;
    case no_exit_ask:       editor_options_.exit_ask = false; break;
    case ignore_case:       editor_options_.ignore_case = true; break;
    case no_ignore_case:    editor_options_.ignore_case = false; break;
    case indent_step:
      if( !parse_int( arg, n ) ) errcode = 1;
      else if( !editor_options_.set_indent_step( n ) ) errcode = 2;
      break;
    case keep_lines:
      if( !parse_int( arg, n ) ) errcode = 1;
      else if( !editor_options_.set_keep_lines( n ) ) errcode = 2;
      break;
    case max_windows:
      if( !parse_int( arg, n ) ) errcode = 1;
      else if( !editor_options_.set_max_windows( n ) ) errcode = 2;
      break;
    case orphan_extra:      editor_options_.orphan_extra = true; break;
    case rectangle_mode:    editor_options_.rectangle_mode = true; break;
    case no_rectangle_mode: editor_options_.rectangle_mode = false; break;
    case search_wrap:       editor_options_.search_wrap = true; break;
    case no_search_wrap:    editor_options_.search_wrap = false; break;
    case smart_home:        editor_options_.smart_home = true; break;
    case no_smart_home:     editor_options_.smart_home = false; break;

    case auto_indent:
    case no_auto_indent:
    case overwrite:
    case no_overwrite:
    case read_only:
    case no_read_only:
    case word_wrap:
    case no_word_wrap:      file_options.add_option( code ); break;
    case lmargin:
      if( !parse_int( arg, n ) ) errcode = 1;
      else if( !Buffer::Options().set_lmargin( n - 1 ) ) errcode = 2;
      else file_options.add_option( code, n - 1 );
      break;
    case rmargin:
      if( !parse_int( arg, n ) ) errcode = 1;
      else if( !Buffer::Options().set_rmargin( n - 1 ) ) errcode = 2;
      else file_options.add_option( code, n - 1 );
      break;
    default: std::fprintf( stderr, "%sinternal error: uncaught option.\n", msg );
             return false;
    }

  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 ( errcode == 0 );
  }


int parse_line_col( const std::string & s, int & line, int & col )
  {
  int c = parse_int( s, line );
  if( c <= 0 || --line < 0 ) return 0;
  if( c < (int)s.size() )
    {
    int i = parse_int( s.c_str() + c + 1, col );
    if( i <= 0 || --col < 0 || s[c] != ',' ) return 0;
    c += i + 1;
    }
  else col = 0;
  return c;
  }


void apply_file_options( Buffer & buffer, const File_options & file_options )
  {
  for( int i = 0; i < file_options.size(); ++i )
    {
    const int code = file_options.code( i );
    const int arg = file_options.argument( i );
    switch( code )
      {
      case auto_indent:    buffer.options.auto_indent = true; break;
      case no_auto_indent: buffer.options.auto_indent = false; break;
      case lmargin:        buffer.options.set_lmargin( arg ); break;
      case rmargin:        buffer.options.set_rmargin( arg ); break;
      case overwrite:      buffer.options.overwrite = true; break;
      case no_overwrite:   buffer.options.overwrite = false; break;
      case read_only:      buffer.options.read_only = true; break;
      case no_read_only:   buffer.options.read_only = false; break;
      case word_wrap:      buffer.options.word_wrap = true; break;
      case no_word_wrap:   buffer.options.word_wrap = false; break;
      }
    }
  }


void apply_regex_options( Buffer & buffer )
  {
  if( buffer.name().empty() ) return;
  for( unsigned i = 0; i < regex_options_vector.size(); ++i )
    {
    const Regex_options & ro = regex_options_vector[i];
    unsigned 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() )
      { apply_file_options( buffer, ro.file_options ); break; }
    }
  }


// Depth first. Files first in each directory. Load regular files only.
// Ignore backups '*~', object files '*.o' and symbolic links.
bool add_handles_recursively( std::string name, const int line, const int col,
                              const File_options & file_options,
                              bool & first_post, bool & read_error_found )
  {
  while( !name.empty() && name[name.size()-1] == '/' )
    name.resize( name.size() - 1 );		// reject "/", "//", etc.
  if( name.empty() ) return false;
  while( name.size() > 2 && name[0] == '.' && name[1] == '/' )
    name.erase( 0, 2 );				// remove leading ./
  struct stat st;
  if( stat( name.c_str(), &st ) != 0 || !S_ISDIR( st.st_mode ) )
    return false;
  std::list< std::string > dirnames;		// only directory names here
  dirnames.push_back( name );
  while( !dirnames.empty() )
    {
    const std::string dirname = dirnames.front();
    dirnames.pop_front();
    DIR * const dirp = opendir( dirname.c_str() );
    if( !dirp ) { read_error_found = true; continue; }
    std::vector< std::string > name_vector;
    const bool isdot = ( dirname == "." );
    while( true )
      {
      const struct dirent * const entryp = readdir( dirp );
      if( !entryp ) { closedir( dirp ); break; }
      std::string tmp( entryp->d_name );
      if( tmp == "." || tmp == ".." || tmp.empty() ||
          tmp[tmp.size()-1] == '~' ||
          ( tmp.size() > 2 && tmp.compare( tmp.size() - 2, 2, ".o" ) == 0 ) )
        continue;
      if( !isdot ) tmp = dirname + "/" + tmp;
      name_vector.push_back( tmp );
      }
    if( name_vector.empty() ) continue;
    if( name_vector.size() > 1 )
      std::sort( name_vector.begin(), name_vector.end() );
    std::list< std::string > tmp_list;		// subdirs temporary list
    for( unsigned i = 0; i < name_vector.size(); )
      {
      const unsigned dot = name_vector[i].rfind( '.' );
      const unsigned slash = name_vector[i].rfind( '/' );
      const unsigned size = name_vector[i].size();
      const unsigned j = i++;
      if( dot < size - 1 && dot > 0 && ( slash >= size || slash + 1 < dot ) &&
          ( !std::isdigit( name_vector[j][dot-1] ) ||
            !std::isdigit( name_vector[j][dot+1] ) ) )
        while( i < name_vector.size() &&
               name_vector[j].compare( 0, dot + 1, name_vector[i],
                                       0, dot + 1 ) == 0 &&
               name_vector[i].rfind( '.' ) == dot ) ++i;
      for( unsigned k = i; k > j; --k )		// reverse sort by extension
        {
        if( lstat( name_vector[k-1].c_str(), &st ) != 0 ) continue;
        if( S_ISDIR( st.st_mode ) ) tmp_list.push_back( name_vector[k-1] );
        else if( S_ISREG( st.st_mode ) )
          {
          try {
            const int h = Bufhandle_vector::find_or_add_handle(
                            &name_vector[k-1], -1, line, col );
            apply_file_options( Bufhandle_vector::handle( h ).buffer(),
                                file_options );
            }
          catch( Buffer::Error e )
            {
            if( first_post ) { first_post = false; std::fputc( '\n', stderr ); }
            std::fprintf( stderr, "warning: '%s': %s\n",
                          name_vector[k-1].c_str(), e.msg );
            read_error_found = true;
            }
          }
        }
      }
    dirnames.splice( dirnames.begin(), tmp_list );	// prepend subdirs
    }
  return true;
  }

} // end namespace RC


RC::Options & RC::editor_options() { return editor_options_; }


void RC::apply_all_file_options( Buffer & buffer )
  {
  apply_file_options( buffer, moerc_file_options );
  apply_regex_options( buffer );
  apply_file_options( buffer, default_file_options );
  }


     // Returns true if first non whitespace character is '+' or '-'.
bool RC::issigned( const std::string & s )
  {
  unsigned i = 0;
  while( i < s.size() && ISO_8859::isspace( s[i] ) ) ++i;	// strip spaces
  return ( i < s.size() && ( s[i] == '+' || s[i] == '-' ) );
  }


    // Returns the number of chars read, or 0 if error.
int RC::parse_int( const std::string & s, int & result )
  {
  if( s.empty() ) return 0;
  const char * const str = s.c_str();
  char * tail;
  errno = 0;
  const long tmp = std::strtol( str, &tail, 0 );
  if( errno || tail == str || tmp < -INT_MAX || tmp > INT_MAX ) return 0;
  result = tmp;
  const int c = tail - str;
  return c;
  }


    // Returns the number of chars read, or 0 if error.
int RC::parse_relative_int( const std::string & s, int & result )
  {
  int tmp = 0;
  const int c = parse_int( s, tmp );
  if( !c ) return 0;
  if( !issigned( s ) ) result = tmp;
  else if( tmp > 0 )
    {
    if( INT_MAX - tmp < result ) result = INT_MAX;
    else result += tmp;
    }
  else if( tmp < 0 )
    {
    if( result + tmp < 1 ) result = 1;
    else result += tmp;
    }
  return c;
  }


    // Returns 0 for success, 1 for file not found, 2 for syntax error.
int RC::process_rcfile( const std::string & name )
  {
  FILE * const 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;
        }
      if( !retval ) std::fputc( '\n', stderr );
      std::fprintf( stderr, "%s %d: error: Line too long\n",
                    name.c_str(), line );
      retval = 2;
      }
    if( len == 0 || ISO_8859::isspace( buf[0] ) ) continue;
    if( buf[0] != '-' )		// Create new file name dependent options
      {
      const char * const 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 * const opt = std::strtok( buf, delimiters );
      const char * const arg = std::strtok( 0, delimiters );
      char msg[80];
      snprintf( msg, sizeof msg, "%s%s %d: ",
                retval ? "" : "\n", name.c_str(), line );
      const 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 ),
                                     moerc_file_options, msg, true ) )
            retval = 2; }
      else
        {
        if( !set_option_from_code( parser.code( 0 ), parser.argument( 0 ),
                                   regex_options_vector.back().file_options,
                                   msg ) ) retval = 2;
        regex_status = 2;
        }
      }
    }
  std::fclose( f );
  if( !retval ) std::fputs( "done\n", stderr );
  return retval;
  }


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

  int argind = 0;
  for( ; argind < parserp->arguments(); ++argind )
    {						// Process "global" options
    const int code = parserp->code( argind );
    if( !code ) break;				// line number or file name
    if( !set_option_from_code( code, parserp->argument( argind ),
                               default_file_options, "\n", true ) )
      return 1;
    }

  int line = 0, col = 0;
  bool first_post = true;
  bool non_regular_found = false;
  bool read_error_found = false;
  bool stdin_used = false;
  while( argind < parserp->arguments() )  // Process files and file options
    {
    if( parserp->argument( argind )[0] == '+' )		// line number
      {
      if( !parse_line_col( parserp->argument( argind ), line, col ) )
        {
        if( first_post ) std::fputc( '\n', stderr );
        show_error( "Bad number of line or column." );
        return 1;
        }
      if( ++argind >= parserp->arguments() ) break;
      if( parserp->code( argind ) )
        {
        if( first_post ) std::fputc( '\n', stderr );
        show_error( "Options not allowed between line number and file name." );
        return 1;
        }
      }
//    else { line = 0; col = 0; }
    std::string name( parserp->argument( argind++ ) );
    Menu::tilde_expansion( name );
    File_options file_options;
    for( ; argind < parserp->arguments(); ++argind )
      {						// Process file options
      const int code = parserp->code( argind );
      if( !code ) break;			// line number or file name
      if( !set_option_from_code( code, parserp->argument( argind ),
                                 file_options, first_post ? "\n" : "" ) )
        return 1;
      }
    bool can_read = false;
    const bool is_stdin = ( name == "-" );
    if( is_stdin && ( stdin_used || isatty( fileno( stdin ) ) ) );
      // ignore stdin if is repeated or is a terminal
    else if( Menu::is_regular( name, &can_read ) || can_read )
      {
      try {
        const int i = Bufhandle_vector::find_or_add_handle( &name, -1, line,
                                                            col, can_read );
        apply_file_options( Bufhandle_vector::handle( i ).buffer(), file_options );
        }
      catch( Buffer::Error e )
        {
        if( first_post ) { first_post = false; std::fputc( '\n', stderr ); }
        std::fprintf( stderr, "warning: '%s': %s\n", name.c_str(), e.msg );
        read_error_found = true;
        }
      }
    else if( !add_handles_recursively( name, line, col, file_options,
                                       first_post, read_error_found ) )
      {
      if( first_post ) { first_post = false; std::fputc( '\n', stderr ); }
      std::fprintf( stderr, "warning: '%s' is not a regular file\n", name.c_str() );
      non_regular_found = true;
      }
    if( is_stdin ) stdin_used = true;
    }
  // Add empty buffer if no files given or stdin is not a tty
  try { Bufhandle_vector::add_handle_if_pending_input_or_empty( line, col ); }
  catch( Buffer::Error e )
    {
    if( first_post ) std::fputc( '\n', stderr );
    show_error( e.msg );
    return 1;
    }

  std::fputs( "done\n", stderr );
  delete parserp;
  if( non_regular_found || read_error_found )
    {
    if( non_regular_found )
      std::fputs( "warning: Non-regular files will be ignored.\n", stderr );
    if( read_error_found )
      std::fputs( "warning: One or more files couldn't be read.\n", stderr );
    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()
  {
  editor_options_.reset();
  default_file_options.reset();
  moerc_file_options.reset();
  regex_options_vector.clear();
  }
