/*  lil-gp Genetic Programming System, version 1.0, 11 July 1995
 *  Copyright (C) 1995  Michigan State University
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of version 2 of the GNU General Public License as
 *  published by the Free Software Foundation.
 * 
 *  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, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *  
 *  Douglas Zongker       (zongker@isl.cps.msu.edu)
 *  Dr. Bill Punch        (punch@isl.cps.msu.edu)
 *
 *  Computer Science Department
 *  A-714 Wells Hall
 *  Michigan State University
 *  East Lansing, Michigan  48824
 *  USA
 *  
 */

#include <stdio.h>
#include <math.h>

#include <blt/fs/loader.h>
#include <random>
#include <cstring>
#include "blt/std/ranges.h"

extern "C" {
#include <lilgp.h>
#include "protos.h"
}

globaldata g;

static int fitness_cases = -1;
#ifdef PART_B
static double* app_fitness_cases[8];
#else
static double* app_fitness_cases[2];
#endif
static double value_cutoff;

template<typename T>
auto cxx_d(T* t)
{
    return reinterpret_cast<double (*)()>(t);
}

template<typename T>
auto cxx_c(T* t)
{
    return reinterpret_cast<char* (*)()>(t);
}

struct record
{
    double area;
    double perimeter;
    double major_axis_length;
    double minor_axis_length;
    double eccentricity;
    double convex_area;
    double extent;
    char type[8]{};
};

struct loaded_data
{
    std::vector<record> cammeo;
    std::vector<record> osmancik;
    std::vector<record> set;
    // everything after these values is testing data.
    size_t last_c = 0;
    size_t last_o = 0;
    
    std::vector<record> getTrainingSet(size_t amount)
    {
        std::vector<record> set;
        static std::random_device dev;
        static std::mt19937_64 engine(dev());
        std::uniform_int_distribution choice(0, 1);
        
        for (size_t i = 0; i < amount; i++)
        {
            if (last_c >= cammeo.size() || last_o >= osmancik.size())
            {
                BLT_FATAL("ERROR IN CREATING SUBSET, REQUESTED SIZE IS NOT POSSIBLE");
                std::exit(1);
            }
            if (choice(engine))
                set.push_back(cammeo[last_c++]);
            else
                set.push_back(osmancik[last_o++]);
        }
        return set;
    }
    
    std::vector<record> getTestingSet()
    {
        if (set.empty())
        {
            static std::random_device dev;
            static std::mt19937_64 engine(dev());
            std::uniform_int_distribution choice(0, 1);
            
            while (true)
            {
                if (last_c >= cammeo.size() || last_o >= osmancik.size())
                    break;
                if (choice(engine))
                    set.push_back(cammeo[last_c++]);
                else
                    set.push_back(osmancik[last_o++]);
            }
            
            for (; last_c < cammeo.size(); last_c++)
                set.push_back(cammeo[last_c]);
            for (; last_o < osmancik.size(); last_o++)
                set.push_back(osmancik[last_o]);
        }
        
        return set;
    }
} rice_data;

void vec_randomizer(std::vector<record>& read, std::vector<record>& write)
{
    auto seed_param = get_parameter("random_seed");
    static std::random_device dev;
    auto seed_val = dev();
    if (seed_param != nullptr)
        seed_val = std::stoi(seed_param);
    std::mt19937_64 engine(seed_val);
    while (!read.empty())
    {
        std::uniform_int_distribution choice(0ul, read.size() - 1);
        auto pos = choice(engine);
        write.push_back(read[pos]);
        std::iter_swap(read.end() - 1, read.begin() + static_cast<blt::i64>(pos));
        read.pop_back();
    }
}

void load_rice_data(std::string_view path)
{
    auto file = blt::fs::getLinesFromFile(path);
    size_t index = 0;
    while (!blt::string::contains(file[index++], "@DATA"))
    {}
    std::vector<record> c;
    std::vector<record> o;
    for (std::string_view v : blt::itr_offset(file, index))
    {
        auto data = blt::string::split(v, ',');
        record r{std::stod(data[0]), std::stod(data[1]), std::stod(data[2]), std::stod(data[3]), std::stod(data[4]), std::stod(data[5]),
                 std::stod(data[6])};
        std::memset(r.type, 0, 8ul);
        std::memcpy(r.type, data[7].data(), std::min(data[7].size(), 8ul));
        if (blt::string::contains(data[7], "Cammeo"))
            c.push_back(r);
        else
            o.push_back(r);
    }
    
    vec_randomizer(c, rice_data.cammeo);
    vec_randomizer(o, rice_data.osmancik);
}

extern "C" int app_build_function_sets(void)
{
    function_set fset;
    user_treeinfo tree_map;
    function sets[] =
            {{cxx_d(f_multiply), nullptr, nullptr, 2, "*", FUNC_DATA, -1, 0, 0, {0, 0}},
             {cxx_d(f_protdivide), nullptr, nullptr, 2, "/", FUNC_DATA, -1, 0, 0, {0, 0}},
             {cxx_d(f_add), nullptr, nullptr, 2, "+", FUNC_DATA, -1, 0, 0, {0, 0}},
             {cxx_d(f_subtract), nullptr, nullptr, 2, "-", FUNC_DATA, -1, 0, 0, {0, 0}},
             {cxx_d(f_exp), nullptr, nullptr, 1, "exp", FUNC_DATA, -1, 0, 0, {0, 0}},
             {cxx_d(f_rlog), nullptr, nullptr, 1, "log", FUNC_DATA, -1, 0, 0, {0, 0}},
#ifndef PART_B
                    {cxx_d(f_sin), nullptr, nullptr, 1, "sin", FUNC_DATA, -1, 0, 0, {0, 0}},
                    {cxx_d(f_cos), nullptr, nullptr, 1, "cos", FUNC_DATA, -1, 0, 0, {0, 0}},
                    {cxx_d(f_var), nullptr, nullptr, 0, "x", TERM_NORM, -1, 0, 0, {0, 0}},
#else
             {cxx_d(f_area), nullptr, nullptr, 0, "area", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_perimeter), nullptr, nullptr, 0, "perimeter", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_major_axis_length), nullptr, nullptr, 0, "major", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_minor_axis_length), nullptr, nullptr, 0, "minor", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_eccentricity), nullptr, nullptr, 0, "eccentricity", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_convex_area), nullptr, nullptr, 0, "convex", TERM_NORM, -1, 0, 0, {0, 0}},
             {cxx_d(f_extent), nullptr, nullptr, 0, "extent", TERM_NORM, -1, 0, 0, {0, 0}},
#endif
             {nullptr, f_erc_gen, cxx_c(f_erc_print), 0, "R", TERM_ERC, -1, 0, 0, {0, 0}}};
    
    binary_parameter("app.use_ercs", 1);
#ifndef PART_B
    if (atoi(get_parameter("app.use_ercs")))
        fset.size = 10;
    else
        fset.size = 9;
#else
    if (atoi(get_parameter("app.use_ercs")))
        fset.size = 14;
    else
        fset.size = 13;
#endif
    
    fset.cset = sets;
    
    tree_map.fset = 0;
    tree_map.return_type = 0;
    tree_map.name = "TREE";
    
    return function_sets_init(&fset, 1, &tree_map, 1);
}

extern "C" void app_eval_fitness(individual* ind)
{
#ifdef PART_B
    int i;
    double v;
    bool dv;
#else
    int i;
    double v, dv;
    double disp;
#endif
    globaldata* g = get_globaldata();
    
    set_current_individual(ind);
    
    ind->r_fitness = 0.0;
    ind->hits = 0;
    
    for (i = 0; i < fitness_cases; ++i)
    {
#ifdef PART_B
        g->area = app_fitness_cases[0][i];
        g->perimeter = app_fitness_cases[1][i];
        g->major_axis_length = app_fitness_cases[2][i];
        g->minor_axis_length = app_fitness_cases[3][i];
        g->eccentricity = app_fitness_cases[4][i];
        g->convex_area = app_fitness_cases[5][i];
        g->extent = app_fitness_cases[6][i];
        dv = app_fitness_cases[7][i] > 0;
#else
        g->x = app_fitness_cases[0][i];
        dv = app_fitness_cases[1][i];
#endif
        v = evaluate_tree(ind->tr[0].data, 0);
#ifdef PART_B
        if ((v >= 0 && dv) || (v < 0 && !dv))
            ind->hits++;
#else
        disp = fabs(dv - v);
        
        if (disp < value_cutoff)
        {
            ind->r_fitness += disp;
            if (disp <= 0.01)
                ++ind->hits;
        } else
        {
            ind->r_fitness += value_cutoff;
        }
#endif
    }
    ind->r_fitness = ind->hits;
    ind->s_fitness = ind->r_fitness;
    ind->a_fitness = 1 - (1 / (1 + ind->s_fitness));
    ind->evald = EVAL_CACHE_VALID;
}

struct annoying
{
    blt::size_t cc = 0;
    blt::size_t co = 0;
    blt::size_t oo = 0;
    blt::size_t oc = 0;
};

extern "C" int app_end_of_evaluation(int gen, multipop* mpop, int newbest, popstats* gen_stats, popstats* run_stats)
{
    globaldata* g = get_globaldata();
    
    if (newbest)
    {
        output_stream_open(OUT_USER);
        
        auto ind = run_stats[0].best[0]->ind;
        
#ifdef PART_B
        //oprintf(OUT_USER, 20, "(%lf, %lf)\n", g->x, v);
        globaldata* g = get_globaldata();
        set_current_individual(ind);
        
        auto testing = rice_data.getTestingSet();
        
        annoying results;
        
        for (const auto& r : testing)
        {
            g->area = r.area;
            g->perimeter = r.perimeter;
            g->major_axis_length = r.major_axis_length;
            g->minor_axis_length = r.minor_axis_length;
            g->eccentricity = r.eccentricity;
            g->convex_area = r.convex_area;
            g->extent = r.extent;
            auto dv = r.type[0] == 'C';
            auto v = evaluate_tree(ind->tr[0].data, 0);
            
            if (dv){
                if (v >= 0)
                    results.cc++;
                else if (v < 0)
                    results.co++;
            } else {
                if (v < 0)
                    results.oo++;
                else if (v >= 0)
                    results.oc++;
            }
        }
        
        oprintf(OUT_USER, 50, "Hits: %ld, Total Size: %ld, Percent Hit: %lf\n", results.cc + results.oo, testing.size(),
                static_cast<double>(results.cc + results.oo) / static_cast<double>(testing.size()) * 100);
        oprintf(OUT_USER, 50, "CC: %ld\nCO: %ld\nOO: %ld\nOC: %ld\n", results.cc, results.co, results.oo, results.oc);
        oprintf(OUT_USER, 50, "\n");
#endif
        pretty_print_tree_equ(ind->tr[0].data, output_filehandle(OUT_USER));
        pretty_print_tree(ind->tr[0].data, output_filehandle(OUT_USER));
        
        output_stream_close(OUT_USER);
        
        if (run_stats[0].best[0]->ind->hits == fitness_cases)
            return 1;
    }
    
    return 0;
}

extern "C" void app_end_of_breeding(int gen, multipop* mpop)
{}

extern "C" int app_create_output_streams(void)
{
    if (create_output_stream(OUT_USER, ".fn", 1, "w", 0) != OUTPUT_OK)
        return 1;
    return 0;
}

extern "C" int app_initialize(int startfromcheckpoint)
{
    // TODO: maybe make this dynamic
#ifdef PART_B
    // evil hack
    auto loc_param = get_parameter("rice_file");
    if (loc_param == nullptr)
    {
        BLT_FATAL("You must provide a rice file to operate on!");
        std::exit(1);
    }
    load_rice_data(loc_param);
#endif
    int i;
    double x, y;
    char* param;
    globaldata* g = get_globaldata();
    
    if (!startfromcheckpoint)
    {
        oprintf(OUT_PRG, 50, "not starting from checkpoint file.\n");
        
        param = get_parameter("app.fitness_cases");
        if (param == NULL)
        {
#ifdef PART_B
            // about 30% of the data
            fitness_cases = 1000;
#else
            fitness_cases = 200;
#endif
        } else
        {
            fitness_cases = atoi(param);
            if (fitness_cases < 0)
                error(E_FATAL_ERROR, "invalid value for \"app.fitness_cases\".");
        }

#ifdef PART_B
        app_fitness_cases[0] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[1] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[2] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[3] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[4] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[5] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[6] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[7] = (double*) MALLOC(fitness_cases * sizeof(double));
#else
        app_fitness_cases[0] = (double*) MALLOC(fitness_cases * sizeof(double));
        app_fitness_cases[1] = (double*) MALLOC(fitness_cases * sizeof(double));
#endif
        
        oprintf(OUT_PRG, 50, "%d fitness cases:\n", fitness_cases);
        auto data = rice_data.getTrainingSet(fitness_cases);
        for (i = 0; i < fitness_cases; ++i)
        {
#ifndef PART_B
            const double range = 10;
            const double half_range = range / 2.0;
            x = (random_double(&globrand) * range) - half_range;
            
            /* change this line to modify the goal function. */
            y = x * x * x * x + x * x * x + x * x + x;
////            y = x * cos(x) + sin(x) * log(x + x * x + x) + x * x;
///*			y = x*x; */
            
            app_fitness_cases[0][i] = x;
            app_fitness_cases[1][i] = y;
            oprintf(OUT_PRG, 50, "    x = %12.5lf, y = %12.5lf\n", x, y);
#else
            app_fitness_cases[0][i] = data[i].area;
            app_fitness_cases[1][i] = data[i].perimeter;
            app_fitness_cases[2][i] = data[i].major_axis_length;
            app_fitness_cases[3][i] = data[i].minor_axis_length;
            app_fitness_cases[4][i] = data[i].eccentricity;
            app_fitness_cases[5][i] = data[i].convex_area;
            app_fitness_cases[6][i] = data[i].extent;
            app_fitness_cases[7][i] = (data[i].type[0] == 'C') ? 50 : -50;
            oprintf(OUT_PRG, 50,
                    "    area = %12.5lf, perimeter = %12.5lf, major_axis_length = %12.5lf, minor_axis_length = %12.5lf, eccentricity = %12.5lf, convex_area = %12.5lf, extent = %12.5lf, type = %c\n",
                    data[i].area, data[i].perimeter, data[i].major_axis_length, data[i].minor_axis_length, data[i].eccentricity, data[i].convex_area,
                    data[i].extent, data[i].type[0]);
#endif
        
        }
    } else
    {
        oprintf(OUT_PRG, 50, "started from checkpoint file.\n");
    }
    
    param = get_parameter("app.value_cutoff");
    if (param == NULL)
        value_cutoff = 1.e15;
    else
        value_cutoff = strtod(param, NULL);
    
    return 0;
}

extern "C" void app_uninitialize(void)
{
    
    
    FREE(app_fitness_cases[0]);
    FREE(app_fitness_cases[1]);
}

extern "C" void app_write_checkpoint(FILE* f)
{
    int i;
    fprintf(f, "fitness-cases: %d\n", fitness_cases);
    for (i = 0; i < fitness_cases; ++i)
    {
        write_hex_block(app_fitness_cases[0] + i, sizeof(double), f);
        fputc(' ', f);
        write_hex_block(app_fitness_cases[1] + i, sizeof(double), f);
        fprintf(f, " %.5lf %.5lf\n",
                app_fitness_cases[0][i], app_fitness_cases[1][i]);
    }
}

extern "C" void app_read_checkpoint(FILE* f)
{
    int i;
    
    fscanf(f, "%*s %d\n", &fitness_cases);
    
    app_fitness_cases[0] = (double*) MALLOC(fitness_cases *
                                            sizeof(double));
    app_fitness_cases[1] = (double*) MALLOC(fitness_cases *
                                            sizeof(double));
    
    for (i = 0; i < fitness_cases; ++i)
    {
        read_hex_block(app_fitness_cases[0] + i, sizeof(double), f);
        fgetc(f);
        read_hex_block(app_fitness_cases[1] + i, sizeof(double), f);
        fscanf(f, " %*f %*f\n");
        fprintf(stderr, "%.5lf %.5lf\n", app_fitness_cases[0][i],
                app_fitness_cases[1][i]);
    }
}
