/* 
 * Copyright (C) 2011 Daniel Richman
 * License: GNU GPL 3
 *
 * flights.cxx: flight and payload document management, GUI and autoconfig.
 */

#include "dl_fldigi/flights.h"

#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <set>
#include <unistd.h>

#include "main.h"
#include "debug.h"
#include "fl_digi.h"
#include "configuration.h"
#include "confdialog.h"
#include "timeops.h"

#include "jsoncpp.h"
#include "habitat/RFC3339.h"
#include "dl_fldigi/dl_fldigi.h"
#include "dl_fldigi/hbtint.h"

using namespace std;

namespace dl_fldigi {
namespace flights {

bool downloaded_flights_once, downloaded_payloads_once;

static string flight_cache_file, payload_cache_file;
static vector<Json::Value> flight_docs, payload_docs;

/* These pointers just point at some part of the heap allocated by something
 * in either the flight_docs vector (if cur_heap == TRACKING_FLIGHT) or 
 * the payload_docs vector (if cur_heap == TRACKING_PAYLOAD).
 * They're invalidated when the relevant vector is modified. When new data is
 * downloaded, the relvant populate_{flights,payloads} function will update
 * these if necessary.
 * hbtint::extrmgr->payload should be called when cur_payload is updated.
 * The data pointed to must not be modified at all while extrmgr has a
 * pointer to it. populate_*'s cleanup actions remove it before modifying. */
static const Json::Value *cur_flight, *cur_payload, *cur_transmission;
static int cur_transmission_index, cur_transmission_count;
static int payload_search_first = 1;
/* managed by select_flight and select_payload. Checked by
 * select_flight_payload, populate_flights and populate_payloads */
static enum tracking_type_enum cur_heap = TRACKING_NOTHING;

static void load_cache_file(const string &name, vector<Json::Value> &target);
static void write_cache_file(const string &name,
                             const vector<Json::Value> &docs);

/* Note: these functions, in the menus they populate, store the index of the
 * Json::Value in the array it's contained in as the userdata of the item,
 * cast (int) -> (void *) */
static void populate_flights();
static void populate_payloads();

static void select_flight_payload(int index);
static void do_select_payload(const Json::Value &payload);
static void select_transmission(int index);

static void autoconfigure_rtty(const Json::Value &settings);
static void autoconfigure_rtty_shift(const Json::Value &value);
static void autoconfigure_rtty_baud(const Json::Value &value);
static void autoconfigure_rtty_encoding(const Json::Value &value);
static void autoconfigure_rtty_parity(const Json::Value &value);
static void autoconfigure_rtty_stop(const Json::Value &value);
static void autoconfigure_dominoex(const Json::Value &settings);
static void autoconfigure_hellschreiber(const Json::Value &settings);

static string escape_menu_string(const string &s_);
static string escape_browser_string(const string &s_);
static string squash_string(const char *str);
static string join_set(const set<string> &items, const string &sep=", ");

static string flight_choice_item(const string &name,
                                 const string &callsign_list,
                                 int attempt);
static string flight_browser_item(const string &name, const string &date,
                                  const string &callsign_list);
static string payload_browser_item(const string &name,
                                   const string &callsign_list,
                                   const string &description);
static string flight_payload_menu_item(const string &name,
                                       const string &callsign_list,
                                       int attempt);
static string mode_menu_name(int index, const Json::Value &settings);

static string flight_callsign_list(const Json::Value &flight);
static string flight_launch_date(const Json::Value &flight);
static string payload_callsign_list(const Json::Value &payload);

static void flight_choice_callback(Fl_Widget *w, void *a);
static void flight_payload_choice_callback(Fl_Widget *w, void *a);
static void mode_choice_callback(Fl_Widget *w, void *a);

void init()
{
    /* called with Fl lock acquired */

    flight_cache_file = HomeDir + "flight_docs.json";
    payload_cache_file = HomeDir + "payload_configuration_docs.json";
}

void cleanup()
{
    /* called with Fl lock acquired */

    flight_docs.clear();
    payload_docs.clear();
    cur_flight = NULL;
    cur_payload = NULL;
    cur_transmission = NULL;
    cur_transmission_index = 0;
    cur_transmission_count = 0;
    cur_heap = TRACKING_NOTHING;
}

void load_cache()
{
    /* resets everything */
    select_flight(-1);

    /* called with Fl lock acquired */

    load_cache_file(flight_cache_file, flight_docs);
    load_cache_file(payload_cache_file, payload_docs);

    populate_flights();
    populate_payloads();
}

void new_flight_docs(const vector<Json::Value> &new_flights)
{
    Fl_AutoLock lock;
    flight_docs = new_flights;
    downloaded_flights_once = true;
    write_cache_file(flight_cache_file, flight_docs);
    populate_flights();
}

void new_payload_docs(const vector<Json::Value> &new_payloads)
{
    Fl_AutoLock lock;
    payload_docs = new_payloads;
    downloaded_payloads_once = true;
    write_cache_file(payload_cache_file, payload_docs);
    populate_payloads();
}

void payload_search(bool next)
{
    /* Searching the payload_browser rather than looking through our JSON docs?
     * Well: it's easier in several ways, and quicker. We've already extracted
     * the important strings in populate_flights and filtered for testing
     * flights; that would have to be duplicated. */

    Fl_AutoLock lock;

    const string search(squash_string(payload_search_text->value()));
    if (!search.size())
        return;

    int n = payload_browser->size();

    if (!n)
        return;

    if (!next || payload_search_first > n)
        payload_search_first = 1;

    int i = payload_search_first;

    do
    {
        const string line(squash_string(payload_browser->text(i)));

        if (line.find(search) != string::npos)
        {
            payload_browser->value(i);
            select_payload(i - 1);

            payload_search_first = i + 1;
            break;
        }

        i++;
        if (i > n)
            i = 1;
    }
    while (i != payload_search_first);
}

void select_flight(int index)
{
    Fl_AutoLock lock;

    LOG_DEBUG("Selecting flight, index %i", index);

    /* Reset */
    cur_flight = NULL;
    cur_heap = TRACKING_NOTHING;

    if (hab_ui_exists)
    {
        habCHPayload->value(-1);
        habCHPayload->clear();
        habCHPayload->deactivate();
    }

    flight_payload_list->value(-1);
    flight_payload_list->clear();
    flight_payload_list->deactivate();

    do_select_payload(Json::Value::null);

    /* Tests */
    if (index < 0 || index >= int(flight_docs.size()))
        return;

    const Json::Value &flight = flight_docs[index];

    if (!flight.isObject() || !flight.size() || !flight["_id"].isString())
        return;

    const string id = flight["_id"].asString();
    const Json::Value &payloads = flight["_payload_docs"];

    if (!id.size() || !payloads.isArray() || !payloads.size())
        return;

    LOG_DEBUG("flight OK. contains %i payloads", payloads.size());

    /* Doc looks ok, so set up */
    cur_flight = &flight;
    cur_heap = TRACKING_FLIGHT;

    if (progdefaults.tracking_type != TRACKING_FLIGHT || 
        progdefaults.tracking_doc != id)
    {
        progdefaults.tracking_type = TRACKING_FLIGHT;
        progdefaults.tracking_doc = id;
        progdefaults.tracking_flight_payload = -1;
        progdefaults.tracking_transmission = -1;
        progdefaults.changed = true;
    }

    set<string> choice_items;

    for (Json::Value::const_iterator it = payloads.begin();
            it != payloads.end(); ++it)
    {
        const Json::Value &root = *it;
        string name, callsigns;

        if (root.isObject() && root["name"].isString())
        {
            name = root["name"].asString();
            callsigns = payload_callsign_list(root);
        }
        else
        {
            name = "Unknown";
        }

        int attempt = 1;
        string item;

        do
        {
            item = flight_payload_menu_item(name, callsigns, attempt);
            attempt++;
        }
        while(choice_items.count(item));
        choice_items.insert(item);

        if (hab_ui_exists)
            habCHPayload->add(item.c_str(), (int) 0,
                              flight_payload_choice_callback, NULL);
        flight_payload_list->add(item.c_str(), (int) 0,
                                 flight_payload_choice_callback, NULL);
    }

    int auto_select = progdefaults.tracking_flight_payload;
    if (auto_select < 0 || auto_select >= int(payloads.size()))
        auto_select = 0;

    if (hab_ui_exists)
    {
        habCHPayload->activate();
        habCHPayload->value(auto_select);
    }

    flight_payload_list->activate();
    flight_payload_list->value(auto_select);

    select_flight_payload(auto_select);
}

void select_payload(int index)
{
    select_flight(-1);
    do_select_payload(Json::Value::null);

    cur_heap = TRACKING_NOTHING;

    LOG_DEBUG("Selecting payload, index %i", index);

    if (index < 0 || index >= int(payload_docs.size()))
        return;

    const Json::Value &payload = payload_docs[index];

    if (!payload.isObject() || !payload.size() || !payload["_id"].isString())
        return;

    const string id = payload["_id"].asString();

    if (progdefaults.tracking_type != TRACKING_PAYLOAD ||
        progdefaults.tracking_doc != id)
    {
        progdefaults.tracking_type = TRACKING_PAYLOAD;
        progdefaults.tracking_doc = id;
        progdefaults.tracking_flight_payload = -1;
        progdefaults.tracking_transmission = -1;
        progdefaults.changed = true;
    }

    cur_heap = TRACKING_PAYLOAD;
    do_select_payload(payload);
}

void auto_configure()
{
    Fl_AutoLock lock;

    LOG_DEBUG("autoconfiguring");

    if (!cur_transmission)
        return;

    const Json::Value &settings = *cur_transmission;

    if (!settings.isObject() || !settings.size())
        return;

    if (!settings["modulation"].isString())
        return;

    const string modulation = settings["modulation"].asString();

    if (modulation == "RTTY")
        autoconfigure_rtty(settings);
    else if (modulation == "DominoEX")
        autoconfigure_dominoex(settings);
    else if (modulation == "Hellschreiber")
        autoconfigure_hellschreiber(settings);
}

void auto_switchmode()
{
    Fl_AutoLock lock;

    LOG_DEBUG("autoswitchmoding");

    int next = cur_transmission_index + 1;
    if (next >= cur_transmission_count)
        next = 0;

    if (hab_ui_exists)
        habCHTransmission->value(next);

    if (cur_heap == TRACKING_PAYLOAD)
        payload_transmission_list->value(next);

    if (cur_heap == TRACKING_FLIGHT)
        flight_payload_transmission_list->value(next);

    select_transmission(next);
    auto_configure();
}

static void load_cache_file(const string &name, vector<Json::Value> &target)
{
    ifstream cf(name.c_str());

    if (cf.fail())
    {
        LOG_DEBUG("Failed to open cache file %s", name.c_str());
        return;
    }

    target.clear();

    while (cf.good())
    {
        string line;
        getline(cf, line, '\n');

        char discard;
        while (cf.good() && cf.peek() == '\n')
            cf.get(discard);

        Json::Reader reader;
        Json::Value root;
        if (!reader.parse(line, root, false))
            break;

        target.push_back(root);
    }

    bool failed = cf.fail() || !cf.eof();

    cf.close();

    if (failed)
    {
        target.clear();
        LOG_WARN("Failed to load %s", name.c_str());
    }
    else
    {
        long int n = target.size();
        LOG_DEBUG("Loaded %li docs from file %s", n, name.c_str());
    }
}

static void write_cache_file(const string &name,
                             const vector<Json::Value> &docs)
{
    ofstream cf(name.c_str(), ios_base::out | ios_base::trunc);

    for (vector<Json::Value>::const_iterator it = docs.begin();
         it != docs.end() && cf.good();
         it++)
    {
        Json::FastWriter writer;
        cf << writer.write(*it);
    }

    bool success = cf.good();

    cf.close();

    if (!success)
    {
        LOG_WARN("unable to save docs to %s", name.c_str());
        unlink(name.c_str());
    }
}

static void populate_flights()
{
    Fl_AutoLock lock;

    LOG_DEBUG("populating flights (%zi)", flight_docs.size());

    set<string> choice_items;

    if (hab_ui_exists)
    {
        habFlight->value(-1);
        habFlight->clear();
    }

    flight_browser->clear();

    if (cur_heap == TRACKING_FLIGHT)
        select_flight(-1);

    for (int i = 0; i < int(flight_docs.size()); i++)
    {
        const Json::Value &root = flight_docs[i];

        string id, name, callsign_list, date;

        if (root.isObject() && root.size() &&
            root["_id"].isString() && root["name"].isString())
        {
            id = root["_id"].asString();
            name = root["name"].asString();
            callsign_list = flight_callsign_list(root);
            date = flight_launch_date(root);
        }

        bool root_ok = (id.size() && name.size());

        if (!root_ok)
        {
            name = "Invalid flight doc";
            LOG_WARN("invalid flight doc");
        }

        if (hab_ui_exists)
        {
            string item;
            int attempt = 1;

            /* Avoid duplicate menu items: fltk removes them */
            do
            {
                item = flight_choice_item(name, callsign_list, attempt);
                attempt++;
            }
            while (choice_items.count(item));
            choice_items.insert(item);

            habFlight->add(item.c_str(), (int) 0, flight_choice_callback,
                           NULL);
        }

        string browser_item = flight_browser_item(name, date, callsign_list);
        flight_browser->add(browser_item.c_str(), NULL);

        if (root_ok &&
            progdefaults.tracking_type == TRACKING_FLIGHT &&
            progdefaults.tracking_doc == id)
        {
            if (hab_ui_exists)
                habFlight->value(i);
            flight_browser->value(i + 1);
            select_flight(i);
        }
    }
}

static void populate_payloads()
{
    Fl_AutoLock lock;

    LOG_DEBUG("populating payloads (%zi)", payload_docs.size());

    payload_browser->clear();

    if (cur_heap == TRACKING_PAYLOAD)
        select_payload(-1);

    for (int i = 0; i < int(payload_docs.size()); i++)
    {
        const Json::Value &root = payload_docs[i];

        string id, name, callsign_list, description;

        if (root.isObject() && root.size() &&
            root["_id"].isString() && root["name"].isString())
        {
            id = root["_id"].asString();
            name = root["name"].asString();
            callsign_list = payload_callsign_list(root);

            if (root["metadata"]["description"].isString())
                description = root["metadata"]["description"].asString();
        }

        bool root_ok = (id.size() && name.size());

        if (!root_ok)
            LOG_WARN("invalid payload doc");

        string browser_item = payload_browser_item(name, callsign_list,
                                                   description);
        payload_browser->add(browser_item.c_str(), NULL);

        if (root_ok &&
            progdefaults.tracking_type == TRACKING_PAYLOAD &&
            progdefaults.tracking_doc == id)
        {
            payload_browser->value(i + 1);
            select_payload(i);
        }
    }
}

static void select_flight_payload(int index)
{
    Fl_AutoLock lock;
    LOG_DEBUG("Selecting flight payload %i", index);

    if (!cur_flight || cur_heap != TRACKING_FLIGHT)
        return;

    const Json::Value &payload_list = (*cur_flight)["_payload_docs"];

    if (!payload_list.isArray() || !payload_list.size())
        return;

    if (index < 0 || index >= int(payload_list.size()))
        return;

    if (progdefaults.tracking_flight_payload != index)
    {
        progdefaults.tracking_flight_payload = index;
        progdefaults.tracking_transmission = -1;
        progdefaults.changed = true;
    }

    do_select_payload(payload_list[index]);
}

static void do_select_payload(const Json::Value &payload)
{
    if (payload.isNull())
        LOG_DEBUG("do_select_payload(null)");
    else
        LOG_DEBUG("do_select_payload(object)");

    /* Disable stuff, incase tests fail */
    cur_payload = NULL;
    hbtint::extrmgr->payload(NULL);

    if (hab_ui_exists)
    {
        habCHTransmission->value(-1);
        habCHTransmission->clear();
        habCHTransmission->deactivate();
        habSwitchModes->deactivate();
    }

    payload_transmission_list->value(-1);
    payload_transmission_list->clear();
    payload_transmission_list->deactivate();

    flight_payload_transmission_list->value(-1);
    flight_payload_transmission_list->clear();
    flight_payload_transmission_list->deactivate();

    select_transmission(-1);

    /* Sanity checks */
    if (!payload.isObject() || !payload.size())
        return;

    /* OK. Setup */
    cur_payload = &payload;
    hbtint::extrmgr->payload(&payload);

    LOG_DEBUG("payload OK, checking transmissions");

    const Json::Value &transmissions = payload["transmissions"];

    if (!transmissions.isArray() || !transmissions.size())
        return;

    cur_transmission_count = transmissions.size();
    if (cur_transmission_count < 0)
        cur_transmission_count = 0;

    LOG_DEBUG("populating transmissions (%i)", cur_transmission_count);

    for (int i = 0; i < cur_transmission_count; i++)
    {
        const Json::Value &transmission = transmissions[i];

        const string name = mode_menu_name(i + 1, transmission);

        if (hab_ui_exists)
            habCHTransmission->add(name.c_str(), (int) 0,
                                   mode_choice_callback, NULL);
        if (cur_heap == TRACKING_FLIGHT)
            flight_payload_transmission_list->add(name.c_str(), (int) 0,
                                                  mode_choice_callback, NULL);
        if (cur_heap == TRACKING_PAYLOAD)
            payload_transmission_list->add(name.c_str(), (int) 0,
                                           mode_choice_callback, NULL);
    }

    int auto_select = progdefaults.tracking_transmission;
    if (auto_select < 0 || auto_select >= cur_transmission_count)
        auto_select = 0;

    if (hab_ui_exists)
    {
        if (cur_transmission_count > 1)
            habSwitchModes->activate();

        habCHTransmission->activate();
        habCHTransmission->value(auto_select);
    }

    if (cur_heap == TRACKING_FLIGHT)
    {
        flight_payload_transmission_list->activate();
        flight_payload_transmission_list->value(auto_select);
    }

    if (cur_heap == TRACKING_PAYLOAD)
    {
        payload_transmission_list->activate();
        payload_transmission_list->value(auto_select);
    }

    select_transmission(auto_select);
}

static void select_transmission(int index)
{
    Fl_AutoLock lock;

    LOG_DEBUG("selecting transmission %i", index);

    /* Reset */
    cur_transmission = NULL;
    cur_transmission_index = 0;

    if (hab_ui_exists)
        habConfigureButton->deactivate();

    payload_autoconfigure_a->deactivate();
    payload_autoconfigure_b->deactivate();

    /* Checks */
    if (!cur_payload)
        return;

    const Json::Value &transmissions = (*cur_payload)["transmissions"];

    if (!transmissions.isArray() ||
            int(transmissions.size()) != cur_transmission_count)
        return;

    if (index < 0 || index >= cur_transmission_count)
        return;

    LOG_DEBUG("transmission OK");

    /* OK. Set stuff */
    cur_transmission = &(transmissions[index]);
    cur_transmission_index = index;

    if (progdefaults.tracking_transmission != index)
    {
        progdefaults.tracking_transmission = index;
        progdefaults.changed = true;
    }

    if (hab_ui_exists)
        habConfigureButton->activate();

    payload_autoconfigure_a->activate();
    payload_autoconfigure_b->activate();
}

static void autoconfigure_rtty(const Json::Value &settings)
{
    LOG_DEBUG("autoconfigure rtty");

    autoconfigure_rtty_shift(settings["shift"]);
    autoconfigure_rtty_baud(settings["baud"]);
    autoconfigure_rtty_encoding(settings["encoding"]);
    autoconfigure_rtty_parity(settings["parity"]);
    autoconfigure_rtty_stop(settings["stop"]);

    init_modem_sync(MODE_RTTY);
    resetRTTY();
    progdefaults.changed = true;
}

static void autoconfigure_rtty_shift(const Json::Value &value)
{
    if (!value.isNumeric())
        return;

    double shift = value.asDouble();

    /* Look in the standard shifts first */
    int search;
    for (search = 0; rtty::SHIFT[search] != 0; search++)
    {
        double diff = rtty::SHIFT[search] - shift;
        /* I love floats :-( */
        if (diff < 0.1 && diff > -0.1)
        {
            selShift->index(search);
            selCustomShift->deactivate();
            progdefaults.rtty_shift = search;
            return;
        }
    }

    /* If not found (i.e., we found the terminating 0, and haven't returned)
     * then search == the index of the "Custom" menu item */
    selShift->index(search);
    selCustomShift->activate();
    progdefaults.rtty_shift = search;
    selCustomShift->value(shift);
    progdefaults.rtty_custom_shift = shift;
}

static void autoconfigure_rtty_baud(const Json::Value &value)
{
    if (!value.isNumeric())
        return;

    double baud = value.asDouble();
    int search;
    for (search = 0; rtty::BAUD[search] != 0; search++)
    {
        double diff = rtty::BAUD[search] - baud;
        if (diff < 0.01 && diff > -0.01)
        {
            selBaud->index(search);
            progdefaults.rtty_baud = search;
            return;
        }
    }
}

static void autoconfigure_rtty_encoding(const Json::Value &value)
{
    if (!value.isString())
        return;

    const string encoding = value.asString();
    int select = -1;

    /* rtty::BITS[] = {5, 7, 8}; */
    if (encoding == "BAUDOT")
        select = 0;
    else if (encoding == "ASCII-7")
        select = 1;
    else if (encoding == "ASCII-8")
        select = 2;
    else
        return;

    selBits->index(select);
    progdefaults.rtty_bits = select;

    /* From selBits' callback */
    if (select == 0)
    {
        progdefaults.rtty_parity = RTTY_PARITY_NONE;
        selParity->index(RTTY_PARITY_NONE);
    }
}

static void autoconfigure_rtty_parity(const Json::Value &value)
{
    if (!value.isString())
        return;

    /* Parity is disabled when using baudot (rtty_bits == 0) */
    if (progdefaults.rtty_bits == 0)
        return;

    const string parity = value.asString();
    int select = -1;

    if (parity == "none")
        select = RTTY_PARITY_NONE;
    else if (parity == "even")
        select = RTTY_PARITY_EVEN;
    else if (parity == "odd")
        select = RTTY_PARITY_ODD;
    else if (parity == "zero")
        select = RTTY_PARITY_ZERO;
    else if (parity == "one")
        select = RTTY_PARITY_ONE;
    else
        return;

    selParity->index(select);
    progdefaults.rtty_parity = select;
}

static void autoconfigure_rtty_stop(const Json::Value &value)
{
    if (!value.isNumeric())
        return;

    int select = -1;

    if (value.isInt())
    {
#ifdef JSON_HAS_INT64   /* test if jsoncpp 0.6 */
        int stop = value.asLargestInt();
#else
        int stop = value.asInt();
#endif
        if (stop == 1)
            select = 0;
        else if (stop == 2)
            select = 2;
    }
    else
    {
        double stop = value.asDouble();
        if (stop > 1.49 && stop < 1.51)
            select = 1;
    }

    if (select == -1)
        return;

    progdefaults.rtty_stop = select;
    selStopBits->index(select);
}

static void autoconfigure_dominoex(const Json::Value &settings)
{
    LOG_DEBUG("autoconfigure dominoex");

    if (!settings["speed"].isInt())
        return;

#ifdef JSON_HAS_INT64   /* jsoncpp 0.6 */
    int type = settings["speed"].asLargestInt();
#else
    int type = settings["speed"].asInt();
#endif
    int modem = -1;

    switch (type)
    {
        case 4:
            modem = MODE_DOMINOEX4;
            break;

        case 5:
            modem = MODE_DOMINOEX5;
            break;

        case 8:
            modem = MODE_DOMINOEX8;
            break;

        case 11:
            modem = MODE_DOMINOEX11;
            break;

        case 16:
            modem = MODE_DOMINOEX16;
            break;

        case 22:
            modem = MODE_DOMINOEX22;
            break;
    }

    if (modem != -1)
    {
        init_modem_sync(modem);
        resetDOMEX();
    }
}

static void autoconfigure_hellschreiber(const Json::Value &settings)
{
    LOG_DEBUG("autoconfigure hellschreiber");

    if (!settings["variant"].isString())
        return;

    const string variant = settings["variant"].asString();
    int modem = -1;

    if (variant == "slowhell")
        modem = MODE_SLOWHELL;
    else if (variant == "feldhell")
        modem = MODE_FELDHELL;

    if (modem != -1)
        init_modem_sync(modem);
}

static string escape_menu_string(const string &orig_str)
{
    string new_str;
    size_t pos = 0;

    do
    {
        size_t start = pos;

        pos = orig_str.find_first_of("&/\\_", start);
        new_str.append(orig_str.substr(start, pos - start));

        if (pos != string::npos)
        {
            new_str.push_back('\\');
            new_str.push_back(orig_str[pos]);
            pos++;
        }
    }
    while (pos != string::npos);

    return new_str;
}

static string escape_browser_string(const string &s_)
{
    string s(s_);
    size_t pos = 0;

    do
    {
        pos = s.find('\t', pos);
        if (pos != string::npos)
            s[pos] = ' ';
    }
    while (pos != string::npos);

    return s;
}

static string squash_string(const char *str)
{
    string result;
    result.reserve(strlen(str));

    while (*str)
    {
        char c = *str;
        if (isalnum(c))
            result.push_back(tolower(c));
        str++;
    }

    return result;
}

static string join_set(const set<string> &items, const string &sep)
{
    string result;

    for (set<string>::const_iterator it = items.begin();
            it != items.end(); ++it)
    {
        if (it != items.begin())
            result.append(sep);

        result.append(*it);
    }

    return result;
}

static string flight_choice_item(const string &name,
                                 const string &callsign_list,
                                 int attempt)
{
    /* "<name>: <payload>,<payload>,<payload>" */

    string attempt_suffix;

    if (attempt != 1)
    {
        ostringstream sfx;
        sfx << " (" << attempt << ")";
        attempt_suffix = sfx.str();
    }

    return escape_menu_string(name + ": " + callsign_list + attempt_suffix);
}

static string flight_browser_item(const string &name, const string &date,
                                  const string &callsign_list)
{
    /* "<name>\t<optional date>\t<callsign>,<callsign>,..." */
    return "@." + escape_browser_string(name) + "\t" +
           "@." + date + "\t" +
           "@." + escape_browser_string(callsign_list);
}

static string payload_browser_item(const string &name,
                                   const string &callsign_list,
                                   const string &description)
{
    return "@." + escape_browser_string(name) + "\t"
         + "@." + "(" + escape_browser_string(callsign_list) + ") "
                + escape_browser_string(description);
}

static string flight_payload_menu_item(const string &name,
                                       const string &callsign_list,
                                       int attempt)
{
    /* "<name>" or "<name> (callsign)" + (attempt) */

    string attempt_suffix;

    if (attempt != 1)
    {
        ostringstream sfx;
        sfx << " (" << attempt << ")";
        attempt_suffix = sfx.str();
    }

    return escape_menu_string(name + " (" + callsign_list + ") "
                               + attempt_suffix);
}

static string mode_menu_name(int index, const Json::Value &settings)
{
    /* index: modulation type
     * e.g.:  2: RTTY 300 */

    ostringstream name;

    name << index << ": ";

    string modulation;

    if (settings.isObject() && settings.size() &&
        settings["modulation"].isString())
    {
        modulation = settings["modulation"].asString();
    }

    if (modulation == "RTTY")
    {
        name << "RTTY";

        if (settings["baud"].isInt())
#ifdef JSON_HAS_INT64   /* jsoncpp 0.6 */
            name << ' ' << settings["baud"].asLargestInt();
#else
            name << ' ' << settings["baud"].asInt();
#endif
        else if (settings["baud"].isDouble())
            name << ' ' << settings["baud"].asDouble();
    }
    else if (modulation == "DominoEX" && settings["speed"].isInt())
    {
        name << "DomEX " << settings["speed"].asInt();
    }
    else if (modulation == "Hellschreiber" && settings["variant"].isString())
    {
        string variant = settings["variant"].asString();
        if (variant == "feldhell")
            name << "FeldHell";
        else if (variant == "slowhell")
            name << "SlowHell";
        else
            name << "Hellschreiber";
    }
    else
    {
        name << "Unknown";
    }

    return escape_menu_string(name.str());
}

static string flight_callsign_list(const Json::Value &flight)
{
    const Json::Value &payloads = flight["_payload_docs"];

    if (!payloads.isArray() || !payloads.size())
        return "";

    set<string> callsigns;

    for (Json::Value::const_iterator it = payloads.begin();
            it != payloads.end(); ++it)
    {
        const Json::Value &payload = *it;

        if (!payload.isObject())
            continue;

        const Json::Value &sentences = payload["sentences"];

        if (!sentences.isArray())
            continue;

        for (Json::Value::const_iterator it2 = sentences.begin();
                it2 != sentences.end(); ++it2)
        {
            const Json::Value &sentence = *it2;

            if (!sentence.isObject())
                continue;

            const Json::Value &callsign = sentence["callsign"];

            if (!callsign.isString())
                continue;

            callsigns.insert(callsign.asString());
        }
    }

    /* sets are sorted, so this comes out in the right order: */
    return join_set(callsigns);
}

static string flight_launch_date(const Json::Value &flight)
{
    const Json::Value &launch = flight["launch"];

    if (!launch.isObject() || !launch.size())
        return "";

    if (!launch.isMember("time") || !launch["time"].isString())
        return "";

    time_t date = RFC3339::rfc3339_to_timestamp(launch["time"].asString());
    char buf[20];
    struct tm tm;

    if (localtime_r(&date, &tm) != &tm)
        return "";

    if (strftime(buf, sizeof(buf), "%a %d %b %y", &tm) <= 0)
        return "";

    return buf;
}

static string payload_callsign_list(const Json::Value &payload)
{
    const Json::Value &sentences = payload["sentences"];
    if (!sentences.isArray())
        return "";

    set<string> callsign_list;

    for (Json::Value::const_iterator it = sentences.begin();
            it != sentences.end(); ++it)
    {
        const Json::Value &sentence = *it;

        if (!sentence.isObject())
            continue;

        const Json::Value &callsign = sentence["callsign"];

        if (!callsign.isString())
            continue;

        callsign_list.insert(callsign.asString());
    }

    return join_set(callsign_list);
}

static void flight_payload_choice_callback(Fl_Widget *w, void *a)
{
    Fl_AutoLock lock;

    Fl_Choice *choice = static_cast<Fl_Choice *>(w);

    if (choice != flight_payload_list)
        flight_payload_list->value(choice->value());
    if (hab_ui_exists && choice != habCHPayload)
        habCHPayload->value(choice->value());

    select_flight_payload(choice->value());
}

static void mode_choice_callback(Fl_Widget *w, void *a)
{
    Fl_AutoLock lock;

    Fl_Choice *choice = static_cast<Fl_Choice *>(w);

    if (hab_ui_exists && choice != habCHTransmission)
        habCHTransmission->value(choice->value());
    if (cur_heap == TRACKING_FLIGHT &&
            choice != flight_payload_transmission_list)
        flight_payload_transmission_list->value(choice->value());
    if (cur_heap == TRACKING_PAYLOAD && choice != payload_transmission_list)
        payload_transmission_list->value(choice->value());

    select_transmission(choice->value());
}

static void flight_choice_callback(Fl_Widget *w, void *a)
{
    Fl_AutoLock lock;

    Fl_Choice *choice = static_cast<Fl_Choice *>(w);
    flight_browser->value(choice->value() + 1);

    payload_browser->deselect();
    select_flight(choice->value());
}


} /* namespace flights */
} /* namespace dl_fldigi */
