#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/un.h>

char buf[4096];


union sockaddr_some {
    struct sockaddr_in6 sa6;
    struct sockaddr_in sa4;
};

void init_address(int ip_family, union sockaddr_some *sa, const char* host, int port) {
    memset((char *) sa, 0, sizeof(*sa));
    if (ip_family == AF_INET) {
        struct sockaddr_in *sa_in = &sa->sa4;
        sa_in->sin_family = ip_family;
        sa_in->sin_port = htons(port);
        unsigned char buf[sizeof(struct in_addr)];
        if (inet_pton(AF_INET, host, buf)==0) {
            perror("inet_aton AF_INET");
        }
        memcpy(&sa_in->sin_addr, buf, sizeof sa_in->sin_addr);
    } else 
    if (ip_family == AF_INET6) {
        struct sockaddr_in6 *sa_in6 = &sa->sa6;
        sa_in6->sin6_family = ip_family;
        sa_in6->sin6_port = htons(port);
        unsigned char buf[sizeof(struct in6_addr)];
        if (inet_pton(AF_INET6, host, buf)==0) {
            perror("inet_aton AF_INET6");
        }
        memcpy(sa_in6->sin6_addr.s6_addr, buf, sizeof sa_in6->sin6_addr);
    } else {
        fprintf(stderr, "Unsupported address family\n");
    }
}

#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 108
#endif

void init_address_un(struct sockaddr_un *sa, const char* path) {
    memset((char*) sa, 0, sizeof(*sa));
    sa->sun_family = AF_UNIX;
    strncpy(sa->sun_path, path, UNIX_PATH_MAX);
    sa->sun_path[UNIX_PATH_MAX-1]=0;
    if(sa->sun_path[0]=='@') sa->sun_path[0]=0;
}

int s1_recv;
int s1_send;
int s2;
struct sockaddr_un peer1_recv;
struct sockaddr_un peer1_send;


union sockaddr_some peer2;
union sockaddr_some peer2_client;
    

int udp_socket(int ip_family) {
    int s2;

    if ((s2=socket(ip_family, SOCK_DGRAM, IPPROTO_UDP))==-1) {
        perror("socket");
        return -1;
    }

    return s2;
}

int unix_socket() {
    int s2;

    if ((s2=socket(AF_UNIX, SOCK_DGRAM, 0))==-1) {
        perror("AF_UNIX socket");
        return -1;
    }
    
    return s2;
}



int main(int argc, char* argv[])
{   
    int ip_family = AF_INET;
    if (argc<6) {
        fprintf(stderr, "Usage: unix2udp {l|c|l6|c6} [@]unix_socket_path_recv [@]unix_socket_path_send host1 port1\n");
        return 1;
    }
    char UDP_listen_or_connect = argv[1][0];
    char* recv_path = argv[2];
    char* send_path = argv[3];
    char* host1 = argv[4];
    int port1 = atoi(argv[5]);

    if (!UDP_listen_or_connect) {
        fprintf(stderr, "unix2udp: Invalid mode\n");
        return 1;
    }
    if (argv[1][1] == '6') ip_family = AF_INET6;
        
    s1_recv = unix_socket();
    s1_send = -1;

    init_address_un(&peer1_recv, recv_path);
    init_address_un(&peer1_send, send_path);
    init_address(ip_family, &peer2, host1, port1);
    memset(&peer2_client, 0, sizeof peer2_client);
    
    unlink(recv_path);
    
    if (bind(s1_recv, (struct sockaddr*)(&peer1_recv), sizeof(peer1_recv))==-1) {
        perror("bind");
        return -1;
    }
    
    if (UDP_listen_or_connect=='c') {
        s2 = -1;
    } else {
        s2 = udp_socket(ip_family);
        if (bind(s2, (struct sockaddr*)&peer2, sizeof(peer2))==-1) {
            perror("bind");
            return -1;
        }    
    }

    fcntl(s1_recv, F_SETFL, O_NONBLOCK);
    fcntl(s2, F_SETFL, O_NONBLOCK);

    fd_set rfds;

    for(;;) {
select_repeat:
        FD_ZERO(&rfds);
        FD_SET(s1_recv, &rfds);
        if (s2!=-1) {
            FD_SET(s2, &rfds);
        }
        int maxfd = (s1_recv>s2)?s1_recv:s2;
        int ret = select(maxfd+1, &rfds, NULL, NULL, NULL);

        if(ret==-1) {
            if(errno==EINTR  || errno==EAGAIN) goto select_repeat;
            perror("select");
            return 2;
        }
        
        if(FD_ISSET(s1_recv, &rfds)) {
            ret = recv(s1_recv, &buf, sizeof(buf), 0);
            if(ret==-1) {
                perror("recv");
                return 4;
            }
            if(ret) {
                if (s2 == -1) {
                    s2 = udp_socket(ip_family);
                    if (UDP_listen_or_connect=='c') {
                        if (connect(s2, (struct sockaddr*)(&peer2), sizeof(peer2))==-1) {
                            perror("connect");
                            close(s2);
                            s2 = -1;
                            continue;
                        }    
                    }
                    fcntl(s2, F_SETFL, O_NONBLOCK);
                }
                
                if (UDP_listen_or_connect=='c') {
                    ret = send(s2, &buf, ret, 0); 
                } else {
                    ret = sendto(s2, &buf, ret, 0, (struct sockaddr*)(&peer2_client), sizeof(peer2_client));
                }
                
                
                if (!ret || ret == -1) {
                    perror("send to inet");
                    close(s2);
                    s2=-1;
                } else {
                    write(1, ">", 1);
                }
            }
        }
        if(s2!=-1 && FD_ISSET(s2, &rfds)) {
            socklen_t slen = sizeof(peer2_client);
            ret = recvfrom(s2, &buf, sizeof(buf), 0,  (struct sockaddr*)(&peer2_client), &slen);
            if(ret==-1) {
                perror("recvfrom");
                close(s2);
                s2=-1;
                continue;
            }
            if(ret) {
                if(s1_send == -1) {
                    s1_send = unix_socket();
                    if (connect(s1_send, (struct sockaddr*)(&peer1_send), sizeof(peer1_send))==-1) {
                        perror("connect AF_UNIX");
                        close(s1_send);
                        s1_send = -1;
                        continue;
                    }
                }
                fcntl(s1_send, F_SETFL, O_NONBLOCK);
                
                ret = send(s1_send, &buf, ret, 0);
                if (!ret || ret==-1) {
                    perror("send to unix");
                    close(s1_send);
                    s1_send = -1;
                } else {
                    write(1, "<", 1);
                }
            }

        }
    }    
}
