#include "make_table.hpp"

#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <fstream>
#include <map>
#include <memory>
#include <tuple>

int closest_level(const std::map<int, Chip_vol>& levels, double sample)
{
    // find the closest volume level to the sample we calculated
    auto iter_next = levels.upper_bound(static_cast<int>(sample));
    if (iter_next == levels.end())
        --iter_next;

    auto iter_prev = iter_next;
    if (iter_prev != levels.begin())
        --iter_prev;

    // we now have two candidates for the level we want

    int level_next = iter_next->first;
    int level_prev = iter_prev->first;

    double diff_next = std::abs(sample - level_next);
    double diff_prev = std::abs(sample - level_prev);

    return (diff_next < diff_prev) ? level_next : level_prev;
}

int closest_level(const std::array<int, 16>& levels, double sample)
{
    // find the closest volume level to the sample we calculated
    auto iter_next = std::upper_bound(levels.begin(), levels.end(), static_cast<int>(sample));
    if (iter_next == levels.end())
        --iter_next;

    auto iter_prev = iter_next;
    if (iter_prev != levels.begin())
        --iter_prev;

    // we now have two candidates for the level we want

    int level_next = *iter_next;
    int level_prev = *iter_prev;

    double diff_next = std::abs(sample - level_next);
    double diff_prev = std::abs(sample - level_prev);

    auto next_index = iter_next - levels.begin();
    auto prev_index = iter_prev - levels.begin();

    return (diff_next < diff_prev) ? next_index : prev_index;
}

auto make_table(const std::map<int, Chip_vol>& volume_map, const std::array<double, 256>& weights, int lower, int range)
{
    auto table = std::make_unique<std::array<Chip_vol, 256>>();
    double error_rate {0};

    // generate all 256 sample values
    for (int i = 0; i != 256; ++i)
    {
        double sample {static_cast<double>(i)*range / 255 + lower};

        int level = closest_level(volume_map, sample);

        double diff {std::abs(sample - level)};
        error_rate += diff * weights[i] / range;

        table->at(i) = volume_map.at(level);

        /*std::cout << "i:" << i << "\tSample: " << sample << "\tChosen: " << level
            << "\tLevels: [" << table->at(i).at(0) << ", " << table->at(i).at(1) << ", " << table->at(i).at(2) << "]" << std::endl;*/
    }

    return std::make_tuple(std::move(table), error_rate);
}

// specify low and high or set both to zero if don't care
std::unique_ptr<std::array<Chip_vol, 256>> best_table(const std::array<int, 16>& levels, int low, int high, bool use_weights)
{
    // create a map of all possible volume levels we can make
    // with a combination of three AY channels

    // we can then use this to get the best possible 8-bit mapping
    std::map<int, Chip_vol> volume_map;

    std::cout << "Generating volume map..." << std::endl;
    for (int a {0}; a != 16; ++a)
    {
        for (int b {0}; b != 16; ++b)
        {
            for (int c {0}; c != 16; ++c)
            {
                int vol {levels.at(a) + levels.at(b) + levels.at(c)};

                volume_map[vol] = Chip_vol {a, b, c};
            }
        }
    }

    std::array<double, 256> weights;

    if (use_weights)
    {
        weights.fill(0);

        std::ifstream wav_sample {"sample.wav", std::ios::binary};
        if (wav_sample.is_open())
        {
            wav_sample.seekg(50, std::ios::beg); // skip past the header

            unsigned char sample;
            int count {0};

            while (wav_sample >> sample)
            {
                ++count;
                ++weights[sample];
            }

            wav_sample.close();

            for (int i {0}; i != 256; ++i)
            {
                weights[i] = weights[i] * 256 / count;

                std::cout << "weights[" << i << "]\t=" << weights[i] << '\n';
            }
        }
    }
    else
    {
        weights.fill(1);
    }

    double best_error_rate {std::numeric_limits<double>::max()};
    int best_lower_val {0};
    int best_upper_val {0};

    int count {0};
    // test (some) possible volume ranges

    if (!low && !high)
    {
        for (int lower_val {-2000}; lower_val <= 2000; lower_val += 10)
        {
            double local_error_rate {std::numeric_limits<double>::max()};
            int local_lower_val {0};
            int local_upper_val {0};

            for (int upper_val {lower_val + 10}; upper_val <= 3 * levels.at(15) + 2000; upper_val += 10)
            {
                int range {upper_val - lower_val};

                /* if (!range)
                    continue;*/

                auto table = make_table(volume_map, weights, lower_val, range);
                double error_rate {std::get<1>(table)};

                if (error_rate < best_error_rate)
                {
                    best_error_rate = error_rate;
                    best_lower_val = lower_val;
                    best_upper_val = upper_val;
                }

                if (error_rate < local_error_rate)
                {
                    local_error_rate = error_rate;
                    local_lower_val = lower_val;
                    local_upper_val = upper_val;
                }
            }

            std::cout << "[" << local_lower_val << ", " << local_upper_val << "] -> " << local_error_rate << std::endl;
        }

        // we've found the best range to use, so we can now generate the table
        // TODO structured bindings
        return std::get<0>(make_table(volume_map, weights, best_lower_val, best_upper_val - best_lower_val));
    }
    else
    {
        int range {high - low};
        return std::get<0>(make_table(volume_map, weights, low, range));
    }
}

std::unique_ptr<std::array<int, 256>> best_table_4bit(const std::array<int, 16>& levels, int low, int high)
{
    auto table {std::make_unique<std::array<int, 256>>()};

    const int range {high - low};

    // generate all 256 sample values
    for (int i = 0; i != 256; ++i)
    {
        const double sample {static_cast<double>(i)*range / 255 + low};

        const int level {closest_level(levels, sample)};

        table->at(i) = level;
    }

    return table;
}
