/*
 * 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 "channels/rdpei.h"
#include "common/surface.h"
#include "plugins/channels.h"
#include "rdp.h"
#include "settings.h"

#include <freerdp/client/rdpei.h>
#include <freerdp/freerdp.h>
#include <freerdp/event.h>
#include <guacamole/client.h>
#include <guacamole/timestamp.h>

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

guac_rdp_rdpei* guac_rdp_rdpei_alloc(guac_client* client) {

    guac_rdp_rdpei* rdpei = malloc(sizeof(guac_rdp_rdpei));
    rdpei->client = client;

    /* Not yet connected */
    rdpei->rdpei = NULL;

    /* No active touches */
    for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++)
        rdpei->touch[i].active = 0;

    return rdpei;

}

void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) {
    free(rdpei);
}

/**
 * Callback which associates handlers specific to Guacamole with the
 * RdpeiClientContext instance allocated by FreeRDP to deal with received
 * RDPEI (multi-touch input) messages.
 *
 * This function is called whenever a channel connects via the PubSub event
 * system within FreeRDP, but only has any effect if the connected channel is
 * the RDPEI channel. This specific callback is registered with the
 * PubSub system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is
 * called.
 *
 * @param context
 *     The rdpContext associated with the active RDP session.
 *
 * @param e
 *     Event-specific arguments, mainly the name of the channel, and a
 *     reference to the associated plugin loaded for that channel by FreeRDP.
 */
static void guac_rdp_rdpei_channel_connected(rdpContext* context,
        ChannelConnectedEventArgs* e) {

    guac_client* client = ((rdp_freerdp_context*) context)->client;
    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
    guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;

    /* Ignore connection event if it's not for the RDPEI channel */
    if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
        return;

    /* Store reference to the RDPEI plugin once it's connected */
    RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface;
    guac_rdpei->rdpei = rdpei;

    /* Declare level of multi-touch support */
    guac_common_surface_set_multitouch(rdp_client->display->default_surface,
            GUAC_RDP_RDPEI_MAX_TOUCHES);

    guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for "
            "multi-touch support.");

}

/**
 * Callback which disassociates Guacamole from the RdpeiClientContext instance
 * that was originally allocated by FreeRDP and is about to be deallocated.
 *
 * This function is called whenever a channel disconnects via the PubSub event
 * system within FreeRDP, but only has any effect if the disconnected channel
 * is the RDPEI channel. This specific callback is registered with the PubSub
 * system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is
 * called.
 *
 * @param context
 *     The rdpContext associated with the active RDP session.
 *
 * @param e
 *     Event-specific arguments, mainly the name of the channel, and a
 *     reference to the associated plugin loaded for that channel by FreeRDP.
 */
static void guac_rdp_rdpei_channel_disconnected(rdpContext* context,
        ChannelDisconnectedEventArgs* e) {

    guac_client* client = ((rdp_freerdp_context*) context)->client;
    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
    guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei;

    /* Ignore disconnection event if it's not for the RDPEI channel */
    if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0)
        return;

    /* Channel is no longer connected */
    guac_rdpei->rdpei = NULL;

    guac_client_log(client, GUAC_LOG_DEBUG, "RDPDI channel disconnected.");

}

void guac_rdp_rdpei_load_plugin(rdpContext* context) {

    /* Subscribe to and handle channel connected events */
    PubSub_SubscribeChannelConnected(context->pubSub,
        (pChannelConnectedEventHandler) guac_rdp_rdpei_channel_connected);

    /* Subscribe to and handle channel disconnected events */
    PubSub_SubscribeChannelDisconnected(context->pubSub,
            (pChannelDisconnectedEventHandler) guac_rdp_rdpei_channel_disconnected);

    /* Add "rdpei" channel */
    guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpei", NULL);

}

int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y,
        double force) {

    guac_client* client = rdpei->client;
    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;

    int contact_id; /* Ignored */

    /* Track touches only if channel is connected */
    RdpeiClientContext* context = rdpei->rdpei;
    if (context == NULL)
        return 1;

    /* Locate active touch having provided ID */
    guac_rdp_rdpei_touch* touch = NULL;
    for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
        if (rdpei->touch[i].active && rdpei->touch[i].id == id) {
            touch = &rdpei->touch[i];
            break;
        }
    }

    /* If no such touch exists, add it */
    if (touch == NULL) {
        for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) {
            if (!rdpei->touch[i].active) {
                touch = &rdpei->touch[i];
                touch->id = id;
                break;
            }
        }
    }

    /* If the touch couldn't be added, we're already at maximum touch capacity.
     * Drop the event. */
    if (touch == NULL)
        return 1;

    /* Signal the end of an established touch if touch force has become zero
     * (this should be a safe comparison, as zero has an exact representation
     * in floating point, and the client side will use an exact value to
     * represent the absence of a touch) */
    if (force == 0.0) {

        /* Ignore release of touches that we aren't tracking */
        if (!touch->active)
            return 1;

        pthread_mutex_lock(&(rdp_client->message_lock));
        context->TouchEnd(context, id, x, y, &contact_id);
        pthread_mutex_unlock(&(rdp_client->message_lock));

        touch->active = 0;

    }

    /* Signal the start of a touch if this is the first we've seen it */
    else if (!touch->active) {

        pthread_mutex_lock(&(rdp_client->message_lock));
        context->TouchBegin(context, id, x, y, &contact_id);
        pthread_mutex_unlock(&(rdp_client->message_lock));

        touch->active = 1;

    }

    /* Established touches need only be updated */
    else {
        pthread_mutex_lock(&(rdp_client->message_lock));
        context->TouchUpdate(context, id, x, y, &contact_id);
        pthread_mutex_unlock(&(rdp_client->message_lock));
    }

    return 0;

}