/*
 * oscsendfile - Send OpenSound Control message.
 *
 * Copyright (C) 2016 Joseph Malloch <joseph.malloch@gmail.com>
 *
 * Adapted from oscsend:
 * Copyright (C) 2008 Kentaro Fukuchi <kentaro@fukuchi.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <config.h>
#include <limits.h>
#ifdef _MSC_VER
  #if (__STDC_VERSION__ < 201112L)
    #define strtok_r strtok_s
  #endif
#else
  #include <unistd.h>
#endif
#include <lo/lo.h>

static FILE* input_file = 0;
static double multiplier = 1.0/((double)(1LL<<32));

void usage(void)
{
    printf("oscsendfile version %s\n"
           "Copyright (C) 2016 Joseph Malloch\n"
           "  adapted from oscsend.c (C) 2008 Kentaro Fukuchi\n\n"
           "Usage: oscsendfile hostname port file <speed>\n"
           "or     oscsendfile url file <speed>\n"
           "Send OpenSound Control messages from a file via UDP.\n\n"
           "Description\n"
           "hostname: specifies the remote host's name.\n"
           "port    : specifies the port number to connect to the remote host.\n"
           "url     : specifies the destination parameters using a liblo URL.\n"
           "          e.g. UDP        \"osc.udp://localhost:9000\"\n"
           "               Multicast  \"osc.udp://224.0.1.9:9000\"\n"
           "               TCP        \"osc.tcp://localhost:9000\"\n"
           "speed   : specifies a speed multiplier (must be > 0).\n",
           VERSION);
           printf("Example\n"
           "$ oscsendfile localhost 7777 myfile.txt 2.5\n");
}

lo_message create_message(char **argv)
{
    /* Note:
     * argv[0] <- types
     * argv[1..] <- values
     */
    int i, argi;
    lo_message message;
    const char *types;
    char *arg = NULL;
    int values;

    message = lo_message_new();
    if (argv[0] == NULL) {
        /* empty message is allowed. */
        values = 0;
    } else {
        types = argv[0];
        values = strlen(types);
    }

    argi = 1;
    for (i = 0; i < values; i++) {
		switch(types[i]) {
		case LO_INT32:
		case LO_FLOAT:
		case LO_STRING:
		case LO_BLOB:
		case LO_INT64:
		case LO_TIMETAG:
		case LO_DOUBLE:
		case LO_SYMBOL:
		case LO_CHAR:
		case LO_MIDI:
			arg = argv[argi];
			if (arg == NULL) {
				fprintf(stderr, "Value #%d is not given.\n", i + 1);
				goto EXIT;
			}
			break;
		default:
			break;
		}
        switch (types[i]) {
        case LO_INT32:
            {
                char *endp;
                int64_t v;

                v = strtol(arg, &endp, 10);
                if (*endp != '\0') {
                    fprintf(stderr, "An invalid value was given: '%s'\n",
                            arg);
                    goto EXIT;
                }
                if ((v == LONG_MAX || v == LONG_MIN) && errno == ERANGE) {
                    fprintf(stderr, "Value out of range: '%s'\n", arg);
                    goto EXIT;
                }
                if (v > INT_MAX || v < INT_MIN) {
                    fprintf(stderr, "Value out of range: '%s'\n", arg);
                    goto EXIT;
                }
                lo_message_add_int32(message, (int32_t) v);
                argi++;
                break;
            }
        case LO_INT64:
            {
                char *endp;
                int64_t v;

                v = strtoll(arg, &endp, 10);
                if (*endp != '\0') {
                    fprintf(stderr, "An invalid value was given: '%s'\n",
                            arg);
                    goto EXIT;
                }
                if ((v == LONG_MAX || v == LONG_MIN) && errno == ERANGE) {
                    fprintf(stderr, "Value out of range: '%s'\n", arg);
                    goto EXIT;
                }
                lo_message_add_int64(message, v);
                argi++;
                break;
            }
        case LO_FLOAT:
            {
                char *endp;
                float v;

#ifdef __USE_ISOC99
                v = strtof(arg, &endp);
#else
                v = (float) strtod(arg, &endp);
#endif                          /* __USE_ISOC99 */
                if (*endp != '\0') {
                    fprintf(stderr, "An invalid value was given: '%s'\n",
                            arg);
                    goto EXIT;
                }
                lo_message_add_float(message, v);
                argi++;
                break;
            }
        case LO_DOUBLE:
            {
                char *endp;
                double v;

                v = strtod(arg, &endp);
                if (*endp != '\0') {
                    perror(NULL);
                    fprintf(stderr, "An invalid value was given: '%s'\n",
                            arg);
                    goto EXIT;
                }
                lo_message_add_double(message, v);
                argi++;
                break;
            }
        case LO_STRING:
        case LO_SYMBOL:
            if (arg[strlen(arg)-1] == '"')
                arg[strlen(arg)-1] = '\0';
            if (arg[0] == '"')
                lo_message_add_string(message, arg+1);
            else
                lo_message_add_string(message, arg);
            argi++;
            break;
        case LO_CHAR:
            lo_message_add_char(message, arg[0]);
            argi++;
            break;
        case LO_MIDI:
            {
                unsigned int midi;
                uint8_t packet[4];
                int ret;

                ret = sscanf(arg, "%08x", &midi);
                if (ret != 1) {
                    fprintf(stderr,
                            "An invalid hexadecimal value was given: '%s'\n",
                            arg);
                    goto EXIT;
                }
                packet[0] = (midi >> 24) & 0xff;
                packet[1] = (midi >> 16) & 0xff;
                packet[2] = (midi >> 8) & 0xff;
                packet[3] = midi & 0xff;
                lo_message_add_midi(message, packet);
                argi++;
                break;
            }
        case LO_TRUE:
            lo_message_add_true(message);
            ++argi;
            break;
        case LO_FALSE:
            lo_message_add_false(message);
            ++argi;
            break;
        case LO_NIL:
            lo_message_add_nil(message);
            ++argi;
            break;
        case LO_INFINITUM:
            lo_message_add_infinitum(message);
            ++argi;
            break;
        default:
            fprintf(stderr, "Type '%c' is not supported or invalid.\n",
                    types[i]);
            goto EXIT;
            break;
        }
    }

    return message;
  EXIT:
    lo_message_free(message);
    return NULL;
}

void timetag_add(lo_timetag *tt, lo_timetag addend)
{
    tt->sec += addend.sec;
    tt->frac += addend.frac;
    if (tt->frac < addend.frac) // overflow
        tt->sec++;
}

void timetag_subtract(lo_timetag *tt, lo_timetag subtrahend)
{
    tt->sec -= subtrahend.sec;
    if (tt->frac < subtrahend.frac) // overflow
        --tt->sec;
    tt->frac -= subtrahend.frac;
}

double timetag_diff(const lo_timetag a, const lo_timetag b)
{
    return ((double)a.sec - (double)b.sec +
            ((double)a.frac - (double)b.frac) * multiplier);
}

double timetag_double(const lo_timetag tt)
{
    return (double)tt.sec + (double)tt.frac * multiplier;
}

void timetag_multiply(lo_timetag *tt, double d)
{
    d *= timetag_double(*tt);
    tt->sec = (uint32_t) d;
    d -= tt->sec;
    tt->frac = (uint32_t) (d * (double)(1LL<<32));
}

int send_file(lo_address target, double speed) {
    double speedmul = 1.0 / speed;
    const char *delim = " \r\n";
    char *args[64];
    char str[1024], *p, *s, *path;
    lo_timetag tt_start = {0, 0}, tt_start_file = {0, 0}, tt_now;
    lo_timetag tt1 = {0, 0}, tt2 = {0, 0}, *tt_last = &tt1, *tt_this = &tt2;
    int tt_init = 0, ret = 0;
    lo_message m;
    lo_bundle b = 0;

    lo_timetag_now(&tt_start);

    while (fgets(str, 1024, input_file)) {
        path = 0;
        s = strtok_r(str, delim, &p);
        m = lo_message_new();
        if (!m)
            return 1;

        if (s) {
            // check if first arg is timetag
            if (s[0] != '/') {
                // first argument is timetag
                char *ttstr = strtok(s, ".");
                if (ttstr) {
                    tt_this->sec = strtoul(ttstr, NULL, 16);
                    if (!tt_init)
                        tt_start_file.sec = tt_this->sec;
                }
                ttstr = strtok(0, ".");
                if (ttstr) {
                    tt_this->frac = strtoul(ttstr, NULL, 16);
                    if (!tt_init)
                        tt_start_file.frac = tt_this->frac;
                }
                if (1) {
                    timetag_subtract(tt_this, tt_start_file);
                    timetag_multiply(tt_this, speedmul);
                    timetag_add(tt_this, tt_start);
                }
                else {
                    if (!tt_init)
                        timetag_subtract(&tt_start, tt_start_file);
                    timetag_add(tt_this, tt_start);
                }
                tt_init = 1;
                s = strtok_r(0, delim, &p);
            }
            else {
                tt_this->sec = 0;
                tt_this->frac = 1;
            }
        }
        if (s)
            path = s;
        else
            continue;

        s = strtok_r(0, delim, &p);
        if (s)
            args[0] = s;    // types

        int i = 0;
        while ((s = strtok_r(0, delim, &p))) {
            args[++i] = s;
        }

        m = create_message(args);
        if (!m) {
            fprintf(stderr, "Failed to create OSC message.\n");
            return 1;
        }

        if (b && memcmp(tt_this, tt_last, sizeof(lo_timetag))==0) {
            lo_bundle_add_message(b, path, m);
        }
        else {
            // wait for timestamp?
            lo_timetag_now(&tt_now);
            double wait_time = timetag_diff(*tt_last, tt_now);
            if (wait_time > 0.) {
#if defined(WIN32) || defined(_MSC_VER)
                Sleep((DWORD)(wait_time * 1000));
#else
                usleep((useconds_t)(wait_time * 1000000));
#endif
            }
            if (b) {
                ret = lo_send_bundle(target, b);
            }
            b = lo_bundle_new(*tt_this);
            lo_bundle_add_message(b, path, m);

            lo_timetag *tmp = tt_last;
            tt_last = tt_this;
            tt_this = tmp;
        }

        if (ret == -1)
            return ret;
    }

    if (b) {
        // wait for timestamp?
        lo_timetag_now(&tt_now);
        double wait_time = timetag_diff(*tt_last, tt_now);
        if (wait_time > 0.) {
#if defined(WIN32) || defined(_MSC_VER)
            Sleep((DWORD)(wait_time * 1000));
#else
            usleep((useconds_t)(wait_time * 1000000));
#endif
        }
        lo_send_bundle(target, b);
    }

    return 0;
}

int main(int argc, char **argv)
{
    lo_address target;
    int ret, i=1;

    if (argc < 2) {
        usage();
        exit(1);
    }

    if (argv[i] == NULL) {
        fprintf(stderr, "No hostname is given.\n");
        exit(1);
    }

    if (strstr(argv[i], "://")!=0) {
        target = lo_address_new_from_url(argv[i]);
        if (target == NULL) {
            fprintf(stderr, "Failed to open %s\n", argv[i]);
            exit(1);
        }
        i++;
    }
    else if (argv[i+1] == NULL) {
        fprintf(stderr, "No port number is given.\n");
        exit(1);
    }
    else {
        target = lo_address_new(argv[i], argv[i+1]);
        if (target == NULL) {
            fprintf(stderr, "Failed to open %s:%s\n", argv[i], argv[i+1]);
            exit(1);
        }
        lo_address_set_ttl(target, 1);
        i += 2;
    }

    if (argv[i] == NULL) {
        fprintf(stderr, "No %s given.\n", argc == i+1 ? "filename" : "path");
        exit(1);
    }

    // next arg should be filename
    input_file = fopen(argv[i], "r");
    if (!input_file) {
        fprintf(stderr, "Failed to open file `%s' for reading.\n", argv[i]);
        exit(1);
    }

    double speed = 1.0;
    if (argc > i+1) {
        // optional speed argument
        speed = atof(argv[i+1]);
        if (speed <= 0.0) {
            fprintf(stderr, "Negative speed multiplier is not supported.\n");
            exit(1);
        }
    }
    ret = send_file(target, speed);

    if (ret == -1) {
        fprintf(stderr, "An error occurred: %s\n",
                lo_address_errstr(target));
        exit(1);
    }

    return 0;
}
