/* usbredirtestclient.c simple usb network redirection test client (guest).

   Copyright 2010-2011 Red Hat, Inc.

   Red Hat Authors:
   Hans de Goede <hdegoede@redhat.com>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <getopt.h>
#include <inttypes.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <netdb.h>
#include <netinet/in.h>
#include "usbredirparser.h"

/* Macros to go from an endpoint address to an index for our ep array */
#define EP2I(ep_address) (((ep_address & 0x80) >> 3) | (ep_address & 0x0f))
#define I2EP(i) (((i & 0x10) << 3) | (i & 0x0f))

#define TESTCLIENT_VERSION "usbredirtestclient " PACKAGE_VERSION

static void usbredirtestclient_device_connect(void *priv,
    struct usb_redir_device_connect_header *device_connect);
static void usbredirtestclient_device_disconnect(void *priv);
static void usbredirtestclient_interface_info(void *priv,
    struct usb_redir_interface_info_header *interface_info);
static void usbredirtestclient_ep_info(void *priv,
    struct usb_redir_ep_info_header *ep_info);
static void usbredirtestclient_configuration_status(void *priv, uint64_t id,
    struct usb_redir_configuration_status_header *configuration_status);
static void usbredirtestclient_alt_setting_status(void *priv, uint64_t id,
    struct usb_redir_alt_setting_status_header *alt_setting_status);
static void usbredirtestclient_iso_stream_status(void *priv, uint64_t id,
    struct usb_redir_iso_stream_status_header *iso_stream_status);
static void usbredirtestclient_interrupt_receiving_status(void *priv, uint64_t id,
    struct usb_redir_interrupt_receiving_status_header *interrupt_receiving_status);
static void usbredirtestclient_bulk_streams_status(void *priv, uint64_t id,
    struct usb_redir_bulk_streams_status_header *bulk_streams_status);
static void usbredirtestclient_control_packet(void *priv, uint64_t id,
    struct usb_redir_control_packet_header *control_packet,
    uint8_t *data, int data_len);
static void usbredirtestclient_bulk_packet(void *priv, uint64_t id,
    struct usb_redir_bulk_packet_header *bulk_packet,
    uint8_t *data, int data_len);
static void usbredirtestclient_iso_packet(void *priv, uint64_t id,
    struct usb_redir_iso_packet_header *iso_packet,
    uint8_t *data, int data_len);
static void usbredirtestclient_interrupt_packet(void *priv, uint64_t id,
    struct usb_redir_interrupt_packet_header *interrupt_packet,
    uint8_t *data, int data_len);

/* id's for all the test commands we send */
enum {
   reset_id,
   get_config_id,
   set_config_id,
   get_alt_id,
   set_alt_id,
   first_cmdline_id
};

static int verbose = usbredirparser_info; /* 3 */
static int client_fd, running = 1;
static struct usbredirparser *parser;
static int id = first_cmdline_id;

static const struct option longopts[] = {
    { "port", required_argument, NULL, 'p' },
    { "verbose", required_argument, NULL, 'v' },
    { "help", no_argument, NULL, 'h' },
    { NULL, 0, NULL, 0 }
};

static void usbredirtestclient_log(void *priv, int level, const char *msg)
{
    if (level <= verbose)
        fprintf(stderr, "%s\n", msg);
}

static int usbredirtestclient_read(void *priv, uint8_t *data, int count)
{
    int r = read(client_fd, data, count);
    if (r < 0) {
        if (errno == EAGAIN)
            return 0;
        return -1;
    }
    if (r == 0) { /* Server disconnected */
        close(client_fd);
        client_fd = -1;
    }
    return r;
}

static int usbredirtestclient_write(void *priv, uint8_t *data, int count)
{
    int r = write(client_fd, data, count);
    if (r < 0) {
        if (errno == EAGAIN)
            return 0;
        if (errno == EPIPE) { /* Server disconnected */
            close(client_fd);
            client_fd = -1;
            return 0;
        }
        return -1;
    }
    return r;
}

static void usbredirtestclient_hello(void *priv,
    struct usb_redir_hello_header *h)
{
    /* Queue a reset + set config the other test commands will be send in
       response to the status packets of previous commands */
    usbredirparser_send_reset(parser);
    usbredirparser_send_get_configuration(parser, get_config_id);
}

static void usage(int exit_code, char *argv0)
{
    fprintf(exit_code? stderr:stdout,
        "Usage: %s [-p|--port <port>] [-v|--verbose <0-3>] <server>\n",
        argv0);
    exit(exit_code);
}

static void run_main_loop(void)
{
    fd_set readfds, writefds;
    int n, nfds;

    while (running && client_fd != -1) {
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        FD_SET(client_fd, &readfds);
        if (usbredirparser_has_data_to_write(parser)) {
            FD_SET(client_fd, &writefds);
        }
        nfds = client_fd + 1;

        n = select(nfds, &readfds, &writefds, NULL, NULL);
        if (n == -1) {
            if (errno == EINTR) {
                continue;
            }
            perror("select");
            break;
        }

        if (FD_ISSET(client_fd, &readfds)) {
            if (usbredirparser_do_read(parser)) {
                break;
            }
        }
        if (FD_ISSET(client_fd, &writefds)) {
            if (usbredirparser_do_write(parser)) {
                break;
            }
        }
    }
    if (client_fd != -1) { /* Broken out of the loop because of an error ? */
        close(client_fd);
        client_fd = -1;
    }
}

static void quit_handler(int sig)
{
    running = 0;
}

int main(int argc, char *argv[])
{
    int o, flags;
    char *endptr, *server;
    struct addrinfo *r, *res, hints;
    struct sigaction act;
    char port_str[16];
    int port = 4000;
    uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0, };

    while ((o = getopt_long(argc, argv, "hp:", longopts, NULL)) != -1) {
        switch (o) {
        case 'p':
            port = strtol(optarg, &endptr, 10);
            if (*endptr != '\0') {
                fprintf(stderr, "Inalid value for --port: '%s'\n", optarg);
                usage(1, argv[0]);
            }
            break;
        case 'v':
            verbose = strtol(optarg, &endptr, 10);
            if (*endptr != '\0') {
                fprintf(stderr, "Inalid value for --verbose: '%s'\n", optarg);
                usage(1, argv[0]);
            }
            break;
        case '?':
        case 'h':
            usage(o == '?', argv[0]);
            break;
        }
    }
    if (optind == argc) {
        fprintf(stderr, "Missing server argument\n");
        usage(1, argv[0]);
    }
    server = argv[optind];
    optind++;
    if (optind != argc) {
        fprintf(stderr, "Excess non option arguments\n");
        usage(1, argv[0]);
    }

    memset(&act, 0, sizeof(act));
    act.sa_handler = quit_handler;
    sigaction(SIGINT, &act, NULL);
    sigaction(SIGHUP, &act, NULL);
    sigaction(SIGTERM, &act, NULL);
    sigaction(SIGQUIT, &act, NULL);

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    sprintf(port_str, "%d", port);
    if (getaddrinfo(server, port_str, &hints, &res) != 0) {
        perror("getaddrinfo");
        exit(1);
    }

    for (r = res; r != NULL; r = r->ai_next) {
        client_fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
        if (client_fd == -1)
            continue;

        if (connect(client_fd, r->ai_addr, r->ai_addrlen) == 0)
            break;

        close(client_fd);
    }
    freeaddrinfo(res);

    if (r == NULL) {
        fprintf(stderr, "Could not connect to: [%s]:%s\n", server, port_str);
        exit(1);
    }

    flags = fcntl(client_fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl F_GETFL");
        exit(1);
    }
    flags = fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
    if (flags == -1) {
        perror("fcntl F_SETFL O_NONBLOCK");
        exit(1);
    }

    parser = usbredirparser_create();
    if (!parser) {
        exit(1);
    }
    parser->log_func = usbredirtestclient_log;
    parser->read_func = usbredirtestclient_read;
    parser->write_func = usbredirtestclient_write;
    parser->hello_func = usbredirtestclient_hello;
    parser->device_connect_func = usbredirtestclient_device_connect;
    parser->device_disconnect_func = usbredirtestclient_device_disconnect;
    parser->interface_info_func = usbredirtestclient_interface_info;
    parser->ep_info_func = usbredirtestclient_ep_info;
    parser->configuration_status_func = usbredirtestclient_configuration_status;
    parser->alt_setting_status_func = usbredirtestclient_alt_setting_status;
    parser->iso_stream_status_func = usbredirtestclient_iso_stream_status;
    parser->interrupt_receiving_status_func = usbredirtestclient_interrupt_receiving_status;
    parser->bulk_streams_status_func = usbredirtestclient_bulk_streams_status;
    parser->control_packet_func = usbredirtestclient_control_packet;
    parser->bulk_packet_func = usbredirtestclient_bulk_packet;
    parser->iso_packet_func = usbredirtestclient_iso_packet;
    parser->interrupt_packet_func = usbredirtestclient_interrupt_packet;

    usbredirparser_caps_set_cap(caps, usb_redir_cap_ep_info_max_packet_size);
    usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);

    usbredirparser_init(parser, TESTCLIENT_VERSION, caps, USB_REDIR_CAPS_SIZE, 0);

    run_main_loop();

    exit(0);
}

static void usbredirtestclient_cmdline_help(void)
{
    printf("Available commands:\n"
        "ctrl <endpoint> <request> <request_type> <value> <index> <length> [data]\n"
        "quit\n"
        "help\n");
}

static int usbredirtestclient_cmdline_ctrl(void)
{
    struct usb_redir_control_packet_header control_packet;
    char *arg, *endptr = NULL;
    uint8_t *data = NULL;
    int data_len;

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.endpoint = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid endpoint\n");
        return 0;
    }

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.request = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid request\n");
        return 0;
    }

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.requesttype = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid request type\n");
        return 0;
    }

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.value = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid value\n");
        return 0;
    }

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.index = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid index\n");
        return 0;
    }

    arg = strtok(NULL, " \t\n");
    if (arg) {
        control_packet.length = strtol(arg, &endptr, 0);
    }
    if (!arg || *endptr != '\0') {
        printf("Missing or invalid length\n");
        return 0;
    }

    if (!(control_packet.endpoint & 0x80)) {
        int i;

        data = malloc(control_packet.length);
        if (!data) {
            fprintf(stderr, "Out of memory!\n");
            close(client_fd);
            client_fd = -1;
            return 0;
        }

        for (i = 0; i < control_packet.length; i++) {
            arg = strtok(NULL, " \t\n");
            if (arg) {
                data[i] = strtol(arg, &endptr, 0);
            }
            if (!arg || *endptr != '\0') {
                printf("Missing or invalid data byte(s)\n");
                return 0;
            }
        }
        data_len = control_packet.length;
    } else {
        data_len = 0;
    }
    usbredirparser_send_control_packet(parser, id, &control_packet,
                                       data, data_len);
    free(data);
    printf("Send control packet with id: %u\n", id);
    id++;
    return 1;
}

static void usbredirtestclient_cmdline_parse(void)
{
    char buf[128];
    char *cmd;

    while (running && client_fd != -1) {
        printf("> ");
        if (!fgets(buf, sizeof(buf), stdin)) {
            close(client_fd);
            client_fd = -1;
            return;
        }

        cmd = strtok(buf, " \t\n");
        if (!cmd)
            continue;

        if (!strcmp(cmd, "help")) {
            usbredirtestclient_cmdline_help();
        } else if (!strcmp(cmd, "quit")) {
            close(client_fd);
            client_fd = -1;
            return;
        } else if (!strcmp(cmd, "ctrl")) {
            if (usbredirtestclient_cmdline_ctrl()) {
                return; /* Run main loop until an answer is received */
            }
        } else {
            printf("unknown command: '%s', type 'help' for help\n", cmd);
        }
    }
}

static void usbredirtestclient_device_connect(void *priv,
    struct usb_redir_device_connect_header *device_connect)
{
    switch (device_connect->speed) {
    case usb_redir_speed_low:   printf("device info: speed: low\n"); break;
    case usb_redir_speed_full:  printf("device info: speed: full\n"); break;
    case usb_redir_speed_high:  printf("device info: speed: high\n"); break;
    case usb_redir_speed_super: printf("device info: speed: super\n"); break;
    default:
        printf("device info: speed: unknown\n");
    }
    printf("  class %2d subclass %2d protocol %2d\n",
           device_connect->device_class, device_connect->device_subclass,
           device_connect->device_protocol);
    printf("  vendor 0x%04x product %04x\n",
           device_connect->vendor_id, device_connect->product_id);
}

static void usbredirtestclient_device_disconnect(void *priv)
{
    printf("device disconnected");
    close(client_fd);
    client_fd = -1;
}

static void usbredirtestclient_interface_info(void *priv,
    struct usb_redir_interface_info_header *info)
{
    int i;

    for (i = 0; i < info->interface_count; i++) {
        printf("interface %d class %2d subclass %2d protocol %2d\n",
               info->interface[i], info->interface_class[i],
               info->interface_subclass[i], info->interface_protocol[i]);
    }
}

static void usbredirtestclient_ep_info(void *priv,
    struct usb_redir_ep_info_header *ep_info)
{
    int i;

    for (i = 0; i < 32; i++) {
       if (ep_info->type[i] != usb_redir_type_invalid) {
           printf("endpoint: %02X, type: %d, interval: %d, interface: %d max-packetsize: %d\n",
                  I2EP(i), (int)ep_info->type[i], (int)ep_info->interval[i],
                  (int)ep_info->interface[i], ep_info->max_packet_size[i]);
       }
    }
}

static void usbredirtestclient_configuration_status(void *priv, uint64_t id,
    struct usb_redir_configuration_status_header *config_status)
{
    struct usb_redir_set_configuration_header set_config;
    struct usb_redir_get_alt_setting_header get_alt;

    switch (id) {
    case get_config_id:
        printf("Get config: %d, status: %d\n", config_status->configuration,
               config_status->status);
        set_config.configuration = config_status->configuration;
        usbredirparser_send_set_configuration(parser, set_config_id,
                                              &set_config);
        break;
    case set_config_id:
        printf("Set config: %d, status: %d\n", config_status->configuration,
               config_status->status);
        get_alt.interface = 0; /* Assume the device has an interface 0 */
        usbredirparser_send_get_alt_setting(parser, get_alt_id, &get_alt);
        break;
    default:
        fprintf(stderr, "Unexpected configuration status packet, id: %"
                PRIu64"\n", id);
    }
}

static void usbredirtestclient_alt_setting_status(void *priv, uint64_t id,
    struct usb_redir_alt_setting_status_header *alt_setting_status)
{
    struct usb_redir_set_alt_setting_header set_alt;

    switch (id) {
    case get_alt_id:
        printf("Get alt: %d, interface: %d, status: %d\n",
               alt_setting_status->alt, alt_setting_status->interface,
               alt_setting_status->status);
        set_alt.interface = alt_setting_status->interface;
        set_alt.alt = alt_setting_status->alt;
        usbredirparser_send_set_alt_setting(parser, set_alt_id, &set_alt);
        break;
    case set_alt_id:
        printf("Set alt: %d, interface: %d, status: %d\n",
               alt_setting_status->alt, alt_setting_status->interface,
               alt_setting_status->status);
        /* Auto tests done, go interactive */
        usbredirtestclient_cmdline_parse();
        break;
    default:
        fprintf(stderr, "Unexpected alt status packet, id: %"PRIu64"\n", id);
    }
}

static void usbredirtestclient_iso_stream_status(void *priv, uint64_t id,
    struct usb_redir_iso_stream_status_header *iso_stream_status)
{
}

static void usbredirtestclient_interrupt_receiving_status(void *priv, uint64_t id,
    struct usb_redir_interrupt_receiving_status_header *interrupt_receiving_status)
{
}

static void usbredirtestclient_bulk_streams_status(void *priv, uint64_t id,
    struct usb_redir_bulk_streams_status_header *bulk_streams_status)
{
}

static void usbredirtestclient_control_packet(void *priv, uint64_t id,
    struct usb_redir_control_packet_header *control_packet,
    uint8_t *data, int data_len)
{
    int i;
    printf("Control packet id: %"PRIu64", status: %d", id,
           control_packet->status);

    if (data_len) {
        printf(", data:");
    }
    for (i = 0; i < data_len; i++) {
        printf(" %02X", (unsigned int)data[i]);
    }
    printf("\n");
    usbredirparser_free_packet_data(parser, data);

    /* Ask what to send next */
    usbredirtestclient_cmdline_parse();
}

static void usbredirtestclient_bulk_packet(void *priv, uint64_t id,
    struct usb_redir_bulk_packet_header *bulk_packet,
    uint8_t *data, int data_len)
{
}

static void usbredirtestclient_iso_packet(void *priv, uint64_t id,
    struct usb_redir_iso_packet_header *iso_packet,
    uint8_t *data, int data_len)
{
}

static void usbredirtestclient_interrupt_packet(void *priv, uint64_t id,
    struct usb_redir_interrupt_packet_header *interrupt_packet,
    uint8_t *data, int data_len)
{
}
