/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdlib.h>
#include <string.h>
#include <time.h>

#define TESTLUCY_USE_SHORT_NAMES
#include "Lucy/Util/ToolSet.h"
#include "Lucy/Test/Util/TestSortExternal.h"

#include "Clownfish/Blob.h"
#include "Clownfish/TestHarness/TestBatchRunner.h"
#include "Lucy/Util/BlobSortEx.h"
#include "Lucy/Util/SortExternal.h"

static Blob *a_blob;
static Blob *b_blob;
static Blob *c_blob;
static Blob *d_blob;
static Blob *x_blob;
static Blob *y_blob;
static Blob *z_blob;

TestSortExternal*
TestSortExternal_new() {
    return (TestSortExternal*)Class_Make_Obj(TESTSORTEXTERNAL);
}

static void
S_init_blobs() {
    a_blob = Blob_new("a", 1);
    b_blob = Blob_new("b", 1);
    c_blob = Blob_new("c", 1);
    d_blob = Blob_new("d", 1);
    x_blob = Blob_new("x", 1);
    y_blob = Blob_new("y", 1);
    z_blob = Blob_new("z", 1);
}

static void
S_destroy_blobs() {
    DECREF(a_blob);
    DECREF(b_blob);
    DECREF(c_blob);
    DECREF(d_blob);
    DECREF(x_blob);
    DECREF(y_blob);
    DECREF(z_blob);
}

static void
test_bbsortex(TestBatchRunner *runner) {
    BlobSortEx *sortex = BlobSortEx_new(4, NULL);

    BlobSortEx_Feed(sortex, INCREF(c_blob));
    TEST_INT_EQ(runner, BlobSortEx_Buffer_Count(sortex), 1,
                "feed elem into cache");

    BlobSortEx_Feed(sortex, INCREF(b_blob));
    BlobSortEx_Feed(sortex, INCREF(d_blob));
    BlobSortEx_Sort_Buffer(sortex);

    {
        Vector *cache  = BlobSortEx_Peek_Cache(sortex);
        Vector *wanted = Vec_new(3);
        Vec_Push(wanted, INCREF(b_blob));
        Vec_Push(wanted, INCREF(c_blob));
        Vec_Push(wanted, INCREF(d_blob));
        TEST_TRUE(runner, Vec_Equals(cache, (Obj*)wanted), "sort cache");
        DECREF(wanted);
        DECREF(cache);
    }

    BlobSortEx_Feed(sortex, INCREF(a_blob));
    TEST_INT_EQ(runner, BlobSortEx_Buffer_Count(sortex), 0,
                "cache flushed automatically when mem_thresh crossed");
    TEST_INT_EQ(runner, BlobSortEx_Get_Num_Runs(sortex), 1, "run added");

    Vector *external = Vec_new(3);
    Vec_Push(external, INCREF(x_blob));
    Vec_Push(external, INCREF(y_blob));
    Vec_Push(external, INCREF(z_blob));
    BlobSortEx *run = BlobSortEx_new(0x1000000, external);
    BlobSortEx_Add_Run(sortex, (SortExternal*)run);
    BlobSortEx_Flip(sortex);

    {
        Vector *got = Vec_new(7);
        Obj *object;
        while (NULL != (object = BlobSortEx_Fetch(sortex))) {
            Vec_Push(got, object);
        }

        Vector *wanted = Vec_new(7);
        Vec_Push(wanted, INCREF(a_blob));
        Vec_Push(wanted, INCREF(b_blob));
        Vec_Push(wanted, INCREF(c_blob));
        Vec_Push(wanted, INCREF(d_blob));
        Vec_Push(wanted, INCREF(x_blob));
        Vec_Push(wanted, INCREF(y_blob));
        Vec_Push(wanted, INCREF(z_blob));

        TEST_TRUE(runner, Vec_Equals(got, (Obj*)wanted), "Add_Run");

        DECREF(wanted);
        DECREF(got);
    }

    DECREF(external);
    DECREF(sortex);
}

static void
test_clear_buffer(TestBatchRunner *runner) {
    BlobSortEx *sortex = BlobSortEx_new(4, NULL);

    BlobSortEx_Feed(sortex, INCREF(c_blob));
    BlobSortEx_Clear_Buffer(sortex);
    TEST_INT_EQ(runner, BlobSortEx_Buffer_Count(sortex), 0, "Clear_Buffer");

    BlobSortEx_Feed(sortex, INCREF(b_blob));
    BlobSortEx_Feed(sortex, INCREF(a_blob));
    BlobSortEx_Flush(sortex);
    BlobSortEx_Flip(sortex);
    Obj *object = BlobSortEx_Peek(sortex);
    TEST_TRUE(runner, Blob_Equals(a_blob, object), "Peek");

    Vector *got = Vec_new(2);
    while (NULL != (object = BlobSortEx_Fetch(sortex))) {
        Vec_Push(got, object);
    }
    Vector *wanted = Vec_new(2);
    Vec_Push(wanted, INCREF(a_blob));
    Vec_Push(wanted, INCREF(b_blob));
    TEST_TRUE(runner, Vec_Equals(got, (Obj*)wanted),
              "elements cleared via Clear_Buffer truly cleared");

    DECREF(wanted);
    DECREF(got);
    DECREF(sortex);
}

static void
S_test_sort(TestBatchRunner *runner, Vector *blobs, uint32_t mem_thresh,
            const char *test_name) {
    size_t       size     = Vec_Get_Size(blobs);
    BlobSortEx  *sortex   = BlobSortEx_new(mem_thresh, NULL);
    Blob       **shuffled = (Blob**)MALLOCATE(size * sizeof(Blob*));

    for (size_t i = 0; i < size; ++i) {
        shuffled[i] = (Blob*)CERTIFY(Vec_Fetch(blobs, i), BLOB);
    }
    for (int i = (int)size - 1; i > 0; --i) {
        int shuffle_pos = rand() % (i + 1);
        Blob *temp = shuffled[shuffle_pos];
        shuffled[shuffle_pos] = shuffled[i];
        shuffled[i] = temp;
    }
    for (size_t i = 0; i < size; ++i) {
        BlobSortEx_Feed(sortex, INCREF(shuffled[i]));
    }

    BlobSortEx_Flip(sortex);
    Vector *got = Vec_new(size);
    Obj *object;
    while (NULL != (object = BlobSortEx_Fetch(sortex))) {
        Vec_Push(got, object);
    }
    TEST_TRUE(runner, Vec_Equals(got, (Obj*)blobs), test_name);

    FREEMEM(shuffled);
    DECREF(got);
    DECREF(sortex);
}

static void
S_test_sort_letters(TestBatchRunner *runner, const char *letters,
                    uint32_t mem_thresh, const char *test_name) {
    size_t  num_letters = strlen(letters);
    Vector *blobs       = Vec_new(num_letters);

    for (size_t i = 0; i < num_letters; ++i) {
        char ch = letters[i];
        size_t size = ch == '_' ? 0 : 1;
        Blob *blob = Blob_new(&ch, size);
        Vec_Push(blobs, (Obj*)blob);
    }

    S_test_sort(runner, blobs, mem_thresh, test_name);

    DECREF(blobs);
}

static void
test_sort_letters(TestBatchRunner *runner) {
    S_test_sort_letters(runner, "abcdefghijklmnopqrstuvwxyz", 0x1000000,
                        "sort letters");
    S_test_sort_letters(runner, "aaabcdxxxxxxyy", 0x1000000,
                        "sort repeated letters");
    S_test_sort_letters(runner, "__abcdefghijklmnopqrstuvwxyz", 0x1000000,
                        "sort letters and empty strings");
    S_test_sort_letters(runner, "abcdefghijklmnopqrstuvwxyz", 30,
                        "... with an absurdly low mem_thresh");
    S_test_sort_letters(runner, "abcdefghijklmnopqrstuvwxyz", 1,
                        "... with an even lower mem_thresh");
}

static void
test_sort_nothing(TestBatchRunner *runner) {
    BlobSortEx *sortex = BlobSortEx_new(0x1000000, NULL);
    BlobSortEx_Flip(sortex);
    TEST_TRUE(runner, BlobSortEx_Fetch(sortex) == NULL,
              "Sorting nothing returns undef");
    DECREF(sortex);
}

static void
test_sort_packed_ints(TestBatchRunner *runner) {
    size_t  num_ints = 11001;
    Vector *blobs    = Vec_new(num_ints);

    for (uint32_t i = 0; i < num_ints; ++i) {
        uint8_t buf[4];
        buf[0] = (uint8_t)((i >> 24) & 0xFF);
        buf[1] = (uint8_t)((i >> 16) & 0xFF);
        buf[2] = (uint8_t)((i >> 8)  & 0xFF);
        buf[3] = (uint8_t)(i & 0xFF);
        Blob *blob = Blob_new((char*)buf, 4);
        Vec_Push(blobs, (Obj*)blob);
    }

    S_test_sort(runner, blobs, 5000, "Sorting packed integers...");

    DECREF(blobs);
}

static void
test_sort_random_strings(TestBatchRunner *runner) {
    size_t  num_strings = 1001;
    Vector *blobs       = Vec_new(num_strings);

    for (uint32_t i = 0; i < num_strings; ++i) {
        uint8_t buf[1201];
        int size = rand() % 1200 + 1;
        for (int i = 0; i < size; ++i) {
            buf[i] = (uint8_t)(rand() % 256);
        }
        Blob *blob = Blob_new((char*)buf, (size_t)size);
        Vec_Push(blobs, (Obj*)blob);
    }

    Vec_Sort(blobs);
    S_test_sort(runner, blobs, 15000,
                "Random binary strings of random length");

    DECREF(blobs);
}

static void
test_run(TestBatchRunner *runner) {
    Vector *letters = Vec_new(26);
    for (char i = 0; i < 26; ++i) {
        char ch = 'a' + i;
        Blob *blob = Blob_new(&ch, 1);
        Vec_Push(letters, (Obj*)blob);
    }
    BlobSortEx *run = BlobSortEx_new(0x1000000, letters);
    BlobSortEx_Set_Mem_Thresh(run, 5);

    BlobSortEx_Refill(run);
    TEST_INT_EQ(runner, BlobSortEx_Buffer_Count(run), 5,
                "Refill doesn't exceed memory threshold");

    Obj *endpost = BlobSortEx_Peek_Last(run);
    Blob *wanted = Blob_new("e", 1);
    TEST_TRUE(runner, Blob_Equals(wanted, endpost), "Peek_Last");

    Vector *elems = Vec_new(26);
    do {
        while (BlobSortEx_Buffer_Count(run) > 0) {
            Obj *object = BlobSortEx_Fetch(run);
            Vec_Push(elems, object);
        }
    } while (BlobSortEx_Refill(run) > 0);
    TEST_TRUE(runner, Vec_Equals(elems, (Obj*)letters), "retrieve all elems");

    DECREF(elems);
    DECREF(wanted);
    DECREF(endpost);
    DECREF(letters);
    DECREF(run);
}

void
TestSortExternal_Run_IMP(TestSortExternal *self, TestBatchRunner *runner) {
    TestBatchRunner_Plan(runner, (TestBatch*)self, 19);

    srand((unsigned int)time((time_t*)NULL));
    S_init_blobs();
    test_bbsortex(runner);
    test_clear_buffer(runner);
    test_sort_letters(runner);
    test_sort_nothing(runner);
    test_sort_packed_ints(runner);
    test_sort_random_strings(runner);
    test_run(runner);
    S_destroy_blobs();
}


