/*
 *  This only works for linux!!
 *
 *  Copyright (C) 2024  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/std/logging.h>
#include <blt/parse/argparse.h>
#include <blt/profiling/profiler_v2.h>
#include <blt/std/utility.h>
#include <blt/fs/loader.h>
#include <unistd.h>
#include <sys/wait.h>
#include <thread>
#include <cstdio>
#include <fcntl.h>
#include <sys/stat.h>
#include "blt/std/assert.h"
#include "blt/std/memory.h"

HASHSET<std::int32_t> pids;

struct ofstream_wrapper
{
    private:
        std::ofstream stream_;
        blt::size_t& offset_;
    public:
        template<typename STREAM>
        ofstream_wrapper(STREAM&& stream, blt::size_t& offset): stream_(std::forward<STREAM>(stream)), offset_(offset)
        {}
        
        /**
         * Im a lazy bastard sometimes
         * Tee Hee :3
         */
        template<typename T>
        inline friend ofstream_wrapper& operator<<(ofstream_wrapper& wrapper, T&& t)
        {
            wrapper.stream_ << t;
            if constexpr (std::is_same_v<std::string, T>)
            {
                for (char c : t)
                    if (c == '\n')
                        wrapper.offset_++;
            } else if constexpr (std::is_same_v<char, T>)
            {
                if (t == '\n')
                    wrapper.offset_++;
            } else if constexpr (std::is_same_v<char*, std::remove_cv_t<T>>)
            {
                for (blt::size_t i = 0; i < std::strlen(t); i++)
                {
                    if (t[i] == '\n')
                        wrapper.offset_++;
                }
            }
            return wrapper;
        }
        
        std::ofstream& stream()
        {
            return stream_;
        }
};

struct record
{
    private:
        template<typename T>
        static inline void fill_value(T& v, const std::string& str)
        {
            if constexpr (std::is_floating_point_v<T>)
            {
                v = std::stod(str);
            } else if constexpr (std::is_integral_v<T>)
            {
                v = std::stoi(str);
            } else
            {
                static_assert("Unsupported type!");
            }
        }
    
    public:
        record() = default;
        
        int gen, sub;
        double mean_fitness;
        double best_fitness, worse_fitness, mean_tree_size, mean_tree_depth;
        int best_tree_size, best_tree_depth, worse_tree_size, worse_tree_depth;
        double mean_fitness_run;
        double best_fitness_run, worse_fitness_run, mean_tree_size_run, mean_tree_depth_run;
        int best_tree_size_run, best_tree_depth_run, worse_tree_size_run, worse_tree_depth_run;
        
        inline record& operator+=(const record& r)
        {
            BLT_ASSERT_MSG(gen == r.gen && "Generation value must be equal! you have an error!",
                           (std::to_string(gen) + " vs " + std::to_string(r.gen)).c_str());
            mean_fitness += r.mean_fitness;
            best_fitness += r.best_fitness;
            worse_fitness += r.worse_fitness;
            mean_tree_size += r.mean_tree_size;
            mean_tree_depth += r.mean_tree_depth;
            
            best_tree_size += r.best_tree_size;
            best_tree_depth += r.best_tree_depth;
            worse_tree_size += r.worse_tree_size;
            worse_tree_depth += r.worse_tree_depth;
            
            mean_fitness_run += r.mean_fitness_run;
            best_fitness_run += r.best_fitness_run;
            worse_fitness_run += r.worse_fitness_run;
            mean_tree_size_run += r.mean_tree_size_run;
            mean_tree_depth_run += r.mean_tree_depth_run;
            
            best_tree_size_run += r.best_tree_size_run;
            best_tree_depth_run += r.best_tree_depth_run;
            worse_tree_size_run += r.worse_tree_size_run;
            worse_tree_depth_run += r.worse_tree_depth_run;
            
            return *this;
        }
        
        inline record& operator/=(int i)
        {
            auto v = static_cast<double>(i);
            
            mean_fitness /= v;
            best_fitness /= v;
            worse_fitness /= v;
            mean_tree_size /= v;
            mean_tree_depth /= v;
            
            best_tree_size /= i;
            best_tree_depth /= i;
            worse_tree_size /= i;
            worse_tree_depth /= i;
            
            mean_fitness_run /= v;
            best_fitness_run /= v;
            worse_fitness_run /= v;
            mean_tree_size_run /= v;
            mean_tree_depth_run /= v;
            
            best_tree_size_run /= i;
            best_tree_depth_run /= i;
            worse_tree_size_run /= i;
            worse_tree_depth_run /= i;
            
            return *this;
        }
        
        static inline record from_string_array(int generation, size_t& idx, blt::span<std::string> values)
        {
            record r{};
            std::memset(&r, 0, sizeof(record));
            // I don't like the reliance on order...
            r.gen = generation;
            fill_value(r.sub, values[idx++]);
            fill_value(r.mean_fitness, values[idx++]);
            fill_value(r.best_fitness, values[idx++]);
            fill_value(r.worse_fitness, values[idx++]);
            fill_value(r.mean_tree_size, values[idx++]);
            fill_value(r.mean_tree_depth, values[idx++]);
            fill_value(r.best_tree_size, values[idx++]);
            fill_value(r.best_tree_depth, values[idx++]);
            fill_value(r.worse_tree_size, values[idx++]);
            fill_value(r.worse_tree_depth, values[idx++]);
            fill_value(r.mean_fitness_run, values[idx++]);
            fill_value(r.best_fitness_run, values[idx++]);
            fill_value(r.worse_fitness_run, values[idx++]);
            fill_value(r.mean_tree_size_run, values[idx++]);
            fill_value(r.mean_tree_depth_run, values[idx++]);
            fill_value(r.best_tree_size_run, values[idx++]);
            fill_value(r.best_tree_depth_run, values[idx++]);
            fill_value(r.worse_tree_size_run, values[idx++]);
            fill_value(r.worse_tree_depth_run, values[idx++]);
            return r;
        }
};

int child(blt::arg_parse::arg_results& args, int run_id)
{
    auto program = "../" + args.get<std::string>("program");
    auto file = "../" + args.get<std::string>("file");
    auto rice_file = "../" + args.get<std::string>("rice");
    auto dir = "./run_" + std::to_string(run_id);
    BLT_DEBUG("Running GP program '%s' on run %d", program.c_str(), run_id);
    
    mkdir(dir.c_str(), S_IREAD | S_IWRITE | S_IEXEC | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
    if (chdir(dir.c_str()))
    {
        BLT_ERROR(errno);
        return 1;
    }
    
    auto command = "" + program + " -f " + file + " -p rice_file='" + rice_file + "'";
    BLT_TRACE("Running command %s", command.c_str());
    
    FILE* process = popen(command.c_str(), "r");
    char buffer[4096];
    while (fgets(buffer, 4096, process) != nullptr)
    {
        BLT_DEBUG_STREAM << buffer;
    }
    
    pclose(process);
    
    return 0;
}

void wait_for_children()
{
    while (!pids.empty())
    {
        int status;
        auto pid = wait(&status);
        if (WIFEXITED(status))
        {
            BLT_END_INTERVAL("Program", "Process: " + std::to_string(pid));
            pids.erase(pid);
            BLT_TRACE("Process %d finished!", pid);
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
}

struct run_data
{
    // per generation (map stores from gen -> list of rows (records))
    HASHMAP<int, std::vector<record>> averages;
    // per RUN generation size
    std::vector<int> runs_generation_size;
    // count of the number of generations that use the same number of generations (used for calculating mode) exists [0, largest_generation]
    blt::scoped_buffer<int> generations_size_count;
    // largest number of generations from all runs
    int largest_generation = 0;
    // total number of generations across all runs (r1...rn)
    int total_generations = 0;
    // total / runs
    int generations_average = 0;
    // # of run lengths value is `data.generations_size_count[data.mode_generation]`
    int generations_mode = 0;
    // the generation that is the mode
    int mode_generation = 0;
};

void process_run(run_data& data, int& max_gen, std::string_view file)
{
    auto lines = blt::fs::getLinesFromFile(file);
    for (std::string_view line : blt::itr_offset(lines, 1))
    {
        auto values = blt::string::split(line, '\t');
        if (values.size() <= 1)
            continue;
        else if (values.size() < 21)
        {
            BLT_WARN("Size of vector (%ld) is less than expected (21). Skipping line!", values.size());
            continue;
        }
        blt::size_t idx = 0;
        
        auto generation = std::stoi(values[idx++]);
        max_gen = std::max(max_gen, generation);
        
        data.averages[generation].push_back(record::from_string_array(generation, idx, values));
    }
}

run_data get_per_generation_averages(const std::string& outfile, int runs)
{
    run_data data;
    
    for (int i = 0; i < runs; i++)
    {
        int max_gen = 0;
        auto file = "./run_" + ((std::to_string(i) += "/") += outfile) += ".stt";
        process_run(data, max_gen, file);
        data.largest_generation = std::max(data.largest_generation, max_gen);
        data.total_generations += max_gen;
        data.runs_generation_size.push_back(max_gen);
    }
    
    // zero out the buffer
    // I am using my scoped buffer because it is meant to be a std::array<T, S> but at runtime.
    // it provides iterators and automatic resource management :3
    //data.generations_size_count = blt::scoped_buffer<int>(static_cast<blt::size_t>(data.largest_generation) + 1);
    data.generations_size_count.resize(data.largest_generation + 1);
    std::memset(data.generations_size_count.data(), 0, data.generations_size_count.size() * sizeof(int));
    
    // count the number of generations per length
    for (auto v : data.runs_generation_size)
        data.generations_size_count[v]++;
    
    // find the larger length that contains the most runs
    for (auto v : blt::enumerate(data.generations_size_count))
    {
        if (v.second > data.generations_size_count[data.mode_generation])
            data.mode_generation = static_cast<int>(v.first);
    }
    data.generations_mode = data.generations_size_count[data.mode_generation];
    data.generations_average = data.total_generations / runs;
    
    return data;
}

// encase you are wondering why all these functions are using template parameters, it is so that I can pass BLT_?*_STREAM into them
// allowing for output to stdout
template<typename T>
void write_record(T& writer, const record& r)
{
    writer << r.gen << '\t';
    writer << r.sub << '\t';
    writer << r.mean_fitness << '\t';
    writer << r.best_fitness << '\t';
    writer << r.worse_fitness << '\t';
    writer << r.mean_tree_size << '\t';
    writer << r.mean_tree_depth << '\t';
    writer << r.best_tree_size << '\t';
    writer << r.best_tree_depth << '\t';
    writer << r.worse_tree_size << '\t';
    writer << r.worse_tree_depth << '\t';
    
    writer << r.mean_fitness_run << '\t';
    writer << r.best_fitness_run << '\t';
    writer << r.worse_fitness_run << '\t';
    writer << r.mean_tree_size_run << '\t';
    writer << r.mean_tree_depth_run << '\t';
    writer << r.best_tree_size_run << '\t';
    writer << r.best_tree_depth_run << '\t';
    writer << r.worse_tree_size_run << '\t';
    writer << r.worse_tree_depth_run << '\n';
}

// std::function<void(std::ofstream&, const char, const Args...)>

// std::function<void(std::ofstream&, const char, const int, const int, const blt::size_t)>
template<typename T, typename FUNC, typename... Args>
void write_data_values(T& writer, const std::string& function, const bool end, FUNC func, Args... args)
{
    const char BASE = 'a';
    for (int i = 0; i < 20; i++)
    {
        char c = static_cast<char>(BASE + i);
        writer << function;
        func(writer, c, args...);
        if (i != 19 || !end)
            writer << '\t';
        // spacing tab
        if (i == 19 && !end)
            writer << '\t';
    }
}

template<typename T>
void write_func_gens(T& writer, const char c, const int current_gen, const int runs, const blt::size_t offset)
{
    for (int j = 0; j < runs; j++)
    {
        writer << c << ((current_gen * runs) + (offset + j));
        if (j != runs - 1)
            writer << ", ";
        else
            writer << ')';
    }
}

template<typename T>
void write_func_pop(T& writer, const char c, const int gen_size, const blt::size_t offset)
{
    for (int j = 0; j < gen_size; j++)
    {
        // get the position of our aggregated data, which the offset contains
        writer << c << (offset - gen_size - 1 + j);
        if (j != gen_size - 1)
            writer << ", ";
        else
            writer << ')';
    }
}

void process_files(blt::arg_parse::arg_results& args)
{
    auto outfile = args.get<std::string>("out_file");
    auto writefile = args.get<std::string>("write_file") + ".tsv";
    auto writefile_run = args.get<std::string>("write_file") + "_runs.tsv";
    auto runs = args.get<std::int32_t>("runs");
    
    auto data = get_per_generation_averages(outfile, runs);
    
    std::vector<record> generation_averages;
    for (const auto& a : data.averages)
    {
        record base{};
        std::memset(&base, 0, sizeof(record));
        base.gen = a.first;
        for (const auto& r : a.second)
            base += r;
        base /= runs;
        generation_averages.push_back(base);
    }
    
    std::sort(generation_averages.begin(), generation_averages.end(), [](const auto& a, const auto& b) {
        return a.gen < b.gen;
    });
    
    {
        std::ofstream writer(writefile);
        writer << "Runs Generation Count Mean: " << data.generations_average << '\n';
        writer << "Runs Generation Count Mode: " << data.mode_generation << " occurred (" << data.generations_mode << ") time(s)\n";
        writer << "GEN#\tSUB#\tμFGEN\tFsBestGEN\tFsWorstGEN\tμTreeSzGEN\tμTreeDpGEN\tbTreeSzGEN\tbTreeDpGEN\twTreeSzGEN\twTreeDpGEN\tμFRUN\t"
                  "FsBestRUN\tFsWorstRUN\tμTreeSzRUN\tμTreeDpRUN\tbTreeSzRUN\tbTreeDpRUN\twTreeSzRUN\twTreeDpRUN\n";
        
        for (const auto& r : generation_averages)
            write_record(writer, r);
    }
    
    std::vector<std::vector<record>> ordered_records;
    for (const auto& v : data.averages)
        ordered_records.push_back(v.second);
    std::sort(ordered_records.begin(), ordered_records.end(), [](const auto& a, const auto& b) {
        BLT_ASSERT(!(a.empty() || b.empty()));
        return a[0].gen < b[0].gen;
    });
    
    {
        blt::size_t offset = 54;
        std::ofstream writer(writefile_run);
        const int middles = 2;
        for (int i = 0; i < middles; i++)
            writer << "GEN#\tSUB#\tμFGEN\tFsBestGEN\tFsWorstGEN\tμTreeSzGEN\tμTreeDpGEN\tbTreeSzGEN\tbTreeDpGEN\twTreeSzGEN\twTreeDpGEN\tμFRUN\t"
                      "FsBestRUN\tFsWorstRUN\tμTreeSzRUN\tμTreeDpRUN\tbTreeSzRUN\tbTreeDpRUN\twTreeSzRUN\twTreeDpRUN\t\t";
        writer << "GEN#\tSUB#\tμFGEN\tFsBestGEN\tFsWorstGEN\tμTreeSzGEN\tμTreeDpGEN\tbTreeSzGEN\tbTreeDpGEN\twTreeSzGEN\twTreeDpGEN\tμFRUN\t"
                  "FsBestRUN\tFsWorstRUN\tμTreeSzRUN\tμTreeDpRUN\tbTreeSzRUN\tbTreeDpRUN\twTreeSzRUN\twTreeDpRUN\n";
        
        const std::string AVG_FN = "=AVERAGE(";
        const std::string STDEV_FN = "=STDEV.P(";
        
        for (int gen = 0; gen < data.largest_generation + 1; gen++)
        {
            write_data_values(writer, AVG_FN, false, write_func_gens<std::ofstream>, gen, runs, offset);
            write_data_values(writer, STDEV_FN, gen != 0, write_func_gens<std::ofstream>, gen, runs, offset);
            if (gen == 0)
                write_data_values(writer, STDEV_FN, true, write_func_pop<std::ofstream>, data.largest_generation + 1, offset);
            writer << '\n';
        }
        writer << '\n';
        
        for (const auto& run : ordered_records)
            for (const auto& v : run)
                write_record(writer, v);
    }
    
    BLT_INFO("Average Number of Generations: %d", data.generations_average);
}

int main(int argc, const char** argv)
{
    blt::arg_parse parser;
    
    parser.addArgument(blt::arg_builder("-r", "--runs").setDefault(10).setHelp("Number of runs to execute").build());
    parser.addArgument(blt::arg_builder("-p", "--program").setDefault("./Assignment_1").setHelp("GP Program to execute per run").build());
    parser.addArgument(blt::arg_builder("-o", "--out_file").setDefault("regress")
                                                           .setHelp("Name of the stats file (without extension) to use in building the final data")
                                                           .build());
    parser.addArgument(blt::arg_builder("-w", "--write_file").setDefault("aggregated").setHelp(
            "Name of the file to write the aggregated data to (without extension)").build());
    parser.addArgument(blt::arg_builder("-f", "--file").setDefault("../input.file").setHelp("File to run the GP on").build());
    parser.addArgument(blt::arg_builder("--rice").setDefault("../Rice_Cammeo_Osmancik.arff").setHelp("Rice file to run the GP on").build());
    parser.addArgument(
            blt::arg_builder("-a", "--aggregate").setAction(blt::arg_action_t::STORE_TRUE).setHelp("Run the aggregation on existing runs folders")
                                                 .build());
    parser.addArgument(blt::arg_builder("-e", "--execute").setAction(blt::arg_action_t::STORE_TRUE).setHelp("Perform the runs").build());
    parser.addArgument(blt::arg_builder("-b", "--partb").setAction(blt::arg_action_t::STORE_FALSE).setDefault(true)
                                                        .setHelp("Disable aggregation on part B data (This defaults to true)").build());
    
    auto args = parser.parse_args(argc, argv);
    
    if (!args.contains("--aggregate") && !args.contains("--execute"))
    {
        BLT_FATAL("You must select --aggregate and/or --execute");
        return 1;
    }
    
    BLT_INFO("Parsing user arguments:");
    for (auto& v : args.data)
        BLT_INFO("\t%s\t= %s", v.first.c_str(), blt::to_string(v.second).c_str());
    
    BLT_START_INTERVAL("Program", "Parent Total");
    if (args.contains("--execute"))
    {
        auto runs = args.get<std::int32_t>("runs");
        BLT_DEBUG("Running with %d runs", runs);
        
        for (auto i = 0; i < runs; i++)
        {
            auto pid = fork();
            if (pid == 0)
            {
                return child(args, i);
            } else if (pid > 0)
            {
                BLT_START_INTERVAL("Program", "Process: " + std::to_string(pid));
                // parent
                pids.insert(pid);
                BLT_TRACE("Forked child to %d", pid);
            } else
            {
                // failure
                BLT_ERROR("Failed to fork process! Error: %d", errno);
                return 1;
            }
        }
        wait_for_children();
    }
    if (args.contains("--aggregate"))
        process_files(args);
    BLT_END_INTERVAL("Program", "Parent Total");
    BLT_PRINT_PROFILE("Program");
}
