/*************************************************************************
*
*   plugin_test.c
*
*   Simple test harness for the pluggable event loop (mainloop)
*   in the Linux Receiver.  This program loads a plugin, requests that it
*   create a session window and runs the event loop, reporting input
*   events on the window.  Optionally, a timer-driven series of pipes
*   pass data in the backround to test the timer and I/O readiness
*   functions of the plugin.
*
*   $Id$
*
*   Copyright 2010-2014 Citrix Systems, Inc.  All Rights Reserved.
*
*************************************************************************/

#include "citrix.h"

#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <sys/time.h>

#include "mainloop.h"
#include "ctxntmr.h"
#include "simplified.h"
#include "UI_if.h"

/* Error and reporting output. */

#ifdef OWN_REPORTING
#include <stdarg.h>

extern void Log_error(char *fmt, ...);
extern void Log_report(char *fmt, ...);
#define ERROR(...) Log_error(__VA_ARGS__)
#define REPORT(...) Log_report(__VA_ARGS__)
#else
#define ERROR(...) fprintf(stderr, __VA_ARGS__)
#define REPORT(...) printf(__VA_ARGS__)
#endif /* not OWN_REPORTING*/

#define PIXEL_SIZE 4      /* Bytes. */

#define PLUGIN_SUPPORTS_X11_SEAMLESS \
    (PLUGIN_SUPPORTS_X11 | PLUGIN_MULTI_WINDOWED) 

struct mainloop_plugin Plug;
static struct timeval  start_time;

/* Logging control. */

#define LOG_TIMER       1
#define LOG_IO          2
#define LOG_MOUSE_MOVE  4
#define LOG_THREAD      8

static unsigned int log_mask =  LOG_TIMER +  LOG_IO;

/* Structure for a timer/pipe stage. */

enum stage_type {Type_timed, Type_IO};

#define BUFFER_SIZE 1000

struct stage {
    int                 in, out;                           /* Open pipes. */
    int                 type;                              /* See below. */
    struct pal_event    read_event, write_event;           /* I/O calls. */
    PTMR                timer;                             /* Timer calls. */
    int                 alarms, read_readys, write_readys; /* Call counts. */
    int                 index;                             /* Identifier. */

    /* Writes are expected to block, so record residual amount. */

    int                 residual;
    char               *data;
    char                buffer[BUFFER_SIZE];
};


/* Push data along the pipeline. */

enum push_result {Push_OK, Push_Hungry, Push_Blocked};

char *push_strings[] = {        /* Printable push_result values. */
    "OK", "hungry", "write blocked"
};

static enum push_result push_data(struct stage *this)
{
    int          written;

    for (;;) {
        if (this->residual == 0) {
            this->data = this->buffer;
            if (this->in < 0) {
                if (log_mask & LOG_IO)
                    REPORT("push_data(%d) New data.\n",  this->index);
                this->residual = BUFFER_SIZE;   /* Dummy data in buffer. */
            } else {
                this->residual = read(this->in, this->buffer, BUFFER_SIZE);
                if (this->residual < 0) {
                    if (errno == EAGAIN) {
                        this->residual = 0;
                        return Push_Hungry;
                    } else {
                        perror("push_data - reading");
                        exit(10);
                    }
                } else if (this->residual == 0) {
                    ERROR("push_data(%d) EOF.\n", this->index);
                    exit(11);
                } else {
                    if (log_mask & LOG_IO) {
                        REPORT("push_data(%d) read %d bytes.\n",
                               this->index, this->residual);
                    }
                }
            }
        }

        if (this->out < 0) {
            /* End of chain - nothing to more do. */

            this->residual = 0;
            return Push_OK;
        }

        /* Not the final stage - pass it on. */

        written = write(this->out, this->data, this->residual);
        if (written < 0) {
            if (errno == EAGAIN) {
                return Push_Blocked;
            } else {
                perror("push_data - writing");
                exit(10);
            }
        }
        if (log_mask & LOG_IO)
            REPORT("push_data(%d) wrote %d bytes.\n", this->index, written);
        this->residual -= written;
        this->data += written;
        if (this->in < 0)       /* New data generated. */
            return Push_OK;
    }
}

/* Timer stages push the data along on a timer callback. */

static void timer_cb(void *it, void *caller)
{
    struct stage       *this = (struct stage *)it;
    enum push_result    result;

    result = push_data(this);
    if (log_mask & LOG_TIMER) {
        REPORT("Timer callback stage %d: %s\n",
               this->index, push_strings[(int)result]);
    }
}

/* I/O stages push the data along on a read or write-ready callback. */

static void read_ready(void *it, void *caller)
{
    struct stage       *this = (struct stage *)it;
    enum push_result    result;

    if (this->residual != 0) {
        ERROR("read_ready(%d) found %d bytes of residual data!\n",
              this->index, this->residual);
        exit(20);
    }

    result = push_data(this);
    if (log_mask & LOG_IO) {
        REPORT("Read ready stage %d: %s\n",
               this->index, push_strings[(int)result]);
    }
    if (result == Push_Blocked) {
        /* Blocked - stop reading and request write-ready callback. */

        Plug.select_fd(NULL, this->in, FALSE);
        Plug.select_fd(&this->write_event, this->out, TRUE);
    }
}
        
static void write_ready(void *it, void *caller)
{
    struct stage       *this = (struct stage *)it;
    enum push_result    result;

    if (this->residual <= 0) {
        ERROR("write_ready(%d) found %d bytes of residual data!\n",
                this->index, this->residual);
        exit(21);
    }

    result = push_data(this);
    if (log_mask & LOG_IO) {
        REPORT("Write ready stage %d: %s\n",
               this->index, push_strings[(int)result]);
    }
    if (result == Push_Hungry) {
        /* No data - wait for input readiness. */

        Plug.select_fd(&this->read_event, this->in, FALSE);
        Plug.select_fd(NULL, this->out, TRUE);
    }
}

/* Internal Receiver utility function. */

UINT32 Os_GetTickCountInMs(void)
{
    struct timeval      tvp;
    unsigned long       retval;

    gettimeofday(&tvp, 0);
    tvp.tv_sec -= start_time.tv_sec;
    if ((tvp.tv_usec -= start_time.tv_usec) < 0) {
        tvp.tv_usec += 1000000;
        --tvp.tv_sec;
    }
    retval = tvp.tv_sec * 1000 + tvp.tv_usec / 1000;
    return (UINT32)retval;
}

/* Dummy mouse and keyboard input handlers. */

void Mouse_changed(int *x, int *y, unsigned int buttons)
{
    static char *squeeks[] = {"motion",
                              "button 1 down", "button 1 up",
                              "button 2 down", "button 2 up",
                              "button 3 down", "button 3 up",
                              "bad bit 0x80 set",
                              "button 4 down", "button 4 up",
                              "button 5 down", "button 5 up",
    };
    unsigned int real_buttons;
    int i;

    if (buttons == MOU_STATUS_MOVED && !(log_mask & LOG_MOUSE_MOVE))
        return;

    REPORT("Mouse at %d, %d buttons %#x:", *x, *y, buttons);
    real_buttons = buttons & 0xFFF;
    for (i = 0; real_buttons; ++i, real_buttons >>= 1) {
        if (real_buttons & 1)
            REPORT(" %s", squeeks[i]);
    }
    if (buttons & MOU_STATUS_XVWUP)
        REPORT(" scroll up");
    if (buttons & MOU_STATUS_XVWDOWN)
        REPORT(" scroll down");
    REPORT("\n");
}

unsigned int Map_button(unsigned int button, BOOLEAN down)
{
    unsigned int x;

    if (button > 5 || button == 0)
        return 0;
    if (button < 4)
        x = MOU_STATUS_B1DOWN << ((button - 1) >> 1);
    else
        x = MOU_STATUS_XB4DOWN << ((button - 4) >> 1);
    if (!down)
        x <<= 1;
    return x;
}

void Send_unicode(unsigned short uc, int flags)
{
    REPORT("Keyboard: %s %d (%#x) %s\n",
           (flags & WND_UNICODECHAR_SPECIAL) ? "special" : "unicode",
           uc, uc,
           (flags & WND_UNICODECHAR_KEYUP) ? "up" : "down");
}

int Send_scancodes(unsigned char *codes, unsigned int count)
{
    int            n;
    char           buf[(5 * count) + 1];

    for (n = 0; count; count--)
        n += sprintf(buf + n, "%#02x ", *codes++);
    REPORT("Scan codes: %s\n", buf);
    return 0;
}

int Send_keysym(UINT32 keysym, int flags)
{
    REPORT("Keysym: %#x %s\n",
           keysym,
           (flags & WND_UNICODECHAR_KEYUP) ? "up" : "down");
    return 0;
}

/* Stubs for other Receiver entry points. */

void Send_session_resize(int width, int height, BOOLEAN now) {
    REPORT("Call to Send_session_resize(width = %d, height = %d, now = %d\n",
           width, height, now);
}

BOOLEAN Do_work(void)
{
    REPORT("Call to Do_work()\n");
    return FALSE;
}

/* Stub error dialogue handler which may be called by a plugin. */

enum _button_type stub_dialogue_handler(enum _EIDialogType  type,
                                        char               *title,
                                        char               *body,
                                        const enum _button_type
                                                           *buttons,
                                        enum _button_type   default_response,
                                        char               *icon,
                                        struct dialogExtra *extra)
{
    static const char * const names[] = {"error", "info", "info text",
                                         "question", "reconnect"
                                        };
    static const char * const button_names[] = {"NULL", "OK", "Cancel",
                                                "Quit",  "Allow", "Deny",
                                                "Logoff", "Disconnect",
                                                "Read", "Write",
                                                "Restart", "Show Options"
                                               };
    static const char * const extra_names[] = {"boolean input",
                                               "integer input",
                                               "text input",
                                               "body text",
                                               "separator"};
    int i;

    ERROR("Dialogue type %s with icon %s and buttons: ", names[type], icon);
    for (i = 0; buttons[i] != B_NULL; ++i) {
        ERROR("%s%s", i > 0 ? ", " : "", button_names[buttons[i]]);
    }
    ERROR(".  Title: %s Body: %s\n", title, body);
    if (extra) {
        while (extra->tag != endTag) {
            ERROR("\tA %s item%s%s is present.\n",
                  extra_names[(int)extra->tag - 1],
                  extra->widgetLabel ? " labelled " : "",
                  extra->widgetLabel ?  extra->widgetLabel : "");
            ++extra;
        }
    }
    fflush(stderr);
    return default_response;
}

void Take_signal(void)
{
    REPORT("Take_signal() called\n");
}

#ifndef STATIC_PLUGIN
/* Load the plugin and return its initialiser. */

static void (*load_plugin(char *path))(struct mainloop_plugin *plug) 
{
    void                       *handle;
    void                      (*init_fn)(struct mainloop_plugin *plug);

    /* Load the plugin. */

    if (path == NULL)
        path = "./KVMEPlugin.so";
    errno = 0;
    handle = dlopen(path, RTLD_NOW);
    if (handle == NULL) {
        ERROR("Failed to load plugin library %s\n", dlerror());
        exit(1);
    }
    init_fn = (void (*)(struct mainloop_plugin *plug))  /* Horrid cast! */
                  dlsym(handle, "Plugin_init");
    if (init_fn == NULL) {
        ERROR("No Plugin_init() in plugin library: %s\n", dlerror());
        exit(1);
    }
    return init_fn;
}
#endif /* not STATIC_PLUGIN */

/* Run the initialiser. */

static void init_plugin(void (*init_fn)(struct mainloop_plugin *plug))
{
#ifdef X11_SUPPORT
    /* Initialize X11 variables. */
    Plug.disp = Disp.display;
#endif

    /* Start it. */

    init_fn(&Plug);
    REPORT("Plugin interface version %d.%d%s%s.\n", 
           Plug.major_version, Plug.minor_version,
           (Plug.plugin_flags & PLUGIN_SUPPORTS_X11) ?
               " has X11 support" : "",
           ((Plug.plugin_flags & PLUGIN_SUPPORTS_X11_SEAMLESS) ==
            PLUGIN_SUPPORTS_X11_SEAMLESS) ?
                 " and Seamless" : "");

    /* May it marry my daughter? */

    if (Plug.major_version !=  KVME_MAJOR_VERSION ||
        Plug.minor_version < KVME_MINOR_VERSION ||
#ifdef X11_SUPPORT
        !
#endif
         (Plug.plugin_flags & PLUGIN_SUPPORTS_X11)) {
        /* Unsuitable suitor. */

        ERROR("Unexpected plugin interface version or flags: "
        "%d.%d/%x mine %d.%d\n",
                  Plug.major_version,  Plug.minor_version, Plug.plugin_flags,
              KVME_MAJOR_VERSION, KVME_MINOR_VERSION);
        exit(2);
    }
}

/* Start the timer/pipe chain. */

static void start_chain(int stages, int interval)
{
    int                 pipefd[2];
    struct stage       *this;
    enum stage_type     type;
    int                 count;

    ++stages;   /* Fenceposts. */
    if (interval == 0)
        interval = 5;   /* Don't buzz. */

    this = (struct stage *)calloc(stages, sizeof (struct stage));
    pipefd[0] = -1;     /* No initial read pipe. */
    type = Type_timed;  /* First stage must be timed. */
    count = 0;

    while (stages--) {
        this->in = pipefd[0];
        if (stages) {
            /* There will be a further, downstream stage.
             * Make a non-blocking pipe.
             */

            pipe(pipefd);
            fcntl(pipefd[0], F_SETFL, (long)O_NONBLOCK);
            fcntl(pipefd[1], F_SETFL, (long)O_NONBLOCK);
            this->out = pipefd[1];
        } else {
            this->out = -1;
        }

        this->type = type;
        if (type == Type_timed) {
            Tmr_create(NULL, interval, this, timer_cb, &this->timer);
            Tmr_setEnabled(this->timer, TRUE);

            /* Increase the interval down the chain for cause write
             * blocking upstream.
             */

            interval += (interval + 1) / 2;
        } else if (Plug.select_fd) {
            /* I/O driven stage. */

            this->read_event.callback.pfnDeliver = read_ready;
            this->read_event.callback.pSubscriber = this;
            Plug.select_fd(&this->read_event, this->in, FALSE);

            /* Do not request the write-ready callback until needed. */

            this->write_event.callback.pfnDeliver = write_ready;
            this->write_event.callback.pSubscriber = this;
        }

        /* Alternate timer and I/O driven stages down the chain. */

        type = (type == Type_timed) ? Type_IO : Type_timed;
        this->index = count++;
        ++this;
    }
}

int
#ifdef NO_MAIN
Fake_main(int argc, char **argv)
#else
    main(int argc, char **argv)
#endif
{
    struct session_size         screen_info;
    struct special_buffer       allocator;
    char                       *video_buffer;
    int                         stages, interval;

    gettimeofday(&start_time, 0);       /* Initialise timing.  */

    /* Load the plugin. */

#ifdef STATIC_PLUGIN
    init_plugin(Plugin_init);
#else
    init_plugin(load_plugin(NULL));
#endif
    /* Get the number of pipe stages to run. */

    interval = 13; /* milliSeconds interval for pipe chain. */
#ifdef NO_MAIN
    stages = 3;
#else
    if (argc > 1) {
        /* Read stage count from command line. */

        stages = atoi(argv[1]);
        if (stages == 0 || argc > 3) {
            ERROR("Usage: %s [pipe_stages [interval (mS)]]\n", argv[0]);
            return 1;
        }

        if (argc > 2)
            interval = atoi(argv[2]);
    } else {
        stages = 0;     /* No timers or I/O testing. */
    }
#endif /* not NO_MAIN */

    /* Start the graphics side. */

    Plug.get_screen_size(&screen_info);
    if (screen_info.depth != (PIXEL_SIZE << 3)) {
        ERROR("Plugin supports depth %d, expected %d\n", 
              screen_info.depth, PIXEL_SIZE << 3);
        return 3;
    }

    /* Request an output window with half of the maximum sizes. */

    screen_info.width /= 2;
    screen_info.height /= 2;

    if (Plug.special_allocate) {
        /* Allocate special video memory. */

        allocator.type = session_image;
        allocator.buf_addr = NULL;
        allocator.priv = NULL;
        allocator.params.image_buffer.pixel_size = PIXEL_SIZE;
        allocator.params.image_buffer.width = screen_info.width;
        allocator.params.image_buffer.height = screen_info.height;
        Plug.special_allocate(&allocator);
        video_buffer = allocator.buf_addr;
        if (video_buffer == NULL) {
            ERROR("Failed to allocate session video buffer.\n");
            return 4;
        }
        memset(video_buffer, 0,
               allocator.params.image_buffer.width *
                   allocator.params.image_buffer.height * PIXEL_SIZE);
    } else {
        video_buffer = malloc(screen_info.width * screen_info.height *
                                  PIXEL_SIZE);
    }
    Plug.start_video(&screen_info);

    /* Start the timer/pipe chain. */

    if (stages > 0)
        start_chain(stages, interval);

    /* Run ... */

    Plug.run();
    return 0;
}

/* Threading test - a stub for now. */

void On_wake(void)
{
    if (log_mask & LOG_THREAD)
        REPORT("On_wake() called.\n");
}

/* Other stubs. */

void Enable_software_cursor(void) {}
void WD_pause_TW(void) {}
void WD_resume_TW(void) {}
void Update_keyboard(unsigned int flags) {}
