/*
 * Copyright © 2017 Broadcom
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <xcb/sync.h>

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

static const int64_t some_values[] = {
        0,
        1,
        -1,
        LLONG_MAX,
        LLONG_MIN,
};

static int64_t
pack_sync_value(xcb_sync_int64_t val)
{
    return ((int64_t)val.hi << 32) | val.lo;
}

static int64_t
counter_value(struct xcb_connection_t *c,
              xcb_sync_query_counter_cookie_t cookie)
{
    xcb_sync_query_counter_reply_t *reply =
        xcb_sync_query_counter_reply(c, cookie, NULL);
    int64_t value = pack_sync_value(reply->counter_value);

    free(reply);
    return value;
}

static xcb_sync_int64_t
sync_value(int64_t value)
{
    xcb_sync_int64_t v = {
        .hi = value >> 32,
        .lo = value,
    };

    return v;
}

/* Initializes counters with a bunch of interesting values and makes
 * sure it comes back the same.
 */
static void
test_create_counter(xcb_connection_t *c)
{
    xcb_sync_query_counter_cookie_t queries[ARRAY_SIZE(some_values)];

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        xcb_sync_counter_t counter = xcb_generate_id(c);
        xcb_sync_create_counter(c, counter, sync_value(some_values[i]));
        queries[i] = xcb_sync_query_counter_unchecked(c, counter);
    }

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        int64_t value = counter_value(c, queries[i]);

        if (value != some_values[i]) {
            fprintf(stderr, "Creating counter with %lld returned %lld\n",
                    (long long)some_values[i],
                    (long long)value);
            exit(1);
        }
    }
}

/* Set a single counter to a bunch of interesting values and make sure
 * it comes the same.
 */
static void
test_set_counter(xcb_connection_t *c)
{
    xcb_sync_counter_t counter = xcb_generate_id(c);
    xcb_sync_query_counter_cookie_t queries[ARRAY_SIZE(some_values)];

    xcb_sync_create_counter(c, counter, sync_value(0));

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        xcb_sync_set_counter(c, counter, sync_value(some_values[i]));
        queries[i] = xcb_sync_query_counter_unchecked(c, counter);
    }

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        int64_t value = counter_value(c, queries[i]);

        if (value != some_values[i]) {
            fprintf(stderr, "Setting counter to %lld returned %lld\n",
                    (long long)some_values[i],
                    (long long)value);
            exit(1);
        }
    }
}

/* Add [0, 1, 2, 3] to a counter and check that the values stick. */
static void
test_change_counter_basic(xcb_connection_t *c)
{
    int iterations = 4;
    xcb_sync_query_counter_cookie_t queries[iterations];

    xcb_sync_counter_t counter = xcb_generate_id(c);
    xcb_sync_create_counter(c, counter, sync_value(0));

    for (int i = 0; i < iterations; i++) {
        xcb_sync_change_counter(c, counter, sync_value(i));
        queries[i] = xcb_sync_query_counter_unchecked(c, counter);
    }

    int64_t expected_value = 0;
    for (int i = 0; i < iterations; i++) {
        expected_value += i;
        int64_t value = counter_value(c, queries[i]);

        if (value != expected_value) {
            fprintf(stderr, "Adding %d to counter expected %lld returned %lld\n",
                    i,
                    (long long)expected_value,
                    (long long)value);
            exit(1);
        }
    }
}

/* Test change_counter where we trigger an integer overflow. */
static void
test_change_counter_overflow(xcb_connection_t *c)
{
    int iterations = 4;
    xcb_sync_query_counter_cookie_t queries[iterations];
    xcb_void_cookie_t changes[iterations];
    static const struct {
        int64_t a, b;
    } overflow_args[] = {
        { LLONG_MAX, 1 },
        { LLONG_MAX, LLONG_MAX },
        { LLONG_MIN, -1 },
        { LLONG_MIN, LLONG_MIN },
    };

    xcb_sync_counter_t counter = xcb_generate_id(c);
    xcb_sync_create_counter(c, counter, sync_value(0));

    for (int i = 0; i < ARRAY_SIZE(overflow_args); i++) {
        int64_t a = overflow_args[i].a;
        int64_t b = overflow_args[i].b;
        xcb_sync_set_counter(c, counter, sync_value(a));
        changes[i] = xcb_sync_change_counter_checked(c, counter,
                                                     sync_value(b));
        queries[i] = xcb_sync_query_counter(c, counter);
    }

    for (int i = 0; i < ARRAY_SIZE(overflow_args); i++) {
        int64_t a = overflow_args[i].a;
        int64_t b = overflow_args[i].b;
        xcb_sync_query_counter_reply_t *reply =
            xcb_sync_query_counter_reply(c, queries[i], NULL);
        int64_t value = (((int64_t)reply->counter_value.hi << 32) |
                         reply->counter_value.lo);
        int64_t expected_value = a;

        /* The change_counter should have thrown BadValue */
        xcb_generic_error_t *e = xcb_request_check(c, changes[i]);
        if (!e) {
            fprintf(stderr, "(%lld + %lld) failed to return an error\n",
                    (long long)a,
                    (long long)b);
            exit(1);
        }

        if (e->error_code != XCB_VALUE) {
            fprintf(stderr, "(%lld + %lld) returned %d, not BadValue\n",
                    (long long)a,
                    (long long)b,
                    e->error_code);
            exit(1);
        }

        /* The change_counter should have had no other effect if it
         * errored out.
         */
        if (value != expected_value) {
            fprintf(stderr, "(%lld + %lld) expected %lld returned %lld\n",
                    (long long)a,
                    (long long)b,
                    (long long)expected_value,
                    (long long)value);
            exit(1);
        }

        free(e);
        free(reply);
    }
}

static void
test_change_alarm_value(xcb_connection_t *c)
{
    xcb_sync_alarm_t alarm = xcb_generate_id(c);
    xcb_sync_query_alarm_cookie_t queries[ARRAY_SIZE(some_values)];

    xcb_sync_create_alarm(c, alarm, 0, NULL);

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        uint32_t values[] = { some_values[i] >> 32, some_values[i] };

        xcb_sync_change_alarm(c, alarm, XCB_SYNC_CA_VALUE, values);
        queries[i] = xcb_sync_query_alarm_unchecked(c, alarm);
    }

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        xcb_sync_query_alarm_reply_t *reply =
            xcb_sync_query_alarm_reply(c, queries[i], NULL);
        int64_t value = pack_sync_value(reply->trigger.wait_value);

        if (value != some_values[i]) {
            fprintf(stderr, "Setting alarm value to %lld returned %lld\n",
                    (long long)some_values[i],
                    (long long)value);
            exit(1);
        }
        free(reply);
    }
}

static void
test_change_alarm_delta(xcb_connection_t *c)
{
    xcb_sync_alarm_t alarm = xcb_generate_id(c);
    xcb_sync_query_alarm_cookie_t queries[ARRAY_SIZE(some_values)];

    xcb_sync_create_alarm(c, alarm, 0, NULL);

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        uint32_t mask = XCB_SYNC_CA_TEST_TYPE | XCB_SYNC_CA_DELTA;
        uint32_t test_type = (some_values[i] >= 0 ?
                               XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON :
                               XCB_SYNC_TESTTYPE_NEGATIVE_COMPARISON);
        uint32_t values[] = { test_type, some_values[i] >> 32, some_values[i] };

        xcb_sync_change_alarm(c, alarm, mask, values);
        queries[i] = xcb_sync_query_alarm_unchecked(c, alarm);
    }

    for (int i = 0; i < ARRAY_SIZE(some_values); i++) {
        xcb_sync_query_alarm_reply_t *reply =
            xcb_sync_query_alarm_reply(c, queries[i], NULL);
        int64_t value = pack_sync_value(reply->delta);

        if (value != some_values[i]) {
            fprintf(stderr, "Setting alarm delta to %lld returned %lld\n",
                    (long long)some_values[i],
                    (long long)value);
            exit(1);
        }
        free(reply);
    }
}

int main(int argc, char **argv)
{
    int screen;
    xcb_connection_t *c = xcb_connect(NULL, &screen);
    const xcb_query_extension_reply_t *ext = xcb_get_extension_data(c, &xcb_sync_id);

    if (!ext->present) {
        printf("No XSync present\n");
        exit(77);
    }

    test_create_counter(c);
    test_set_counter(c);
    test_change_counter_basic(c);
    test_change_counter_overflow(c);
    test_change_alarm_value(c);
    test_change_alarm_delta(c);

    xcb_disconnect(c);
    exit(0);
}