#include "synth.h"
#include "code.h"
#include <iostream>
#include <map>
#include <magic_enum.hpp>
#include "midi.h"
#include <argparse/argparse.hpp>
#include <log4cxx/logger.h>
#include <log4cxx/basicconfigurator.h>

#include <thread>
#include <chrono>

// TODO JMT: Does fluidsynth define this itself?
constexpr int ALL_CHANNELS = -1;
constexpr double DEFAULT_TONE_DURATION = 1.0;
constexpr double DEFAULT_SILENCE_DURATION = 0.2;

static auto logger = log4cxx::Logger::getLogger("tone-generator");

/**
 * The MIDI format only has a fixed number of notes it can play - 128 individual frequencies.
 * The default tuning is an equal temperament around middle C, but that means that it is impossible
 * to exactly play the the SELCAL tones. Worse again, using the nearest standard MIDI note available
 * results in collisions for SELCAL tones.
 * The way around this is to define a custom MIDI tuning. We're still limited to 128 frequencies,
 * but we can make those frequencies anything we want. This method generates a tuning in the format
 * that fluidsynth accepts which _only_ contains the SELCAL tones.
 */
Tuning getSelcalTunings() {
    Tuning tunings;
    tunings.fill(0);

    for (auto pair : SELCAL::KeyFrequencies) {
        // fluidsynth wants tunings in MIDI cents, which effectively means MIDI key * 100.
        // As we only care about the SELCAL 32 tones, only tune those specific keys. We don't
        // need to tune the full 128-key keyboard.
        constexpr double MIDI_CENT_SCALE = 100.0;
        const int keyIndex = static_cast<int>(pair.first);
        tunings[keyIndex] = frequencyToMidi(pair.second) * MIDI_CENT_SCALE;
    }

    return tunings;
}


int main(int argc, char** argv) {
    // Required to be up front before any logging occurs.
    log4cxx::BasicConfigurator::configure();

    double toneDuration = DEFAULT_TONE_DURATION;
    double silenceDuration = DEFAULT_SILENCE_DURATION;
    bool listInstruments = false;

    argparse::ArgumentParser parser("tone-generator");

    parser.add_argument("-d", "--duration")
        .default_value(DEFAULT_TONE_DURATION)
        .store_into(toneDuration)
        .help("Duration of the tone for each group in the SELCAL code.");

    parser.add_argument("-s", "--silence")
        .default_value(DEFAULT_SILENCE_DURATION)
        .store_into(silenceDuration)
        .help("Duration of the silence between tone groups.");

    parser.add_argument("--soundfont")
        .nargs(argparse::nargs_pattern::any)
        .help("Additional soundfont(s) to load into the synth. Accepts files in .sf2 format.");

    parser.add_argument("--instrument")
        .default_value("midi")
        .help("The soundfont synth profile to use when generating tones.");

    parser.add_argument("--list-instruments")
        .flag()
        .store_into(listInstruments)
        .help("List available instrument profiles and exit.");

    parser.add_argument("code")
        .help("Selcal code in the format AB-CD.");

    try {
        parser.parse_args(argc, argv);
    }
    catch (const std::exception& err) {
        std::cerr << err.what() << std::endl;
        std::cerr << parser;
        return 1;
    }


    Synth synth;

    auto tunings = getSelcalTunings();
    if (!synth.setTuning(tunings)) {
        LOG4CXX_ERROR(logger, "Failed to set SELCAL tuning on synth");
        return 1;
    }

    auto files = parser.get<std::vector<std::string>>("--soundfont");
    for (auto file : files) {
        LOG4CXX_INFO(logger, "Loading soundfont from " << file);
        synth.loadSoundfont(file);
    }

    if (listInstruments) {
        for (const Synth::Program& program : synth.getPrograms()) {
            std::cout << program.name << ", ";
        }
        std::cout << std::endl;
        return 0;
    }
    else {
        for (const Synth::Program& program : synth.getPrograms()) {
            if (program.name == parser.get("--instrument")) {
                LOG4CXX_INFO(logger, "Loading program " << program.name);
                synth.loadProgram(program);
                break;
            }
        }
    }


    // TODO JMT: Hacky, find a better way to interact with the synth
    fluid_synth_t* raw_synth = synth.getSynth();

    SELCAL::Code code{parser.get<std::string>("code")};

    for (auto& group : code.getGroups()) {
        fluid_synth_noteon(raw_synth, 0, static_cast<int>(group[0]), 127);
        fluid_synth_noteon(raw_synth, 1, static_cast<int>(group[1]), 127);
        std::this_thread::sleep_for(std::chrono::duration<double>(toneDuration));

        // Silence between tone groups
        fluid_synth_all_notes_off(raw_synth, ALL_CHANNELS);
        std::this_thread::sleep_for(std::chrono::duration<double>(silenceDuration));
    }

    // Wait long enough for the full tone to play before tearing down.
    using namespace std::chrono_literals;
    std::this_thread::sleep_for(500ms);
}
