//Copyright 2022, FOSS-VG Developers and Contributers
//
// Author(s):
//   BodgeMaster, Shwoomple
//
//This program is free software: you can redistribute it and/or modify it
//under the terms of the GNU Affero General Public License as published
//by the Free Software Foundation, version 3.
//
//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 Affero General Public License for more details.
//
//You should have received a copy of the GNU Affero General Public License
//version 3 along with this program.
//If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <chrono>
#include <csignal>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <string>
#include <sockpp/tcp_acceptor.h>
#include <sockpp/tcp_connector.h>
#include <sockpp/tcp6_acceptor.h>
#include <sockpp/tcp6_connector.h>
#include <sockpp/udp_socket.h>
#include <sockpp/udp6_socket.h>
#include <thread>
#include <vector>

#include "../lib/cli.hpp"
#include "../lib/error.hpp"

#define EXIT_SUCCESS 0
#define EXIT_RUNTIME 1
#define EXIT_USAGE 2
#define EXIT_UNIMPLEMENTED 3
#define EXIT_SIGNAL 4

// TCP v4 server
sockpp::tcp_socket tcpSocket;
sockpp::tcp_acceptor tcpAcceptor;
// TCP v4 client
sockpp::tcp_connector tcpConnector;
// TCP v6 server
sockpp::tcp6_socket tcp6Socket;
sockpp::tcp6_acceptor tcp6Acceptor;
// TCP v6 client
sockpp::tcp6_connector tcp6Connector;
// UDP v4 server and client
sockpp::udp_socket udpSocket;
// UDP v6 server and client
sockpp::udp6_socket udp6Socket;

bool exitProgram = false;

bool ipv4 = false;
bool ipv6 = false;
bool tcp = false;
bool udp = false;
bool outgoing = false;
std::string host;
in_port_t port;

// This is probably bigger than the MTU on any given network.
// This should allow us to read entire packets at once when they arrive
// slowly enough to be read individually.
const uint32_t bufferSize = 2048;
uint8_t networkBuffer[bufferSize];

std::string readByteFromStdin() {
    std::string input = "";
    // read 2 characters from stdin
    char characterInput;
    bool readByte = false;
    uint16_t iterationsSinceLastInput = 0;
    while (input.length() < 2 && !exitProgram) {
        if (std::cin.good()) {
            std::cin.get(characterInput);
            readByte = true;
        } else {
            readByte = false;
        }
        // ignore space, tabs, and newlines
        if (readByte && characterInput!=' ' && characterInput!='\n' && characterInput!='\r' && characterInput!='\t') {
            input.push_back(characterInput);
        }

        iterationsSinceLastInput++;
        if (readByte) {
            iterationsSinceLastInput = 0;
        }
        if (iterationsSinceLastInput>1024) {
            // prevent integer overflow
            iterationsSinceLastInput = 1024;

            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        }
    }
    return exitProgram? "" : std::string(1, (char) std::stoi(input, nullptr, 16));
}

void signalHandler(int signal) {
    // shut down gracefully
    exitProgram = true;

    // tell sockpp to close TCP socket if open because it blocks when trying
    // to read and there is no data
    tcpAcceptor.shutdown();
    if (tcpSocket) {
        tcpSocket.shutdown(SHUT_RD);
    }

    if (tcpConnector) {
        tcpConnector.shutdown(SHUT_RD);
    }

    if (tcp6Socket) {
        tcp6Socket.shutdown(SHUT_RD);
    }

    if (tcp6Connector) {
        tcp6Connector.shutdown(SHUT_RD);
    }

    if (udpSocket) {
        udpSocket.shutdown(SHUT_RD);
    }

    if (udp6Socket) {
        udp6Socket.shutdown(SHUT_RD);
    }

    std::cerr << "Received signal " << signal << ", bye!" << std::endl;
    std::exit(EXIT_SIGNAL);
}

void readFromTCP() {
    ssize_t byteCount;
    while (!exitProgram && (outgoing? (byteCount = tcpConnector.read(networkBuffer, bufferSize)) > 0 : (byteCount = tcpSocket.read(networkBuffer, bufferSize)) > 0)) {
        for (ssize_t i=0; i<byteCount; i++) {
            std::cout << std::hex << std::setfill('0') << std::setw(2) << (short) networkBuffer[i] << " ";
        }
        std::cout.flush();
    }
}

void writeToTCP() {
    while (!exitProgram) {
        if (outgoing) {
            if (tcpConnector.write(readByteFromStdin()) == -1 && !exitProgram) {
                exitProgram = true;
                tcpConnector.shutdown(SHUT_RD);
                std::cerr << "Error while sending data: " << tcpConnector.last_error_str() << std::endl;
                return;
            }
        } else {
            if (tcpSocket.write(readByteFromStdin()) == -1 && !exitProgram) {
                exitProgram = true;
                tcpSocket.shutdown(SHUT_RD);
                std::cerr << "Error while sending data: " << tcpSocket.last_error_str() << std::endl;
                return;
            }
        }
    }
}

void readFromTCP6() {
    ssize_t byteCount;
    while (!exitProgram && (outgoing? (byteCount = tcp6Connector.read(networkBuffer, bufferSize)) > 0 : (byteCount = tcp6Socket.read(networkBuffer, bufferSize)) > 0)) {
        for (ssize_t i=0; i<byteCount; i++) {
            std::cout << std::hex << std::setfill('0') << std::setw(2) << (short) networkBuffer[i] << " ";
        }
        std::cout.flush();
    }
}

void writeToTCP6() {
    while (!exitProgram) {
        if (outgoing) {
            if (tcp6Connector.write(readByteFromStdin()) == -1 && !exitProgram) {
                exitProgram = true;
                tcp6Connector.shutdown(SHUT_RD);
                std::cerr << "Error while sending data: " << tcp6Connector.last_error_str() << std::endl;
                return;
            }
        } else {
            if (tcp6Socket.write(readByteFromStdin()) == -1 && !exitProgram) {
                exitProgram = true;
                tcp6Socket.shutdown(SHUT_RD);
                std::cerr << "Error while sending data: " << tcp6Socket.last_error_str() << std::endl;
                return;
            }
        }
    }
}

void readFromUDP() {
    ssize_t byteCount;
    sockpp::udp_socket::addr_t peer;
    while ((byteCount = udpSocket.recv_from(networkBuffer, bufferSize, &peer)) > 0) {
        std::cout << peer << ": ";
        for (ssize_t i=0; i<byteCount; i++) {
            std::cout << std::hex << std::setfill('0') << std::setw(2) << (short) networkBuffer[i] << " ";
        }
        std::cout << std::dec << std::endl;
    }
}

void writeToUDP(sockpp::udp_socket::addr_t peer) {
    while (!exitProgram) {
        if (udpSocket.send_to(readByteFromStdin(), peer) == -1 && !exitProgram) {
            exitProgram = true;
            udpSocket.shutdown(SHUT_RD);
            std::cerr << "Error while sending data: " << udpSocket.last_error_str() << std::endl;
            return;
        }
    }
}

void readFromUDP6() {
    ssize_t byteCount;
    sockpp::udp6_socket::addr_t peer;
    while ((byteCount = udp6Socket.recv_from(networkBuffer, bufferSize, &peer)) > 0) {
        std::cout << peer << ": ";
        for (ssize_t i=0; i<byteCount; i++) {
            std::cout << std::hex << std::setfill('0') << std::setw(2) << (short) networkBuffer[i] << " ";
        }
        std::cout << std::dec << std::endl;
    }
}

void writeToUDP6(sockpp::udp6_socket::addr_t peer) {
    while (!exitProgram) {
        if (udp6Socket.send_to(readByteFromStdin(), peer) == -1 && !exitProgram) {
            exitProgram = true;
            udp6Socket.shutdown(SHUT_RD);
            std::cerr << "Error while sending data: " << udp6Socket.last_error_str() << std::endl;
            return;
        }
    }
}

int main(int argc, char* argv[]) {
    std::signal(SIGINT, signalHandler);
    std::signal(SIGTERM, signalHandler);

    std::vector<CLI::Flag> flags;
    flags.push_back(CLI::Flag('4', "ipv4", "use IPv4, either this or IPv6 has to be specified"));
    flags.push_back(CLI::Flag('6', "ipv6", "use IPv6, either this or IPv4 has to be specified"));
    flags.push_back(CLI::Flag('t', "tcp", "use TCP, either this or UDP has to be specified"));
    flags.push_back(CLI::Flag('u', "udp", "use UDP, either this or TCP has to be specified"));
    flags.push_back(CLI::Flag('h', "help", "print this information and exit"));
    flags.push_back(CLI::Flag('l', "license", "print license information and exit"));

    std::vector<CLI::Option> options;
    options.push_back(CLI::Option('c', "connect", "HOST", "make an outgoing connection to HOST instead of listening for an incoming connection"));
    options.push_back(CLI::Option('b', "bind", "ADDRESS", "(UDP only) bind to ADDRESS instead of localhost"));

    std::vector<CLI::Argument> arguments;
    arguments.push_back(CLI::Argument("PORT", "the port to lsiten on (or connect to)"));

    CLI::ArgumentsParser cliParser = CLI::ArgumentsParser(argc, argv, flags, options, arguments, "Arbitrary TCP/UDP connections in hex format", "Spaces, tabs, newlines, and carriage returns in the input are ignored.\n\tYou may want to disable input echoing using `stty -echo` (reenable using `stty echo`).\n\tNote that many terminals do line buffering on the input by default.");

    if (cliParser.getFlag("help").value) {
        std::cout << cliParser.getUsage() << std::endl;
        return EXIT_SUCCESS;
    }
    if (cliParser.getFlag("license").value) {
        std::cout
            << "Copyright 2022, FOSS-VG Developers and Contributers\n"
            << "\n"
            << "Hexnet is part of the FOSS-VG development tool suite.\n"
            << "\n"
            << "This program is free software: you can redistribute it and/or modify it\n"
            << "under the terms of the GNU Affero General Public License as published\n"
            << "by the Free Software Foundation, version 3.\n"
            << "\n"
            << "This program is distributed in the hope that it will be useful,\n"
            << "but WITHOUT ANY WARRANTY; without even the implied\n"
            << "warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
            << "See the GNU Affero General Public License for more details.\n"
            << "\n"
            << "You should have received a copy of the GNU Affero General Public License\n"
            << "version 3 along with this program.\n"
            << "If not, see https://www.gnu.org/licenses/agpl-3.0.en.html"
            << std::endl;
        return EXIT_SUCCESS;
    }
    if (cliParser.wrongUsage) {
        std::cout << cliParser.getUsage() << std::endl;
        return EXIT_USAGE;
    }

    ipv4 = cliParser.getFlag("ipv4").value;
    ipv6 = cliParser.getFlag("ipv6").value;
    tcp = cliParser.getFlag("tcp").value;
    udp = cliParser.getFlag("udp").value;

    if (cliParser.getOption("connect").errorCode != ErrorCodes::NOT_PRESENT) {
        outgoing = true;
        host = cliParser.getOption("connect").value;
    }

    if (!(ipv4 || ipv6) || (ipv4 && ipv6) || !(tcp || udp) || (tcp && udp)) {
        std::cout << "Please specify which protocols to use (one of IPv4/IPv6, one of TCP/UDP)." << std::endl;
        return EXIT_USAGE;
    }

    port = (in_port_t) std::stoi(cliParser.getArgument(0).value);

    if (outgoing) {
        if (tcp) {
            std::cerr << "Connecting to " << host << " on port " << (int) port << " (TCP)..." << std::endl;
            if (ipv4) {
                // TCP v4 out
                tcpConnector = sockpp::tcp_connector({host, port});
                if (!tcpConnector) {
                    std::cerr << "Error connecting to " << host << " on port " << port << std::endl;
                    std::cerr << tcpConnector.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                std::thread threadReadFromTCP = std::thread(readFromTCP);
                std::thread threadWriteToTCP = std::thread(writeToTCP);

                threadReadFromTCP.join();
                threadWriteToTCP.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            } else {
                // TCP v6 out
                tcp6Connector = sockpp::tcp6_connector({host, port});
                if (!tcp6Connector) {
                    std::cerr << "Error connecting to " << host << " on port " << port << std::endl;
                    std::cerr << tcp6Connector.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                std::thread threadReadFromTCP6 = std::thread(readFromTCP6);
                std::thread threadWriteToTCP6 = std::thread(writeToTCP6);

                threadReadFromTCP6.join();
                threadWriteToTCP6.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            }
        } else {
            std::cerr << "Talking to " << host << " on port " << (int) port << " (UDP)..." << std::endl;
            if (ipv4) {
                // UDP v4 out
                if (!udpSocket) {
                    std::cerr << "Error creating UDP socket: " << udpSocket.last_error_str() << std::endl;
                }
                // Btw: Did you know that UDP has no concept of a connection?
                sockpp::udp_socket::addr_t peer = sockpp::inet_address(host, port);
                if (!udpSocket.connect(peer)) {
                    std::cerr << "Error associating socket with " << host << " port " << port << std::endl;
                    std::cerr << udpSocket.last_error_str() << std::endl;
                }

                std::thread threadReadFromUDP = std::thread(readFromUDP);
                std::thread threadWriteToUDP = std::thread(writeToUDP, peer);

                threadReadFromUDP.join();
                threadWriteToUDP.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            } else {
                // UDP v6 out
                if (!udp6Socket) {
                    std::cerr << "Error creating UDP socket: " << udp6Socket.last_error_str() << std::endl;
                }
                // Btw: Did you know that UDP has no concept of a connection?
                sockpp::udp6_socket::addr_t peer = sockpp::inet6_address(host, port);
                if (!udp6Socket.connect(peer)) {
                    std::cerr << "Error associating socket with " << host << " port " << port << std::endl;
                    std::cerr << udp6Socket.last_error_str() << std::endl;
                }

                std::thread threadReadFromUDP6 = std::thread(readFromUDP6);
                std::thread threadWriteToUDP6 = std::thread(writeToUDP6, peer);

                threadReadFromUDP6.join();
                threadWriteToUDP6.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            }
        }
    } else {
        if (tcp) {
            std::cerr << "Listening on port " << (int) port << " (TCP)..." << std::endl;
            if (ipv4) {
                // TCP v4 in
                tcpAcceptor = sockpp::tcp_acceptor(port);
                if (!tcpAcceptor) {
                    std::cerr << "Error while creating TCP acceptor: " << tcpAcceptor.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                sockpp::inet_address peer;
                tcpSocket = tcpAcceptor.accept(&peer);
                std::cerr << "Incoming connection from " << peer << std::endl;
                if (!tcpSocket) {
                    std::cerr << "Error on incoming connection: " << tcpAcceptor.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                std::thread threadReadFromTCP = std::thread(readFromTCP);
                std::thread threadWriteToTCP = std::thread(writeToTCP);

                threadReadFromTCP.join();
                threadWriteToTCP.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            } else {
                // TCP v6 in
                tcp6Acceptor = sockpp::tcp6_acceptor(port);
                if (!tcp6Acceptor) {
                    std::cerr << "Error while creating TCP acceptor: " << tcp6Acceptor.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                sockpp::inet6_address peer;
                tcp6Socket = tcp6Acceptor.accept(&peer);
                std::cerr << "Incoming connection from " << peer << std::endl;
                if (!tcp6Socket) {
                    std::cerr << "Error on incoming connection: " << tcp6Acceptor.last_error_str() << std::endl;
                    return EXIT_RUNTIME;
                }

                std::thread threadReadFromTCP6 = std::thread(readFromTCP6);
                std::thread threadWriteToTCP6 = std::thread(writeToTCP6);

                threadReadFromTCP6.join();
                threadWriteToTCP6.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            }
        } else {
            std::string address = "localhost";
            if (cliParser.getOption("bind").errorCode != ErrorCodes::NOT_PRESENT) {
                address = cliParser.getOption("bind").value;
            }
            std::cerr << "Listening on " << address << " port " << (int) port << " (UDP)..." << std::endl;
            if (ipv4) {
                // UDP v4 in
                if (!udpSocket) {
                    std::cerr << "Error creating UDP socket: " << udpSocket.last_error_str() << std::endl;
                }
                if (!udpSocket.bind(sockpp::inet_address(address, port))) {
                    std::cerr << "Error binding UDP socket to " << address << " port " << port << ": " << udpSocket.last_error_str() << std::endl;
                }

                std::thread threadReadFromUDP = std::thread(readFromUDP);
                // Can't send bc we have no idea where to send to.
                //std::thread threadWriteToUDP = std::thread(writeToUDP);

                threadReadFromUDP.join();
                //threadWriteToUDP.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            } else {
                // UDP v6 in
                if (!udp6Socket) {
                    std::cerr << "Error creating UDP socket: " << udp6Socket.last_error_str() << std::endl;
                }
                if (!udp6Socket.bind(sockpp::inet6_address(address, port))) {
                    std::cerr << "Error binding UDP socket to " << address << " port " << port << ": " << udp6Socket.last_error_str() << std::endl;
                }

                std::thread threadReadFromUDP6 = std::thread(readFromUDP6);
                // Can't send bc we have no idea where to send to.
                //std::thread threadWriteToUDP6 = std::thread(writeToUDP6);

                threadReadFromUDP6.join();
                //threadWriteToUDP6.join();

                std::cout << std::endl;
                return EXIT_SUCCESS;
            }
        }
    }
}
