/* GNU moe - My Own Editor
   Copyright (C) 2005-2025 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 <cstdio>
#include <ctime>
#include <string>
#include <vector>

#include "buffer.h"
#include "buffer_handle.h"
#include "block.h"
#include "encoding.h"
#include "iso_8859.h"
#include "menu.h"
#include "rc.h"
#include "regex.h"
#include "screen.h"
#include "window.h"
#include "window_vector.h"


namespace Window_vector {

int curwin_;			// current window (the one with the cursor in)
std::vector< Window > data;


const char * already_being_edited( const std::string & name,
                                   const Buffer * const bufferp = 0 )
  {
  if( name.size() )			// allow multiple unnamed buffers
    for( int i = 0; i < Bufhandle_vector::handles(); ++i )
      if( name == Bufhandle_vector::handle( i ).buffer().name() &&
          bufferp != &Bufhandle_vector::handle( i ).buffer() )
        {
        int bufsize = 0;
        char * const buf = Screen::msg_line_buf( &bufsize );
        snprintf( buf, bufsize, "File '%s' is already being edited",
                  Screen::cut_filename( name, 31 ) );
        return buf;
        }
  return 0;
  }


bool no_block()
  {
  if( !Block::valid() ) { Screen::show_message( "No block" ); return true; }
  return false;
  }


bool no_block_in_this_file( const Buffer & buffer )
  {
  if( Block::bufferp() != &buffer )
    { Screen::show_message( "Block is not in this file" ); return true; }
  return false;
  }


bool rectangular_block()
  {
  if( RC::editor_options().rectangle_mode )
    {
    Screen::show_message( "Operation not available in rectangular block mode" );
    return true;
    }
  return false;
  }


bool read_only( const Buffer & buffer )
  {
  if( buffer.options.read_only )
    { Screen::show_message( rdonly_msg ); return true; }
  return false;
  }


void replace_and_repaint( const Basic_buffer & tmp )
  {
  Buffer & buffer = *Block::bufferp();
  Point p = Block::end();
  buffer.reset_appendable();
  buffer.replace( Block::begin(), p, tmp, tmp.bof(), tmp.eof() );
  buffer.reset_appendable();
  if( RC::editor_options().auto_unmark ) Block::unmark();
  else Block::set_end( buffer, p );
  Screen::repaint( &buffer );
  }


bool insert_year( std::vector< int > & years, const int year )
  {
  if( year >= 1000 )
    {
    unsigned i = years.size();
    while( i > 0 && years[i-1] > year ) --i;
    if( i == 0 || years[i-1] < year )
      { years.insert( years.begin() + i, year ); return true; }
    }
  return false;
  }

void print_year( Basic_buffer & outbuf, Point & p, int year )
  {
  while( year )
    { outbuf.pputc( p, ( year % 10 ) + '0' ); outbuf.pprev( p ); year /= 10; }
  p = outbuf.eof();
  }

bool insert_new_year( const Buffer & buffer, const Point & p0,
                      const Point & pn, const Point & py,
                      const Point & ph1, const Point & ph2,
                      const int new_year, Basic_buffer & outbuf )
  {
  std::vector< int > years;	// years are ordered and duplicates removed
  int year = 0, prev_year = 0;
  for( Point p = py; p <= ph1; buffer.pnext( p ) )	// parse years
    {
    const int ch = buffer[p];
    if( std::isdigit( ch ) ) { year *= 10; year += ch - '0'; continue; }
    if( ch < 0 || ch == '\n' || ch == ',' || ch == '-' || p >= ph1 )
      {
      if( prev_year >= 1000 )
        {
        insert_year( years, prev_year );
        for( int y = prev_year + 1; y < year; ++y ) insert_year( years, y );
        }
      insert_year( years, year );
      if( ch == '-' ) prev_year = year; else prev_year = 0;
      year = 0;
      if( ch < 0 ) break;
      }
    else if( std::ispunct( ch ) ) year = 0;
    }

  bool change = insert_year( years, new_year );		// new_year inserted

  const int rmargin = buffer.options.rmargin();
  Point p = outbuf.eof();
  int first_year = years[0];
  prev_year = first_year;
  for( unsigned i = 1; i <= years.size(); ++i )		// write years
    {
    year = ( i < years.size() ) ? years[i] : 0;
    if( prev_year + 1 != year )				// end of range
      {
      const bool range = first_year + 1 < prev_year;	// 3 or more years
      const int size = range ? 11 : 6;
      if( first_year > years[0] ) outbuf.pputc( p, ',' );
      if( outbuf.to_cursor( p ).col + size > rmargin + 1 )
        {
        outbuf.pputc( p, '\n' );
        outbuf.pputb( p, buffer, p0, pn );	// add prefix of notice
        }
      else if( first_year > years[0] ) outbuf.pputc( p, ' ' );
      if( first_year < prev_year )
        {
        print_year( outbuf, p, first_year );
        if( range ) outbuf.pputc( p, '-' );
        else { outbuf.pputc( p, ',' ); outbuf.pputc( p, ' ' ); }
        }
      print_year( outbuf, p, prev_year );
      first_year = year;
      }
    prev_year = year;
    }

  if( ph1 < ph2 )				// write copyright holder
    {
    Point pt = p;
    outbuf.pputb( p, buffer, ph1, ph2 );
    if( outbuf.to_cursor( p ).col + 1 > rmargin + 1 )
      {
      outbuf.pputc( pt, '\n' );
      outbuf.pputb( pt, buffer, p0, pn );	// add prefix of notice
      }
    else outbuf.pputc( pt, ' ' );
    }

  if( !change ) change = outbuf.last_line() != ph2.line - p0.line;
  if( !change )
    {
    std::string s1, s2;
    buffer.to_string( p0, ph2, s1 );
    outbuf.to_string( outbuf.bof(), outbuf.eof(), s2 );
    change = s1 != s2;
    }
  return change;			// new_year inserted or list modified
  }


/* Replace constant escape sequences with special characters;
   "\n" --> '\n' or "\u00E1" --> "á" (but not "\?" --> '?' or "\\" --> '\').
   If !allow_set, leave "\0".."\9" and "\u<non_hex_digit>" unmodified.
   "\u" and "\U" may not be specified in sets nor after "\+".
   The backslash and the right square bracket must be real (not "\x5C").
*/
bool deescapize_string( const std::string & in, std::string & out,
                        const bool allow_set )
  {
  bool in_set = false;
  bool plus = false;
  out.clear();
  for( unsigned long i = 0; i < in.size(); )
    {
    if( in[i] != '\\' )			// ordinary character
      { out += in[i]; if( in[i++] == ']' ) in_set = false; continue; }
    if( i + 1 >= in.size() ) return false;
    const unsigned char ch = in[i+1];	// character after the backslash
    if( ch == '\\' ) { out += in[i++]; out += in[i++]; continue; }
    if( !allow_set && ( std::isdigit( ch ) || ( ch == 'u' &&
        ( i + 2 >= in.size() || ISO_8859::xvalue( in[i+2] ) < 0 ) ) ) )
      { out += in[i++]; out += in[i++]; continue; }
    if( !in_set && ch == '[' )
      {
      if( !allow_set || i + 3 >= in.size() ) return false;
      out += in[i++]; out += in[i++];		// "\["
      if( in[i] == '^' ) out += in[i++];	// complemented set
      if( in[i] == ']' ) out += in[i++];	// first character in set
      in_set = true; continue;
      }
    if( ch == '=' )				// equivalence class
      { if( i + 2 >= in.size() ) return false;
        out += in[i++]; out += in[i++]; out += in[i++]; continue; }
    int len, code = ISO_8859::escape( in, i + 1, &len, !in_set && !plus );
    if( code < 0 )
      { if( in_set || code != -1 ) return false;
        plus = ch == '+'; out += in[i++]; out += in[i++]; continue; }
    if( code == '\\' || code == ']' ) return false;	// reject "\x5C", etc
    if( code >= 128 && ( ch == 'u' || ch == 'U' ) )
      Encoding::ucs_to_utf8( code, out );
    else out += code;
    i += len + 1;
    }
  return !in_set;
  }

} // end namespace Window_vector


int Window_vector::init() { curwin_ = 0; return load( 1 ); }


Window & Window_vector::curwin() { return data[curwin_]; }
int Window_vector::windows() { return data.size(); }


/* If 'new_size' == 0, reload same number of windows.
   If 'new_size' < 0, toggle 1 window <--> max_windows.
   Center cursor the first time each handle is displayed.
*/
int Window_vector::load( int new_size )
  {
  const int handles = Bufhandle_vector::handles();
  const bool toggle = new_size < 0 && handles > 1;

  if( !handles ) { curwin_ = 0; data.clear(); return 0; }
  if( new_size == 0 ) new_size = windows();
  else if( new_size < 0 )
    {
    for( int i = 0; i < curwin_; ++i ) Bufhandle_vector::next();
    new_size = ( windows() != 1 ) ? 1 : handles;
    curwin_ = 0;
    }
  new_size = std::min( new_size, handles );
  new_size = std::min( new_size, Screen::max_windows() );
  if( RC::editor_options().max_windows() > 0 )
    new_size = std::min( new_size, RC::editor_options().max_windows() );
  new_size = std::max( new_size, 1 );
  while( curwin_ >= new_size ) { --curwin_; Bufhandle_vector::next(); }
  const int wheight = Screen::height() / new_size;

  data.clear();
  int i = 0, y = 0;
  while( i + 1 < new_size )
    {
    data.push_back( Window( Bufhandle_vector::handle( i ), y, wheight,
                            Bufhandle_vector::handle( i ).first_post() ) );
    ++i; y += wheight;
    }
  data.push_back( Window( Bufhandle_vector::handle( i ), y,
                          Screen::height() - y,
                          Bufhandle_vector::handle( i ).first_post() ) );
  if( toggle && windows() == 1 ) data[0].center_cursor();
  for( i = 0; i < windows(); ++i ) data[i].repaint();
  return windows();
  }


void Window_vector::bufhandle_menu( const int abort_key )
  {
  const int handle = Menu::bufhandle_menu( curwin_, abort_key );
  if( handle >= 0 ) { curwin_ = handle; if( curwin_ >= windows() ) load(); }
  }


const char * Window_vector::change_buffer_name()
  {
  Buffer & buffer = curwin().buffer();
  std::string name( buffer.name() );

  if( Menu::get_filename( "New buffer name (^C to abort): ", name ) < 0 ||
      name == buffer.name() ) return 0;
  const char * const msg = already_being_edited( name, &buffer );
  if( !msg ) { buffer.name( name ); Screen::show_status_lines( &buffer ); }
  return msg;
  }


const char * Window_vector::load_file( const int abort_key )
  {
  std::string name;
  if( Menu::get_filename( "Name of file to edit (^C to abort): ",
                          name, abort_key ) < 0 ) return 0;
  Screen::show_feedback( "loading..." );
  const int old_handles = Bufhandle_vector::handles();
  int handle;
  try { handle = Bufhandle_vector::find_or_add_handle( name, curwin_ + 1 ); }
  catch( Buffer::Error & e )
    { data.back().repaint(); return e.msg; }		// remove feedback
  curwin_ = handle;
  if( windows() > 1 && Bufhandle_vector::handles() > old_handles )
    load( windows() + 1 );
  else load();
  return 0;
  }


void Window_vector::split_window()
  {
  Bufhandle_vector::duplicate_handle( curwin_++ );
  load( windows() + 1 );
  }


const char * Window_vector::save_file( const int abort_key )
  {
  const Buffer & buffer = curwin().buffer();
  std::string name( buffer.name() );		// name may be empty (unnamed)

  if( Menu::get_filename( "Name of file to save (^C to abort): ", name,
                          abort_key ) <= 0 ) return 0;
  const char * const msg = already_being_edited( name, &buffer );
  if( msg ) return msg;
  bool done;
  try { done = buffer.save( &name ); }
  catch( Buffer::Error & e ) { return e.msg; }
  if( done )
    {
    Screen::show_status_lines( &buffer );	// update modified status
    int bufsize = 0;
    char * const buf = Screen::msg_line_buf( &bufsize );
    snprintf( buf, bufsize, "File '%s' saved",
              Screen::cut_filename( name, 13 ) );
    return buf;
    }
  if( name != buffer.name() || !read_only( buffer ) ) return "File not saved";
  return 0;
  }


// Return number of windows, 0 if none, -1 if aborted.
//
int Window_vector::close( bool abort, const bool quiet )
  {
  if( !abort || !quiet )
    {
    Buffer & buffer = curwin().buffer();
    const bool last_handle = Bufhandle_vector::handles( buffer ) <= 1;
    const bool modified = buffer.modified();
    const bool exit_ask = RC::editor_options().exit_ask && !quiet;

    if( abort && modified && last_handle )
      {
      int key = Screen::show_message( "Lose changes to this file ? (y/n/^C) ",
                                      true, true );
      if( key == 'n' || key == 'N' ) abort = false;
      else if( key != 'y' && key != 'Y' ) return -1;
      }
    if( !abort && modified )
      {
      if( read_only( buffer ) ) return -1;
      std::string name( buffer.name() );
      if( ( exit_ask || name.empty() ) &&
          Menu::get_filename( "Name of file to save (^C to abort): ",
                              name ) <= 0 ) return -1;
      if( name.empty() ) return -1;
      try { if( !buffer.save( &name ) ) return -1; }
      catch( Buffer::Error & e )
        { if( !quiet ) Screen::show_message( e.msg ); return -1; }
      if( Bufhandle_vector::handles() <= 1 ) curwin().show_status_line();
      }
    else if( exit_ask && last_handle && !modified )
      {
      int key = Screen::show_message( "Abandon this file ? (y/N) ", true, true );
      if( key != 'y' && key != 'Y' ) return -1;
      }
    }
  Bufhandle_vector::delete_handle( curwin_ );
  if( curwin_ > 0 ) --curwin_;
  return load();
  }


int Window_vector::close_and_exit( const bool abort )
  {
  const int key = Screen::show_message( abort ?
                  "Are you sure you want to abort ? (y/N) " :
                  "Are you sure you want to exit ? (y/N) ", true, true );
  if( key == 'y' || key == 'Y' )
    {
    if( !abort )			// save all modified buffers
      {
      Bufhandle_vector::save_all_named();
      for( int i = Bufhandle_vector::handles(); i > 0; next(), --i )
        {
        const Buffer & buffer = curwin().buffer();
        if( buffer.modified() )
          {
          if( read_only( buffer ) ) return windows();	//can't save
          const char * const msg = save_file();
          if( buffer.modified() )			// not saved
            { if( msg ) Screen::show_message( msg ); return windows(); }
          }
        }
      }
    if( windows() > 1 ) load( 1 );
    Bufhandle_vector::delete_all_handles(); curwin_ = 0; data.clear();
    }
  return windows();
  }


Window & Window_vector::window( int i )
  {
  if( i < 0 ) i = 0; else if( i >= windows() ) i = windows() - 1;
  return data[i];
  }


void Window_vector::last_visited()
  {
  if( Bufhandle_vector::handles() <= 1 ) return;
  curwin_ = Bufhandle_vector::last_visited( curwin_ );
  if( curwin_ >= windows() ) load();
  }


void Window_vector::next()
  {
  const int handles = Bufhandle_vector::handles();
  if( handles <= 1 ) return;
  if( curwin_ + 1 < windows() ) ++curwin_;
  else
    {
    if( handles <= windows() ) curwin_ = 0;
    else { Bufhandle_vector::next(); load(); }
    }
  }


void Window_vector::prev()
  {
  const int handles = Bufhandle_vector::handles();
  if( handles <= 1 ) return;
  if( curwin_ > 0 ) --curwin_;
  else
    {
    if( handles <= windows() ) curwin_ = windows() - 1;
    else { Bufhandle_vector::prev(); load(); }
    }
  }


const char * Window_vector::add_char( const int key, const bool force )
  {
  Window & w = curwin();
  if( read_only( w.buffer() ) ) return 0;

  if( key >= 0 && key < 256 &&
      ( force || key == '\t' || key == '\n' || ISO_8859::isprint( key ) ) )
    {
    const long col = w.pointer().col;
    if( w.buffer().pputc( w.pointer(), key ) )
      {
      if( w.pointer().col < col ) w.to_left();		// pointer wrapped
      Screen::repaint( &w.buffer() );
      }
    return 0;
    }
  int bufsize = 0;
  char * const buf = Screen::msg_line_buf( &bufsize );
  snprintf( buf, bufsize, "Key code %u(0x%02X) ignored", key, key );
  return buf;
  }


void Window_vector::delete_char( const bool back )
  {
  Window & w = curwin();

  if( !read_only( w.buffer() ) && w.buffer().pdelc( w.pointer(), back ) )
    Screen::repaint( &w.buffer() );
  }


void Window_vector::delete_line()
  {
  Window & w = curwin();

  if( !read_only( w.buffer() ) && w.buffer().pdell( w.pointer() ) )
    Screen::repaint( &w.buffer() );
  }


void Window_vector::copy_block()
  {
  Window & w = curwin();
  if( no_block() || read_only( w.buffer() ) ) return;

  const Buffer & buffer = *Block::bufferp();
  if( Block::copy_block( w.buffer(), w.pointer() ) )
    {
    if( &buffer != &w.buffer() ) Screen::repaint( &buffer );
    Screen::repaint( &w.buffer() );
    }
  }


void Window_vector::delete_block()
  {
  Window & w = curwin();
  Buffer & buffer = w.buffer();
  if( no_block() || no_block_in_this_file( buffer ) || read_only( buffer ) )
    return;
  const Point p = w.buffer_handle().pointer;
  const long left = w.buffer_handle().top_left.col;
  const bool move = p > Block::begin() && p.line <= Block::end().line &&
                    buffer.to_cursor( Block::begin() ).col < left;

  if( Block::delete_block() )
    { if( move ) w.to_left(); Screen::repaint( &buffer ); }
  }


void Window_vector::move_block()
  {
  Window & w = curwin();
  Point & p = w.pointer();

  if( !no_block() && !Block::in_block_or_end( w.buffer(), p ) )
    {
    const Buffer & buffer = *Block::bufferp();
    if( !read_only( w.buffer() ) && !read_only( buffer ) &&
        Block::copy_block( w.buffer(), p, true ) )
      {
      if( &buffer != &w.buffer() ) Screen::repaint( &buffer );
      Screen::repaint( &w.buffer() );
      }
    }
  }


const char * Window_vector::read_block()
  {
  Window & w = curwin();
  if( read_only( w.buffer() ) ) return 0;

  std::string name;
  if( Menu::get_filename( "Name of file to insert (^C to abort): ", name ) <= 0 )
    return 0;
  try {
    Buffer tmp( name.c_str() );
    if( tmp.empty() )
      throw Buffer::Error( tmp.existed ? "File is empty" : "File not found" );
    Point p = w.pointer();
    if( w.buffer().pputb( p, tmp, tmp.bof(), tmp.eof() ) )
      Screen::repaint( &w.buffer() );
    }
  catch( Buffer::Error & e ) { return e.msg; }
  return 0;
  }


const char * Window_vector::write_block()
  {
  if( no_block() ) return 0;
  const Buffer & buffer = *Block::bufferp();
  std::string name;
  if( Menu::get_filename( "Name of file to write (^C to abort): ", name ) <= 0 )
    return 0;
  const char * const msg = already_being_edited( name );
  if( msg ) return msg;
  try {
    bool done;
    if( !RC::editor_options().rectangle_mode )
      done = buffer.save( &name, true, false, Block::begin(), Block::end() );
    else
      {
      Buffer tmp;
      Block::save_block_position();
      Block::copy_block( tmp, tmp.bof() );
      Block::restore_block_position();
      done = tmp.save( &name );
      }
    if( !done ) return "Block not written";
    if( RC::editor_options().auto_unmark )
      { Block::unmark(); Screen::repaint( &buffer ); }
    }
  catch( Buffer::Error & e ) { return e.msg; }
  int bufsize = 0;
  char * const buf = Screen::msg_line_buf( &bufsize );
  snprintf( buf, bufsize, "Block written to file '%s'",
            Screen::cut_filename( name, 24 ) );
  return buf;
  return 0;
  }


void Window_vector::indent_block()
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return;

  const long last_line = Block::last_mline();
  Point p = Block::begin();
  bool first_post = true;
  const bool ow = buffer.options.overwrite;
  buffer.options.overwrite = false;
  buffer.reset_appendable();
  while( p.line <= last_line )
    {
    p = buffer.bot( p );
    if( p.col > 0 || p < buffer.eol( p ) )
      {
      if( first_post ) first_post = false; else buffer.force_append();
      for( int i = 0; i < RC::editor_options().indent_step(); ++i )
        buffer.pputc( p, ' ' );
      }
    ++p.line;
    }
  buffer.reset_appendable();
  buffer.options.overwrite = ow;
  if( RC::editor_options().auto_unmark ) Block::unmark();
  Screen::repaint( &buffer );
  }


const char * Window_vector::unindent_block()
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  const long last_line = Block::last_mline();
  Point p = Block::begin();
  bool fail = false;
  while( p.line <= last_line && !fail )
    {
    if( buffer.eol( p ).col > 0 &&
        buffer.to_cursor( buffer.bot( p ) ).col < RC::editor_options().indent_step() )
      fail = true;
    ++p.line;
    }
  if( fail ) return "Can't unindent more";

  bool first_post = true;
  const bool ow = buffer.options.overwrite;
  buffer.options.overwrite = false;
  buffer.reset_appendable();
  p = Block::begin();
  while( p.line <= last_line )
    {
    const long target_col = buffer.to_cursor( buffer.bot( p ) ).col -
                            RC::editor_options().indent_step();
    if( buffer.eol( p ).col > 0 )
      while( true )
        {
        p = buffer.bot( p );
        const long col = buffer.to_cursor( p ).col;
        if( col > target_col )
          {
          if( first_post ) first_post = false; else buffer.force_append();
          buffer.pdelc( p, true );
          }
        else if( col < target_col )
          {
          if( first_post ) first_post = false; else buffer.force_append();
          buffer.pputc( p, ' ' );
          }
        else break;
        }
    ++p.line;
    }
  buffer.reset_appendable();
  buffer.options.overwrite = ow;
  if( RC::editor_options().auto_unmark ) Block::unmark();
  Screen::repaint( &buffer );
  return 0;
  }


const char * Window_vector::undo()
  {
  Window & w = curwin();

  if( !read_only( w.buffer() ) )
    {
    if( w.buffer().undo( w.pointer() ) ) Screen::repaint( &w.buffer() );
    else return "Nothing to undo";
    }
  return 0;
  }


const char * Window_vector::redo()
  {
  Window & w = curwin();

  if( !read_only( w.buffer() ) )
    {
    if( w.buffer().redo( w.pointer() ) ) Screen::repaint( &w.buffer() );
    else return "Nothing to redo";
    }
  return 0;
  }


void Window_vector::center_line()
  {
  Window & w = curwin();
  Buffer & buffer = w.buffer();
  const Point bol = buffer.bol( w.pointer() );
  const Point bot = buffer.bot( bol );
  const Point cbot = buffer.to_cursor( bot );
  const Point ceot = buffer.to_cursor( buffer.eot( bol ) );
  long l = cbot.col - buffer.options.lmargin();
  long r = buffer.options.rmargin() - ceot.col;

  if( read_only( buffer ) || cbot >= ceot || l == r || l == r + 1 ) return;
  buffer.reset_appendable();
  const long off = ( bot != cbot ) ? cbot.col : 0;
  if( off )
    { buffer.pdelb( bol, bot ); l -= off; r += off; buffer.force_append(); }
  else if( l > r + 1 && bot.col > 0 )
    buffer.pdelb( bol, Point( bot.line, std::min( bot.col, ( l - r ) / 2 ) ) );
  if( l < r )
    {
    const bool ow = buffer.options.overwrite;
    buffer.options.overwrite = false;
    Point p = bol;
    for( ; l < r; ++l, --r ) buffer.pputc( p, ' ' );
    buffer.options.overwrite = ow;
    }
  buffer.reset_appendable();
  Screen::repaint( &buffer );
  }


const char * Window_vector::reformat_paragraph()
  {
  Window & w = curwin();
  Buffer & buffer = w.buffer();
  if( read_only( buffer ) ) return 0;

  Point cursor = w.buffer_handle().cursor;
  Screen::show_feedback( "reformatting..." );
  try {
    if( buffer.reformat_paragraph( cursor, false ) )
      {
      buffer.reset_appendable();
      Screen::repaint( &buffer );
      w.update_points( cursor, true, false, Window::from_cursor );
      }
    else w.repaint();				// remove feedback
    }
  catch( Buffer::Error & e ) { w.repaint(); return e.msg; } // remove feedback
  return 0;
  }


const char * Window_vector::reformat_block()
  {
  Window & w = curwin();
  Buffer & buffer = w.buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  const char * msg = 0;
  Point cursor = w.buffer_handle().cursor;
  cursor.line = Block::begin().line;			// keep cursor col
  const bool incomplete = Block::end().col > 0;
  bool change = false;
  Screen::show_feedback( "reformatting..." );
  while( cursor.line < Block::end().line + incomplete &&
         cursor.line < buffer.lines() )
    if( buffer.blank( cursor.line ) )	// remove whitespace from blank lines
      {
      const long c = buffer.characters( cursor.line );
      if( c > 1 || ( c == 1 && cursor.line == buffer.last_line() ) )
        { if( change ) buffer.force_append(); else buffer.reset_appendable();
          buffer.pdelb( buffer.bol( cursor ), buffer.eol( cursor ) );
          change = true; }
      ++cursor.line;					// keep cursor col
      }
    else
      {
      try { if( buffer.reformat_paragraph( cursor, change ) ) change = true; }
      catch( Buffer::Error & e )
        { if( change ) { msg = e.msg; break; }
          else { w.repaint(); return e.msg; } }	// remove feedback
      cursor.line = buffer.eop( cursor ).line;	// end of paragraph, keep col
      }
  if( change && !msg ) cursor.line = buffer.bop( Block::begin() ).line;
  if( RC::editor_options().auto_unmark ) Block::unmark();
  if( change )
    {
    buffer.reset_appendable();
    Screen::repaint( &buffer );
    w.update_points( cursor, true, false, Window::from_cursor );
    }
  else { w.repaint();				// remove feedback
         if( !msg ) msg = "Paragraphs in block are already reformatted"; }
  return msg;
  }


/* Can't use to_lowercase, etc, because of rectangle mode.
   Can't change characters in place because of atomic undo.
   Keep in sync with Basic_buffer::capitalize.
*/
const char * Window_vector::change_case( const int caps_level )
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || read_only( buffer ) )
    return 0;

  Basic_buffer tmp;
  Point p = tmp.bof();
  bool change = false;
  bool prev_is_whitespace = true;
  for( Point p1 = Block::begin(); p1 < Block::end(); )
    {
    const bool in_block = Block::in_block( buffer, p1 );
    unsigned char ch = buffer.pgetc( p1 );
    switch( caps_level )
      {
      case 0: if( in_block && ISO_8859::isupper( ch ) )
                { ch = ISO_8859::tolower( ch ); change = true; } break;
      case 1: if( in_block && ISO_8859::islower( ch ) )
                { ch = ISO_8859::toupper( ch ); change = true; } break;
      case 2: {
              if( !in_block ) { prev_is_whitespace = true; break; }
              const bool is_whitespace = ISO_8859::isspace( ch );
              if( prev_is_whitespace && ISO_8859::islower( ch ) )
                { ch = ISO_8859::toupper( ch ); change = true; }
              if( !prev_is_whitespace && ISO_8859::isupper( ch ) )
                { ch = ISO_8859::tolower( ch ); change = true; }
              prev_is_whitespace = is_whitespace;
              }
      }
    tmp.pputc( p, ch );
    }
  if( !change ) return "No letters to change in block";
  replace_and_repaint( tmp );
  return 0;
  }


void Window_vector::encode_base64()
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return;

  std::string in, out;
  Block::bufferp()->to_string( Block::begin(), Block::end(), in );
  Encoding::base64_encode( in, out );
  Basic_buffer tmp;
  Point p = tmp.bof();
  int c = 0;
  for( unsigned long i = 0; i < out.size(); ++i )
    {
    tmp.pputc( p, out[i] );
    if( ++c >= 76 ) { c = 0; tmp.pputc( p, '\n' ); }
    }
  if( c ) tmp.pputc( p, '\n' );
  replace_and_repaint( tmp );
  }


const char * Window_vector::decode_base64()
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  std::string in, out;
  Block::bufferp()->to_string_ws( Block::begin(), Block::end(), in );
  long idx;
  if( !Encoding::base64_decode( in, out, &idx ) )
    {
    Block::disable_marking();
    Point p = Block::begin();
    while( idx > 0 ) if( !ISO_8859::isspace( buffer.pgetc( p ) ) ) --idx;
    while( ISO_8859::isspace( buffer[p] ) ) buffer.pnext( p );
    if( idx == 0 && buffer.pisvalid( p ) )
      curwin().update_points( p, true, true );
    return "Invalid Base64 data in block";
    }
  Basic_buffer tmp;
  Point p = tmp.bof();
  for( unsigned long i = 0; i < out.size(); ++i )
    tmp.pputc( p, out[i] );
  replace_and_repaint( tmp );
  return 0;
  }


bool Window_vector::encode_ascii_utf8( const char ** const msgp,
                                       const bool mode_a, const bool force )
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) { *msgp = 0; return false; }

  std::string in, out;
  Block::bufferp()->to_string( Block::begin(), Block::end(), in );
  if( mode_a && !Encoding::ascii_encode( in, out ) )
    { *msgp = "Text in block is already ASCII encoded"; return false; }
  if( !mode_a && !Encoding::utf8_encode( in, out, force ) )
    { *msgp = "Text in block is already UTF-8 encoded"; return false; }
  Basic_buffer tmp;
  Point p = tmp.bof();
  for( unsigned long i = 0; i < out.size(); ++i )
    tmp.pputc( p, out[i] );
  replace_and_repaint( tmp );
  *msgp = "Done";
  return true;
  }


const char * Window_vector::decode_quoted_printable_utf8( const bool mode_q,
                                                          const bool force )
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  std::string in, out;
  Block::bufferp()->to_string( Block::begin(), Block::end(), in );
  if( mode_q )
    {
    long idx;
    if( !Encoding::quoted_printable_decode( in, out, &idx ) )
      {
      Block::disable_marking();
      Point p = Block::begin();
      if( buffer.pisvalid( p ) && buffer.pseek( p, idx ) )
        curwin().update_points( p, true, true );
      return "Invalid Quoted-Printable data in block";
      }
    }
  else
    {
    long idx;
    const int ret = Encoding::utf8_decode( in, out, &idx, force );
    if( ret != 0 )
      {
      Block::disable_marking();
      Point p = Block::begin();
      if( buffer.pisvalid( p ) && buffer.pseek( p, idx ) )
        curwin().update_points( p, true, true );
      if( ret > 0 )
        {
        int bufsize = 0;
        char * const buf = Screen::msg_line_buf( &bufsize );
        snprintf( buf, bufsize, "UTF-8 code 'U%04X' out of range in block", ret );
        return buf;
        }
      else
        return "Invalid UTF-8 data in block";
      }
    }
  if( in == out ) return "Text in block is already decoded";
  Basic_buffer tmp;
  Point p = tmp.bof();
  for( unsigned long i = 0; i < out.size(); ++i )
    tmp.pputc( p, out[i] );
  replace_and_repaint( tmp );
  return "Done";
  }


const char * Window_vector::remove_utf8_out_of_range()
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  std::string in, out;
  Block::bufferp()->to_string( Block::begin(), Block::end(), in );
  long idx;
  const int ret = Encoding::remove_utf8_out_of_range( in, out, &idx );
  if( ret == 0 ) return "UTF-8 text in block is already in range";
  if( ret < 0 )
    {
    Block::disable_marking();
    Point p = Block::begin();
    if( buffer.pisvalid( p ) && buffer.pseek( p, idx ) )
      curwin().update_points( p, true, true );
    return "Invalid UTF-8 data in block";
    }
  Basic_buffer tmp;
  Point p = tmp.bof();
  for( unsigned long i = 0; i < out.size(); ++i )
    tmp.pputc( p, out[i] );
  replace_and_repaint( tmp );
  return "Done";
  }


void Window_vector::encode_rot1347( const bool mode13 )
  {
  Buffer & buffer = curwin().buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return;

  Basic_buffer tmp;
  Point p = tmp.bof();
  if( mode13 )
    for( Point p1 = Block::begin(); p1 < Block::end(); )
      tmp.pputc( p, Encoding::rot13( buffer.pgetc( p1 ) ) );
  else
    for( Point p1 = Block::begin(); p1 < Block::end(); )
      tmp.pputc( p, Encoding::rot47( buffer.pgetc( p1 ) ) );
  replace_and_repaint( tmp );
  }


const char * Window_vector::remove_duplicate_lines( const bool back )
  {
  Window & w = curwin();
  Buffer & buffer = w.buffer();
  if( no_block() || no_block_in_this_file( buffer ) || rectangular_block() ||
      read_only( buffer ) ) return 0;

  bool change = false;
  const long first_line = Block::begin().line + ( Block::begin().col > 0 );
  long last_line = std::min( Block::end().line, buffer.lines() ) - 1;
  if( back )
    {
    for( long line1 = last_line; line1 > first_line; --line1 )
      for( long line2 = line1 - 1; line2 >= first_line; --line2 )
        if( buffer.compare_lines( line1, line2 ) )
          {
          if( change ) buffer.force_append(); else change = true;
          Point p( line2, 0 );
          buffer.pdell( p );
          --line1;			// adjust current line
          }
    }
  else
    {
    for( long line1 = first_line; line1 < last_line; ++line1 )
      for( long line2 = last_line; line2 > line1; --line2 )
        if( buffer.compare_lines( line1, line2 ) )
          {
          if( change ) buffer.force_append(); else change = true;
          Point p( line2, 0 );
          buffer.pdell( p );
          --last_line;
          }
    }
  if( !change ) return "No duplicate lines in block";
  buffer.reset_appendable();
  if( Block::valid() )
    {
    if( back ) w.update_points( Block::end(), true, true );
    else w.update_points( Block::begin(), true, true );
    }
  if( RC::editor_options().auto_unmark ) Block::unmark();
  Screen::repaint( &buffer );
  return 0;
  }


const char * Window_vector::copyright_update()
  {
  static std::vector< std::string > history;
  const std::string search_string( "\\<Copyright\\>\\+\\[^\\n]\\[1-9]\\+\\+\\+\\+\\[0-9]\\*\\<\\[^0-9]" );
  const std::string search_string2( "\\[1-9]\\+\\+\\+\\+\\[0-9]" );

  std::string year_string;
  const time_t tt = std::time( 0 );
  const std::tm * const t = std::localtime( &tt );
  if( t && t->tm_year >= 0 )
    {
    int year = 1900 + t->tm_year;
    while( year )
      { year_string.insert( year_string.begin(), ( year % 10 ) + '0' );
        year /= 10; }
    }
  history.push_back( year_string );
  if( Screen::get_string( "Year to add (^C to abort): ", history ) <= 0 )
    return 0;
  year_string = history.back();
  int new_year = 0;
  for( unsigned i = 0; i < year_string.size(); ++i )
    {
    if( !std::isdigit( (unsigned char)year_string[i] ) )
      { year_string.clear(); break; }
    new_year *= 10; new_year += year_string[i] - '0';
    }
  if( year_string.size() < 4 || year_string[0] == '0' ||
      new_year < 1000 || new_year > 9999 ) return "Invalid year";
  bool ask = true, found = false, modified = false, outdated = false;
  bool skipped = false;
  for( int i = Bufhandle_vector::handles(); i > 0; next(), --i )
    {
    Window & w = curwin();
    Buffer & buffer = w.buffer();
    if( buffer.options.read_only ) { skipped = true; continue; }
    Point p = buffer.bof();
    std::vector< std::string > regex_pieces;
    while( true )
      {
      Point pn = p;			// begin of notice
      if( Regex::find( buffer, pn, p, search_string, regex_pieces ) != 0 )
        break;				// no more copyrights found
      Point py = pn;			// begin of range of years
      Point pt;
      if( Regex::find( buffer, py, pt, search_string2, regex_pieces ) != 0 ||
          py <= pn || pt >= p )
        continue;			// not a valid copyright notice
      found = true;
      const Point p0 = buffer.bol( pn );	// bol of notice
      const Point ph2 = buffer.eol( p );	// end of copyright holder
      buffer.pprev( p );			// begin of copyright holder
      Basic_buffer tmp( buffer, p0, py );	// "<prefix> Copyright <text>"
      if( insert_new_year( buffer, p0, pn, py, p, ph2, new_year, tmp ) )
        {
        outdated = true;
        p = ph2;
        w.update_points( p, true, true );
        int key = 0;
        if( ask || Screen::kbhit() >= 0 )
          {
          key = Screen::show_message( "Update (Y)es (N)o (R)est (^C to abort): ",
                                      true );
          if( key == 3 ) return 0;
          if( ask && ( key == 'r' || key == 'R' ) ) ask = false;
          }
        if( !ask || key == 'y' || key == 'Y' )
          {
          buffer.replace( p0, p, tmp, tmp.bof(), tmp.eof() );
          w.update_points( p, true, true );
          Screen::repaint( &buffer );
          modified = true;
          }
        }
      }
    }
  if( skipped )
    {
    if( !found ) return "No copyright notices found (read-only buffers were skipped)";
    if( !outdated ) return "All copyright notices are up to date (read-only buffers were skipped)";
    if( !modified ) return "No copyright notices have been modified (read-only buffers were skipped)";
    return "Finished (read-only buffers were skipped)";
    }
  else
    {
    if( !found ) return "No copyright notices found";
    if( !outdated ) return "All copyright notices are up to date";
    if( !modified ) return "No copyright notices have been modified";
    return "Finished";
    }
  }


const char * Window_vector::search_word( const int abort_key )
  {
  const Buffer & buffer = curwin().buffer();
  Point p1 = curwin().pointer();
  if( ISO_8859::isalnum_( buffer[p1] ) || buffer.piseow( p1 ) )
    {
    Point p2 = p1;
    while( !buffer.pisbow( p1 ) ) if( !buffer.pprev( p1 ) ) break;
    while( !buffer.piseow( p2 ) ) if( !buffer.pnext( p2 ) ) break;
    if( buffer.pisbow( p1 ) && buffer.piseow( p2 ) )
      {
      std::string s_ini;
      buffer.to_string( p1, p2, s_ini );
      s_ini.insert( 0, "\\<" ); s_ini += "\\>";
      return search( abort_key, false, s_ini );
      }
    }
  return 0;
  }


// 'toggle' reverses direction; 1 for this call, 2 permanently
const char * Window_vector::search( const int abort_key, const bool again,
                                    const std::string & s_ini,
                                    const int toggle )
  {
  static std::vector< std::string > search_history;
  static std::vector< std::string > replace_history;
  static std::vector< std::string > options_history;
  static std::string search_string;		// deescapized search string
  static std::string replace_string;		// deescapized replace string
  static int replace = 0;			// 1 = replace, 2 = delete
  static bool backward_opt = false;
  static bool block = false;
  static bool global = false;
  static bool icase = false;

  if( !again || search_string.empty() )
    {
    search_history.push_back( s_ini );
    long size = Screen::get_string( "Find (^C to abort): ", search_history,
                                    abort_key );
    if( size <= 0 )
      {
      if( s_ini.size() && search_history.size() ) search_history.pop_back();
      if( search_history.empty() ) search_string.clear();
      return 0;
      }
    if( !deescapize_string( search_history.back(), search_string, true ) )
      { search_string.clear(); return "Syntax error in search string"; }
    std::string prompt;
    if( RC::editor_options().ignore_case )
      prompt = "(N)o_icase (R)eplace (D)elete (B)ackwards (G)lobal Bloc(K) (^C to abort): ";
    else prompt = "(I)case (R)eplace (D)elete (B)ackwards (G)lobal Bloc(K) (^C to abort): ";
    options_history.push_back( std::string() );
    size = Screen::get_string( prompt, options_history, abort_key );
    if( size < 0 ) return 0;
    replace = 0; backward_opt = false; block = false; global = false;
    icase = RC::editor_options().ignore_case;
    if( size > 0 )
      {
      const std::string & options = options_history.back();
      for( unsigned i = 0; i < options.size(); ++i )
        switch( ISO_8859::toupper( options[i] ) )
          {
          case 'B': backward_opt = true; break;
          case 'D': replace = 2; break;			// delete
          case 'G': global = true; break;
          case 'I': icase = true; break;
          case 'K': block = true; break;
          case 'N': icase = false; break;
          case 'R': replace = 1; break;			// replace
          }
      }
    if( block ) global = false;
    if( replace != 0 )			// replace or delete
      {
      replace_string.clear();		// by default replace with nothing
      if( replace == 1 )
        {
        replace_history.push_back( std::string() );
        size = Screen::get_string( "Replace with (^C to abort): ",
                                   replace_history, abort_key );
        if( size < 0 ) { replace = 0; return 0; }	// aborted
        if( size > 0 &&
            !deescapize_string( replace_history.back(), replace_string, false ) )
          { search_string.clear(); return "Syntax error in replace string"; }
        }
      }
    }
  if( toggle == 2 ) backward_opt = !backward_opt;	// toggle permanently
  bool backward = backward_opt;		// leave backward as-is by default
  if( toggle == 1 ) backward = !backward;		// toggle this call

  Point p = curwin().pointer();			// search point
  if( block )
    {
    if( no_block() || no_block_in_this_file( curwin().buffer() ) ) return 0;
    if( !again || !Block::in_block_or_end( curwin().buffer(), p ) )
      { if( backward ) p = Block::end(); else p = Block::begin(); }
    }

  const Buffer * prev_replace = 0;
  const char * msg = 0;
  bool found = false, ask = true, found_any = false, wrapped = false;
  bool empty_replace = false, feedback = true;
  const int handles = global ? Bufhandle_vector::handles() : 1;
  for( int i = handles - 1; i >= 0 && !found; --i )
    {
    if( feedback )
      { feedback = false;
        Screen::show_feedback( ask ? "searching..." : "replacing..." ); }
    Window & w = curwin();
    std::vector< std::string > regex_pieces;
    Point p1 = p, p2 = p;
    { const int ret = Regex::find( w.buffer(), p1, p2, search_string,
                                   regex_pieces, icase, backward );
    if( ret == 2 ) { msg = "Search error: check syntax"; break; }
    found = ret == 0; }
    empty_replace = found && p1 == p2;
    if( found && block &&
        ( p1 < Block::begin() || p1 >= Block::end() || p2 > Block::end() ) )
      found = false;
    if( backward ) p = p1; else p = p2;

    if( found )
      {
      if( block && RC::editor_options().rectangle_mode &&
          ( p1.col < Block::begin().col || p2.col > Block::end().col ) )
        { ++i; found = false; continue; }
      found_any = true;
      if( replace == 0 ) { w.update_points( p, true, true ); break; }
      Block::save_block_position(); w.update_points( p, false, true );
      if( ask )
        {
        const bool rm = RC::editor_options().rectangle_mode;
        RC::editor_options().rectangle_mode = false;
        Block::set_block( w.buffer(), p1, p2 ); w.repaint();
        RC::editor_options().rectangle_mode = rm;
        }
      while( true )
        {
        int key = 0;
        if( ask || Screen::kbhit() >= 0 )
          {
          key = Screen::show_message( "Replace (Y)es (N)o (R)est (^C to abort): ",
                                      true, false, false );
          if( key == 3 ) { if( !ask ) msg = "Interrupted"; break; }
          if( ask )
            {
            if( key == 'n' || key == 'N' ) { found = false; break; }
            if( key == 'r' || key == 'R' ) ask = false;
            }
          }
        if( !ask || key == 'y' || key == 'Y' )
          {
          Block::restore_block_position();
          if( !ask && empty_replace )
            { msg = "Infinite replacement loop"; break; }
          if( !ask && prev_replace == &w.buffer() ) w.buffer().force_append();
          const bool done =
            Regex::replace( w.buffer(), p1, p2, replace_string, regex_pieces );
          Block::save_block_position();
          if( done )
            {
            if( !ask ) prev_replace = &w.buffer();
            if( empty_replace && ( p1 < p2 ) ) empty_replace = false;
            found = false; if( backward ) p = p1; else p = p2;
            w.update_points( p, false, true );
            }
          else
            {
            prev_replace = 0; w.buffer().reset_appendable();
            if( w.buffer().options.read_only ) msg = rdonly_msg;
            else msg = "Replace error: check syntax";
            }
          break;
          }
        }
      ++i; Block::restore_block_position();
      if( ask || found ) Screen::repaint( &w.buffer() );
      feedback = true;
      continue;
      }

    prev_replace = 0; w.buffer().reset_appendable();
    if( !ask && ( block || wrapped || !RC::editor_options().search_wrap ) )
      Screen::repaint( &w.buffer() );
    if( block ) break;
    if( !wrapped )
      {
      if( global )
        {
        if( backward ) { prev(); p = curwin().buffer().eof(); }
        else { next(); p = curwin().buffer().bof(); }
        if( i == 0 ) { wrapped = true; ++i; }
        }
      else if( RC::editor_options().search_wrap )
        {
        if( backward ) p = w.buffer().eof();
        else p = w.buffer().bof();
        wrapped = true; ++i;
        }
      }
    }
  data.back().repaint();			// remove feedback
  if( !msg )
    {
    if( !found_any )
      {
      if( block ) msg = "Not found (search restricted to marked block)";
      else msg = "Not found";
      }
    else if( wrapped ) msg = "Wrapped";
    else if( !ask ) msg = "Done";
    }
  return msg;
  }
