/*
 * MIT License
 *
 * Copyright (c) 2019 Jianhui Zhao <zhaojh329@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <time.h>
#include <glob.h>

#include "log/log.h"
#include "utils.h"
#include "rtty.h"

enum {
    LONG_OPT_HELP = 1,
#ifdef KCP_SUPPORT
    LONG_OPT_KCP,
    LONG_OPT_KCP_NODELAY,
    LONG_OPT_KCP_INTERVAL,
    LONG_OPT_KCP_RESEND,
    LONG_OPT_KCP_NC,
    LONG_OPT_KCP_SNDWND,
    LONG_OPT_KCP_RCVWND,
    LONG_OPT_KCP_MTU,
    LONG_OPT_KCP_KEY,
#endif
};

#ifdef SSL_SUPPORT
static void load_default_ca_cert(struct ssl_context *ctx)
{
	glob_t gl;
	size_t i;

	glob("/etc/ssl/certs/*.crt", 0, NULL, &gl);

	for (i = 0; i < gl.gl_pathc; i++)
		ssl_load_ca_cert_file(ctx, gl.gl_pathv[i]);

	globfree(&gl);
}
#endif

static void signal_cb(struct ev_loop *loop, ev_signal *w, int revents)
{
    struct rtty *rtty = ev_userdata(loop);

    if (w->signum == SIGINT) {
        rtty->reconnect = false;
        ev_break(loop, EVBREAK_ALL);
        log_info("Normal quit\n");
    }
}

static struct option long_options[] = {
    {"group",       required_argument, NULL, 'g'},
    {"id",          required_argument, NULL, 'I'},
    {"host",        required_argument, NULL, 'h'},
    {"port",        required_argument, NULL, 'p'},
    {"description", required_argument, NULL, 'd'},
    {"token",       required_argument, NULL, 't'},
#ifdef KCP_SUPPORT
    {"kcp",         no_argument, NULL, LONG_OPT_KCP},
    {"kcp-nodelay", no_argument, NULL, LONG_OPT_KCP_NODELAY},
    {"kcp-interval", required_argument, NULL, LONG_OPT_KCP_INTERVAL},
    {"kcp-resend",  required_argument, NULL, LONG_OPT_KCP_RESEND},
    {"kcp-nc",      no_argument, NULL, LONG_OPT_KCP_NC},
    {"kcp-sndwnd",  required_argument, NULL, LONG_OPT_KCP_SNDWND},
    {"kcp-rcvwnd",  required_argument, NULL, LONG_OPT_KCP_RCVWND},
    {"kcp-mtu",     required_argument, NULL, LONG_OPT_KCP_MTU},
    {"kcp-key",     required_argument, NULL, LONG_OPT_KCP_KEY},
#endif
#ifdef SSL_SUPPORT
    {"cacert",      required_argument, NULL, 'C'},
    {"insecure",    no_argument, NULL, 'x'},
    {"cert",        required_argument, NULL, 'c'},
    {"key",         required_argument, NULL, 'k'},
#endif
    {"verbose",     no_argument,       NULL, 'v'},
    {"version",     no_argument,       NULL, 'V'},
    {"help",        no_argument,       NULL, LONG_OPT_HELP},
    {0, 0, 0, 0}
};

static void usage(const char *prog)
{
    fprintf(stderr, "Usage: %s [option]\n"
            "      -g, --group=string       Set a group for the device(max 16 chars, no spaces allowed)\n"
            "      -I, --id=string          Set an ID for the device(max 32 chars, no spaces allowed)\n"
            "      -h, --host=string        Server's host or ipaddr(Default is localhost)\n"
            "      -p, --port=number        Server port(Default is 5912)\n"
            "      -d, --description=string Add a description to the device(Maximum 126 bytes)\n"
            "      -a                       Auto reconnect to the server\n"
            "      -i number                Set heartbeat interval in seconds(Default is 30s)\n"
#ifdef KCP_SUPPORT
            "      --kcp                    Using KCP protocol\n"
            "      --kcp-nodelay            Whether enable nodelay mode for KCP\n"
            "      --kcp-interval number    KCP protocol internal work interval(Default is 100ms)\n"
            "      --kcp-resend number      Fast retransmission mode for KCP\n"
            "      --kcp-nc                 Whether to turn off flow control for KCP\n"
            "      --kcp-sndwnd number      Maximum send window for KCP\n"
            "      --kcp-rcvwnd number      Maximum receive window for KCP\n"
            "      --kcp-mtu number         Maximum transmission unit for KCP\n"
            "      --kcp-key                Key used to encrypt data\n"
#endif
#ifdef SSL_SUPPORT
            "      -s                       SSL on\n"
            "      -C, --cacert             CA certificate to verify peer against\n"
            "      -x, --insecure           Allow insecure server connections when using SSL\n"
            "      -c, --cert               Certificate file to use\n"
            "      -k, --key                Private key file to use\n"
#endif
            "      -D                       Run in the background\n"
            "      -t, --token=string       Authorization token\n"
            "      -f username              Skip a second login authentication. See man login(1) about the details\n"
            "      -R                       Receive file\n"
            "      -S file                  Send file\n"
            "      -v, --verbose            verbose\n"
            "      -V, --version            Show version\n"
            "      --help                   Show usage\n",
            prog);
    exit(1);
}

int main(int argc, char **argv)
{
#ifdef SSL_SUPPORT
#define SSL_SHORTOPTS "sC:xc:k:"
#else
#define SSL_SHORTOPTS ""
#endif

    const char *shortopts = "g:I:i:h:p:d:aDt:f:RS:vV"SSL_SHORTOPTS;
    struct ev_loop *loop = EV_DEFAULT;
    struct ev_signal signal_watcher;
    bool background = false;
    bool verbose = false;
    struct rtty rtty = {
        .heartbeat = 30,
        .host = "localhost",
        .port = 5912,
        .loop = loop,
        .sock = -1,
#ifdef KCP_SUPPORT
        .kcp.interval = -1
#endif
    };
#ifdef SSL_SUPPORT
    bool has_cacert = false;
#endif
    int option_index;
    int c;

#ifdef SSL_SUPPORT
    rtty.ssl_ctx = ssl_context_new(false);
    if (!rtty.ssl_ctx)
        return -1;
#endif

    ev_set_userdata(loop, &rtty);

    while (true) {
        c = getopt_long(argc, argv, shortopts, long_options, &option_index);
        if (c == -1)
            break;

        switch (c) {
        case 'g':
            if (!valid_id(optarg, 16)) {
                log_err("invalid group\n");
                return -1;
            }
            rtty.group = optarg;
            break;
        case 'I':
            if (!valid_id(optarg, 32)) {
                log_err("invalid device id\n");
                return -1;
            }
            rtty.devid = optarg;
            break;
        case 'i':
            rtty.heartbeat = atoi(optarg);

            if (rtty.heartbeat < 5) {
                rtty.heartbeat = 5;
                log_warn("Heartbeat interval too short, set to 5s\n");
            }

            if (rtty.heartbeat > 255) {
                rtty.heartbeat = 255;
                log_warn("Heartbeat interval too long, set to 255s\n");
            }
            break;
        case 'h':
            rtty.host = optarg;
            break;
        case 'p':
            rtty.port = atoi(optarg);
            break;
        case 'd':
            if (strlen(optarg) > 126) {
                log_err("Description too long\n");
                usage(argv[0]);
            }
            rtty.description = optarg;
            break;
        case 'a':
            rtty.reconnect = true;
            break;
#ifdef KCP_SUPPORT
        case LONG_OPT_KCP:
            rtty.kcp.on = true;
            break;
        case LONG_OPT_KCP_NODELAY:
            rtty.kcp.nodelay = true;
            break;
        case LONG_OPT_KCP_INTERVAL:
            rtty.kcp.interval = atoi(optarg);
            break;
        case LONG_OPT_KCP_RESEND:
            rtty.kcp.resend = atoi(optarg);
            break;
        case LONG_OPT_KCP_NC:
            rtty.kcp.nc = true;
            break;
        case LONG_OPT_KCP_SNDWND:
            rtty.kcp.sndwnd = atoi(optarg);
            break;
        case LONG_OPT_KCP_RCVWND:
            rtty.kcp.rcvwnd = atoi(optarg);
            break;
        case LONG_OPT_KCP_MTU:
            rtty.kcp.mtu = atoi(optarg);
            break;
        case LONG_OPT_KCP_KEY:
            rtty.kcp.password = optarg;
            break;
#endif
#ifdef SSL_SUPPORT
        case 's':
            rtty.ssl_on = true;
            break;
        case 'C':
            if (ssl_load_ca_cert_file(rtty.ssl_ctx, optarg)) {
                log_err("load ca certificate file fail\n");
                return -1;
            }
            has_cacert = true;
            break;
        case 'x':
            rtty.insecure = true;
            ssl_set_require_validation(rtty.ssl_ctx, false);
            break;
        case 'c':
            if (ssl_load_cert_file(rtty.ssl_ctx, optarg)) {
                log_err("load certificate file fail\n");
                return -1;
            }
            break;
        case 'k':
            if (ssl_load_key_file(rtty.ssl_ctx, optarg)) {
                log_err("load private key file fail\n");
                return -1;
            }
            break;
#endif
        case 'D':
            background = true;
            break;
        case 't':
            rtty.token = optarg;
            break;
        case 'f':
            rtty.username = optarg;
            break;
        case 'R':
            request_transfer_file('R', NULL);
            return 0;
        case 'S':
            request_transfer_file('S', optarg);
            return 0;
        case 'v':
            verbose = true;
            break;
        case 'V':
            log_info("rtty version %s\n", RTTY_VERSION_STRING);
            exit(0);
        case LONG_OPT_HELP:
            usage(argv[0]);
            break;
        default: /* '?' */
            usage(argv[0]);
            break;
        }
    }

    signal(SIGPIPE, SIG_IGN);

    if (!rtty.devid) {
        log_err("you must specify an id for your device\n");
        return -1;
    }

    if (find_login(rtty.login_path, sizeof(rtty.login_path) - 1) < 0) {
        log_err("the program 'login' is not found\n");
        return -1;
    }

    if (getuid() > 0) {
        log_err("Operation not permitted, must be run as root\n");
        return -1;
    }

    if (background && daemon(0, 0))
        log_err("Can't run in the background: %s\n", strerror(errno));

    if (verbose)
        log_level(LOG_DEBUG);

    log_info("rtty version %s\n", RTTY_VERSION_STRING);

    ev_signal_init(&signal_watcher, signal_cb, SIGINT);
    ev_signal_start(loop, &signal_watcher);

#ifdef SSL_SUPPORT
    if (rtty.ssl_ctx && !has_cacert)
        load_default_ca_cert(rtty.ssl_ctx);
#endif

    srand(time(NULL));

    if (rtty_start(&rtty) < 0)
        return -1;

    ev_run(loop, 0);

    rtty_exit(&rtty);

#ifdef SSL_SUPPORT
    ssl_context_free(rtty.ssl_ctx);
#endif

    ev_loop_destroy(loop);

    return 0;
}
