/*  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 <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/stat.h>

#include "buffer.h"
#include "buffer_handle.h"
#include "block.h"
#include "iso_8859.h"
#include "menu.h"
#include "rc.h"
#include "screen.h"


namespace {

const std::string unnamed( "(Unnamed)" );


std::vector< Point * > & get_marks_and_pointers( Buffer & b )
  {
  static std::vector< Point * > v;
  v.clear();
  for( int i = 0; i < 10; ++i ) v.push_back( &b.mark[i] );
  for( int i = 0; i < Bufhandle_vector::handles(); ++i )
    {
    Buffer_handle & bh = Bufhandle_vector::handle( i );
    if( &b == &bh.buffer() ) v.push_back( &bh.pointer );
    }
  if( Block::bufferp() == &b )
    {
    v.push_back( &Block::anchor() );
    v.push_back( &Block::begin() );
    v.push_back( &Block::end() );
    }
  return v;
  }

} // end namespace


bool Buffer::Change_history::Record::append_record( const Basic_buffer & buffer,
                                                    Record & r )
  {
  if( r.atom_vector.size() != 1 ) return false;
  Atom & a1 = atom_vector.back();
  Atom & a2 = r.atom_vector.back();

  if( !a1.bbp && !a2.bbp && a1.end == a2.begin )
    { a1.end = a2.end; return true; }		// consecutive insert
  if( a1.bbp && a2.bbp )
    {
    if( a1.begin == a1.end && a2.begin == a2.end )	// two deletes
      {
      if( a1.begin == a2.begin )		// delete at same place
        {
        Point p = a1.bbp->eof();
        a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
        delete a2.bbp; a2.bbp = 0; return true;
        }
      if( a2.bbp->one_character() && buffer.pprev( a1.begin ) && a1.begin == a2.begin )
        {					// consecutive delete
        Point p = a1.bbp->bof();
        a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
        a1.end = a1.begin; delete a2.bbp; a2.bbp = 0; return true;
        }
      else a1.begin = a1.end;
      }
    else if( a1.begin < a1.end && a2.begin < a2.end && a1.end == a2.begin )
      {						// consecutive overwrite
      Point p = a1.bbp->eof();
      a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
      a1.end = a2.end; delete a2.bbp; a2.bbp = 0; return true;
      }
    }
  return false;
  }


void Buffer::Change_history::Record::delete_atoms() throw()
  {
  for( unsigned int i = 0; i < atom_vector.size(); ++i )
    if( atom_vector[i].bbp )
      { delete atom_vector[i].bbp; atom_vector[i].bbp = 0; }
  }


Buffer::Change_history::~Change_history() throw()
  {
  for( unsigned int i = 0; i < record_vector.size(); ++i )
    record_vector[i].delete_atoms();
  }


void Buffer::Change_history::add_record( const Basic_buffer & buffer,
                                         Record & r )
  {
  if( index + 1 < records() )
    {
    for( unsigned int i = index + 1; i < record_vector.size(); ++i )
      record_vector[i].delete_atoms();
    record_vector.erase( record_vector.begin() + index + 1, record_vector.end() );
    appendable = false;
    }
  if( !records() ) { appendable = false; force_append = false; }
  if( ( appendable || force_append ) && record_vector.back().append_record( buffer, r ) )
    record_vector.back().after = r.after;
  else if( force_append )
    {
    std::vector< Atom > & av = record_vector.back().atom_vector;
    av.insert( av.end(), r.atom_vector.begin(), r.atom_vector.end() );
    record_vector.back().after = r.after;
    }
  else { record_vector.push_back( r ); ++index; }
  appendable = true; force_append = false;
  }


bool Buffer::Options::set_lmargin( const int l ) throw()
  {
  if( l >= 0 && l < _rmargin ) { _lmargin = l; return true; }
  return false;
  }


bool Buffer::Options::set_rmargin( const int r ) throw()
  {
  if( r > _lmargin && r < 1000 ) { _rmargin = r; return true; }
  return false;
  }


Buffer::Buffer( const Options & opts, const std::string * namep )
  : _modified( false ), saved( false ), options( opts ), crlf( false )
  {
  FILE *f = 0;
  errno = 0;
  if( ( !namep || !namep->size() || *namep == "-" ) && !isatty( fileno( stdin ) ) )
    f = stdin;
  else if( namep && namep->size() )
    { _name = *namep; f = std::fopen( _name.c_str(), "r" ); }
  else return;

  if( !f )
    { if( errno == ENOENT ) return; else throw Error( "Error opening file" ); }

  const int bufsize = 65536;
  char buf[bufsize];
  bool file_eof = true;
  while( file_eof )
    {
    const int size = std::fread( buf, 1, bufsize, f );
    if( size <= 0 ) break;
    int pos = 0;
    while( pos < size )
      {
      int newpos = pos;
      while( buf[newpos] != '\n' && newpos + 1 < size ) ++newpos;
      try { add_line( &buf[pos], newpos - pos + 1 ); }
      catch( ... ) { file_eof = false; break; }
      pos = newpos + 1;
      }
    }

  const bool file_error = std::ferror( f );
  if( file_eof ) file_eof = std::feof( f );
  if( f != stdin )
    {
    if( std::freopen( _name.c_str(), "r+", f ) ) std::fclose( f );
    else options.read_only = true;
    }
  else if( !ttyname( 1 ) || !std::freopen( ttyname( 1 ), "r", stdin ) )
    throw Error( "Error opening terminal for input" );
  if( file_error ) throw Error( "Error reading file" );
  if( !file_eof ) throw Error( "Not enough memory" );
  crlf = detect_and_remove_cr();
  if( lines() >= 23 ) mark[0].line = 22;
  for( int i = 1; i < 10 && lines() >= 100 * i; ++i )
    mark[i].line = 100 * i - 1;
  }


bool Buffer::save( const std::string * namep, bool set_name, bool append,
                   Point p1, Point p2 ) const
  {
  int mode = 0;
  struct stat st;
  if( append ) mode = S_IRUSR | S_IWUSR;
  else if( ( namep && namep->size() && !stat( namep->c_str(), &st ) ) ||
           ( _name.size() && !stat( _name.c_str(), &st ) ) )
         mode = st.st_mode;

  const bool had_name = ( _name.size() > 0 );
  bool same_name = set_name;
  if( set_name )
    {
    if( namep && namep->size() )
      {
      if( _name != *namep )
        {
        FILE *f = std::fopen( namep->c_str(), "r+" );
        if( f )
          {
          std::fclose( f );
          int key = Screen::show_message( "File exists. Overwrite ? (y/N) ", true, true );
          if( key != 'y' && key != 'Y' ) return false;
          }
        }
      if( !_name.size() ) _name = *namep;
      else if( _name != *namep ) same_name = false;
      }
    else { if( !_name.size() ) return false; else namep = &_name; }
    }
  if( !namep ) return false;

  if( same_name )
    {
    if( options.read_only ) return false;
    if( !saved && RC::editor_options().backup )
      {
      std::string backname( *namep ); backname += '~';
      std::remove( backname.c_str() );
      std::rename( namep->c_str(), backname.c_str() );
      }
    }

  FILE *f = 0;
  if( Menu::make_path( *namep ) )
    f = std::fopen( namep->c_str(), append ? "a" : "w" );
  if( !f )
    { if( !had_name ) { _name.clear(); } throw Error( "Error opening file" ); }
  if( mode ) fchmod( fileno( f ), mode );

  if( !this->pisvalid( p1 ) ) p1 = this->bof();
  if( !this->pisvalid( p2 ) ) p2 = this->eof();
  if( !crlf )
    for( Point p = p1; p < p2; ) { std::fputc( pgetc( p ), f ); }
  else for( Point p = p1; p < p2; )
    {
    const int ch = pgetc( p );
    if( ch == '\n' ) std::fputc( '\r', f );
    std::fputc( ch, f );
    }
  std::fclose( f );
  if( same_name )
    {
    for( int i = change_history.records() - 1; i >= 0; --i )
      if( change_history.record_vector[i].modified == false )
        { change_history.record_vector[i].modified = true; break; }
    reset_appendable();
    _modified = false; saved = true;
    }
  return true;
  }


const std::string & Buffer::character_info( const Point & p ) const throw()
  {
  static std::string s;
  char tmp1[60], tmp2[40];
  int ch = (*this)[p];

  if( ch >= 0 )
    snprintf( tmp2, sizeof tmp2, "Code %d(0x%02X/0%o) **", ch, ch ,ch );
  else if( p == eof() )
    snprintf( tmp2, sizeof tmp2, "(End Of File) **" );
  else
    { snprintf( tmp2, sizeof tmp2, "*** Error: Bad Pointer: Line %d  Col %d",
                p.line + 1, p.col + 1 ); s = tmp2; return s; }

  int percent = 0;
  for( int i = 0; i < p.line; ++i ) percent += characters( i );
  int offset = percent + p.col;
  if( crlf ) { offset += p.line; if( ch == '\n' ) ++offset; }
  for( int i = p.line; i < eof().line; ++i ) percent += characters( i );
  if( crlf ) percent += eof().line;
  percent += eof().col;
  if( percent > 0 ) percent = ( 100LL * offset ) / percent;

  snprintf( tmp1, sizeof tmp1, "** Line %d  Col %d  Offset %d(0x%X)  %d%%  ",
            p.line + 1, p.col + 1, offset, offset, percent );
  s = tmp1; s += tmp2;
  return s;
  }


const std::string & Buffer::uname() const throw()
  { if( _name.size() ) return _name; else return unnamed; }


// insert block b.[p1,p2) at p and move next
bool Buffer::pputb( Point & p, const Basic_buffer & b, const Point & p1,
                    const Point & p2, const int save_change )
  {
  if( options.read_only || !pisvalid( p ) || !b.pisvalid( p1 ) ||
      !b.pisvalid( p2 ) || p1 >= p2 ) return false;
  const Point p0 = p;
  Basic_buffer::pputb( p, b, p1, p2 );
  if( save_change )		// save_change == 0 only for undo/redo
    {
    const Point & after = (save_change != 2) ? p0 : p;
    Change_history::Record r( p0, after, _modified );
    r.add_atom( Change_history::Atom( p0, p ) );
    change_history.add_record( *this, r );
    _modified = true;
    }

  const int line_dif = p2.line - p1.line;
  std::vector< Point * > & pv = get_marks_and_pointers( *this );
  for( unsigned int i = 0; i < pv.size(); ++i )
    if( *pv[i] > p0 && pv[i] != &p )
      {
      if( pv[i]->line == p0.line )
        {
        if( line_dif == 0 ) pv[i]->col += p2.col - p1.col;
        else pv[i]->col += p2.col - p0.col;
        }
      pv[i]->line += line_dif;
      }
  return true;
  }


// put char and move next
bool Buffer::pputc( Point & p, const unsigned char ch )
  {
  if( options.read_only || !pisvalid( p ) ) return false;
  Basic_buffer tmp;
  Point dummy = tmp.bof();
  if( ch == '\n' )
    {
    tmp.pputc( dummy, ch );
    if( options.auto_indent )
      {
      const Point pt = bot( p );
      if( pt.col > 0 && p.col >= pt.col )
        tmp.pputb( dummy, *this, bol( pt ), pt );
      }
    const bool result = pputb( p, tmp, tmp.bof(), tmp.eof(), 2 );
    reset_appendable();
    return result;
    }
  else if( options.lmargin() > 0 && p.col == 0 && p == eol( p ) )
    {
    for( int i = 0; i < options.lmargin(); ++i ) tmp.pputc( dummy, ' ' );
    tmp.pputc( dummy, ch );
    reset_appendable();
    return pputb( p, tmp, tmp.bof(), tmp.eof(), 2 );
    }
  else
    {
    if( options.overwrite && p < eol( p ) )
      {
      const Point p1 = Point( p.line, p.col + 1 );
      Change_history::Record r( p, p1, _modified );
      Change_history::Atom a( p, p1, *this );
      Basic_buffer::pchgc( p, ch );
      a.end = p1; r.add_atom( a );
      change_history.add_record( *this, r );
      }
    else
      {
      const Point p0 = p;
      Basic_buffer::pputc( p, ch );
      Change_history::Record r( p0, p, _modified );
      r.add_atom( Change_history::Atom( p0, p ) );
      change_history.add_record( *this, r );
      std::vector< Point * > & pv = get_marks_and_pointers( *this );
      for( unsigned int i = 0; i < pv.size(); ++i )
        if( pv[i]->line == p0.line && pv[i]->col > p0.col && pv[i] != &p )
          ++pv[i]->col;
      if( options.word_wrap && to_cursor( p ).col > options.rmargin() + 1 &&
          !ISO_8859::isblank_( ch ) )
        {
        const Point pt = bot( p0 );
        Point p2 = p0;
        while( p2 > pt && pprev( p2 ) && !ISO_8859::isblank_( (*this)[p2] ) ) ;
        Point p1 = p2;
        while( p1 > pt && pprev( p1 ) && ISO_8859::isblank_( (*this)[p1] ) ) ;
        if( p1 >= pt && p1 < p2 && pnext( p1 ) && pnext( p2 ) )
          {
          reset_appendable(); pdelb( p1, p2 ); p2 = p1;
          force_append(); pputc( p2, '\n' );
          if( !options.auto_indent && options.lmargin() > 0 )
            {
            for( int i = 0; i < options.lmargin(); ++i ) tmp.pputc( dummy, ' ' );
            force_append(); pputb( p2, tmp, tmp.bof(), tmp.eof(), 2 );
            }
          }
        }
      }
    _modified = true;
    }
  return true;
  }


// delete block at [p1,p2)
bool Buffer::pdelb( const Point & p1, const Point & p2, const int save_change )
  {
  if( options.read_only || p1 >= p2 || !pisvalid( p1 ) || !pisvalid( p2 ) )
    return false;
  if( save_change )		// save_change == 0 only for undo/redo
    {
    const Point & p0 = (save_change != 2) ? p1 : p2;
    Change_history::Record r( p0, p1, _modified );
    r.add_atom( Change_history::Atom( p1, p2, *this ) );
    change_history.add_record( *this, r );
    _modified = true;
    }
  Basic_buffer::pdelb( p1, p2 );

  const int line_dif = p2.line - p1.line;
  std::vector< Point * > & pv = get_marks_and_pointers( *this );
  for( unsigned int i = 0; i < pv.size(); ++i )
    if( *pv[i] > p1 )
      {
      if( *pv[i] <= p2 ) *pv[i] = p1;
      else
        {
        if( pv[i]->line == p2.line ) pv[i]->col += p1.col - p2.col;
        pv[i]->line -= line_dif;
        }
      }
  return true;
  }


// delete char at 'p', or at '--p' if back is true
bool Buffer::pdelc( Point & p, const bool back )
  {
  Point p1 = p;
  if( back ) return ( pprev( p ) && pdelb( p, p1, 2 ) );
  else return ( pnext( p1 ) && pdelb( p, p1 ) );
  }


// delete line and set 'p' to bol()
bool Buffer::pdell( Point & p ) throw()
  {
  Point p1 = eol( p );
  if( options.read_only || characters( p.line ) <= 0 ||
      ( p1 != eof() && !pnext( p1 ) ) ) return false;
  p.col = 0;
  return pdelb( p, p1 );
  }


// replace block at [p1,p2) with b.[bp1,bp2) and move p2 next
bool Buffer::replace( const Point & p1, Point & p2, const Basic_buffer & b,
                      const Point & bp1, const Point & bp2 )
  {
  Change_history::Record r( p1, p1, _modified );
  Change_history::Atom a( p1, p2, *this );
  if( !pdelb( p1, p2, 0 ) && a.bbp ) { delete a.bbp; a.bbp = 0; }
  p2 = p1;
  if( pputb( p2, b, bp1, bp2, 0 ) ) a.end = p2;
  if( a.begin < a.end || a.bbp )
    {
    r.add_atom( a ); change_history.add_record( *this, r );
    _modified = true;
    return true;
    }
  return false;
  }


bool Buffer::undo( Point & p )
  {
  if( change_history.index < 0 ) return false;
  Change_history::Record & rec = change_history.record_vector[change_history.index];
  for( unsigned int i = 0; i < rec.atom_vector.size(); ++i )
    {
    Change_history::Atom & atom = rec.atom_vector[rec.atom_vector.size()-i-1];
    Basic_buffer *tmp = 0;
    if( atom.begin < atom.end )
      {
      tmp = new Basic_buffer( *this, atom.begin, atom.end );
      pdelb( atom.begin, atom.end, 0 );
      }
    atom.end = atom.begin;
    if( atom.bbp )
      {
      pputb( atom.end, *atom.bbp, atom.bbp->bof(), atom.bbp->eof(), 0 );
      delete atom.bbp;
      }
    atom.bbp = tmp;
    }
  p = rec.before;
  std::swap( _modified, rec.modified );
  --change_history.index;
  return true;
  }


bool Buffer::redo( Point & p )
  {
  if( change_history.index + 1 >= change_history.records() ) return false;
  ++change_history.index;
  Change_history::Record & rec = change_history.record_vector[change_history.index];
  for( unsigned int i = 0; i < rec.atom_vector.size(); ++i )
    {
    Change_history::Atom & atom = rec.atom_vector[i];
    Basic_buffer *tmp = 0;
    if( atom.begin < atom.end )
      {
      tmp = new Basic_buffer( *this, atom.begin, atom.end );
      pdelb( atom.begin, atom.end, 0 );
      }
    atom.end = atom.begin;
    if( atom.bbp )
      {
      pputb( atom.end, *atom.bbp, atom.bbp->bof(), atom.bbp->eof(), 0 );
      delete atom.bbp;
      }
    atom.bbp = tmp;
    }
  p = rec.after;
  std::swap( _modified, rec.modified );
  return true;
  }
