/*
 * 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 "encode-webp.h"
#include "guacamole/error.h"
#include "guacamole/protocol.h"
#include "guacamole/stream.h"
#include "palette.h"

#include <cairo/cairo.h>
#include <webp/encode.h>

#include <assert.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

/**
 * Structure which describes the current state of the WebP image writer.
 */
typedef struct guac_webp_stream_writer {

    /**
     * The socket over which all WebP blobs will be written.
     */
    guac_socket* socket;

    /**
     * The Guacamole stream to associate with each WebP blob.
     */
    guac_stream* stream;

    /**
     * Buffer of pending WebP data.
     */
    char buffer[GUAC_PROTOCOL_BLOB_MAX_LENGTH];

    /**
     * The number of bytes currently stored in the buffer.
     */
    int buffer_size;

} guac_webp_stream_writer;

/**
 * Writes the contents of the WebP stream writer as a blob to its associated
 * socket.
 *
 * @param writer
 *     The writer structure to flush.
 */
static void guac_webp_flush_data(guac_webp_stream_writer* writer) {

    /* Send blob */
    guac_protocol_send_blob(writer->socket, writer->stream,
            writer->buffer, writer->buffer_size);

    /* Clear buffer */
    writer->buffer_size = 0;

}

/**
 * Configures the given stream writer object to use the given Guacamole stream
 * object for WebP output.
 *
 * @param writer
 *     The Guacamole WebP stream writer structure to configure.
 *
 * @param socket
 *     The Guacamole socket to use when sending blob instructions.
 *
 * @param stream
 *     The stream over which WebP-encoded blobs of image data should be sent.
 */
static void guac_webp_stream_writer_init(guac_webp_stream_writer* writer,
        guac_socket* socket, guac_stream* stream) {

    writer->buffer_size = 0;

    /* Store Guacamole-specific objects */
    writer->socket = socket;
    writer->stream = stream;

}

/**
 * WebP output function which appends the given WebP data to the internal
 * buffer of the Guacamole stream writer structure, automatically flushing the
 * writer as necessary.
 *
 * @param data
 *     The segment of data to write.
 *
 * @param data_size
 *     The size of segment of data to write.
 *
 * @param picture
 *     The WebP picture associated with this write operation. Provides access to
 *     picture->custom_ptr which contains the Guacamole stream writer structure.
 *
 * @return
 *     Non-zero if writing was successful, zero on failure.
 */
static int guac_webp_stream_write(const uint8_t* data, size_t data_size,
        const WebPPicture* picture) {

    guac_webp_stream_writer* const writer =
        (guac_webp_stream_writer*) picture->custom_ptr;
    assert(writer != NULL);

    const unsigned char* current = data;
    int length = data_size;

    /* Append all data given */
    while (length > 0) {

        /* Calculate space remaining */
        int remaining = sizeof(writer->buffer) - writer->buffer_size;

        /* If no space remains, flush buffer to make room */
        if (remaining == 0) {
            guac_webp_flush_data(writer);
            remaining = sizeof(writer->buffer);
        }

        /* Calculate size of next block of data to append */
        int block_size = remaining;
        if (block_size > length)
            block_size = length;

        /* Append block */
        memcpy(writer->buffer + writer->buffer_size,
               current, block_size);

        /* Next block */
        current += block_size;
        writer->buffer_size += block_size;
        length -= block_size;

    }

    return 1;
}

int guac_webp_write(guac_socket* socket, guac_stream* stream,
        cairo_surface_t* surface, int quality, int lossless) {

    guac_webp_stream_writer writer;
    WebPPicture picture;
    uint32_t* argb_output;

    int x, y;

    int width = cairo_image_surface_get_width(surface);
    int height = cairo_image_surface_get_height(surface);
    int stride = cairo_image_surface_get_stride(surface);
    cairo_format_t format = cairo_image_surface_get_format(surface);
    unsigned char* data = cairo_image_surface_get_data(surface);

    if (format != CAIRO_FORMAT_RGB24 && format != CAIRO_FORMAT_ARGB32) {
        guac_error = GUAC_STATUS_INTERNAL_ERROR;
        guac_error_message = "Invalid Cairo image format. Unable to create WebP.";
        return -1;
    }

    /* Flush pending operations to surface */
    cairo_surface_flush(surface);

    /* Configure WebP compression bits */
    WebPConfig config;
    if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality))
        return -1;

    /* Add additional tuning */
    config.lossless = lossless;
    config.quality = quality;
    config.thread_level = 1; /* Multi threaded */
    config.method = 2; /* Compression method (0=fast/larger, 6=slow/smaller) */

    /* Validate configuration */
    WebPValidateConfig(&config);

    /* Set up WebP picture */
    WebPPictureInit(&picture);
    picture.use_argb = 1;
    picture.width = width;
    picture.height = height;

    /* Allocate and init writer */
    WebPPictureAlloc(&picture);
    picture.writer = guac_webp_stream_write;
    picture.custom_ptr = &writer;
    guac_webp_stream_writer_init(&writer, socket, stream);

    /* Copy image data into WebP picture */
    argb_output = picture.argb;
    for (y = 0; y < height; y++) {

        /* Get pixels at start of each row */
        uint32_t* src = (uint32_t*) data;
        uint32_t* dst = argb_output;

        /* For each pixel in row */
        for (x = 0; x < width; x++) {

            /* Pull pixel data, removing alpha channel if necessary */
            uint32_t src_pixel = *src;
            if (format != CAIRO_FORMAT_ARGB32)
                src_pixel |= 0xFF000000;

            /* Store converted pixel data */
            *dst = src_pixel;

            /* Next pixel */
            src++;
            dst++;

        }

        /* Next row */
        data += stride;
        argb_output += picture.argb_stride;

    }

    /* Encode image */
    WebPEncode(&config, &picture);

    /* Free picture */
    WebPPictureFree(&picture);

    /* Ensure all data is written */
    guac_webp_flush_data(&writer);

    return 0;

}

