#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <cursor.h>
#include <mouse.h>
#include <keyboard.h>
#include <frame.h>
#include <fcall.h>
#include <plumb.h>
#include "dat.h"
#include "fns.h"
#include "edit.h"

static char Wsequence[] = "warning: changes out of sequence\n";
static int      warned = FALSE;

/*
* Log of changes made by editing commands.  Three reasons for this:
* 1) We want addresses in commands to apply to old file, not file-in-change.
* 2) It's difficult to track changes correctly as things move, e.g. ,x m$
* 3) This gives an opportunity to optimize by merging adjacent changes.
* It's a little bit like the Undo/Redo log in Files, but Point 3) argues for a
* separate implementation.  To do this well, we use Replace as well as
* Insert and Delete
*/

typedef struct Buflog Buflog;
struct Buflog
{
       short   type;           /* Replace, Filename */
       uint            q0;             /* location of change (unused in f) */
       uint            nd;             /* # runes to delete */
       uint            nr;             /* # runes in string or file name */
};

enum
{
       Buflogsize = sizeof(Buflog)/sizeof(Rune),
};

/*
* Minstring shouldn't be very big or we will do lots of I/O for small changes.
* Maxstring is RBUFSIZE so we can fbufalloc() once and not realloc elog.r.
*/
enum
{
       Minstring = 16,         /* distance beneath which we merge changes */
       Maxstring = RBUFSIZE,   /* maximum length of change we will merge into one */
};

void
eloginit(File *f)
{
       if(f->elog.type != Empty)
               return;
       f->elog.type = Null;
       if(f->elogbuf == nil)
               f->elogbuf = emalloc(sizeof(Buffer));
       if(f->elog.r == nil)
               f->elog.r = fbufalloc();
       bufreset(f->elogbuf);
}

void
elogclose(File *f)
{
       if(f->elogbuf){
               bufclose(f->elogbuf);
               free(f->elogbuf);
               f->elogbuf = nil;
       }
}

void
elogreset(File *f)
{
       f->elog.type = Null;
       f->elog.nd = 0;
       f->elog.nr = 0;
}

void
elogterm(File *f)
{
       elogreset(f);
       if(f->elogbuf)
               bufreset(f->elogbuf);
       f->elog.type = Empty;
       fbuffree(f->elog.r);
       f->elog.r = nil;
       warned = FALSE;
}

void
elogflush(File *f)
{
       Buflog b;

       b.type = f->elog.type;
       b.q0 = f->elog.q0;
       b.nd = f->elog.nd;
       b.nr = f->elog.nr;
       switch(f->elog.type){
       default:
               warning(nil, "unknown elog type 0x%ux\n", f->elog.type);
               break;
       case Null:
               break;
       case Insert:
       case Replace:
               if(f->elog.nr > 0)
                       bufinsert(f->elogbuf, f->elogbuf->nc, f->elog.r, f->elog.nr);
               /* fall through */
       case Delete:
               bufinsert(f->elogbuf, f->elogbuf->nc, (Rune*)&b, Buflogsize);
               break;
       }
       elogreset(f);
}

void
elogreplace(File *f, int q0, int q1, Rune *r, int nr)
{
       uint gap;

       if(q0==q1 && nr==0)
               return;
       eloginit(f);
       if(f->elog.type!=Null && q0<f->elog.q0){
               if(warned++ == 0)
                       warning(nil, Wsequence);
               elogflush(f);
       }
       /* try to merge with previous */
       gap = q0 - (f->elog.q0+f->elog.nd);     /* gap between previous and this */
       if(f->elog.type==Replace && f->elog.nr+gap+nr<Maxstring){
               if(gap < Minstring){
                       if(gap > 0){
                               bufread(f, f->elog.q0+f->elog.nd, f->elog.r+f->elog.nr, gap);
                               f->elog.nr += gap;
                       }
                       f->elog.nd += gap + q1-q0;
                       runemove(f->elog.r+f->elog.nr, r, nr);
                       f->elog.nr += nr;
                       return;
               }
       }
       elogflush(f);
       f->elog.type = Replace;
       f->elog.q0 = q0;
       f->elog.nd = q1-q0;
       f->elog.nr = nr;
       if(nr > RBUFSIZE)
               editerror("internal error: replacement string too large(%d)", nr);
       runemove(f->elog.r, r, nr);
}

void
eloginsert(File *f, int q0, Rune *r, int nr)
{
       int n;

       if(nr == 0)
               return;
       eloginit(f);
       if(f->elog.type!=Null && q0<f->elog.q0){
               if(warned++ == 0)
                       warning(nil, Wsequence);
               elogflush(f);
       }
       /* try to merge with previous */
       if(f->elog.type==Insert && q0==f->elog.q0 && f->elog.nr+nr<Maxstring){
               runemove(f->elog.r+f->elog.nr, r, nr);
               f->elog.nr += nr;
               return;
       }
       while(nr > 0){
               elogflush(f);
               f->elog.type = Insert;
               f->elog.q0 = q0;
               n = nr;
               if(n > RBUFSIZE)
                       n = RBUFSIZE;
               f->elog.nr = n;
               runemove(f->elog.r, r, n);
               r += n;
               nr -= n;
       }
}

void
elogdelete(File *f, int q0, int q1)
{
       if(q0 == q1)
               return;
       eloginit(f);
       if(f->elog.type!=Null && q0<f->elog.q0+f->elog.nd){
               if(warned++ == 0)
                       warning(nil, Wsequence);
               elogflush(f);
       }
       /* try to merge with previous */
       if(f->elog.type==Delete && f->elog.q0+f->elog.nd==q0){
               f->elog.nd += q1-q0;
               return;
       }
       elogflush(f);
       f->elog.type = Delete;
       f->elog.q0 = q0;
       f->elog.nd = q1-q0;
}

#define tracelog 0
void
elogapply(File *f)
{
       Buflog b;
       Rune *buf;
       uint i, n, up, mod;
       uint tq0, tq1;
       Buffer *log;
       Text *t;
       int owner;

       elogflush(f);
       log = f->elogbuf;
       t = f->curtext;

       buf = fbufalloc();
       mod = FALSE;

       owner = 0;
       if(t->w){
               owner = t->w->owner;
               if(owner == 0)
                       t->w->owner = 'E';
       }

       /*
        * The edit commands have already updated the selection in t->q0, t->q1,
        * but using coordinates relative to the unmodified buffer.  As we apply the log,
        * we have to update the coordinates to be relative to the modified buffer.
        * Textinsert and textdelete will do this for us; our only work is to apply the
        * convention that an insertion at t->q0==t->q1 is intended to select the
        * inserted text.
        */

       /*
        * We constrain the addresses in here (with textconstrain()) because
        * overlapping changes will generate bogus addresses.   We will warn
        * about changes out of sequence but proceed anyway; here we must
        * keep things in range.
        */

       while(log->nc > 0){
               up = log->nc-Buflogsize;
               bufread(log, up, (Rune*)&b, Buflogsize);
               switch(b.type){
               default:
                       fprint(2, "elogapply: 0x%ux\n", b.type);
                       abort();
                       break;

               case Replace:
                       if(tracelog)
                               warning(nil, "elog replace %d %d (%d %d)\n",
                                       b.q0, b.q0+b.nd, t->q0, t->q1);
                       if(!mod){
                               mod = TRUE;
                               filemark(f);
                       }
                       textconstrain(t, b.q0, b.q0+b.nd, &tq0, &tq1);
                       textdelete(t, tq0, tq1, TRUE);
                       up -= b.nr;
                       for(i=0; i<b.nr; i+=n){
                               n = b.nr - i;
                               if(n > RBUFSIZE)
                                       n = RBUFSIZE;
                               bufread(log, up+i, buf, n);
                               textinsert(t, tq0+i, buf, n, TRUE);
                       }
                       if(t->q0 == b.q0 && t->q1 == b.q0)
                               t->q1 += b.nr;
                       break;

               case Delete:
                       if(tracelog)
                               warning(nil, "elog delete %d %d (%d %d)\n",
                                       b.q0, b.q0+b.nd, t->q0, t->q1);
                       if(!mod){
                               mod = TRUE;
                               filemark(f);
                       }
                       textconstrain(t, b.q0, b.q0+b.nd, &tq0, &tq1);
                       textdelete(t, tq0, tq1, TRUE);
                       break;

               case Insert:
                       if(tracelog)
                               warning(nil, "elog insert %d %d (%d %d)\n",
                                       b.q0, b.q0+b.nr, t->q0, t->q1);
                       if(!mod){
                               mod = TRUE;
                               filemark(f);
                       }
                       textconstrain(t, b.q0, b.q0, &tq0, &tq1);
                       up -= b.nr;
                       for(i=0; i<b.nr; i+=n){
                               n = b.nr - i;
                               if(n > RBUFSIZE)
                                       n = RBUFSIZE;
                               bufread(log, up+i, buf, n);
                               textinsert(t, tq0+i, buf, n, TRUE);
                       }
                       if(t->q0 == b.q0 && t->q1 == b.q0)
                               t->q1 += b.nr;
                       break;

/*              case Filename:
                       f->seq = u.seq;
                       fileunsetname(f, epsilon);
                       f->mod = u.mod;
                       up -= u.n;
                       free(f->name);
                       if(u.n == 0)
                               f->name = nil;
                       else
                               f->name = runemalloc(u.n);
                       bufread(delta, up, f->name, u.n);
                       f->nname = u.n;
                       break;
*/
               }
               bufdelete(log, up, log->nc);
       }
       fbuffree(buf);
       elogterm(f);

       /*
        * Bad addresses will cause bufload to crash, so double check.
        * If changes were out of order, we expect problems so don't complain further.
        */
       if(t->q0 > f->nc || t->q1 > f->nc || t->q0 > t->q1){
               if(!warned)
                       warning(nil, "elogapply: can't happen %d %d %d\n", t->q0, t->q1, f->nc);
               t->q1 = min(t->q1, f->nc);
               t->q0 = min(t->q0, t->q1);
       }

       if(t->w)
               t->w->owner = owner;
}