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

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

#include <stdlib.h>

guac_terminal_scrollbar* guac_terminal_scrollbar_alloc(guac_client* client,
        const guac_layer* parent, int parent_width, int parent_height, int visible_area) {

    /* Allocate scrollbar */
    guac_terminal_scrollbar* scrollbar =
        malloc(sizeof(guac_terminal_scrollbar));

    /* Associate client */
    scrollbar->client = client;

    /* Init default min/max and value */
    scrollbar->min   = 0;
    scrollbar->max   = 0;
    scrollbar->value = 0;

    /* Init parent data */
    scrollbar->parent        = parent;
    scrollbar->parent_width  = 0;
    scrollbar->parent_height = 0;
    scrollbar->visible_area  = 0;

    /* Init handle render state */
    scrollbar->render_state.handle_x      = 0;
    scrollbar->render_state.handle_y      = 0;
    scrollbar->render_state.handle_width  = 0;
    scrollbar->render_state.handle_height = 0;

    /* Init container render state */
    scrollbar->render_state.container_x      = 0;
    scrollbar->render_state.container_y      = 0;
    scrollbar->render_state.container_width  = 0;
    scrollbar->render_state.container_height = 0;

    /* Allocate and init layers */
    scrollbar->container = guac_client_alloc_layer(client);
    scrollbar->handle    = guac_client_alloc_layer(client);

    /* Init mouse event state tracking */
    scrollbar->dragging_handle = 0;

    /* Reposition and resize to fit parent */
    guac_terminal_scrollbar_parent_resized(scrollbar,
            parent_width, parent_height, visible_area);

    return scrollbar;

}

void guac_terminal_scrollbar_free(guac_terminal_scrollbar* scrollbar) {

    /* Free layers */
    guac_client_free_layer(scrollbar->client, scrollbar->handle);
    guac_client_free_layer(scrollbar->client, scrollbar->container);

    /* Free scrollbar */
    free(scrollbar);

}

/**
 * Moves the main scrollbar layer to the position indicated within the given
 * scrollbar render state, sending any necessary Guacamole instructions over
 * the given socket.
 *
 * @param scrollbar
 *     The scrollbar to reposition.
 *
 * @param state
 *     The guac_terminal_scrollbar_render_state describing the new scrollbar
 *     position.
 *
 * @param socket
 *     The guac_socket over which any instructions necessary to perform the
 *     render operation should be sent.
 */
static void guac_terminal_scrollbar_move_container(
        guac_terminal_scrollbar* scrollbar,
        guac_terminal_scrollbar_render_state* state,
        guac_socket* socket) {

    /* Send scrollbar position */
    guac_protocol_send_move(socket,
            scrollbar->container, scrollbar->parent,
            state->container_x,
            state->container_y,
            0);

}

/**
 * Resizes and redraws the main scrollbar layer according to the given
 * scrollbar render state, sending any necessary Guacamole instructions over
 * the given socket.
 *
 * @param scrollbar
 *     The scrollbar to resize and redraw.
 *
 * @param state
 *     The guac_terminal_scrollbar_render_state describing the new scrollbar
 *     size and appearance.
 *
 * @param socket
 *     The guac_socket over which any instructions necessary to perform the
 *     render operation should be sent.
 */
static void guac_terminal_scrollbar_draw_container(
        guac_terminal_scrollbar* scrollbar,
        guac_terminal_scrollbar_render_state* state,
        guac_socket* socket) {

    /* Set container size */
    guac_protocol_send_size(socket, scrollbar->container,
            state->container_width,
            state->container_height);

    /* Fill container with solid color */
    guac_protocol_send_rect(socket, scrollbar->container, 0, 0,
            state->container_width,
            state->container_height);

    guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->container,
            0x80, 0x80, 0x80, 0x40);

}

/**
 * Moves the handle layer of the scrollbar to the position indicated within the
 * given scrollbar render state, sending any necessary Guacamole instructions
 * over the given socket. The handle is the portion of the scrollbar that
 * indicates the current scroll value and which the user can click and drag to
 * change the value.
 *
 * @param scrollbar
 *     The scrollbar associated with the handle being repositioned.
 *
 * @param state
 *     The guac_terminal_scrollbar_render_state describing the new scrollbar
 *     handle position.
 *
 * @param socket
 *     The guac_socket over which any instructions necessary to perform the
 *     render operation should be sent.
 */
static void guac_terminal_scrollbar_move_handle(
        guac_terminal_scrollbar* scrollbar,
        guac_terminal_scrollbar_render_state* state,
        guac_socket* socket) {

    /* Send handle position */
    guac_protocol_send_move(socket,
            scrollbar->handle, scrollbar->container,
            state->handle_x,
            state->handle_y,
            0);

}

/**
 * Resizes and redraws the handle layer of the scrollbar according to the given
 * scrollbar render state, sending any necessary Guacamole instructions over
 * the given socket. The handle is the portion of the scrollbar that indicates
 * the current scroll value and which the user can click and drag to change the
 * value.
 *
 * @param scrollbar
 *     The scrollbar associated with the handle being resized and redrawn.
 *
 * @param state
 *     The guac_terminal_scrollbar_render_state describing the new scrollbar
 *     handle size and appearance.
 *
 * @param socket
 *     The guac_socket over which any instructions necessary to perform the
 *     render operation should be sent.
 */
static void guac_terminal_scrollbar_draw_handle(
        guac_terminal_scrollbar* scrollbar,
        guac_terminal_scrollbar_render_state* state,
        guac_socket* socket) {

    /* Set handle size */
    guac_protocol_send_size(socket, scrollbar->handle,
            state->handle_width,
            state->handle_height);

    /* Fill handle with solid color */
    guac_protocol_send_rect(socket, scrollbar->handle, 0, 0,
            state->handle_width,
            state->handle_height);

    guac_protocol_send_cfill(socket, GUAC_COMP_SRC, scrollbar->handle,
            0xA0, 0xA0, 0xA0, 0x8F);

}

/**
 * Calculates the state of the scroll bar, given its minimum, maximum, current
 * values, and the state of any dragging operation. The resulting render state
 * will not be reflected graphically unless the scrollbar is flushed, and any
 * resulting value will not be assigned to the scrollbar unless explicitly set
 * with guac_terminal_scrollbar_set_value().
 *
 * @param scrollbar
 *     The scrollbar whose state should be calculated.
 *
 * @param render_state
 *     A pointer to an existing guac_terminal_scrollbar_render_state that will
 *     be populated with the calculated result.
 *
 * @param value
 *     A pointer to an existing int that will be populated with the updated
 *     scrollbar value.
 */
static void calculate_state(guac_terminal_scrollbar* scrollbar,
        guac_terminal_scrollbar_render_state* render_state,
        int* value) {

    /* Use unchanged current value by default */
    *value = scrollbar->value;

    /* Calculate container dimensions */
    render_state->container_width  = GUAC_TERMINAL_SCROLLBAR_WIDTH;
    render_state->container_height = scrollbar->parent_height;

    /* Calculate container position */
    render_state->container_x = scrollbar->parent_width
                              - render_state->container_width;

    render_state->container_y = 0;

    /* Calculate handle dimensions */
    render_state->handle_width  = render_state->container_width
                                - GUAC_TERMINAL_SCROLLBAR_PADDING*2;

    /* Handle can be no bigger than the scrollbar itself */
    int max_handle_height = render_state->container_height
                          - GUAC_TERMINAL_SCROLLBAR_PADDING*2;

    /* Calculate legal delta between scroll values */
    int scroll_delta;
    if (scrollbar->max > scrollbar->min)
        scroll_delta = scrollbar->max - scrollbar->min;
    else
        scroll_delta = 0;

    /* Scale handle relative to visible area vs. scrolling region size */
    int proportional_height = max_handle_height
                            * scrollbar->visible_area
                            / (scroll_delta + scrollbar->visible_area);

    /* Ensure handle is no smaller than minimum height */
    if (proportional_height > GUAC_TERMINAL_SCROLLBAR_MIN_HEIGHT)
        render_state->handle_height = proportional_height;
    else
        render_state->handle_height = GUAC_TERMINAL_SCROLLBAR_MIN_HEIGHT;

    /* Ensure handle is no larger than maximum height */
    if (render_state->handle_height > max_handle_height)
        render_state->handle_height = max_handle_height;

    /* Calculate handle X position */
    render_state->handle_x = GUAC_TERMINAL_SCROLLBAR_PADDING;

    /* Calculate handle Y range */
    int min_handle_y = GUAC_TERMINAL_SCROLLBAR_PADDING;
    int max_handle_y = min_handle_y + max_handle_height
                     - render_state->handle_height;

    /* Position handle relative to mouse if being dragged */
    if (scrollbar->dragging_handle) {

        int dragged_handle_y = scrollbar->drag_current_y
                             - scrollbar->drag_offset_y;

        /* Keep handle within bounds */
        if (dragged_handle_y < min_handle_y)
            dragged_handle_y = min_handle_y;
        else if (dragged_handle_y > max_handle_y)
            dragged_handle_y = max_handle_y;

        render_state->handle_y = dragged_handle_y;

        /* Calculate scrollbar value */
        if (max_handle_y > min_handle_y) {
            *value = scrollbar->min
                   + (dragged_handle_y - min_handle_y)
                      * scroll_delta
                      / (max_handle_y - min_handle_y);
        }

    }

    /* Handle Y position is relative to current scroll value */
    else if (scroll_delta > 0)
        render_state->handle_y = min_handle_y
                               + (max_handle_y - min_handle_y)
                                  * (scrollbar->value - scrollbar->min)
                                  / scroll_delta;

    /* ... unless there is only one possible scroll value */
    else
        render_state->handle_y = GUAC_TERMINAL_SCROLLBAR_PADDING;

}

void guac_terminal_scrollbar_dup(guac_terminal_scrollbar* scrollbar,
        guac_user* user, guac_socket* socket) {

    /* Get old state */
    guac_terminal_scrollbar_render_state* state = &scrollbar->render_state;

    /* Send scrollbar container */
    guac_terminal_scrollbar_draw_container(scrollbar, state, socket);
    guac_terminal_scrollbar_move_container(scrollbar, state, socket);

    /* Send handle */
    guac_terminal_scrollbar_draw_handle(scrollbar, state, socket);
    guac_terminal_scrollbar_move_handle(scrollbar, state, socket);

}

void guac_terminal_scrollbar_flush(guac_terminal_scrollbar* scrollbar) {

    guac_socket* socket = scrollbar->client->socket;

    /* Get old state */
    int old_value = scrollbar->value;
    guac_terminal_scrollbar_render_state* old_state = &scrollbar->render_state;

    /* Calculate new state */
    int new_value;
    guac_terminal_scrollbar_render_state new_state;
    calculate_state(scrollbar, &new_state, &new_value);

    /* Notify of scroll if value is changing */
    if (new_value != old_value && scrollbar->scroll_handler)
        scrollbar->scroll_handler(scrollbar, new_value);

    /* Reposition container if moved */
    if (old_state->container_x != new_state.container_x
     || old_state->container_y != new_state.container_y) {
        guac_terminal_scrollbar_move_container(scrollbar, &new_state, socket);
    }

    /* Resize and redraw container if size changed */
    if (old_state->container_width  != new_state.container_width
     || old_state->container_height != new_state.container_height) {
        guac_terminal_scrollbar_draw_container(scrollbar, &new_state, socket);
    }

    /* Reposition handle if moved */
    if (old_state->handle_x != new_state.handle_x
     || old_state->handle_y != new_state.handle_y) {
        guac_terminal_scrollbar_move_handle(scrollbar, &new_state, socket);
    }

    /* Resize and redraw handle if size changed */
    if (old_state->handle_width  != new_state.handle_width
     || old_state->handle_height != new_state.handle_height) {
        guac_terminal_scrollbar_draw_handle(scrollbar, &new_state, socket);
    }

    /* Store current render state */
    scrollbar->render_state = new_state;

}

void guac_terminal_scrollbar_set_bounds(guac_terminal_scrollbar* scrollbar,
        int min, int max) {

    /* Fit value within bounds */
    if (scrollbar->value > max)
        scrollbar->value = max;
    else if (scrollbar->value < min)
        scrollbar->value = min;

    /* Update bounds */
    scrollbar->min = min;
    scrollbar->max = max;

}

void guac_terminal_scrollbar_set_value(guac_terminal_scrollbar* scrollbar,
        int value) {

    /* Fit value within bounds */
    if (value > scrollbar->max)
        value = scrollbar->max;
    else if (value < scrollbar->min)
        value = scrollbar->min;

    /* Update value */
    scrollbar->value = value;

}

void guac_terminal_scrollbar_parent_resized(guac_terminal_scrollbar* scrollbar,
        int parent_width, int parent_height, int visible_area) {

    /* Assign new dimensions */
    scrollbar->parent_width  = parent_width;
    scrollbar->parent_height = parent_height;
    scrollbar->visible_area  = visible_area;

}

int guac_terminal_scrollbar_handle_mouse(guac_terminal_scrollbar* scrollbar,
        int x, int y, int mask) {

    /* Get container rectangle bounds */
    int parent_left   = scrollbar->render_state.container_x;
    int parent_top    = scrollbar->render_state.container_y;
    int parent_right  = parent_left + scrollbar->render_state.container_width;
    int parent_bottom = parent_top  + scrollbar->render_state.container_height;

    /* Calculate handle rectangle bounds */
    int handle_left   = parent_left + scrollbar->render_state.handle_x;
    int handle_top    = parent_top  + scrollbar->render_state.handle_y;
    int handle_right  = handle_left + scrollbar->render_state.handle_width;
    int handle_bottom = handle_top  + scrollbar->render_state.handle_height;

    /* Handle click on handle */
    if (scrollbar->dragging_handle) {

        /* Update drag while mouse button is held */
        if (mask & GUAC_CLIENT_MOUSE_LEFT)
            scrollbar->drag_current_y = y;

        /* Stop drag if mouse button is released */
        else
            scrollbar->dragging_handle = 0;

        /* Mouse event was handled by scrollbar */
        return 1;

    }
    else if (mask == GUAC_CLIENT_MOUSE_LEFT
            && x >= handle_left && x < handle_right
            && y >= handle_top  && y < handle_bottom) {

        /* Start drag */
        scrollbar->dragging_handle = 1;
        scrollbar->drag_offset_y = y - handle_top;
        scrollbar->drag_current_y = y;

        /* Mouse event was handled by scrollbar */
        return 1;

    }

    /* Eat any events that occur within the scrollbar */
    return x >= parent_left && x < parent_right
        && y >= parent_top  && y < parent_bottom;

}

