/*  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 <string>
#include <vector>

#include "buffer.h"
#include "iso_8859.h"
#include "regex.h"


namespace Regex {

class Set
  {
  int _parsed_len;
  bool in;
  std::string data;

public:
  Set( const std::string & regex, const unsigned int i0 = 0 ) throw();
  bool includes( const unsigned char ch ) const throw()
    { return ( _parsed_len && ( in == ( data.find( ch ) < data.size() ) ) ); }
  int parsed_len() const throw() { return _parsed_len; }
  };


Set::Set( const std::string & regex, const unsigned int i0 ) throw()
  : _parsed_len( 0 ), in( true )
  {
  if( i0 + 2 >= regex.size() || regex[i0] != '[' ) return;
  unsigned int i = i0 + 1;
  bool fail = true;
  if( regex[i] == '^' ) { ++i; in = false; }
  if( regex[i] == ']' ) data += regex[i++];

  for( ; i < regex.size(); ++i )
    {
    unsigned char ch1 = regex[i];
    if( ch1 == ']' ) { ++i; fail = false; break; }
    if( ch1 == '\\' )
      {
      int len, cht = ISO_8859::escape( regex, i + 1, &len );
      if( cht < 0 ) break;
      ch1 = cht; i += len;
      }
    if( i + 2 >= regex.size() || regex[i+1] != '-' || regex[i+2] == ']' )
      data += ch1;
    else
      {
      i += 2;
      unsigned char ch2 = regex[i];
      if( ch2 == '\\' )
        {
        int len, cht = ISO_8859::escape( regex, i + 1, &len );
        if( cht < 0 ) break;
        ch2 = cht; i += len;
        }
      for( int c = ch1; c <= ch2; ++c ) data += c;
      }
    }
  if( !fail ) _parsed_len = i - i0;
  }


bool match_regex( const Basic_buffer & buffer, Point & p,
                  const std::string & regex, unsigned int i,
                  std::vector< std::string > & pieces, const bool icase ) throw();

inline bool case_plus( const Basic_buffer & buffer, Point & p,
                       const std::string & regex, unsigned int & i,
                       std::vector< std::string > & pieces, const bool icase ) throw()
  {
  if( ++i >= regex.size() ) return false;
  const int ps = pieces.size();
  std::string piece;		// Find shortest matching sequence
  int counter = 0;		// Minimum number of matches required

  while( i + 1 < regex.size() && regex[i] == '\\' && regex[i+1] == '+' )
    { ++counter; i += 2; }

  unsigned char ch1 = regex[i];
  if( ch1 == '\\' )
    {
    if( i + 1 < regex.size() )
      {
      const unsigned char chr = regex[i+1];
      if( chr == '[' )
        {
        Set set( regex, i + 1 );		// Set of characters to match
        if( !set.parsed_len() ) return false;
        i += set.parsed_len();
        for( int j = 0; j < counter || !match_regex( buffer, p, regex, i + 1, pieces, icase ); ++j )
          {
          const int ch2 = buffer.pgetc( p );
          if( ch2 < 0 || !set.includes( ch2 ) ) return false;
          piece += ch2;
          }
        pieces.insert( pieces.begin() + ps, piece ); i = regex.size();
        return true;
        }
      if( chr == 'w' || chr == 'W' )
        {
        ++i;
        for( int j = 0; j < counter || !match_regex( buffer, p, regex, i + 1, pieces, icase ); ++j )
          {
          const int ch2 = buffer.pgetc( p );
          if( ch2 < 0 || ( chr == 'w' ) != ISO_8859::isalnum_( ch2 ) )
            return false;
          piece += ch2;
          }
        pieces.insert( pieces.begin() + ps, piece ); i = regex.size();
        return true;
        }
      }
    int len, cht = ISO_8859::escape( regex, i + 1, &len );
    if( cht < 0 ) return false;
    ch1 = cht; i += len;
    }
  for( int j = 0; j < counter || !match_regex( buffer, p, regex, i + 1, pieces, icase ); ++j )
    {
    const int ch2 = buffer.pgetc( p );
    if( ch2 < 0 ||
        ( icase && ISO_8859::tolower( ch1 ) != ISO_8859::tolower( ch2 ) ) ||
        ( !icase && ch1 != ch2 ) ) return false;
    piece += ch2;
    }
  pieces.insert( pieces.begin() + ps, piece ); i = regex.size();
  return true;
  }


// Returns true if substring of 'buffer' beginning at 'p' matches 'regex'.
// If found, sets 'p' after found substring and returns in 'pieces' the
// text matching every \*, \? etc from the regular expresion.
//
bool match_regex( const Basic_buffer & buffer, Point & p,
                  const std::string & regex, unsigned int i,
                  std::vector< std::string > & pieces, const bool icase ) throw()
  {
  if( i >= regex.size() ) return true;
  const Point old_p = p;
  const unsigned int pieces_old_size = pieces.size();
  bool fail = false;

  for( ; i < regex.size() && !fail; ++i )
    {
    unsigned char ch1 = regex[i];
    if( ch1 != '\\' )			// normal (not escaped) char
      {
      const int ch2 = buffer.pgetc( p );
      if( ch2 < 0 || ( !icase && ch1 != ch2 ) ||
          ( icase && ISO_8859::tolower( ch1 ) != ISO_8859::tolower( ch2 ) ) )
        fail = true;
      continue;
      }

    if( ++i >= regex.size() ) { fail = true; break; }
    ch1 = regex[i];
    switch( ch1 )	// ch1 is first (maybe only) of a escape secuence
      {
      case '^': if( p != buffer.bol( p ) || p == buffer.eof() ) fail = true; break;
      case '$': if( p != buffer.eol( p ) ) fail = true; break;
      case '<': if( !buffer.pisbow( p ) ) fail = true; break;
      case '>': if( !buffer.piseow( p ) ) fail = true; break;
      case '?': {
                const int ch2 = buffer.pgetc( p );
                if( ch2 < 0 ) { fail = true; break; }
                pieces.push_back( std::string( 1, ch2 ) );
                } break;
      case '*': {			// Find shortest matching sequence
                const int ps = pieces.size();
                std::string piece;
                while( !match_regex( buffer, p, regex, i + 1, pieces, icase ) )
                  {
                  const int ch2 = buffer.pgetc( p );
                  if( ch2 < 0 ) { fail = true; break; }
                  piece += ch2;
                  }
                if( fail ) break;
                pieces.insert( pieces.begin() + ps, piece );
                i = regex.size();
                } break;
      case 'c': {
                const Point begin = p;
                Point end = p;
                const int ps = pieces.size();
                while( !match_regex( buffer, p, regex, i + 1, pieces, icase ) )
                  {
                  const int ch2 = buffer.pgetc( p );
                  if( ch2 < 0 || ch2 == ')' || ch2 == ']' || ch2 == '}' )
                    { fail = true; break; }
                  if( ch2 == '(' || ch2 == '[' || ch2 == '{' )
                    {
                    p = end;
                    if( buffer.set_to_matching_delimiter( p, true, true ) <= 0 ||
                        !buffer.pseek( p, 1 ) ) { fail = true; break; }
                    }
                  end = p;
                  }
                if( fail ) break;
                std::string piece;
                buffer.to_string( begin, end, piece );
                pieces.insert( pieces.begin() + ps, piece );
                i = regex.size();
                } break;
      case '[': {
                Set set( regex, i );	// Set of characters to match
                const int ch2 = buffer.pgetc( p );
                if( ch2 < 0 || !set.includes( ch2 ) ) { fail = true; break; }
                i += ( set.parsed_len() - 1 );
                pieces.push_back( std::string( 1, ch2 ) );
                } break;
      case '+': if( !case_plus( buffer, p, regex, i, pieces, icase ) )
                  fail = true;
                break;
      case 'S': {			// Find longest matching sequence
                std::string piece;
                while( true )
                  {
                  Point pn = p;
                  const int ch2 = buffer.pgetc( pn );
                  if( ch2 < 0 || !std::isspace( ch2 ) ) break;
                  p = pn; piece += ch2;
                  }
                if( piece.size() ) pieces.push_back( piece );
                else fail = true;
                } break;
      case 's': {			// Find shortest matching sequence
                const int ps = pieces.size();
                std::string piece;
                while( !match_regex( buffer, p, regex, i + 1, pieces, icase ) )
                  {
                  const int ch2 = buffer.pgetc( p );
                  if( ch2 < 0 || !std::isspace( ch2 ) ) { fail = true; break; }
                  piece += ch2;
                  }
                if( !fail ) { pieces.insert( pieces.begin() + ps, piece ); i = regex.size(); }
                } break;
      case 'W':
      case 'w': {
                const int ch2 = buffer.pgetc( p );
                if( ch2 >= 0 && ( ch1 == 'w' ) == ISO_8859::isalnum_( ch2 ) )
                  pieces.push_back( std::string( 1, ch2 ) );
                else fail = true;
                } break;
      default: {
               int len, cht = ISO_8859::escape( regex, i, &len );
               if( cht < 0 ) { fail = true; break; }
               ch1 = cht;
               const int ch2 = buffer.pgetc( p );
               if( ch2 < 0 ||
                   ( icase && ISO_8859::tolower( ch1 ) != ISO_8859::tolower( ch2 ) ) ||
                   ( !icase && ch1 != ch2 ) ) fail = true;
               else i += ( len - 1 );
               }
      }
    }
  if( fail )
    { p = old_p;
    if( pieces_old_size != pieces.size() ) pieces.resize( pieces_old_size ); }
  return !fail;
  }

} // end namespace Regex


// Searches 'buffer' from 'p1' ('p1-1' if backward) for 'regex'.
// If found, sets 'p1' at the first char of, and 'p2' after the
// found substring and returns in 'pieces' the text matching every \*,
// \? etc from the regular expresion.
//
bool Regex::find( const Basic_buffer & buffer, Point & p1, Point & p2,
                  const std::string & regex, std::vector< std::string > & pieces,
                  const bool icase, const bool backward ) throw()
  {
  if( regex.size() == 0 ) return true;
  if( pieces.size() ) pieces.clear();

  const int bs = regex.find('\\');
  if( bs > 0 )
    {
    const std::string header( regex, 0, bs );
    const bool same_size = ( header.size() == regex.size() );
    if( backward ) while( true )
      {
      if( !buffer.rfind_text( p1, header, icase ) ) return false;
      p2 = p1;
      if( same_size ) { buffer.pseek( p2, header.size() ); return true; }
      if( match_regex( buffer, p2, regex, 0, pieces, icase ) ) return true;
      if( !buffer.pprev( p1 ) ) return false;
      }
    else while( true )
      {
      if( !buffer.find_text( p1, header, icase ) ) return false;
      p2 = p1; buffer.pseek( p1, -header.size() );
      if( same_size ) return true; else p2 = p1;
      if( match_regex( buffer, p2, regex, 0, pieces, icase ) ) return true;
      if( !buffer.pnext( p1 ) || p1 == buffer.eof() ) return false;
      }
    }

  if( backward ) while( true )
    {
    if( !buffer.pprev( p1 ) ) return false; else p2 = p1;
    if( match_regex( buffer, p2, regex, 0, pieces, icase ) ) return true;
    }
  else while( true )
    {
    p2 = p1;
    if( match_regex( buffer, p2, regex, 0, pieces, icase ) ) return true;
    else if( !buffer.pnext( p1 ) || p1 == buffer.eof() ) return false;
    }
  }


// Deletes block at [p1,p2) and replaces it with the text produced from
// 'regex' and 'pieces'.
// If there is no error, returns true and sets 'p2' after the newly
// inserted text.
//
bool Regex::replace( Buffer & buffer, const Point & p1, Point & p2,
                     const std::string & regex,
                     const std::vector< std::string > & pieces ) throw()
  {
  const Basic_buffer matched( buffer, p1, p2 );
  Basic_buffer tmp;
  Point p = tmp.bof();
  bool fail = false;

  for( unsigned int i = 0; i < regex.size() && !fail; ++i )
    {
    unsigned char ch1 = regex[i];
    if( ch1 != '\\' )			// normal (not escaped) char
      { tmp.pputc( p, ch1 ); continue; }

    if( ++i >= regex.size() ) { fail = true; break; }
    ch1 = regex[i];	// ch1 is first (maybe only) of a escape secuence
    if( std::isdigit( ch1 ) )
      {
      const unsigned int n = ch1 - '0';
      if( pieces.size() > n )
        for( unsigned int j = 0; j < pieces[n].size(); ++j )
          tmp.pputc( p, pieces[n][j] );
      else fail = true;
      }
    else if( ch1 == '&' || ch1 == 'l' || ch1 == 'u' )
      {
      if( !matched.empty() )
        {
        const Point begin = p;
        tmp.pputb( p, matched, matched.bof(), matched.eof() );
        if( ch1 == 'l' ) tmp.to_lowercase( begin, p );
        else if( ch1 == 'u' ) tmp.to_uppercase( begin, p );
        }
      }
    else
      {
      int len, cht = ISO_8859::escape( regex, i, &len );
      if( cht >= 0 ) { tmp.pputc( p, cht ); i += ( len - 1 ); }
      else fail = true;
      }
    }
  if( !fail && !buffer.replace( p1, p2, tmp, tmp.bof(), tmp.eof() ) )
    fail = true;
  return !fail;
  }


// Returns true if 'regex' matches 'name'
//
bool Regex::match_filename( const std::string & regex, const std::string & name,
                            unsigned int i1, unsigned int i2 ) throw()
  {
  if( i1 >= regex.size() || i2 >= name.size() ) return false;
  bool fail = false;

  for( ; i1 < regex.size() && !fail; ++i1 )
    {
    unsigned char ch1 = regex[i1];
    switch( ch1 )
      {
      case '?': if( ++i2 > name.size() ) fail = true; break;
      case '*': if( i1 + 1 >= regex.size() ) { i2 = name.size(); break; }
                if( regex[i1+1] == '*' ) break;
                for( ; i2 < name.size(); ++i2 )
                  if( match_filename( regex, name, i1 + 1, i2 ) )
                    break;
                if( i2 >= name.size() ) { fail = true; break; }
                i1 = regex.size(); i2 = name.size(); break;
      case '[': {
                if( i2 >= name.size() ) { fail = true; break; }
                unsigned char ch2 = name[i2++];
                Set set( regex, i1 );		// Set of characters to match
                if( set.includes( ch2 ) ) i1 += ( set.parsed_len() - 1 );
                else fail = true;
                } break;
      default: if( ch1 == '\\' )
                 {
                 int len, cht = ISO_8859::escape( regex, i1 + 1, &len );
                 if( cht < 0 ) { fail = true; break; }
                 ch1 = cht; i1 += ( len - 1 );
                 }
               if( i2 >= name.size() || ch1 != name[i2++] ) fail = true;
               break;
      }
    }
  if( !fail && i2 < name.size() ) fail = true;
  return !fail;
  }
