/*
 *  Copyright (C) 2014 Steve Harris et al. (see AUTHORS)
 *
 *  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.
 *
 *  $Id$
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#if defined(WIN32) || defined(_MSC_VER)
#include <io.h>
#define snprintf _snprintf
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#else
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#ifdef HAVE_GETIFADDRS
#include <ifaddrs.h>
#endif
#endif

#include "lo_types_internal.h"
#include "lo_internal.h"
#include "lo/lo.h"
#include "lo/lo_throw.h"

static void lo_address_set_flags(lo_address t, int flags);

lo_address lo_address_new_with_proto(int proto, const char *host,
                                     const char *port)
{
    lo_address a;

    if (proto != LO_UDP && proto != LO_TCP && proto != LO_UNIX)
        return NULL;

    a = (lo_address) calloc(1, sizeof(struct _lo_address));
    if (a == NULL)
        return NULL;

    a->ai = NULL;
    a->ai_first = NULL;
    a->socket = -1;
    a->ownsocket = 1;
    a->protocol = proto;
    a->flags = (lo_proto_flags) 0;
    switch (proto) {
    default:
    case LO_UDP:
    case LO_TCP:
        if (host) {
            a->host = strdup(host);
        } else {
            a->host = strdup("localhost");
        }
        break;
    case LO_UNIX:
        a->host = strdup("localhost");
        break;
    }
    if (port) {
        a->port = strdup(port);
    } else {
        a->port = NULL;
    }

    a->ttl = -1;
    a->addr.size = 0;
    a->addr.iface = 0;
    a->source_server = 0;
    a->source_path = 0;

    return a;
}

lo_address lo_address_new(const char *host, const char *port)
{
    return lo_address_new_with_proto(LO_UDP, host, port);
}

lo_address lo_address_new_from_url(const char *url)
{
    lo_address a;
    int protocol;
    char *host, *port, *proto;

    if (!url || !*url) {
        return NULL;
    }

    protocol = lo_url_get_protocol_id(url);
    if (protocol == LO_UDP || protocol == LO_TCP) {
        host = lo_url_get_hostname(url);
        port = lo_url_get_port(url);
        a = lo_address_new_with_proto(protocol, host, port);
        if (host)
            free(host);
        if (port)
            free(port);
#if !defined(WIN32) && !defined(_MSC_VER)
    } else if (protocol == LO_UNIX) {
        port = lo_url_get_path(url);
        a = lo_address_new_with_proto(LO_UNIX, NULL, port);
        if (port)
            free(port);
#endif
    } else {
        proto = lo_url_get_protocol(url);
        fprintf(stderr,
                PACKAGE_NAME ": protocol '%s' not supported by this "
                "version\n", proto);
        if (proto)
            free(proto);

        return NULL;
    }

    return a;
}

static void lo_address_resolve_source(lo_address a)
{
    char hostname[LO_HOST_SIZE];
    char portname[32];
    int err;
    lo_server s = a->source_server;

    if (a->protocol == LO_UDP && s && s->addr_len > 0)
    {
        err = getnameinfo((struct sockaddr *) &s->addr, s->addr_len,
                          hostname, sizeof(hostname),
                          portname, sizeof(portname),
                          NI_NUMERICHOST | NI_NUMERICSERV);
        if (err) {
            switch (err) {
            case EAI_AGAIN:
                lo_throw(s, err, "Try again", a->source_path);
                break;
            case EAI_BADFLAGS:
                lo_throw(s, err, "Bad flags", a->source_path);
                break;
            case EAI_FAIL:
                lo_throw(s, err, "Failed", a->source_path);
                break;
            case EAI_FAMILY:
                lo_throw(s, err, "Cannot resolve address family",
                         a->source_path);
                break;
            case EAI_MEMORY:
                lo_throw(s, err, "Out of memory", a->source_path);
                break;
            case EAI_NONAME:
                lo_throw(s, err, "Cannot resolve", a->source_path);
                break;
#if !defined(WIN32) && !defined(_MSC_VER)
            case EAI_SYSTEM:
                lo_throw(s, err, strerror(err), a->source_path);
                break;
#endif
            default:
                lo_throw(s, err, "Unknown error", a->source_path);
                break;
            }

            return;
        }

        a->host = strdup(hostname);
        a->port = strdup(portname);
#if !defined(WIN32) && !defined(_MSC_VER)
    } else if (a->protocol== LO_UNIX) {
        struct sockaddr_un * addr = (struct sockaddr_un *) &s->addr;
        a->host = strdup("");
        a->port = strdup(addr->sun_path);
#endif
    } else {
        a->host = strdup("");
        a->port = strdup("");
    }
}

const char *lo_address_get_hostname(lo_address a)
{
    if (!a) {
        return NULL;
    }

    if (!a->host)
        lo_address_resolve_source(a);

    return a->host;
}

int lo_address_get_protocol(lo_address a)
{
    if (!a) {
        return -1;
    }

    return a->protocol;
}

const char *lo_address_get_port(lo_address a)
{
    if (!a) {
        return NULL;
    }

    if (!a->host)
        lo_address_resolve_source(a);

    return a->port;
}

static const char *get_protocol_name(int proto)
{
    switch (proto) {
    case LO_UDP:
        return "udp";
    case LO_TCP:
        return "tcp";
#if !defined(WIN32) && !defined(_MSC_VER)
    case LO_UNIX:
        return "unix";
#endif
    }
    return NULL;
}


char *lo_address_get_url(lo_address a)
{
    char *buf;
    int ret = 0;
    int needquote = 0;
    const char *fmt;

    if (!a->host)
        lo_address_resolve_source(a);

    if (!a->host)
		return NULL;

    needquote = strchr(a->host, ':') ? 1 : 0;

    if (needquote) {
        fmt = "osc.%s://[%s]:%s/";
    } else {
        fmt = "osc.%s://%s:%s/";
    }
#ifndef _MSC_VER
    ret = snprintf(NULL, 0, fmt,
                   get_protocol_name(a->protocol), a->host, a->port);
#endif
    if (ret <= 0) {
        /* this libc is not C99 compliant, guess a size */
        ret = 1023;
    }
    buf = (char*) malloc((ret + 2) * sizeof(char));
    snprintf(buf, ret + 1, fmt,
             get_protocol_name(a->protocol), a->host, a->port);

    if (a->protocol==LO_UNIX) {
        buf[ret-1] = 0;
    }

    return buf;
}

void lo_address_free(lo_address a)
{
    if (a) {
        if (a->socket != -1 && a->ownsocket) {
#ifdef SHUT_WR
            shutdown(a->socket, SHUT_WR);
#endif
            closesocket(a->socket);
        }
        lo_address_free_mem(a);
        free(a);
    }
}

void lo_address_free_mem(lo_address a)
{
    if (a) {
        if (a->host)
            free(a->host);
        if (a->port)
            free(a->port);
        if (a->ai_first)
            freeaddrinfo(a->ai_first);
        if (a->addr.iface)
            free(a->addr.iface);

        memset(a, 0, sizeof(struct _lo_address));
        a->socket = -1;
    }
}

int lo_address_errno(lo_address a)
{
    return a->errnum;
}

const char *lo_address_errstr(lo_address a)
{
    char *msg;

    if (a->errstr) {
        return a->errstr;
    }

    if (a->errnum == 0)
        return "Success";

    msg = strerror(a->errnum);
    if (msg) {
        return msg;
    } else {
        return "unknown error";
    }

    return "unknown error";
}

char *lo_url_get_protocol(const char *url)
{
    char *protocol, *ret;

    if (!url) {
        return NULL;
    }

    protocol = (char*) malloc(strlen(url));

    if (sscanf(url, "osc://%s", protocol)) {
        fprintf(stderr,
                PACKAGE_NAME " warning: no protocol specified in URL, "
                "assuming UDP.\n");
        ret = strdup("udp");
    } else if (sscanf(url, "osc.%[^:/[]", protocol)) {
        ret = strdup(protocol);
    } else {
        ret = NULL;
    }

    free(protocol);

    return ret;
}

int lo_url_get_protocol_id(const char *url)
{
    if (!url) {
        return -1;
    }

    if (!strncmp(url, "osc:", 4)) {
        fprintf(stderr,
                PACKAGE_NAME " warning: no protocol specified in URL, "
                "assuming UDP.\n");
        return LO_UDP;          // should be LO_DEFAULT?
    } else if (!strncmp(url, "osc.udp:", 8)) {
        return LO_UDP;
    } else if (!strncmp(url, "osc.tcp:", 8)) {
        return LO_TCP;
    } else if (!strncmp(url, "osc.unix:", 9)) {
        return LO_UNIX;
    }
    return -1;
}

char *lo_url_get_hostname(const char *url)
{
    char *hostname = (char*) malloc(strlen(url));

    if (sscanf(url, "osc://%[^[:/]", hostname)) {
        return hostname;
    }
    if (sscanf(url, "osc.%*[^:/]://[%[^]/]]", hostname)) {
        return hostname;
    }
    if (sscanf(url, "osc.%*[^:/]://%[^[:/]", hostname)) {
        return hostname;
    }

    /* doesnt look like an OSC URL */
    free(hostname);

    return NULL;
}

char *lo_url_get_port(const char *url)
{
    char *port = (char*) malloc(strlen(url));

    if (sscanf(url, "osc://%*[^:]:%[0-9]", port) > 0) {
        return port;
    }
    if (sscanf(url, "osc.%*[^:]://%*[^:]:%[0-9]", port) > 0) {
        return port;
    }
    if (sscanf(url, "osc://[%*[^]]]:%[0-9]", port) > 0) {
        return port;
    }
    if (sscanf(url, "osc.%*[^:]://[%*[^]]]:%[0-9]", port) > 0) {
        return port;
    }
    if (sscanf(url, "osc://:%[0-9]", port) > 0) {
        return port;
    }
    if (sscanf(url, "osc.%*[^:]://:%[0-9]", port) > 0) {
        return port;
    }

    /* doesnt look like an OSC URL with port number */
    free(port);

    return NULL;
}

char *lo_url_get_path(const char *url)
{
    char *path = (char*) malloc(strlen(url));

    if (sscanf(url, "osc://%*[^:]:%*[0-9]%s", path)) {
        return path;
    }
    if (sscanf(url, "osc.%*[^:]://%*[^:]:%*[0-9]%s", path) == 1) {
        return path;
    }
    if (sscanf(url, "osc.unix://%*[^/]%s", path)) {
        int i = (int) strlen(path)-1;
        if (path[i]=='/') // remove trailing slash
            path[i] = 0;
        return path;
    }
    if (sscanf(url, "osc.%*[^:]://%s", path)) {
        int i = (int) strlen(path)-1;
        if (path[i]=='/') // remove trailing slash
            path[i] = 0;
        return path;
    }

    /* doesnt look like an OSC URL with port number and path */
    free(path);

    return NULL;
}

void lo_address_set_ttl(lo_address t, int ttl)
{
    if (t->protocol == LO_UDP)
        t->ttl = ttl;
}

int lo_address_get_ttl(lo_address t)
{
    return t->ttl;
}

int lo_address_set_tcp_nodelay(lo_address t, int enable)
{
    int r = (t->flags & LO_NODELAY) != 0;
    lo_address_set_flags(t, enable
                         ? t->flags | LO_NODELAY
                         : t->flags & ~LO_NODELAY);
    return r;
}

int lo_address_set_stream_slip(lo_address t, lo_slip_encoding encoding)
{
    int r = t->flags & LO_SLIP_DBL_END
        ? LO_SLIP_DOUBLE
        : t->flags & LO_SLIP
            ? LO_SLIP_SINGLE
            : LO_SLIP_NONE;
    lo_address_set_flags(t, encoding > 0
                         ? t->flags | LO_SLIP
                         : t->flags & ~LO_SLIP);
    lo_address_set_flags(t, encoding > 1
                         ? t->flags | LO_SLIP_DBL_END
                         : t->flags & ~LO_SLIP_DBL_END);
    return r;
}

static
void lo_address_set_flags(lo_address t, int flags)
{
    if (((t->flags & LO_NODELAY) != (flags & LO_NODELAY))
        && t->socket > 0)
    {
        int option = (t->flags & LO_NODELAY)!=0;
        setsockopt(t->socket, IPPROTO_TCP, TCP_NODELAY,
                   (const char*)&option, sizeof(option));
    }

    t->flags = (lo_proto_flags) flags;
}

#ifdef ENABLE_IPV6
static int is_dotted_ipv4_address (const char* address)
{
    int a[4];
    return sscanf(address, "%u.%u.%u.%u", &a[0], &a[1], &a[2], &a[3]);
}
#endif

void lo_address_copy(lo_address to, lo_address from)
{
    /* Initialize all members that are not auto-initialized when the
     * lo_address is used. (e.g. resolving addrinfo) */
    memset(to, 0, sizeof(struct _lo_address));
    to->socket = from->socket;
    if (from->host) {
        free(to->host);
        to->host = strdup(from->host);
    }
    if (from->port) {
        free(to->port);
        to->port = strdup(from->port);
    }
    to->protocol = from->protocol;
    to->flags = from->flags;
    to->ttl = from->ttl;
    to->addr = from->addr;
    if (from->addr.iface)
        to->addr.iface = strdup(from->addr.iface);
}

void lo_address_init_with_sockaddr(lo_address a,
                                   void *sa, size_t sa_len,
                                   int sock, int prot)
{
    int err = 0;
    assert(a != NULL);
    lo_address_free_mem(a);
    a->host = (char*) malloc(INET_ADDRSTRLEN);
    a->port = (char*) malloc(8);

    err = getnameinfo((struct sockaddr *)sa, (socklen_t) sa_len,
                      a->host, INET_ADDRSTRLEN, a->port, 8,
                      NI_NUMERICHOST | NI_NUMERICSERV);

    if (err) {
        free(a->host);
        free(a->port);
        a->host = a->port = 0;
    }

    a->socket = sock;
    a->protocol = prot;
}

int lo_address_resolve(lo_address a)
{
    int ret;

    if (a->protocol == LO_UDP || a->protocol == LO_TCP) {
        struct addrinfo *ai=NULL;
        struct addrinfo hints;
        const char* host = lo_address_get_hostname(a);
#ifdef ENABLE_IPV6
        char hosttmp[7+16+1]; // room for ipv6 prefix + a dotted quad
#endif

        memset(&hints, 0, sizeof(hints));
#ifdef ENABLE_IPV6
        hints.ai_family = PF_UNSPEC;

        if (is_dotted_ipv4_address(host)) {
            host = hosttmp;
            strcpy(hosttmp, "::FFFF:");
            strncpy(hosttmp + 7, lo_address_get_hostname(a), 16);
        }
#else
        hints.ai_family = PF_INET;
#endif
        hints.ai_socktype =
            a->protocol == LO_UDP ? SOCK_DGRAM : SOCK_STREAM;

        if ((ret = getaddrinfo(host, lo_address_get_port(a), &hints, &ai))) {
            a->errnum = ret;
            a->errstr = gai_strerror(ret);
            a->ai = NULL;
            a->ai_first = NULL;
            return -1;
        }
        
        a->ai = ai;
        a->ai_first = ai;
    }

    return 0;
}

#if defined(WIN32) || defined(_MSC_VER) || defined(HAVE_GETIFADDRS)

int lo_address_set_iface(lo_address t, const char *iface, const char *ip)
{
    int fam;
    if (!t->ai) {
        lo_address_resolve(t);
        if (!t->ai)
            return 2;  // Need the address family to continue
    }
    fam = t->ai->ai_family;

    return lo_inaddr_find_iface(&t->addr, fam, iface, ip);
}

int lo_inaddr_find_iface(lo_inaddr t, int fam,
                         const char *iface, const char *ip)
{
#if defined(WIN32) || defined(_MSC_VER)
    ULONG size;
    int tries;
    PIP_ADAPTER_ADDRESSES paa, aa;
    DWORD rc;
    int found;
#endif

	union {
        struct in_addr addr;
#ifdef ENABLE_IPV6
        struct in6_addr addr6;
#endif
    } a;

    if (ip) {
#ifdef HAVE_INET_PTON
        int rc = inet_pton(fam, ip, &a);
        if (rc!=1)
            return (rc<0) ? 3 : 4;
#else
        if (fam!=AF_INET6)
            *((unsigned long*)&a.addr) = inet_addr(ip);
#endif
    }

#if defined(WIN32) || defined(_MSC_VER)

    /* Start with recommended 15k buffer for GetAdaptersAddresses. */
    size = 15*1024/2;
    tries = 3;
    paa = malloc(size*2);
    rc = ERROR_SUCCESS-1;
    while (rc!=ERROR_SUCCESS && paa && tries-- > 0) {
        size *= 2;
        paa = realloc(paa, size);
        rc = GetAdaptersAddresses(fam, 0, 0, paa, &size);
    }
    if (rc!=ERROR_SUCCESS)
        return 2;

    aa = paa;
    found=0;
    while (aa && rc==ERROR_SUCCESS) {
        if (iface) {
            if (strcmp(iface, aa->AdapterName)==0)
                found = 1;
            else {
                WCHAR ifaceW[256];
                MultiByteToWideChar(CP_ACP, 0, iface, -1,
                                    ifaceW, 256);
                if (lstrcmpW(ifaceW, aa->FriendlyName)==0)
                    found = 1;
            }
        }
        if (ip) {
            PIP_ADAPTER_UNICAST_ADDRESS pua = aa->FirstUnicastAddress;
            while (pua && !found) {
                if (fam==AF_INET) {
                    struct sockaddr_in *s =
                        (struct sockaddr_in*)pua->Address.lpSockaddr;
                    if (fam == s->sin_family
                        && memcmp(&a.addr, &s->sin_addr,
                                  sizeof(struct in_addr))==0) {
                        memcpy(&t->a.addr, &s->sin_addr,
                               sizeof(struct in_addr));
                        found = 1;
                    }
                }
#ifdef ENABLE_IPV6
                else if (fam==AF_INET6) {
                    struct sockaddr_in6 *s =
                        (struct sockaddr_in6*)pua->Address.lpSockaddr;
                    if (fam == s->sin6_family
                        && memcmp(&a.addr6, &s->sin6_addr,
                                  sizeof(struct in6_addr))==0) {
                        memcpy(&t->a.addr6, &s->sin6_addr,
                               sizeof(struct in6_addr));
                        found = 1;
                    }
                }
#endif
                pua = pua->Next;
            }
        }

        if (aa && found) {
            t->iface = strdup(aa->AdapterName);
            if (!ip && aa->FirstUnicastAddress) {
                PIP_ADAPTER_UNICAST_ADDRESS pua = aa->FirstUnicastAddress;
                while (pua) {
                    struct sockaddr_in *s =
                        (struct sockaddr_in*)pua->Address.lpSockaddr;
                    if (s->sin_family==fam) {
                        if (fam==AF_INET) {
                            memcpy(&t->a.addr, &s->sin_addr,
                                   sizeof(struct in_addr));
                            break;
                        }
#ifdef ENABLE_IPV6
                        else if (fam==AF_INET6) {
                            struct sockaddr_in6 *s6 =
                                (struct sockaddr_in6*)pua->Address.lpSockaddr;
                            memcpy(&t->a.addr6, &s6->sin6_addr,
                                   sizeof(struct in6_addr));
                            break;
                        }
#endif
                    }
                    pua = pua->Next;
                }
            }
            break;
        }

        aa = aa->Next;
    }

    if (paa) free(paa);

    return !found;

#else // !WIN32

    struct ifaddrs *ifa, *ifa_list;
    if (getifaddrs(&ifa_list)==-1)
        return 5;
    ifa = ifa_list;

    int found = 0;
    while (ifa) {
        if (!ifa->ifa_addr) {
            ifa = ifa->ifa_next;
            continue;
        }
        if (ip) {
            if (ifa->ifa_addr->sa_family == AF_INET && fam == AF_INET)
            {
                if (memcmp(&((struct sockaddr_in*)ifa->ifa_addr)->sin_addr,
                           &a.addr, sizeof(struct in_addr))==0) {
                    found = 1;
                    t->size = sizeof(struct in_addr);
                    memcpy(&t->a, &a, t->size);
                    break;
                }
            }
#ifdef ENABLE_IPV6
            else if (ifa->ifa_addr->sa_family == AF_INET6 && fam == AF_INET6)
            {
                if (memcmp(&((struct sockaddr_in6*)ifa->ifa_addr)->sin6_addr,
                           &a.addr6, sizeof(struct in6_addr))==0) {
                    found = 1;
                    t->size = sizeof(struct in6_addr);
                    memcpy(&t->a, &a, t->size);
                    break;
                }
            }
#endif
        }
        if (iface) {
            if (ifa->ifa_addr->sa_family == fam
                && strcmp(ifa->ifa_name, iface)==0)
            {
                if (fam==AF_INET) {
                    found = 1;
                    t->size = sizeof(struct in_addr);
                    memcpy(&t->a, &((struct sockaddr_in*)
                                    ifa->ifa_addr)->sin_addr,
                           t->size);
                    break;
                }
#ifdef ENABLE_IPV6
                else if (fam==AF_INET6) {
                    found = 1;
                    t->size = sizeof(struct in6_addr);
                    memcpy(&t->a, &((struct sockaddr_in6*)
                                    ifa->ifa_addr)->sin6_addr,
                           t->size);
                    break;
                }
#endif
            }
        }
        ifa = ifa->ifa_next;
    }

    if (found && ifa->ifa_name) {
        if (t->iface) free(t->iface);
        t->iface = strdup(ifa->ifa_name);
    }

    freeifaddrs(ifa_list);
    return !found;
#endif
}

const char* lo_address_get_iface(lo_address t)
{
    if (t)
        return t->addr.iface;
    return 0;
}

#endif // HAVE_GETIFADDRS

/* vi:set ts=8 sts=4 sw=4: */
