/* buffer - simple gap-buffer package
**
** See: https://en.wikipedia.org/wiki/Gap_buffer
**
** Copyright (C) 2022 by Jef Poskanzer <jef@mail.acme.com>.
** All rights reserved.
**
** You may use this software however you like as long as you keep my
** name on it and don't sue me.
**
** For commentary on this license please see http://acme.com/license.html
*/

#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>

#include "buffer.h"
#include "leakfinder.h"


#define INITIAL_SIZE 10000


/* Private macros and routines. */

#define gap_size(buf) ((buf)->size - (buf)->len)

#define buffer_index(buf,ind) ((ind) < (buf)->gap ? (ind) : (ind) + gap_size(buf))


/* Move the gap to the point, if necessary. */
static void gap_to_point( buffer* buf )
    {
    if ( buf->point < buf->gap )
	{
	size_t movesize = buf->gap - buf->point;
	memmove(
	  &buf->bytes[buf->gap + gap_size( buf ) - movesize],
	  &buf->bytes[buf->point],
	  movesize );
	buf->gap = buf->point;
	}
    else if ( buf->point > buf->gap )
	{
	size_t movesize = buf->point - buf->gap;
	memmove(
	  &buf->bytes[buf->gap],
	  &buf->bytes[buf->gap + gap_size( buf )],
	  movesize );
	buf->gap = buf->point;
	}
    }


/* Check if there's enough room for more bytes, and if not then enlarge. */
static bool check_size( buffer* buf, size_t newlen )
    {
    size_t newsize, post_gap_bytes;
    char* newbytes;

    if ( newlen <= buf->size )
	return true;
    /* Got to enlarge the byte array. */
    newsize = newlen * 2;
    newbytes = (char*) REALLOC( (void*) buf->bytes, newsize );
    if ( newbytes == (char*) 0 )
	return false;
    /* Move the post-gap bytes to the new end. */
    post_gap_bytes = buf->len - buf->gap;
    memmove(
      &newbytes[newsize - post_gap_bytes],
      &newbytes[buf->size - post_gap_bytes],
      post_gap_bytes );
    buf->bytes = newbytes;
    buf->size = newsize;
    return true;
    }


/* Public routines. */


buffer* buffer_create( void )
    {
    buffer* buf;

    buf = (buffer*) MALLOC( sizeof(buffer) );
    if ( buf == (buffer*) 0 )
	return (buffer*) 0;
    buf->size = INITIAL_SIZE;
    buf->bytes = (char*) MALLOC( buf->size );
    if ( buf->bytes == (char*) 0 )
	{
	FREE( buf );
	return (buffer*) 0;
	}
    buf->us = undo_stack_create();
    if ( buf->us == (undo_stack*) 0 )
	{
	FREE( buf->bytes );
	FREE( buf );
	return (buffer*) 0;
	}
    buffer_clear( buf );
    return buf;
    }


void buffer_clear( buffer* buf )
    {
    buf->len = 0;
    buf->gap = 0;
    buf->point = 0;
    buf->mark = 0;
    buf->first = 0;
    buf->last = 0;
    buf->modified = false;
    buf->readonly = false;
    (void) buffer_undo_disable( buf );
    buf->undo_group = -1;
    undo_stack_clear( buf->us );
    }


void buffer_destroy( buffer* buf )
    {
    FREE( buf->bytes );
    undo_stack_destroy( buf->us );
    FREE( buf );
    }

void buffer_readonly( buffer* buf )
    {
    buf->readonly = true;
    }


size_t buffer_len( buffer* buf )
    {
    return buf->len;
    }


char buffer_get_byte( buffer* buf, size_t ind )
    {
    return buf->bytes[buffer_index( buf, ind )];
    }


bool buffer_set_byte( buffer* buf, size_t ind, char c )
    {
    if ( buf->readonly )
	return false;
    if ( ind < buf->len )
	{
	/* Just set the byte. */
	if ( ! buffer_undo_save( buf, ind, 1, 0 ) )
	    return false;
	buf->bytes[buffer_index( buf, ind )] = c;
	buf->modified = true;
	return true;
	}
    else if ( ind == buf->len )
        {
        /* At the end - do an insert and possibly enlarge the buffer. */
	size_t saved_point = buf->point;
	bool r;

	buf->point = ind;
	r = buffer_insert_byte( buf, c );
	buf->point = saved_point;
	return r;
        }
    else
	{
	/* Past the end - no can do. */
	return false;
	}
    }


bool buffer_insert_byte( buffer* buf, char c )
    {
    if ( buf->readonly )
	return false;
    if ( ! check_size( buf, buf->len + 1 ) )
	return false;
    gap_to_point( buf );
    if ( ! buffer_undo_save( buf, buf->point, 1, 1 ) )
	return false;
    buf->bytes[buf->gap] = c;
    ++buf->gap;
    ++buf->len;
    if ( buf->mark > buf->point )
	++buf->mark;
    ++buf->point;
    buf->modified = true;
    return true;
    }


bool buffer_insert_bytes( buffer* buf, char* bytes, size_t len )
    {
    size_t i;

    for ( i = 0; i < len; ++i )
	if ( ! buffer_insert_byte( buf, bytes[i] ) )
	    return false;
    return true;
    }


bool buffer_insert_string( buffer* buf, char* s )
    {
    while ( *s != '\0' )
	if ( ! buffer_insert_byte( buf, *s++ ) )
	    return false;
    return true;
    }


bool buffer_delete_backward( buffer* buf, size_t len )
    {
    if ( buf->readonly )
	return false;
    if ( len > buf->point )
	return false;
    gap_to_point( buf );
    if ( ! buffer_undo_save( buf, buf->point - len, len, -len ) )
	return false;
    buf->gap -= len;
    buf->len -= len;
    if ( buf->mark > buf->point )
	buf->mark -= len;
    buf->point -= len;
    buf->modified = true;
    return true;
    }


bool buffer_delete_forward( buffer* buf, size_t len )
    {
    if ( buf->readonly )
	return false;
    if ( len > buf->len - buf->point )
	return false;
    gap_to_point( buf );
    if ( ! buffer_undo_save( buf, buf->point, len, -len ) )
	return false;
    buf->len -= len;
    if ( buf->mark > buf->point )
	buf->mark -= len;
    buf->modified = true;
    return true;
    }


/* Special secret back-door routine to return a span of bytes in two parts. */
bool buffer_get_parts( buffer* buf, size_t ind, size_t len, char** part1_bytesP, size_t* part1_lenP, char** part2_bytesP, size_t* part2_lenP )
    {
    if ( ind + len > buf->len )
	return false;
    if ( ind < buf->gap && ind + len > buf->gap )
	{
	/* The span is split, return two parts. */
	*part1_bytesP = &buf->bytes[ind];
	*part1_lenP = buf->gap - ind;
	*part2_bytesP = &buf->bytes[buf->gap + gap_size(buf)];
	*part2_lenP = len - *part1_lenP;
	}
    else
	{
	/* The span is all either before or after the gap, return one part. */
	*part1_bytesP = &buf->bytes[buffer_index( buf, ind )];
	*part1_lenP = len;
	*part2_bytesP = *part1_bytesP;
	*part2_lenP = 0;
	}
    return true;
    }


/* Buffer undo. */


bool buffer_undo_enable( buffer* buf )
    {
    buf->undo_enabled = true;
    ++buf->undo_group;
    return true;
    }


bool buffer_undo_aggregate( buffer* buf )
    {
    if ( undo_stack_empty( buf->us ) )
	return false;
    /* Combine this change group with the previous one. */
    buf->undo_group = undo_stack_top( buf->us )->group;
    return true;
    }


bool buffer_undo_save( buffer* buf, size_t loc, size_t len, ssize_t delta )
    {
    undo_record* ur;

    /* If undo is disabled, just return. */
    if ( ! buf->undo_enabled )
	return true;

    /* Create an undo record. */
    ur = undo_record_create();
    if ( ur == (undo_record*) 0 )
	return false;

    /* Fill it in. */
    ur->group = buf->undo_group;
    ur->loc = loc;
    ur->len = len;
    ur->delta = delta;
    if ( delta <= 0 )
	{
	char* bytes = &buf->bytes[buffer_index( buf, loc )];
	/* This always gets called after gap_to_point(), which guarantees
	** that the bytes are in one piece. (Exception for single-byte
	** changes, but those can't be split anyway.)
	*/
	if ( ! str_copy( ur->s, bytes, len ) )
	    {
	    undo_record_destroy( ur );
	    return false;
	    }
	}
    ur->point = buf->point;
    ur->mark = buf->mark;
    ur->modified = buf->modified;

    /* Save it. */
    if ( ! undo_stack_push( buf->us, ur ) )
	{
	undo_record_destroy( ur );
	return false;
	}
    return true;
    }


/* This saves a null undo record. Compound commands that do some movement
** before their first buffer modification will save point/mark from after
** the movement. Saving a null record first ensures that an undo will
** restore point/mark properly.
*/
bool buffer_undo_save_null( buffer* buf )
    {
    return buffer_undo_save( buf, buf->point, 0, 0 );
    }


bool buffer_undo_disable( buffer* buf )
    {
    buf->undo_enabled = false;
    return true;
    }


bool buffer_undo_revert( buffer* buf )
    {
    int group;
    undo_record* ur;
    size_t ind;
    bool r;

    if ( undo_stack_empty( buf->us ) )
	return false;	/* nothing to undo */

    /* Get the group i.d. at the top of the stack. */
    group = undo_stack_top( buf->us )->group;

    /* Pop & revert undo records while the group matches. */
    while ( ( ! undo_stack_empty( buf->us ) ) && undo_stack_top( buf->us )->group == group )
	{
	ur = undo_stack_pop( buf->us );

	buf->point = ur->loc;
	if ( ur->delta < 0 )
	    {
	    /* Negative delta means bytes were deleted; add them back. */
	    if ( ur->len != -ur->delta || ur->len != ur->s->len )
		r = false;
	    else
		r = buffer_insert_bytes( buf, ur->s->bytes, ur->len );
	    }
	else if ( ur->delta > 0 )
	    {
	    /* Positive delta means bytes were added; delete them. */
	    if ( ur->len != ur->delta || ur->s->len != 0 )
		r = false;
	    else
		r = buffer_delete_forward( buf, ur->len );
	    }
	else
	    {
	    /* Zero delta means bytes were overwritten in place; overwrite
	    ** them back.
	    */
	    if ( ur->len != ur->s->len )
		r = false;
	    else
		{
		r = true;
		for ( ind = 0; ind < ur->len; ++ind )
		    if ( ! buffer_set_byte( buf, ur->loc + ind, ur->s->bytes[ind] ) )
			{
			r = false;
			break;
			}
		}
	    }

	if ( r )
	    {
	    buf->point = ur->point;
	    buf->mark = ur->mark;
	    buf->modified = ur->modified;
	    }
	undo_record_destroy( ur );
	if ( ! r )
	    return false;
	}

    return true;
    }
