/*
 * 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 "config.h"

#include "guacamole/argv.h"
#include "guacamole/client.h"
#include "guacamole/protocol.h"
#include "guacamole/socket.h"
#include "guacamole/stream.h"
#include "guacamole/string.h"
#include "guacamole/user.h"

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

/**
 * The state of an argument that will be automatically processed. Note that
 * this is distinct from the state of an argument value that is currently being
 * processed. Argument value states are dynamically-allocated and scoped by the
 * associated guac_stream.
 */
typedef struct guac_argv_state {

    /**
     * The name of the argument.
     */
    char name[GUAC_ARGV_MAX_NAME_LENGTH];

    /**
     * Whether at least one value for this argument has been received since it
     * was registered.
     */
    int received;

    /**
     * Bitwise OR of all option flags that should affect processing of this
     * argument.
     */
    int options;

    /**
     * The callback that should be invoked when a new value for the associated
     * argument has been received. If the GUAC_ARGV_OPTION_ONCE flag is set,
     * the callback will be invoked at most once.
     */
    guac_argv_callback* callback;

    /**
     * The arbitrary data that should be passed to the callback.
     */
    void* data;

} guac_argv_state;

/**
 * The current state of automatic processing of "argv" streams.
 */
typedef struct guac_argv_await_state {

    /**
     * Whether automatic argument processing has been stopped via a call to
     * guac_argv_stop().
     */
    int stopped;

    /**
     * The total number of arguments registered.
     */
    unsigned int num_registered;

    /**
     * All registered arguments and their corresponding callbacks.
     */
    guac_argv_state registered[GUAC_ARGV_MAX_REGISTERED];

    /**
     * Lock which protects multi-threaded access to this entire state
     * structure, including the condition that signals specific modifications
     * to the structure.
     */
    pthread_mutex_t lock;

    /**
     * Condition which is signalled whenever the overall state of "argv"
     * processing changes, either through the receipt of a new argument value
     * or due to a call to guac_argv_stop().
     */
    pthread_cond_t changed;

} guac_argv_await_state;

/**
 * The value or current status of a connection parameter received over an
 * "argv" stream.
 */
typedef struct guac_argv {

    /**
     * The state of the specific setting being updated.
     */
    guac_argv_state* state;

    /**
     * The mimetype of the data being received.
     */
    char mimetype[GUAC_ARGV_MAX_MIMETYPE_LENGTH];

    /**
     * Buffer space for containing the received argument value.
     */
    char buffer[GUAC_ARGV_MAX_LENGTH];

    /**
     * The number of bytes received so far.
     */
    int length;

} guac_argv;

/**
 * Statically-allocated, shared state of the guac_argv_*() family of functions.
 */
static guac_argv_await_state await_state = {
    .lock = PTHREAD_MUTEX_INITIALIZER,
    .changed = PTHREAD_COND_INITIALIZER
};

/**
 * Returns whether at least one value for each of the provided arguments has
 * been received.
 *
 * @param args
 *     A NULL-terminated array of the names of all arguments to test.
 *
 * @return
 *     Non-zero if at least one value has been received for each of the
 *     provided arguments, zero otherwise.
 */
static int guac_argv_is_received(const char** args) {

    for (int i = 0; i < await_state.num_registered; i++) {

        /* Ignore all received arguments */
        guac_argv_state* state = &await_state.registered[i];
        if (state->received)
            continue;

        /* Fail immediately for any matching non-received arguments */
        for (const char** arg = args; *arg != NULL; arg++) {
            if (strcmp(state->name, *arg) == 0)
                return 0;
        }

    }

    /* All arguments were received */
    return 1;

}

int guac_argv_register(const char* name, guac_argv_callback* callback, void* data, int options) {

    pthread_mutex_lock(&await_state.lock);

    if (await_state.num_registered == GUAC_ARGV_MAX_REGISTERED) {
        pthread_mutex_unlock(&await_state.lock);
        return 1;
    }

    guac_argv_state* state = &await_state.registered[await_state.num_registered++];
    guac_strlcpy(state->name, name, sizeof(state->name));
    state->options = options;
    state->callback = callback;
    state->data = data;

    pthread_mutex_unlock(&await_state.lock);
    return 0;

}

int guac_argv_await(const char** args) {

    /* Wait for all requested arguments to be received (or for receipt to be
     * stopped) */
    pthread_mutex_lock(&await_state.lock);
    while (!await_state.stopped && !guac_argv_is_received(args))
        pthread_cond_wait(&await_state.changed, &await_state.lock);

    /* Arguments were successfully received only if receipt was not stopped */
    int retval = await_state.stopped;
    pthread_mutex_unlock(&await_state.lock);
    return retval;

}

/**
 * Handler for "blob" instructions which appends the data from received blobs
 * to the end of the in-progress argument value buffer.
 *
 * @see guac_user_blob_handler
 */
static int guac_argv_blob_handler(guac_user* user, guac_stream* stream,
        void* data, int length) {

    guac_argv* argv = (guac_argv*) stream->data;

    /* Calculate buffer size remaining, including space for null terminator,
     * adjusting received length accordingly */
    int remaining = sizeof(argv->buffer) - argv->length - 1;
    if (length > remaining)
        length = remaining;

    /* Append received data to end of buffer */
    memcpy(argv->buffer + argv->length, data, length);
    argv->length += length;

    return 0;

}

/**
 * Handler for "end" instructions which applies the changes specified by the
 * argument value buffer associated with the stream.
 *
 * @see guac_user_end_handler
 */
static int guac_argv_end_handler(guac_user* user, guac_stream* stream) {

    int result = 0;

    /* Append null terminator to value */
    guac_argv* argv = (guac_argv*) stream->data;
    argv->buffer[argv->length] = '\0';

    pthread_mutex_lock(&await_state.lock);

    /* Invoke callback, limiting to a single invocation if
     * GUAC_ARGV_OPTION_ONCE applies */
    guac_argv_state* state = argv->state;
    if (!(state->options & GUAC_ARGV_OPTION_ONCE) || !state->received) {
        if (state->callback != NULL)
            result = state->callback(user, argv->mimetype, state->name, argv->buffer, state->data);
    }

    /* Alert connected clients regarding newly-accepted values if echo is
     * enabled */
    if (!result && (state->options & GUAC_ARGV_OPTION_ECHO)) {
        guac_client* client = user->client;
        guac_client_stream_argv(client, client->socket, argv->mimetype, state->name, argv->buffer);
    }

    /* Notify that argument has been received */
    state->received = 1;
    pthread_cond_broadcast(&await_state.changed);

    pthread_mutex_unlock(&await_state.lock);

    free(argv);
    return 0;

}

int guac_argv_received(guac_stream* stream, const char* mimetype, const char* name) {

    pthread_mutex_lock(&await_state.lock);

    for (int i = 0; i < await_state.num_registered; i++) {

        /* Ignore any arguments that have already been received if they are
         * declared as being acceptable only once */
        guac_argv_state* state = &await_state.registered[i];
        if ((state->options & GUAC_ARGV_OPTION_ONCE) && state->received)
            continue;

        /* Argument matched */
        if (strcmp(state->name, name) == 0) {

            guac_argv* argv = malloc(sizeof(guac_argv));
            guac_strlcpy(argv->mimetype, mimetype, sizeof(argv->mimetype));
            argv->state = state;
            argv->length = 0;

            stream->data = argv;
            stream->blob_handler = guac_argv_blob_handler;
            stream->end_handler = guac_argv_end_handler;

            pthread_mutex_unlock(&await_state.lock);
            return 0;

        }

    }

    /* No such argument awaiting processing */
    pthread_mutex_unlock(&await_state.lock);
    return 1;

}

void guac_argv_stop() {
    pthread_mutex_lock(&await_state.lock);

    /* Signal any waiting threads that no further argument values will be
     * received */
    if (!await_state.stopped) {
        await_state.stopped = 1;
        pthread_cond_broadcast(&await_state.changed);

    }

    pthread_mutex_unlock(&await_state.lock);
}

int guac_argv_handler(guac_user* user, guac_stream* stream,
        char* mimetype, char* name) {

    /* Refuse stream if argument is not registered */
    if (guac_argv_received(stream, mimetype, name)) {
        guac_protocol_send_ack(user->socket, stream, "Not allowed.",
                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
        guac_socket_flush(user->socket);
        return 0;
    }

    /* Signal stream is ready */
    guac_protocol_send_ack(user->socket, stream, "Ready for updated "
            "parameter.", GUAC_PROTOCOL_STATUS_SUCCESS);
    guac_socket_flush(user->socket);
    return 0;

}

