/* vim: set ts=4 et sw=4: */
#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>

#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <ctype.h>
#include "xdiff.h"

/* This value taken from libxdiff-0.23/test/xtestutils.c */
#define MMF_STD_BLKSIZE (1024 * 8)

static void *std_malloc(void *priv, unsigned int size) {
	return malloc(size);
}

static void std_free(void *priv, void *ptr) {
	free(ptr);
}

static void *std_realloc(void *priv, void *ptr, unsigned int size) {
	return realloc(ptr, size);
}

static int _file_outf(void *priv, mmbuffer_t *mb, int nbuf) {
	int i;
	for (i = 0; i < nbuf; i++)
		if (!fwrite(mb[i].ptr, mb[i].size, 1, (FILE *) priv))
			return -1;
	return 0;
}

static int _mmfile_outf(void *priv, mmbuffer_t *mb, int nbuf) {
	mmfile_t *mmf = priv;
	if (xdl_writem_mmfile(mmf, mb, nbuf) < 0) {
		return -1;
	}
	return 0;
}

memallocator_t memallocator = { malloc, 0 }; /* Paranoid... */

static void initialize_allocator(void) {
    if (! memallocator.malloc) {
        memallocator.priv = NULL;
        memallocator.malloc = std_malloc;
        memallocator.free = std_free;
        memallocator.realloc = std_realloc;
        xdl_set_allocator(&memallocator);
    }
}

#define CONTEXT_string_result(_INDEX_) (context->string_result[_INDEX_])
#define CONTEXT_string_result_length(_INDEX_) (context->string_result_length[_INDEX_])
#define CONTEXT_mmf(_INDEX_) (context->mmf[_INDEX_])
#define CONTEXT_mmf_result(_INDEX_) (context->mmf_result[_INDEX_])
#define CONTEXT_add_error(_ERROR_) context->error[++context->error_counter - 1] = _ERROR_;

#define CONTEXT_error_size 3
#define CONTEXT_string_result_size 2
#define CONTEXT_mmf_size 3
#define CONTEXT_mmf_result_size 2

typedef struct {
    char *string_result[CONTEXT_string_result_size];
    int string_result_length[CONTEXT_string_result_size];
	mmfile_t mmf[CONTEXT_mmf_size];
	mmfile_t mmf_result[2];
    const char *error[CONTEXT_error_size];
    int error_counter;
} context_t;
context_t result;

static int CONTEXT_mmf_result_2_string_result( context_t* context, int index ) {

    mmfile_t *mmf_r1 = &CONTEXT_mmf_result(index);
    int size = xdl_mmfile_size( mmf_r1 );
    int wrote = 0;
    char *string_result = CONTEXT_string_result(index) = malloc( sizeof(char) * (size + 1) );

    xdl_seek_mmfile( mmf_r1, 0);
    if ( (wrote = xdl_read_mmfile( mmf_r1, string_result, size )) < size ) {
        return size - wrote;
    }
    string_result[size] = 0;
    CONTEXT_string_result_length(index) = size;
    return 0;
}

static int CONTEXT_mmf_result_2_binary_result( context_t* context, int index ) {

    mmfile_t *mmf_r1 = &CONTEXT_mmf_result(index);
    int size = xdl_mmfile_size( mmf_r1 );
    int wrote = 0;
    char *string_result = CONTEXT_string_result(index) = malloc( sizeof(char) * (size + 1) );

    xdl_seek_mmfile( mmf_r1, 0);
    if ( (wrote = xdl_read_mmfile( mmf_r1, string_result, size )) < size ) {
        return size - wrote;
    }
    CONTEXT_string_result_length( index ) = size;
    return 0;
}

static void CONTEXT_cleanup( context_t* context ) {
    int ii;

    for (ii = 0; ii < CONTEXT_string_result_size; ii++)
        free( context->string_result[ ii ] );

    for (ii = 0; ii < CONTEXT_mmf_size; ii++)
        xdl_free_mmfile( &( context->mmf[ ii ] ) );

    for (ii = 0; ii < CONTEXT_mmf_result_size; ii++)
        xdl_free_mmfile( &( context->mmf_result[ ii ] ) );
}

static const char* _binary_2_mmfile( mmfile_t* mmf, const char* string, const int length ) {

    initialize_allocator();
	if ( xdl_init_mmfile( mmf, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0 ) {
		return "Unable to initialize mmfile";
	}

    int wrote = 0;
    if ( (wrote = xdl_write_mmfile( mmf, string, length )) < length ) {
        return "Couldn't write entire string to mmfile";
    }

    return 0;
}
static const char* _string_2_mmfile( mmfile_t* mmf, const char* string ) {

    initialize_allocator();

	if ( xdl_init_mmfile( mmf, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0 ) {
		return "Unable to initialize mmfile";
	}

    int wrote = 0;
    int length = strlen(string);
    if ( (wrote = xdl_write_mmfile( mmf, string, length )) < length ) {
        return "Couldn't write entire string to mmfile";
    }

    return 0;
}

void __xpatch( context_t *context, const char *string1, const char *string2 ) {

	mmfile_t *mmf1, *mmf2, *mmf_r1, *mmf_r2;
    const char *error;

    mmf1 = &CONTEXT_mmf(0);
    mmf2 = &CONTEXT_mmf(1);
    mmf_r1 = &CONTEXT_mmf_result(0);
    mmf_r2 = &CONTEXT_mmf_result(1);

    initialize_allocator();

    if ( error = _string_2_mmfile( mmf1, string1 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string1 into mmfile" );
        return;
    }

    if ( error = _string_2_mmfile( mmf2, string2 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string2 into mmfile" );
        return;
    }
    
    {
        xdemitcb_t ecb1, ecb2;

        ecb1.priv = mmf_r1;
        ecb1.outf = _mmfile_outf;

        ecb2.priv = mmf_r2;
        ecb2.outf = _mmfile_outf;

        if (xdl_init_mmfile( mmf_r1, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0) {
            CONTEXT_add_error( "Couldn't initialize accumulating mmfile mmf_r1  (xdl_init_atomic)" );
            return;
        }

        if (xdl_init_mmfile( mmf_r2, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0) {
            CONTEXT_add_error( "Couldn't initialize accumulating mmfile mmf_r2  (xdl_init_atomic)" );
            return;
        }

		if (xdl_patch( mmf1, mmf2, XDL_PATCH_NORMAL, &ecb1, &ecb2) < 0) {
            CONTEXT_add_error( "Couldn't perform patch (xdl_patch)" );
            return;
		}

        if ( CONTEXT_mmf_result_2_string_result( context, 0 ) ) {
            CONTEXT_add_error( "Wasn't able to read entire mmfile result (mmf_r1) (xdl_read_mmfile)" );
            return;
        }

        if ( CONTEXT_mmf_result_2_string_result( context, 1 ) ) {
            CONTEXT_add_error( "Wasn't able to read entire mmfile result (mmf_r2) (xdl_read_mmfile)" );
            return;
        }
    }
}

void __xbpatch( context_t *context, const char *string1, const int len1, const char *string2, const int len2 ) {

	mmfile_t *mmf1, *mmf2, *mmf_r1, *mmf_r2;
    const char *error;

    mmf1 = &CONTEXT_mmf(0);
    mmf2 = &CONTEXT_mmf(1);
    mmf_r1 = &CONTEXT_mmf_result(0);
    mmf_r2 = &CONTEXT_mmf_result(1);

    initialize_allocator();

    if ( error = _binary_2_mmfile( mmf1, string1, len1 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string1 into mmfile" );
        return;
    }

    if ( error = _binary_2_mmfile( mmf2, string2, len2 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string2 into mmfile" );
        return;
    }
    
    /* Compact the files - needed for binary operations */
    mmfile_t  mmf1c;
    if (xdl_mmfile_compact( mmf1, &mmf1c,MMF_STD_BLKSIZE, XDL_MMF_ATOMIC) < 0) {
        CONTEXT_add_error( "mmf1 is not compact - and unable to compact it!");
        return;
    }

    mmfile_t  mmf2c;
    if (xdl_mmfile_compact( mmf2, &mmf2c,MMF_STD_BLKSIZE, XDL_MMF_ATOMIC) < 0) {
        CONTEXT_add_error( "mmf2 is not compact - and unable to compact it!");
        return;
    }

    {
        xdemitcb_t ecb;

        ecb.priv = mmf_r1;
        ecb.outf = _mmfile_outf;

        if (xdl_init_mmfile( mmf_r1, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0) {
            CONTEXT_add_error( "Couldn't initialize accumulating mmfile mmf_r1  (xdl_init_atomic)" );
            return;
        }

		if (xdl_bpatch( mmf1, mmf2, &ecb) < 0) {
            CONTEXT_add_error( "Couldn't perform patch (xdl_bpatch)" );
            return;
		}

        if ( CONTEXT_mmf_result_2_binary_result( context, 0 ) ) {
            CONTEXT_add_error( "Wasn't able to read entire mmfile result (mmf_r1) (xdl_read_mmfile)" );
        }
    }
}

void __xdiff( context_t *context, const char *string1, const char *string2 ) {

	mmfile_t *mmf1, *mmf2, *mmf_r1;
    const char *error;

    mmf1 = &CONTEXT_mmf(0);
    mmf2 = &CONTEXT_mmf(1);
    mmf_r1 = &CONTEXT_mmf_result(0);

    initialize_allocator();

    if ( error = _string_2_mmfile( mmf1, string1 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string1 into mmfile" );
        return;
    }

    if ( error = _string_2_mmfile( mmf2, string2 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load string2 into mmfile" );
        return;
    }
    
    {
        xpparam_t xpp;
	    xpp.flags = 0;

        xdemitconf_t xecfg;
	    xecfg.ctxlen = 3;

        xdemitcb_t ecb;
        ecb.priv = mmf_r1;
        ecb.outf = _mmfile_outf;

        if (xdl_init_mmfile( mmf_r1, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0) {
            CONTEXT_add_error( "Couldn't initialize accumulating mmfile (xdl_init_atomic)" );
            return;
        }

		if (xdl_diff( mmf1, mmf2, &xpp, &xecfg, &ecb ) < 0) {
            CONTEXT_add_error( "Couldn't perform diff (xdl_diff)" );
            return;
		}

        if ( CONTEXT_mmf_result_2_string_result( context, 0 ) ) {
            CONTEXT_add_error( "Wasn't able to read entire mmfile result (xdl_read_mmfile)" );
        }
    }
}

void __xbdiff( context_t *context, const char *string1, const int len1,const char *string2, const int len2 ) {

	mmfile_t *mmf1, *mmf2, *mmf_r1;
    const char *error;

    mmf1 = &CONTEXT_mmf(0);
    mmf2 = &CONTEXT_mmf(1);
    mmf_r1 = &CONTEXT_mmf_result(0);

    initialize_allocator();

    if ( error = _binary_2_mmfile( mmf1, string1,len1 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load binary1 into mmfile" );
        return;
    }

    if ( error = _binary_2_mmfile( mmf2, string2, len2 ) ) {
        CONTEXT_add_error( error );
        CONTEXT_add_error( "Couldn't load binary2 into mmfile" );
        return;
    }
    /* Compact the files - needed for binary operations */
    mmfile_t  mmf1c;
    if (xdl_mmfile_compact( mmf1, &mmf1c,MMF_STD_BLKSIZE, XDL_MMF_ATOMIC) < 0) {
        CONTEXT_add_error( "mmf1 is not compact - and unable to compact it!");
        return;
    }

    mmfile_t  mmf2c;
    if (xdl_mmfile_compact( mmf2, &mmf2c,MMF_STD_BLKSIZE, XDL_MMF_ATOMIC) < 0) {
        CONTEXT_add_error( "mmf2 is not compact - and unable to compact it!");
        return;
    }

    {
        bdiffparam_t bdp;
            bdp.bsize=16;

        xdemitcb_t ecb;
            ecb.priv = mmf_r1;
            ecb.outf = _mmfile_outf;

        if (xdl_init_mmfile( mmf_r1, MMF_STD_BLKSIZE, XDL_MMF_ATOMIC ) < 0) {
            CONTEXT_add_error( "Couldn't initialize accumulating mmfile (xdl_init_atomic)" );
            return;
        }

		if (xdl_bdiff( &mmf1c, &mmf2c, &bdp, &ecb ) < 0) {
            CONTEXT_add_error( "Couldn't perform diff (xdl_bdiff)" );
            return;
		}
        xdl_free_mmfile(&mmf1c);
        xdl_free_mmfile(&mmf2c);

        if ( CONTEXT_mmf_result_2_binary_result( context, 0 ) ) {
            CONTEXT_add_error( "Wasn't able to read entire mmfile result (xdl_read_mmfile)" );
        }
    }
}

MODULE = Diff::LibXDiff PACKAGE = Diff::LibXDiff

PROTOTYPES: disable

SV*
_xdiff(string1, string2)
    SV* string1
    SV* string2
    INIT:
        context_t context = { 0 };
        RETVAL = &PL_sv_undef;
    CODE:
        __xdiff( &context, SvPVX(string1), SvPVX(string2) );
        HV* hash_result = (HV*) sv_2mortal( (SV*) newHV() );
        AV* error_result = (AV*) sv_2mortal( (SV*) newAV() );
        int ii;
        for (ii = 0; ii < context.error_counter; ii++) {
            av_push( error_result, newSVpv( context.error[ii], 0 ) );
        }
        hv_store(  hash_result, "result", 6, newSVpv( context.string_result[0], 0 ), 0);
        hv_store(  hash_result, "error", 5, newRV( (SV*) error_result ), 0);
        CONTEXT_cleanup( &context );
        RETVAL = newRV( (SV*) hash_result );
    OUTPUT:
        RETVAL

SV*
_xbdiff(string1, string2)
    SV* string1
    SV* string2
    INIT:
        context_t context = { 0 };
        RETVAL = &PL_sv_undef;
    CODE:
        int str1_len=sv_len(string1);
        int str2_len=sv_len(string2);
        __xbdiff( &context, SvPVX(string1), str1_len, SvPVX(string2), str2_len);
        HV* hash_result = (HV*) sv_2mortal( (SV*) newHV() );
        AV* error_result = (AV*) sv_2mortal( (SV*) newAV() );
        int ii;
        for (ii = 0; ii < context.error_counter; ii++) {
            av_push( error_result, newSVpv( context.error[ii], 0 ) );
        }
        hv_store(  hash_result, "result", 6, newSVpv(
            context.string_result[0],
            context.string_result_length[0]),
        0);
        hv_store(  hash_result, "error", 5, newRV( (SV*) error_result ), 0);
        CONTEXT_cleanup( &context );
        RETVAL = newRV( (SV*) hash_result );
    OUTPUT:
        RETVAL

SV*
_xpatch(string1, string2)
    SV* string1
    SV* string2
    INIT:
        context_t context = { 0 };
        RETVAL = &PL_sv_undef;
    CODE:
        __xpatch( &context, SvPVX(string1), SvPVX(string2) );
        HV* hash_result = (HV*) sv_2mortal( (SV*) newHV() );
        AV* error_result = (AV*) sv_2mortal( (SV*) newAV() );
        int ii;
        for (ii = 0; ii < context.error_counter; ii++) {
            av_push( error_result, newSVpv( context.error[ii], 0 ) );
        }
        hv_store(  hash_result, "result", 6, newSVpv( context.string_result[0], 0 ), 0);
        hv_store(  hash_result, "rejected_result", 15, newSVpv( context.string_result[1], 0 ), 0);
        hv_store(  hash_result, "error", 5, newRV( (SV*) error_result ), 0);
        CONTEXT_cleanup( &context );
        RETVAL = newRV( (SV*) hash_result );
    OUTPUT:
        RETVAL

SV*
_xbpatch(string1, string2)
    SV* string1
    SV* string2
    INIT:
        context_t context = { 0 };
        RETVAL = &PL_sv_undef;
    CODE:
        int str1_len=sv_len(string1);
        int str2_len=sv_len(string2);
        __xbpatch( &context, SvPVX(string1), str1_len, SvPVX(string2), str2_len);
        HV* hash_result = (HV*) sv_2mortal( (SV*) newHV() );
        AV* error_result = (AV*) sv_2mortal( (SV*) newAV() );
        int ii;
        for (ii = 0; ii < context.error_counter; ii++) {
            av_push( error_result, newSVpv( context.error[ii], 0 ) );
        }
        hv_store(  hash_result, "result", 6, newSVpv(
            context.string_result[0],
            context.string_result_length[0] ),
        0);
        hv_store(  hash_result, "error", 5, newRV( (SV*) error_result ), 0);
        CONTEXT_cleanup( &context );
        RETVAL = newRV( (SV*) hash_result );
    OUTPUT:
        RETVAL