/*
 * 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 "common/cursor.h"
#include "common/display.h"
#include "common/surface.h"

#include <guacamole/client.h>
#include <guacamole/socket.h>

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

/**
 * Synchronizes all surfaces within the given linked list to the given socket.
 * If the provided pointer to the linked list is NULL, this function has no
 * effect.
 *
 * @param layers
 *     The head element of the linked list of layers to synchronize, which may
 *     be NULL if the list is currently empty.
 *
 * @param user
 *     The user receiving the layers.
 *
 * @param socket
 *     The socket over which each layer should be sent.
 */
static void guac_common_display_dup_layers(guac_common_display_layer* layers,
        guac_user* user, guac_socket* socket) {

    guac_common_display_layer* current = layers;

    /* Synchronize all surfaces in given list */
    while (current != NULL) {
        guac_common_surface_dup(current->surface, user, socket);
        current = current->next;
    }

}

/**
 * Frees all layers and associated surfaces within the given list, as well as
 * their corresponding list elements. If the provided pointer to the linked
 * list is NULL, this function has no effect.
 *
 * @param layers
 *     The head element of the linked list of layers to free, which may be NULL
 *     if the list is currently empty.
 *
 * @param client
 *     The client owning the layers wrapped by each of the layers in the list.
 */
static void guac_common_display_free_layers(guac_common_display_layer* layers,
        guac_client* client) {

    guac_common_display_layer* current = layers;

    /* Free each surface in given list */
    while (current != NULL) {

        guac_common_display_layer* next = current->next;
        guac_layer* layer = current->layer;

        /* Free surface */
        guac_common_surface_free(current->surface);

        /* Destroy layer within remotely-connected client */
        guac_protocol_send_dispose(client->socket, layer);

        /* Free layer or buffer depending on index */
        if (layer->index < 0)
            guac_client_free_buffer(client, layer);
        else if (layer->index > 0)
            guac_client_free_layer(client, layer);

        /* Free current element and advance to next */
        free(current);
        current = next;

    }

}

/**
 * Allocates a display and a cursor which are used to represent the remote
 * display and cursor.
 *
 * @param client
 *     The client owning the cursor.
 *
 * @param width
 *     The desired width of the display.
 *
 * @param height
 *     The desired height of the display.
 *
 * @return
 *     The newly-allocated display or NULL if display cannot be allocated.
 */
guac_common_display* guac_common_display_alloc(guac_client* client,
        int width, int height) {

    /* Allocate display */
    guac_common_display* display = malloc(sizeof(guac_common_display));
    if (display == NULL)
        return NULL;

    /* Allocate shared cursor */
    display->cursor = guac_common_cursor_alloc(client);
    if (display->cursor == NULL) {
        free(display);
        return NULL;
    }

    pthread_mutex_init(&display->_lock, NULL);

    /* Associate display with given client */
    display->client = client;

    display->default_surface = guac_common_surface_alloc(client,
            client->socket, GUAC_DEFAULT_LAYER, width, height);

    /* No initial layers or buffers */
    display->layers = NULL;
    display->buffers = NULL;

    return display;

}

void guac_common_display_free(guac_common_display* display) {

    /* Free shared cursor */
    guac_common_cursor_free(display->cursor);

    /* Free default surface */
    guac_common_surface_free(display->default_surface);

    /* Free all layers and buffers */
    guac_common_display_free_layers(display->buffers, display->client);
    guac_common_display_free_layers(display->layers, display->client);

    pthread_mutex_destroy(&display->_lock);
    free(display);

}

void guac_common_display_dup(guac_common_display* display, guac_user* user,
        guac_socket* socket) {

    pthread_mutex_lock(&display->_lock);

    /* Sunchronize shared cursor */
    guac_common_cursor_dup(display->cursor, user, socket);

    /* Synchronize default surface */
    guac_common_surface_dup(display->default_surface, user, socket);

    /* Synchronize all layers and buffers */
    guac_common_display_dup_layers(display->layers, user, socket);
    guac_common_display_dup_layers(display->buffers, user, socket);

    pthread_mutex_unlock(&display->_lock);

}

void guac_common_display_set_lossless(guac_common_display* display,
        int lossless) {

    pthread_mutex_lock(&display->_lock);

    /* Update lossless setting to be applied to all newly-allocated
     * layers/buffers */
    display->lossless = lossless;

    /* Update losslessness of all allocated layers/buffers */
    guac_common_display_layer* current = display->layers;
    while (current != NULL) {
        guac_common_surface_set_lossless(current->surface, lossless);
        current = current->next;
    }

    /* Update losslessness of default display layer (not included within layers
     * list) */
    guac_common_surface_set_lossless(display->default_surface, lossless);

    pthread_mutex_unlock(&display->_lock);

}

void guac_common_display_flush(guac_common_display* display) {

    pthread_mutex_lock(&display->_lock);

    guac_common_display_layer* current = display->layers;

    /* Flush all surfaces */
    while (current != NULL) {
        guac_common_surface_flush(current->surface);
        current = current->next;
    }

    guac_common_surface_flush(display->default_surface);

    pthread_mutex_unlock(&display->_lock);

}

/**
 * Allocates and inserts a new element into the given linked list of display
 * layers, associating it with the given layer and surface.
 *
 * @param head
 *     A pointer to the head pointer of the list of layers. The head pointer
 *     will be updated by this function to point to the newly-allocated
 *     display layer.
 *
 * @param layer
 *     The Guacamole layer to associated with the new display layer.
 *
 * @param surface
 *     The surface associated with the given Guacamole layer and which should
 *     be associated with the new display layer.
 *
 * @return
 *     The newly-allocated display layer, which has been associated with the
 *     provided layer and surface.
 */
static guac_common_display_layer* guac_common_display_add_layer(
        guac_common_display_layer** head, guac_layer* layer,
        guac_common_surface* surface) {

    guac_common_display_layer* old_head = *head;

    guac_common_display_layer* display_layer =
        malloc(sizeof(guac_common_display_layer));

    /* Init layer/surface pair */
    display_layer->layer = layer;
    display_layer->surface = surface;

    /* Insert list element as the new head */
    display_layer->prev = NULL;
    display_layer->next = old_head;
    *head = display_layer;

    /* Update old head to point to new element, if it existed */
    if (old_head != NULL)
        old_head->prev = display_layer;

    return display_layer;

}

/**
 * Removes the given display layer from the linked list whose head pointer is
 * provided.
 *
 * @param head
 *     A pointer to the head pointer of the list of layers. The head pointer
 *     will be updated by this function if necessary, and will be set to NULL
 *     if the display layer being removed is the only layer in the list.
 *
 * @param display_layer
 *     The display layer to remove from the given list.
 */
static void guac_common_display_remove_layer(guac_common_display_layer** head,
        guac_common_display_layer* display_layer) {

    /* Update previous element, if it exists */
    if (display_layer->prev != NULL)
        display_layer->prev->next = display_layer->next;

    /* If there is no previous element, update the list head */
    else
        *head = display_layer->next;

    /* Update next element, if it exists */
    if (display_layer->next != NULL)
        display_layer->next->prev = display_layer->prev;

}

guac_common_display_layer* guac_common_display_alloc_layer(
        guac_common_display* display, int width, int height) {

    pthread_mutex_lock(&display->_lock);

    /* Allocate Guacamole layer */
    guac_layer* layer = guac_client_alloc_layer(display->client);

    /* Allocate corresponding surface */
    guac_common_surface* surface = guac_common_surface_alloc(display->client,
            display->client->socket, layer, width, height);

    /* Apply current display losslessness */
    guac_common_surface_set_lossless(surface, display->lossless);

    /* Add layer and surface to list */
    guac_common_display_layer* display_layer =
        guac_common_display_add_layer(&display->layers, layer, surface);

    pthread_mutex_unlock(&display->_lock);
    return display_layer;

}

guac_common_display_layer* guac_common_display_alloc_buffer(
        guac_common_display* display, int width, int height) {

    pthread_mutex_lock(&display->_lock);

    /* Allocate Guacamole buffer */
    guac_layer* buffer = guac_client_alloc_buffer(display->client);

    /* Allocate corresponding surface */
    guac_common_surface* surface = guac_common_surface_alloc(display->client,
            display->client->socket, buffer, width, height);

    /* Apply current display losslessness */
    guac_common_surface_set_lossless(surface, display->lossless);

    /* Add buffer and surface to list */
    guac_common_display_layer* display_layer =
        guac_common_display_add_layer(&display->buffers, buffer, surface);

    pthread_mutex_unlock(&display->_lock);
    return display_layer;

}

void guac_common_display_free_layer(guac_common_display* display,
        guac_common_display_layer* display_layer) {

    pthread_mutex_lock(&display->_lock);

    /* Remove list element from list */
    guac_common_display_remove_layer(&display->layers, display_layer);

    /* Free associated layer and surface */
    guac_common_surface_free(display_layer->surface);
    guac_client_free_layer(display->client, display_layer->layer);

    /* Free list element */
    free(display_layer);

    pthread_mutex_unlock(&display->_lock);

}

void guac_common_display_free_buffer(guac_common_display* display,
        guac_common_display_layer* display_buffer) {

    pthread_mutex_lock(&display->_lock);

    /* Remove list element from list */
    guac_common_display_remove_layer(&display->buffers, display_buffer);

    /* Free associated layer and surface */
    guac_common_surface_free(display_buffer->surface);
    guac_client_free_buffer(display->client, display_buffer->layer);

    /* Free list element */
    free(display_buffer);

    pthread_mutex_unlock(&display->_lock);

}

