/*
 *  Main file containing the program entry point. I really want to name all this all femboy themed
 *  Copyright (C) 2023  Brett Terpstra
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include <blt/parse/argparse.h>
#include <blt/std/string.h>
#include <blt/std/logging.h>
#include <blt/std/memory.h>
#include <blt/std/random.h>
#include <blt/std/time.h>
#include <blt/std/assert.h>
#include <fstream>
#include <iostream>
#include <ios>
#include <cstdint>
#include <memory>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <crc.h>
#include <sstream>
#include <cstring>
#include <queue>

constexpr size_t PACKET_SIZE = 512;

// header:
// 4 bytes for CRC32
// 4 byte type
//      0: data send
//      7: data resend
//      15: data ack
//      31: ack ack
// 8 bytes send_id
// 8 bytes recv_id
// 8 bytes unused
constexpr size_t HEADER_SIZE = 32;

bool running = true;

class packet
{
    protected:
        blt::scoped_buffer<std::uint8_t> buffer;
    public:
        packet(): buffer(PACKET_SIZE)
        {}
        
        explicit packet(size_t size): buffer(size)
        {}
        
        size_t used = 0;
        
        packet(const packet& copy) = default;
        
        packet(packet&& move) noexcept = default;
        
        packet& operator=(const packet& copy) = default;
        
        packet& operator=(packet&& move) noexcept = default;
        
        std::uint8_t* data()
        {
            return buffer.data();
        }
        
        [[nodiscard]] const std::uint8_t* data() const
        {
            return buffer.data();
        }
        
        std::uint8_t& operator[](size_t index)
        {
            return buffer[index];
        }
        
        [[nodiscard]] const std::uint8_t& operator[](size_t index) const
        {
            return buffer[index];
        }
        
        [[nodiscard]] size_t size() const
        {
            return used;
        }
        
        [[nodiscard]] size_t buffer_size() const
        {
            return buffer.size();
        }
        
        ~packet() = default;
};

void mangle(packet& packet)
{
    static std::random_device dev;
    static std::mt19937_64 engine(dev());
    static std::uniform_real_distribution dist(0.0, 1.0);
    static std::uniform_int_distribution randomizer(0, 7);
    
    // 50% chance to mangle 10% of the data.
    if (dist(engine) < 0.5)
    {
        for (size_t i = 0; i < PACKET_SIZE; i++)
            if (dist(engine) < 0.1)
            {
                // this is bad
                auto pos = static_cast<uint8_t>(exp2(randomizer(engine)));
                // flip the bit
                auto bit = ~(packet[i] & pos);
                // extra everything but the bit
                auto data = packet[i] & ~pos;
                // recombine bit
                packet[i] = data | bit;
            }
    }
}

// kek
class predicator
{
    private:
        std::function<void()> func;
    public:
        explicit predicator(std::function<void()> func): func(std::move(func))
        {}
        
        ~predicator()
        {
            func();
        }
};

/**
 * @class pipe
 * @brief Represents a pipe for sending and receiving packets between threads
 */
class pipe
{
    private:
        std::random_device dev{};
        std::mt19937_64 engine{dev()};
        std::uniform_real_distribution<double> dist{0.0, 1.0};
        
        packet buffer;
        std::atomic<bool> is_written{false};
        std::mutex mutex;
        std::condition_variable cv;
    public:
        pipe() = default;
        
        [[nodiscard]] bool waiting_to_send() const
        {
            return is_written.load(std::memory_order::memory_order_acquire);
        }
        
        [[nodiscard]] bool waiting_to_receive() const
        {
            return !is_written.load(std::memory_order::memory_order_acquire);
        }
        
        void send(packet packet)
        {
            // wait until the receiver has received
            std::unique_lock lock(mutex);
            cv.wait(lock, [this] { return !is_written.load(std::memory_order::memory_order_acquire); });
            // we now own the backing memory and can write to it
            // RAII is fun, we should use it here due to including a return statement
            predicator pred{[&]() -> void {
                try
                {
                    lock.unlock();
                    cv.notify_all();
                } catch (const std::exception& e)
                {
                    BLT_ERROR(e.what());
                }
            }};
            
            // 10% chance to drop the packet all together
            if (dist(engine) < 0.1)
                return;
            
            buffer = std::move(packet);
            is_written.store(true, std::memory_order::memory_order_release);
        }
        
        void receive(packet& out)
        {
            std::unique_lock lock(mutex);
            cv.wait(lock, [this] { return is_written.load(std::memory_order::memory_order_acquire); });
            
            out = packet(buffer);
            
            mangle(out);
            
            is_written.store(false, std::memory_order::memory_order_release);
            
            lock.unlock();
            cv.notify_all();
        }
};

class transceiver
{
    private:
        static constexpr size_t MAX_QUEUE_SIZE = 32;
        // the pipe to send on
        pipe& uplink;
        // the pipe to receive on
        pipe& downlink;
        
        std::mutex send_mutex;
        std::mutex receive_mutex;
        std::condition_variable receive_cv;
        std::condition_variable send_cv;
        std::queue<packet> send_queue;
        std::queue<packet> receive_queue;
        
        static DWORD crc(const packet& p)
        {
            DWORD c = 0;
            for (size_t i = 0; i < p.buffer_size() - 12; i++)
                c = UPDC32(p[i], c);
            return c;
        }
    
    public:
        transceiver(pipe& uplink, pipe& downlink): uplink(uplink), downlink(downlink)
        { virtual_kernel_subsystem_init(); }
        
        void send(const packet& p)
        {
            std::unique_lock lock(send_mutex);
            send_cv.wait(lock, [this] { return send_queue.size() < MAX_QUEUE_SIZE; });
            
            predicator pred{[&]() -> void { lock.unlock(); }};
            
            send_queue.push(p);
        }
        
        bool receive(packet& p)
        {
            std::unique_lock lock(receive_mutex);
            receive_cv.wait(lock, [this] { return !receive_queue.empty(); });
            
            predicator pred{[&]() -> void { lock.unlock(); }};
            
            p = receive_queue.front();
            receive_queue.pop();
        }
        
        /**
         * Because the 'TCP' is normally handled by the kernel, which happens outside our process, we need a way of simulating that
         * introducing the virtual kernel subsystem to handle all your reliability needs!
         */
        void virtual_kernel_subsystem_init()
        {
            std::thread kernel([this] {
                while (running)
                {
                    bool ack = false;
                    while (!ack)
                    {
                    
                    }
                }
            });
        }
};

/**
 * Load a file and split it into data segments
 * @param path
 * @param packets
 */
void load_packets(const std::string& path, std::vector<packet>& packets)
{
    std::ifstream file(path);
    
    file.seekg(0, std::ifstream::end);
    size_t length = file.tellg();
    file.seekg(0, std::ifstream::beg);
    
    if (length <= 0)
        return;
    
    BLT_INFO("Loading file of length %d", length);
    blt::scoped_buffer<std::uint8_t> data(length);
    
    file.read(reinterpret_cast<char*>(data.data()), static_cast<long>(length));
    
    if (file)
        BLT_INFO("All bytes have been read from the file, parsing into packets");
    else
        BLT_WARN("Only read %d characters, expected %d", file.gcount(), length);
    file.close();
    
    size_t rp = 0;
    while (rp < length)
    {
        packet p;
        if (rp + PACKET_SIZE < length)
            p.used = PACKET_SIZE;
        else
            p.used = length - rp;
        std::memcpy(p.data(), &data[rp], p.used);
        rp += PACKET_SIZE;
        packets.push_back(std::move(p));
    }
}

void process_packets(const std::vector<packet>& /*packets*/)
{

}

int main(int argc, const char** argv)
{
    
    blt::arg_parse parser;
    parser.addArgument(
            blt::arg_builder{"-f", "--file"}.setNArgs(1).setHelp("Specify a file to read from instead of cin").setAction(blt::arg_action_t::STORE)
                                            .build());
    
    auto args = parser.parse_args(argc, argv);
    
    std::vector<packet> packets;
    
    if (args.contains("--file"))
    {
        auto& path = blt::arg_parse::get<std::string>(args.data["file"]);
        load_packets(path, packets);
    } else
    {
        do
        {
            packet p;
            std::cin.read(reinterpret_cast<char*>(p.data()), PACKET_SIZE);
            packets.push_back(std::move(p));
        } while (!std::cin.eof() && !std::cin.fail());
    }
    
    BLT_INFO("Data was split into %d packets", packets.size());
    
    pipe uplink;
    pipe downlink;
    
    std::thread sender([&] {
        size_t send = 0;
        
        transceiver server(uplink, downlink);
        while (running)
        {
            send %= packets.size();
            server.send(packets[send++]);
        }
    });
    
    while (running)
    {
        packet pack;
        //p.receive(pack);
        
        std::cout << "-----------------{ I HAVE A PACKET }-----------------\n";
        for (size_t i = 0; i < pack.size(); i++)
            std::cout << pack[i];
        std::cout << "\n";
    }
    
    process_packets(packets);
    
    return 0;
}