/* 
 * Mr. 4th Dimention - Allen Webster
 *  Four Tech
 *
 * public domain -- no warranty is offered or implied; use this code at your own risk
 * 
 * 06.11.2015
 * 
 * Buffer experiment testing layer
 * 
 */

// TOP

#include "4coder_external_name.h"

#include "shared_test_config.cpp"

#ifdef fast_test
#define debug_4tech(x)
#define assert_4tech(x)
#endif
#define hard_assert_4tech(x) assert(x)

#include "4coder_shared.cpp"
#include "4coder_golden_array.cpp"
#include "4coder_gap_buffer.cpp"
#include "4coder_multi_gap_buffer.cpp"
#include "4coder_rope_buffer.cpp"

#define Buffer_Type Buffer
#include "4coder_buffer_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Gap_Buffer
#include "4coder_buffer_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Multi_Gap_Buffer
#include "4coder_buffer_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Rope_Buffer
#include "4coder_buffer_abstract.cpp"
#undef Buffer_Type

#include "shared_test_utils.cpp"

int int_into_str(char *out, int *rem, unsigned int x){
    char *start = out;
    int size = *rem;
    int result;
    char t;
    
    if (x == 0 && size > 1){
        *out = '0';
        ++out;
        --size;
    }
    else{
        for (;x > 0 && size > 1;--size, ++out){
            *out = (x%10 + '0');
            x /= 10;
        }
    }
    
    *rem = size;
    *out = 0;

    result = (int)(out - start);

    --out;
    for (; start < out; ++start, --out){
        t = *out;
        *out = *start;
        *start = t;
    }

    return(result);
}

int uscore_into_str(char *out, int *rem){
    int result;
    result = 0;
    if (*rem > 1){
        --*rem;
        *out++ = '_';
        *out = 0;
        result = 1;
    }
    return(result);
}

#if defined(_WIN32)
#include <Windows.h>

typedef unsigned long long time_int;

unsigned long long win32_counts_per_second_4tech;

int time_init(unsigned long long *resolution){
    int result;
    LARGE_INTEGER time;
    result = 0;
    if (QueryPerformanceFrequency(&time)){
        win32_counts_per_second_4tech = (unsigned long long)(time.QuadPart);
        result = 1;
        *resolution = win32_counts_per_second_4tech;
    }
    return(result);
}

time_int get_time(){
    LARGE_INTEGER time;
    time_int result;
    
    result = 0;
    if (QueryPerformanceCounter(&time)){
        result = (time_int)(time.QuadPart);
        result = result * 1000000 / win32_counts_per_second_4tech;
    }
    
    return(result);
}

void time_into_str(char *out, int max){
    SYSTEMTIME systime;
    int pos;

    GetSystemTime(&systime);
    pos = uscore_into_str(out, &max);
    pos += int_into_str(out + pos, &max, systime.wYear);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wMonth);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wDay);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wHour);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wMinute);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wSecond);
    
    pos += uscore_into_str(out + pos, &max);
    pos += int_into_str(out + pos, &max, systime.wMilliseconds);
}

#elif defined(__linux__)
#include <time.h>

typedef unsigned long long time_int;

int time_init(unsigned long long *resolution){
    int result;
    struct timespec res;
    result = 0;
    
    if (!clock_getres(CLOCK_MONOTONIC, &res)){
        result = 1;
	if (res.tv_sec > 0 || res.tv_nsec == 0) *resolution = 0;
	else *resolution = (unsigned long long)(1000000/res.tv_nsec);
    }

    return(result);
}

time_int get_time(){
    time_int result;
    struct timespec time;
    
    result = 0;
    if (!clock_gettime(CLOCK_MONOTONIC, &time)){
        result = (time.tv_sec * 1000000) + (time.tv_nsec / 1000);
    }
    
    return(result);
}

#else
#error Timer not supported on this platform
#endif

#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"

float* get_font_data(const char *font_file, float *font_height){
    float *data = 0;
    stbtt_bakedchar *baked;
    File_Data file = get_file(font_file);
    if (!file.data) exit(1);

    if (file.data){
        int size = sizeof(*baked)*256;
        baked = (stbtt_bakedchar*)malloc(size);
        memset_4tech(baked, 0, sizeof(*baked)*256);

        stbtt_fontinfo font;
        if (stbtt_InitFont(&font, (unsigned char*)file.data, 0)){
            float scale;
            int a,d,g;
            
            scale = stbtt_ScaleForPixelHeight(&font, 17.f);
            stbtt_GetFontVMetrics(&font, &a, &d, &g);
            *font_height = scale*(a - d + g);
            
            int w, h;
            w = 10*256;
            h = 25;
            unsigned char *pixels = (unsigned char*)malloc(w * h);
            stbtt_BakeFontBitmap((unsigned char*)file.data, 0, 17.f, pixels, w, h, 0, 128, baked);
            free(pixels);
            free_file(file);

            data = (float*)malloc(sizeof(float)*256);
            memset_4tech(data, 0, sizeof(float)*256);

            stbtt_bakedchar *baked_ptr = baked;
            for (int i = 0; i < 128; ++i, ++baked_ptr){
                data[i] = baked_ptr->xadvance;
            }
        }
        free(baked);
    }
    else{
        printf("error: cannot continue without font\n");
    }
    
    return data;
}

void setup(){
    unsigned long long resolution;
    if (!time_init(&resolution)){
        printf("error: could not initialize timer");
        exit(1);
    }

    if (resolution < 1000000)
        printf("warning: timer is not actually at high enough resolution for good measurements!\n");

}

typedef struct Time_Record{
    time_int buffer;
    time_int gap_buffer;
    time_int multi_gap_buffer;
    time_int rope_buffer;
} Time_Record;

typedef struct Record_Statistics{
    Time_Record max, min;
    Time_Record expected;
    int count;
} Record_Statistics;

typedef struct Log_Section{
    int *counter;
} Log_Section;

typedef struct Stats_Log{
    char *out;
    int size, max;
    
    Log_Section *sections;
    int sec_top, sec_max;
    
    unsigned int error;
} Stats_Log;

#define log_er_buffer_overflow 0x1
#define log_er_stack_overflow 0x2
#define log_er_stack_underflow 0x4
#define log_er_time_too_large 0x8

#define logid_begin_section 0
#define logid_data_item 1

#if fast_test
#define use_stats_log 1
#else
#define use_stats_log 0
#endif

void log_write_int(Stats_Log *log, int x){
#if use_stats_log
    if (log->error == 0){
        if (log->size+4 <= log->max){
            *(int*)(log->out + log->size) = x;
            log->size += 4;
        }
        else{
            log->error |= log_er_buffer_overflow;
        }
    }
#endif
}

void log_write_time(Stats_Log *log, time_int x){
#if use_stats_log
    if (log->error == 0){
        if (x < 0x7FFFFFFF){
            if (log->size+4 <= log->max){
                *(int*)(log->out + log->size) = (int)x;
                log->size += 4;
            }
            else{
                log->error |= log_er_buffer_overflow;
            }
        }
        else{
            log->error |= log_er_time_too_large;
        }
    }
#endif
}

void log_write_str(Stats_Log *log, char *str, int len){
#if use_stats_log
    int up = (len + 3) & ~3;
    if (log->error == 0){
        if (log->size+4+up <= log->max){
            *(int*)(log->out + log->size) = up;
            memcpy_4tech(log->out + log->size + 4, str, len);
            log->size += 4+up;
        }
        else{
            log->error |= log_er_buffer_overflow;
        }
    }
#endif
}

void log_begin_section(Stats_Log *log, char *name, int name_len){
#if use_stats_log
    Log_Section *section;
    if (log->error == 0){
        if (log->sec_top < log->sec_max){
            if (log->sec_top > 0){
                section = log->sections + log->sec_top - 1;
                ++section->counter;
            }
            
            section = log->sections + (log->sec_top++);
        
            log_write_int(log, logid_begin_section);
            log_write_str(log, name, name_len);

            section->counter = (int*)(log->out + log->size);
            log_write_int(log, 0);
        }
        else{
            log->error |= log_er_stack_overflow;
        }
    }
#endif
}

void log_end_section(Stats_Log *log){
#if use_stats_log
    if (log->error == 0){
        if (log->sec_top > 0){
            --log->sec_top;
        }
        else{
            log->error |= log_er_stack_underflow;
        }
    }
#endif
}

void log_data_item(Stats_Log *log, char *name, int name_len, time_int t){
#if use_stats_log
    Log_Section *section;
    if (log->error == 0){
        if (log->sec_top > 0){
            section = log->sections + log->sec_top - 1;
            ++section->counter;
        }
        
        log_write_int(log, logid_data_item);
        log_write_str(log, name, name_len);
        log_write_time(log, t);
    }
#endif
}

void log_finish(Stats_Log *log){
#if use_stats_log
    assert_4tech(sizeof(external_name) < 512);
    if (log->error == 0){
        char fname[1024];
        memcpy_4tech(fname, "out/", 4);
        memcpy_4tech(fname + 4, external_name, sizeof(external_name)-1);
        time_into_str(fname + 4 + sizeof(external_name) - 1, 1023 - sizeof(external_name) + 1);

        File_Data log_file;
        log_file.data = log->out;
        log_file.size = log->size;
        
        save_file(fname, log_file);
    }
    else{
        printf("\n");
        if (log->error & log_er_buffer_overflow)
            printf("log error: buffer overflow\n");
        if (log->error & log_er_stack_overflow)
            printf("log error: stack overflow\n");
        if (log->error & log_er_stack_underflow)
            printf("log error: stack underflow\n");
        printf("there were log error so the log was not saved\n\n");
    }
#endif
}

#define litstr(s) (char*)(s), (sizeof(s)-1)

void log_time_record(Stats_Log *log, char *name, int name_len, Time_Record record){
    log_begin_section(log, name, name_len);
    log_data_item(log, litstr("golden-array"), record.buffer);
    log_data_item(log, litstr("gap-buffer"), record.gap_buffer);
    log_data_item(log, litstr("multi-gap-buffer"), record.multi_gap_buffer);
    log_data_item(log, litstr("rope"), record.rope_buffer);
    log_end_section(log);
}

Time_Record
operator+(const Time_Record &a, const Time_Record &b){
    Time_Record r;
    r.buffer = a.buffer + b.buffer;
    r.gap_buffer = a.gap_buffer + b.gap_buffer;
    r.multi_gap_buffer = a.multi_gap_buffer + b.multi_gap_buffer;
    r.rope_buffer = a.rope_buffer + b.rope_buffer;
    return r;
}

Time_Record&
operator/=(Time_Record &r, int x){
    r.buffer /= x;
    r.gap_buffer /= x;
    r.multi_gap_buffer /= x;
    r.rope_buffer /= x;
    
    return(r);
}

#define minify(a,b) if ((a)>(b)) (a) = (b)
#define maxify(a,b) if ((a)<(b)) (a) = (b)

void get_record_statistics(Record_Statistics *stats_out, Time_Record *records, int count){
    Record_Statistics stats;
    stats.max = records[0];
    stats.min = records[0];
    stats.expected = records[0];
    stats.count = count;

    Time_Record *record = records + 1;
    
    for (int i = 1; i < count; ++i, ++record){
        stats.expected = stats.expected + *record;
        
        minify(stats.min.buffer, record->buffer);
        minify(stats.min.gap_buffer, record->gap_buffer);
        minify(stats.min.multi_gap_buffer, record->multi_gap_buffer);
        minify(stats.min.rope_buffer, record->rope_buffer);
        
        maxify(stats.max.buffer, record->buffer);
        maxify(stats.max.gap_buffer, record->gap_buffer);
        maxify(stats.max.multi_gap_buffer, record->multi_gap_buffer);
        maxify(stats.max.rope_buffer, record->rope_buffer);
    }

    stats.expected /= count;
    
    *stats_out = stats;
}

int test_is_silenced;

void silence_test(){
    test_is_silenced = 1;
}

void print_record(Time_Record record){
    printf("%-16s - %25lluus\n%-16s - %25lluus\n%-16s - %25lluus\n%-16s - %25lluus\n",
           "Golden Array", record.buffer,
           "Gap Buffer", record.gap_buffer,
           "Multi-Gap Buffer", record.multi_gap_buffer,
           "Rope", record.rope_buffer);
}

void print_statistics(Time_Record *records, int count, Record_Statistics *stats_out){
    Record_Statistics stats;
    get_record_statistics(&stats, records, count);
    if (!test_is_silenced){
        printf("samples: %d\n", count);
        printf("---averages---\n");
        print_record(stats.expected);
        printf("---max---\n");
        print_record(stats.max);
        printf("---min---\n");
        print_record(stats.min);
    }

    if (stats_out) *stats_out = stats;
}

typedef struct Buffer_Set{
    Buffer buffer;
    Gap_Buffer gap_buffer;
    Multi_Gap_Buffer multi_gap_buffer;
    Rope_Buffer rope_buffer;
} Buffer_Set;

#define Buffer_Type Buffer
#include "4coder_test_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Gap_Buffer
#include "4coder_test_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Multi_Gap_Buffer
#include "4coder_test_abstract.cpp"
#undef Buffer_Type

#define Buffer_Type Rope_Buffer
#include "4coder_test_abstract.cpp"
#undef Buffer_Type

void log_sample_set(Stats_Log *log, char *name, int name_len, Record_Statistics *stats,
               Time_Record *samples, int sample_count){
    log_begin_section(log, name, name_len);
        
    log_data_item(log, litstr("sample-count"), sample_count);
    log_time_record(log, litstr("max"), stats->max);
    log_time_record(log, litstr("min"), stats->min);
    log_time_record(log, litstr("average"), stats->expected);
    
    for (int i = 0; i < sample_count; ++i){
        log_time_record(log, litstr("item"), samples[i]);
    }
    
    log_end_section(log);
}

typedef struct Sample_Machine{
    time_int tstart, tend;
    Time_Record *samples;
    int count;
} Sample_Machine;

Sample_Machine begin_machine(int count, void **data, int *max){
    Sample_Machine result;
    
    result.count = count;
    result.samples = (Time_Record*)*data;
    *data = result.samples + count;
    assert_4tech(count*sizeof(*result.samples) < *max);
    *max -= count*sizeof(*result.samples);
    
    return(result);
}

void end_machine(Sample_Machine *machine, Record_Statistics *stats_out, char *func_name){
    if (!test_is_silenced) printf("%s\n", func_name);
    print_statistics(machine->samples, machine->count, stats_out);
    if (!test_is_silenced) printf("\n");
    test_is_silenced = 0;
}
                  
void start(Sample_Machine *machine){
    machine->tstart = get_time();
}
                  
time_int stop(Sample_Machine *machine){
    machine->tend = get_time();
    return machine->tend - machine->tstart;
}
                  
void initialization_test(Stats_Log *log, Buffer_Set *set, File_Data file, int test_repitions,
                    void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        init_buffer(&set->buffer, file, scratch, scratch_size);
        machine.samples[i].buffer = stop(&machine);

        start(&machine);
        init_buffer(&set->gap_buffer, file, scratch, scratch_size);
        machine.samples[i].gap_buffer = stop(&machine);
    
        start(&machine);
        init_buffer(&set->multi_gap_buffer, file, scratch, scratch_size);
        machine.samples[i].multi_gap_buffer = stop(&machine);
    
        start(&machine);
        init_buffer(&set->rope_buffer, file, scratch, scratch_size);
        machine.samples[i].rope_buffer = stop(&machine);
 
        if (i+1 != test_repitions){
            free(set->buffer.data);
            free(set->gap_buffer.data);
            for (int j = 0; j < set->multi_gap_buffer.chunk_alloced; ++j){
                free(set->multi_gap_buffer.gaps[j].data);
            }
            free(set->multi_gap_buffer.gaps);
            free(set->rope_buffer.data);
            free(set->rope_buffer.nodes);
        }
    }

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("initialization"), stats_out, machine.samples, machine.count);
}

void measure_starts_widths_test(Stats_Log *log, Buffer_Set *set, int test_repitions, void *scratch,
                           int scratch_size, Record_Statistics *stats_out, float *font_widths){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        measure_starts_widths(&set->buffer, font_widths);
        machine.samples[i].buffer = stop(&machine);
    
        start(&machine);
        measure_starts_widths(&set->gap_buffer, font_widths);
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        measure_starts_widths(&set->multi_gap_buffer, font_widths);
        machine.samples[i].multi_gap_buffer = stop(&machine);
    
        start(&machine);
        measure_starts_widths(&set->rope_buffer, font_widths);
        machine.samples[i].rope_buffer = stop(&machine);

        if (i+1 != test_repitions){
            free(set->buffer.line_starts);
            free(set->gap_buffer.line_starts);
            free(set->multi_gap_buffer.line_starts);
            free(set->rope_buffer.line_starts);
            
            free(set->buffer.line_widths);
            free(set->gap_buffer.line_widths);
            free(set->multi_gap_buffer.line_widths);
            free(set->rope_buffer.line_widths);

            set->buffer.line_max = 0;
            set->buffer.line_starts = 0;
            set->buffer.widths_max = 0;
            set->buffer.line_widths = 0;
            
            set->gap_buffer.line_max = 0;
            set->gap_buffer.line_starts = 0;
            set->gap_buffer.widths_max = 0;
            set->gap_buffer.line_widths = 0;
            
            set->multi_gap_buffer.line_max = 0;
            set->multi_gap_buffer.line_starts = 0;
            set->multi_gap_buffer.widths_max = 0;
            set->multi_gap_buffer.line_widths = 0;
            
            set->rope_buffer.line_max = 0;
            set->rope_buffer.line_starts = 0;
            set->rope_buffer.widths_max = 0;
            set->rope_buffer.line_widths = 0;
        }
    }

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("measure-starts-widths"), stats_out, machine.samples, machine.count);
}

int page_compare(void *page_1_, void *page_2_, int page_size){
    char *page_1 = (char*)page_1_;
    char *page_2 = (char*)page_2_;
    int result = 1;
    for (int i = 0; i < page_size; ++i){
        hard_assert_4tech(page_1[i] == page_2[i]);
    }
    return result;
}

float* measure_wraps_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, void *scratch,
                   int scratch_size, Record_Statistics *stats_out, float font_height, float max_width){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    
    float *wrap_ys, *wrap_ys2;    
    wrap_ys = (float*)malloc(sizeof(float)*buffers->buffer.line_count);
    wrap_ys2 = (float*)malloc(sizeof(float)*buffers->buffer.line_count);
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        buffer_measure_wrap_y(&buffers->buffer, wrap_ys, font_height, max_width);
        machine.samples[i].buffer = stop(&machine);

        start(&machine);
        buffer_measure_wrap_y(&buffers->gap_buffer, wrap_ys2, font_height, max_width);
        machine.samples[i].gap_buffer = stop(&machine);
        if (i == 0)
            page_compare((char*)wrap_ys, (char*)wrap_ys2, sizeof(float)*buffers->buffer.line_count);
    
        start(&machine);
        buffer_measure_wrap_y(&buffers->multi_gap_buffer, wrap_ys2, font_height, max_width);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        if (i == 0)
            page_compare((char*)wrap_ys, (char*)wrap_ys2, sizeof(float)*buffers->buffer.line_count);
    
        start(&machine);
        buffer_measure_wrap_y(&buffers->rope_buffer, wrap_ys2, font_height, max_width);
        machine.samples[i].rope_buffer = stop(&machine);
        if (i == 0)
            page_compare((char*)wrap_ys, (char*)wrap_ys2, sizeof(float)*buffers->buffer.line_count);
    }
    
    free(wrap_ys2);

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("measure-wrap-ys"), stats_out, machine.samples, machine.count);
    
    return wrap_ys;
}

int cursor_eq(Full_Cursor c1, Full_Cursor c2){
    int result = 0;
    if (c1.pos == c2.pos && c1.line == c2.line && c1.character == c2.character &&
        c1.wrapped_x == c2.wrapped_x && c1.wrapped_y == c2.wrapped_y &&
        c1.unwrapped_x == c2.unwrapped_x && c1.unwrapped_y == c2.unwrapped_y){
        result = 1;
    }
    return(result);
}

void full_cursor_test(Stats_Log *log, Buffer_Set *buffers, int pos,
                 float *wrap_ys, float *advance_data, float font_height, float max_width,
                 int test_repitions, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    Full_Cursor cursor, cursor2;
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        cursor = buffer_cursor_from_pos(&buffers->buffer, pos, wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].buffer = stop(&machine);

        start(&machine);
        cursor2 = buffer_cursor_from_pos(&buffers->gap_buffer, pos, wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_pos(&buffers->multi_gap_buffer, pos, wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_pos(&buffers->rope_buffer, pos, wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].rope_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    }

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("full-cursor-seek"), stats_out, machine.samples, machine.count);
}

void full_cursor_line_test(Stats_Log *log, Buffer_Set *buffers, int line, int character,
                 float *wrap_ys, float *advance_data, float font_height, float max_width,
                 int test_repitions, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    Full_Cursor cursor, cursor2;
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        cursor = buffer_cursor_from_line_character(&buffers->buffer, line, character,
                                                   wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].buffer = stop(&machine);

        start(&machine);
        cursor2 = buffer_cursor_from_line_character(&buffers->gap_buffer, line, character,
                                                    wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_line_character(&buffers->multi_gap_buffer, line, character,
                                                    wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_line_character(&buffers->rope_buffer, line, character,
                                                    wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].rope_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    }

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("full-cursor-seek"), stats_out, machine.samples, machine.count);
}

void full_cursor_xy_test(Stats_Log *log, Buffer_Set *buffers, float x, float y, int round_down,
                    float *wrap_ys, float *advance_data, float font_height, float max_width,
                    int test_repitions, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    Full_Cursor cursor, cursor2;
    
    for (int i = 0; i < test_repitions; ++i){
        start(&machine);
        cursor = buffer_cursor_from_unwrapped_xy(&buffers->buffer, x, y, round_down,
                                                 wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].buffer = stop(&machine);

        start(&machine);
        cursor2 = buffer_cursor_from_unwrapped_xy(&buffers->gap_buffer, x, y, round_down,
                                                  wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_unwrapped_xy(&buffers->multi_gap_buffer, x, y, round_down,
                                                  wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    
        start(&machine);
        cursor2 = buffer_cursor_from_unwrapped_xy(&buffers->rope_buffer, x, y, round_down,
                                                  wrap_ys, max_width, font_height, advance_data);
        machine.samples[i].rope_buffer = stop(&machine);
        if (i == 0) assert_4tech(cursor_eq(cursor, cursor2));
    }

    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("full-cursor-seek"), stats_out, machine.samples, machine.count);
}

void word_seek_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions,
                    int incremental_position, char *word, int len,
                    void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    assert_4tech(scratch_size >= len);
    
    int pos, pos2, old_pos;
    old_pos = 0;
    
    for (int i = 0; i < machine.count; ++i){
        start(&machine);
        pos = buffer_find_string(&buffers->buffer, old_pos, word, len, (char*)scratch);
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        pos2 = buffer_find_string(&buffers->gap_buffer, old_pos, word, len, (char*)scratch);
        machine.samples[i].gap_buffer = stop(&machine);
        assert_4tech(pos2 == pos);
        
        start(&machine);
        pos2 = buffer_find_string(&buffers->multi_gap_buffer, old_pos, word, len, (char*)scratch);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        assert_4tech(pos2 == pos);
        
        start(&machine);
        pos2 = buffer_find_string(&buffers->rope_buffer, old_pos, word, len, (char*)scratch);
        machine.samples[i].rope_buffer = stop(&machine);
        assert_4tech(pos2 == pos);
        
        if (incremental_position) old_pos = pos;
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);
    
    log_sample_set(log, litstr("word-seek"), stats_out, machine.samples, machine.count);
}

void stream_check_test(Buffer_Set *buffers, void *scratch, int scratch_size){
    int i, page_size, size;
    
    size = buffer_size(&buffers->buffer);
    {
        int size2;
        size2 = buffer_size(&buffers->gap_buffer);
        hard_assert_4tech(size == size2);
        size2 = buffer_size(&buffers->multi_gap_buffer);
        hard_assert_4tech(size == size2);
        size2 = buffer_size(&buffers->rope_buffer);
        hard_assert_4tech(size == size2);
    }

    page_size = 1 << 10;

    char *page_1 = (char*)scratch;
    char *page_2 = page_1 + page_size;
    scratch_size -= page_size*2;
    hard_assert_4tech(scratch_size > 0);
    
    for (i = 0; i < size; i += page_size){
        int end = i + page_size;
        if (end > size) end = size;
        
        buffer_stringify(&buffers->buffer, i, end, page_1);
        
        buffer_stringify(&buffers->gap_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
        
        buffer_stringify(&buffers->multi_gap_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
        
        buffer_stringify(&buffers->rope_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
    }

    for (i = size-1; i > 0; i -= page_size){
        int end = i - page_size;
        if (end < 0) end = 0;
        
        buffer_backify(&buffers->buffer, i, end, page_1);
        
        buffer_backify(&buffers->gap_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
        
        buffer_backify(&buffers->multi_gap_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
        
        buffer_backify(&buffers->rope_buffer, i, end, page_2);
        page_compare(page_1, page_2, page_size);
    }
}

void insert_bottom_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                        int edit_count, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    
    char word[] = "stuff";
    int word_len = sizeof(word) - 1;
    
    int i, j;
    for (i = 0; i < test_repitions; ++i){
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_bottom(&buffers->buffer, word, word_len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_bottom(&buffers->gap_buffer, word, word_len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_bottom(&buffers->multi_gap_buffer, word, word_len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_bottom(&buffers->rope_buffer, word, word_len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].rope_buffer = stop(&machine);

        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("insert-bottom"), stats_out, machine.samples, machine.count);
}

void insert_top_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                     int edit_count, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);
    
    char word[] = "stuff";
    int word_len = sizeof(word) - 1;
    
    int i, j;
    for (i = 0; i < test_repitions; ++i){
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_top(&buffers->buffer, word, word_len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_top(&buffers->gap_buffer, word, word_len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_top(&buffers->multi_gap_buffer, word, word_len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            insert_top(&buffers->rope_buffer, word, word_len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].rope_buffer = stop(&machine);

        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("insert-top"), stats_out, machine.samples, machine.count);
}

void delete_bottom_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                        int edit_count, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    int len = 5;
    
    int i, j;
    for (i = 0; i < test_repitions; ++i){
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_bottom(&buffers->buffer, len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_bottom(&buffers->gap_buffer, len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_bottom(&buffers->multi_gap_buffer, len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_bottom(&buffers->rope_buffer, len,
                          advance_data, scratch, scratch_size);
        }
        machine.samples[i].rope_buffer = stop(&machine);

        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("delete-bottom"), stats_out, machine.samples, machine.count);
}

void delete_top_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                     int edit_count, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    int len = 5;
    
    int i, j;
    for (i = 0; i < test_repitions; ++i){
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_top(&buffers->buffer, len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_top(&buffers->gap_buffer, len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_top(&buffers->multi_gap_buffer, len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        for (j = 0; j < edit_count; ++j){
            delete_top(&buffers->rope_buffer, len,
                       advance_data, scratch, scratch_size);
        }
        machine.samples[i].rope_buffer = stop(&machine);

        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("delete-top"), stats_out, machine.samples, machine.count);
}

void natural_edits_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                        Replay *replay, void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    printf("edit count: %d\n", replay->replay.count);
    log_data_item(log, litstr("edit-count"), replay->replay.count);
    int i, j;
    for (i = 0; i < test_repitions; ++i){
        j = i*buffer_size(&buffers->buffer) / (1+test_repitions);
        
        start(&machine);
        natural_edits(&buffers->buffer, advance_data, replay, j, scratch, scratch_size);
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        natural_edits(&buffers->gap_buffer, advance_data, replay, j, scratch, scratch_size);
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        natural_edits(&buffers->multi_gap_buffer, advance_data, replay, j, scratch, scratch_size);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        natural_edits(&buffers->rope_buffer, advance_data, replay, j, scratch, scratch_size);
        machine.samples[i].rope_buffer = stop(&machine);
        
        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("natural-edits"), stats_out, machine.samples, machine.count);
}

void batch_edit_test(Stats_Log *log, Buffer_Set *buffers, int test_repitions, float *advance_data,
                     Buffer_Edit *batch, char *str_base, int batch_size,
                     void *scratch, int scratch_size, Record_Statistics *stats_out){
    Sample_Machine machine;
    machine = begin_machine(test_repitions, &scratch, &scratch_size);

    printf("batch size: %d\n", batch_size);
    log_data_item(log, litstr("batch-size"), batch_size);    
    int i;
    for (i = 0; i < test_repitions; ++i){
        start(&machine);
        batch_edit(&buffers->buffer, advance_data, batch, str_base, batch_size, scratch, scratch_size);
        machine.samples[i].buffer = stop(&machine);
        
        start(&machine);
        batch_edit(&buffers->gap_buffer, advance_data, batch, str_base, batch_size, scratch, scratch_size);
        machine.samples[i].gap_buffer = stop(&machine);
        
        start(&machine);
        batch_edit(&buffers->multi_gap_buffer, advance_data, batch, str_base, batch_size, scratch, scratch_size);
        machine.samples[i].multi_gap_buffer = stop(&machine);
        
        start(&machine);
        batch_edit(&buffers->rope_buffer, advance_data, batch, str_base, batch_size, scratch, scratch_size);
        machine.samples[i].rope_buffer = stop(&machine);
        
        if (i == 0){
            stream_check_test(buffers, scratch, scratch_size);
        }
    }
    
    end_machine(&machine, stats_out, __FUNCTION__);

    log_sample_set(log, litstr("batch-edit"), stats_out, machine.samples, machine.count);
}

void measure_check_test(Buffer_Set *buffers){
    int count;
    count = buffers->buffer.line_count;
    assert_4tech(count == buffers->buffer.widths_count);
    
    assert_4tech(count == buffers->gap_buffer.line_count);
    assert_4tech(count == buffers->multi_gap_buffer.line_count);
    assert_4tech(count == buffers->rope_buffer.line_count);
    
    assert_4tech(count == buffers->gap_buffer.widths_count);
    assert_4tech(count == buffers->multi_gap_buffer.widths_count);
    assert_4tech(count == buffers->rope_buffer.widths_count);

    page_compare(buffers->buffer.line_starts, buffers->gap_buffer.line_starts, sizeof(int)*count);
    page_compare(buffers->buffer.line_starts, buffers->multi_gap_buffer.line_starts, sizeof(int)*count);
    page_compare(buffers->buffer.line_starts, buffers->rope_buffer.line_starts, sizeof(int)*count);

    page_compare(buffers->buffer.line_widths, buffers->gap_buffer.line_widths, sizeof(float)*count);
    page_compare(buffers->buffer.line_widths, buffers->multi_gap_buffer.line_widths, sizeof(float)*count);
    page_compare(buffers->buffer.line_widths, buffers->rope_buffer.line_widths, sizeof(float)*count);
}

#define reps 50

int main(int argc, char **argv){
    Buffer_Set buffers;
    File_Data file;
    float *widths_data;
    float *wrap_ys;
    float font_height;
    float max_width;

    void *scratch;
    int scratch_size;
    
    Stats_Log log;
    
    int do_replay = 0;
    char *replay_filename = 0;
    
    if (argc < 2){
        printf("usage: buffer_test <filename> <message> [-h <.hst file>]\n");
        exit(1);
    }
    
    setup();
    
    for (int i = 3; i < argc; ++i){
        if (do_replay){
            replay_filename = argv[i];
            do_replay = 0;
        }
        if (strcmp(argv[i], "-h") == 0){
            if (replay_filename != 0){
                printf("found -h twice, ignoring duplicates\n");
            }
            else{
                do_replay = 1;
            }
        }
    }
    
    do_replay = (replay_filename != 0);
    
    memzero_4tech(buffers);
    
    log.max = 1 << 20;
    log.size = 0;
    log.out = (char*)malloc(log.max);

    log.sec_max = 32;
    log.sec_top = 0;
    log.sections = (Log_Section*)malloc(sizeof(Log_Section)*log.sec_max);

    log.error = 0;

    scratch_size = 1 << 20;
    scratch = malloc(scratch_size);
    
    file = get_file(argv[1]);
    if (!file.data) exit(1);
    widths_data = get_font_data("LiberationSans-Regular.ttf", &font_height);
    max_width = 500.f;
    
    log_begin_section(&log, litstr("which-test"));
    {
        log_write_str(&log, argv[1], (int)strlen(argv[1]));
        log_write_int(&log, file.size);
        log_write_str(&log, argv[2], (int)strlen(argv[2]));
    }
    log_end_section(&log);
    
    log_begin_section(&log, litstr("file-open"));
    {
        Record_Statistics init_rec, starts_widths_rec, wraps_rec;
    
        initialization_test(&log, &buffers, file, reps, scratch, scratch_size, &init_rec);
        stream_check_test(&buffers, scratch, scratch_size);
    
        measure_starts_widths_test(&log, &buffers, reps, scratch, scratch_size,
                                   &starts_widths_rec, widths_data);
        measure_check_test(&buffers);

        wrap_ys = measure_wraps_test(&log, &buffers, reps, scratch, scratch_size,
                                     &wraps_rec, font_height, max_width);
    
        Time_Record expected_file_open;
        expected_file_open = init_rec.expected + starts_widths_rec.expected + wraps_rec.expected;
    
        printf("average file open:\n");
        print_record(expected_file_open);
        printf("\n");
        log_time_record(&log, litstr("average"), expected_file_open);
    }
    log_end_section(&log);
    
    log_begin_section(&log, litstr("cursor-seek"));
    {
        Record_Statistics full_cursor;
        Time_Record full_cursor_average;

        log_begin_section(&log, litstr("to-pos"));
        {
            memzero_4tech(full_cursor_average);
            for (int i = 0; i < 5; ++i){
                silence_test();
                int pos = (file.size*i) / 5;
                full_cursor_test(&log, &buffers, pos,
                                 wrap_ys, widths_data, font_height, max_width,
                                 5, scratch, scratch_size, &full_cursor);
                full_cursor_average = full_cursor_average + full_cursor.expected;
            }
            full_cursor_average /= 5;
            printf("average cursor from position:\n");
            print_record(full_cursor_average);
            printf("\n");
            log_time_record(&log, litstr("average"), full_cursor_average);
        }
        log_end_section(&log);

        log_begin_section(&log, litstr("to-line-character"));
        {
            memzero_4tech(full_cursor_average);
            for (int i = 0; i < 5; ++i){
                silence_test();
                int line = (buffers.buffer.line_count*i) / 5;
                full_cursor_line_test(&log, &buffers, line, 20,
                                      wrap_ys, widths_data, font_height, max_width,
                                      5, scratch, scratch_size, &full_cursor);
                full_cursor_average = full_cursor_average + full_cursor.expected;
            }
            full_cursor_average /= 5;
            printf("average cursor from line & character:\n");
            print_record(full_cursor_average);
            printf("\n");
            log_time_record(&log, litstr("average"), full_cursor_average);
        }
        log_end_section(&log);

        log_begin_section(&log, litstr("to-unwrapped-x-y"));
        {
            memzero_4tech(full_cursor_average);
            for (int i = 0; i < 5; ++i){
                silence_test();
                float y = font_height * (buffers.buffer.line_count*i) / 4.f;
                full_cursor_xy_test(&log, &buffers, y, 37.f, 0,
                                    wrap_ys, widths_data, font_height, max_width,
                                    5, scratch, scratch_size, &full_cursor);
                full_cursor_average = full_cursor_average + full_cursor.expected;
            }
            full_cursor_average /= 5;
            printf("average cursor from line & character:\n");
            print_record(full_cursor_average);
            printf("\n");
            log_time_record(&log, litstr("average"), full_cursor_average);
        }
        log_end_section(&log);
        
    }
    log_end_section(&log);
    
    log_begin_section(&log, litstr("word-seek"));
    {
        Record_Statistics word_seek;

        {
            char word[] = "not-going-to-find-this";
            int word_len = sizeof(word) - 1;
            word_seek_test(&log, &buffers, reps, 0, word, word_len, scratch, scratch_size, &word_seek);
        }

        {
            char word[] = "return";
            int word_len = sizeof(word) - 1;
            word_seek_test(&log, &buffers, reps, 1, word, word_len, scratch, scratch_size, &word_seek);
        }
    }
    log_end_section(&log);
    
    log_begin_section(&log, litstr("one-hundred-single-edits"));
    {
        Record_Statistics edits;
        insert_bottom_test(&log, &buffers, reps, widths_data, 100, scratch, scratch_size, &edits);
        insert_top_test(&log, &buffers, reps, widths_data, 100, scratch, scratch_size, &edits);
        delete_bottom_test(&log, &buffers, reps, widths_data, 100, scratch, scratch_size, &edits);
        delete_top_test(&log, &buffers, reps, widths_data, 100, scratch, scratch_size, &edits);
    }
    log_end_section(&log);
    
    File_Data replay_file = {};
    Replay replay;
    
    if (do_replay){
        replay_file = get_file(replay_filename);
        prepare_replay(replay_file, &replay);
    }

    if (replay_file.data){
        log_begin_section(&log, litstr("natural-edits"));
        {
            Record_Statistics edits;
            natural_edits_test(&log, &buffers, reps, widths_data, &replay,
                               scratch, scratch_size, &edits);
        }
        log_end_section(&log);
    }
    else{
        printf("skipped natural-edits test\n\n");
    }
    
    log_begin_section(&log, litstr("batch-edit"));
    {
        Buffer_Edit *batch;
        char *str_base;
        int batch_size, str_size;

        batch_size = 1000;
        str_size = 10000;
        batch = (Buffer_Edit*)malloc(sizeof(Buffer_Edit)*batch_size);
        str_base = (char*)malloc(str_size);

        int character_stride;
        character_stride = buffer_size(&buffers.buffer);
        if (character_stride < batch_size * 10){
            batch_size = character_stride / 10;
            character_stride = 10;
        }
        else{
            character_stride /= batch_size;
        }

        int p, curs;
        curs = 0;
        p = 0;
        for (int i = 0; i < batch_size; ++i){
            Buffer_Edit edit;
            edit.start = p;
            edit.end = p+8;
            p += character_stride;
            
            edit.str_start = curs;
            if (i & 1){
                edit.len = 9;
                memcpy_4tech(str_base + curs, "123456789", 9);
                curs += 9;
            }
            else{
                edit.len = 7;
                memcpy_4tech(str_base + curs, "abcdefg", 7);
                curs += 7;
            }
            
            batch[i] = edit;
        }
        
        Record_Statistics batch_stats;
        batch_edit_test(&log, &buffers, reps, widths_data, batch, str_base, batch_size,
                        scratch, scratch_size, &batch_stats);
    }
    log_end_section(&log);
    
    log_finish(&log);
    
    return(0);
}

// BOTTOM