/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 * Copyright (C) 2016 Furrtek
 * Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
 * Copyright (C) 2023 Kyle Reed
 *
 * This file is part of PortaPack.
 *
 * 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 2, 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "convert.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "tone_key.hpp"
#include "utility.hpp"

#include <array>
#include <cctype>
#include <string_view>
#include <vector>

namespace fs = std::filesystem;

const std::filesystem::path freqman_dir{u"/FREQMAN"};
const std::filesystem::path freqman_extension{u".TXT"};

// NB: Don't include UI headers to keep this code unit testable.
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;

options_t freqman_modulations = {
    {"AM", 0},
    {"NFM", 1},
    {"WFM", 2},
    {"SPEC", 3},
};

options_t freqman_bandwidths[4] = {
    {
        // AM
        {"DSB 9k", 0},
        {"DSB 6k", 1},
        {"USB+3k", 2},
        {"LSB-3k", 3},
        {"CW", 4},
    },
    {
        // NFM
        {"8k5", 0},
        {"11k", 1},
        {"16k", 2},
    },
    {
        // WFM
        {"40k", 2},
        {"180k", 1},
        {"200k", 0},
    },
    {
        // SPEC -- TODO: these should be indexes.
        {"12k5", 12500},
        {"16k", 16000},
        {"25k", 25000},
        {"32k", 32000},
        {"50k", 50000},
        {"75k", 75000},
        {"100k", 100000},
        {"150k", 150000},
        {"250k", 250000},
        {"500k", 500000},
        {"600k", 600000},
        {"750k", 750000},
        {"1000k", 1000000},  // Max bandwith for recording in C16 (with fast SD card).
        {"1250k", 1250000},
        {"1500k", 1500000},
        {"1750k", 1750000},
        {"2000k", 2000000},
        {"2250k", 2250000},  // Max bandwith for recording in C8 (with fast SD card).
        {"2500k", 2500000},  // Here and up, LCD is ok, but M4 CPU drops samples.
        {"3000k", 3000000},
        {"3500k", 3500000},
        {"4000k", 4000000},
        {"4500k", 4500000},
        {"5000k", 5500000},
        {"5500k", 5500000},  // Max capture, needs /4 decimation, (22Mhz sampling ADC).
    },
};

// TODO: these should be indexes.
options_t freqman_steps = {
    {"0.1kHz      ", 100},
    {"1kHz        ", 1000},
    {"5kHz (SA AM)", 5000},
    {"6.25kHz(NFM)", 6250},
    {"8.33kHz(AIR)", 8330},
    {"9kHz (EU AM)", 9000},
    {"10kHz(US AM)", 10000},
    {"12.5kHz(NFM)", 12500},
    {"15kHz  (HFM)", 15000},
    {"25kHz   (N1)", 25000},
    {"30kHz (OIRT)", 30000},
    {"50kHz  (FM1)", 50000},
    {"100kHz (FM2)", 100000},
    {"250kHz  (N2)", 250000},
    {"500kHz (WFM)", 500000},
    {"1MHz        ", 1000000},
};

// TODO: these should be indexes.
options_t freqman_steps_short = {
    {"0.1kHz", 100},
    {"1kHz", 1000},
    {"5kHz", 5000},
    {"6.25kHz", 6250},
    {"8.33kHz", 8330},
    {"9kHz", 9000},
    {"10kHz", 10000},
    {"12.5kHz", 12500},
    {"15kHz", 15000},
    {"25kHz", 25000},
    {"30kHz", 30000},
    {"50kHz", 50000},
    {"100kHz", 100000},
    {"250kHz", 250000},
    {"500kHz", 500000},
    {"1MHz", 1000000},
};

uint8_t find_by_name(const options_t& options, std::string_view name) {
    for (auto ix = 0u; ix < options.size(); ++ix)
        if (options[ix].first == name)
            return ix;

    return freqman_invalid_index;
}

const option_t* find_by_index(const options_t& options, freqman_index_t index) {
    if (index < options.size())
        return &options[index];
    else
        return nullptr;
}

/* Impl for next round of changes.
 *template <typename T, size_t N>
 *const T* find_by_name(const std::array<T, N>& info, std::string_view name) {
 *    for (const auto& it : info) {
 *        if (it.name == name)
 *            return &it;
 *    }
 *
 *    return nullptr;
 *}
 */

bool operator==(const freqman_entry& lhs, const freqman_entry& rhs) {
    auto equal = lhs.type == rhs.type &&
                 lhs.frequency_a == rhs.frequency_a &&
                 lhs.description == rhs.description &&
                 lhs.modulation == rhs.modulation &&
                 lhs.bandwidth == rhs.bandwidth;

    if (!equal)
        return false;

    if (lhs.type == freqman_type::Range) {
        equal = lhs.frequency_b == rhs.frequency_b &&
                lhs.step == rhs.step;
    } else if (lhs.type == freqman_type::HamRadio) {
        equal = lhs.frequency_b == rhs.frequency_b &&
                lhs.tone == rhs.tone;
    } else if (lhs.type == freqman_type::Repeater) {
        equal = lhs.frequency_b == rhs.frequency_b;
    }

    return equal;
}

std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
    if (auto opt = find_by_index(freqman_modulations, modulation))
        return opt->first;
    return {};
}

std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) {
    if (modulation < freqman_modulations.size()) {
        if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
            return opt->first;
    }
    return {};
}

std::string freqman_entry_get_step_string(freqman_index_t step) {
    if (auto opt = find_by_index(freqman_steps, step))
        return opt->first;
    return {};
}

std::string freqman_entry_get_step_string_short(freqman_index_t step) {
    if (auto opt = find_by_index(freqman_steps_short, step))
        return opt->first;
    return {};
}

const std::filesystem::path get_freqman_path(const std::string& stem) {
    return freqman_dir / stem + freqman_extension;
}

bool create_freqman_file(const std::string& file_stem) {
    auto fs_error = make_new_file(get_freqman_path(file_stem));
    return fs_error.ok();
}

bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
    return parse_freqman_file(get_freqman_path(file_stem), db, options);
}

void delete_freqman_file(const std::string& file_stem) {
    delete_file(get_freqman_path(file_stem));
}

std::string pretty_string(const freqman_entry& entry, size_t max_length) {
    std::string str;

    switch (entry.type) {
        case freqman_type::Single:
            str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
            break;
        case freqman_type::Range:
            str = to_string_rounded_freq(entry.frequency_a, 1) + "M-" +
                  to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
            break;
        case freqman_type::HamRadio:
            str = "R:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" +
                  to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
            break;
        case freqman_type::Repeater:
            str = "L:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" +
                  to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
            break;
        case freqman_type::Raw:
            str = entry.description;
            break;
        default:
            str = "UNK:" + entry.description;
            break;
    }

    // Truncate. '+' indicates if string has been truncated.
    if (str.size() > max_length)
        return str.substr(0, max_length - 1) + "+";

    return str;
}

std::string to_freqman_string(const freqman_entry& entry) {
    std::string serialized;
    serialized.reserve(0x80);

    // Append a key=value to the string.
    auto append_field = [&serialized](std::string_view name, std::string_view value) {
        if (!serialized.empty())
            serialized += ",";
        serialized += std::string{name} + "=" + std::string{value};
    };

    switch (entry.type) {
        case freqman_type::Single:
            append_field("f", to_string_dec_uint(entry.frequency_a));
            break;
        case freqman_type::Range:
            append_field("a", to_string_dec_uint(entry.frequency_a));
            append_field("b", to_string_dec_uint(entry.frequency_b));

            if (is_valid(entry.step))
                append_field("s", freqman_entry_get_step_string_short(entry.step));
            break;
        case freqman_type::HamRadio:
            append_field("r", to_string_dec_uint(entry.frequency_a));
            append_field("t", to_string_dec_uint(entry.frequency_b));

            if (is_valid(entry.tone))
                append_field("c", tonekey::tone_key_value_string(entry.tone));
            break;
        case freqman_type::Repeater:
            append_field("l", to_string_dec_uint(entry.frequency_a));
            append_field("t", to_string_dec_uint(entry.frequency_b));
            break;
        case freqman_type::Raw:
            return entry.description;
        default:
            return {};
    };

    if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) {
        append_field("m", freqman_entry_get_modulation_string(entry.modulation));

        if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size())
            append_field("bw", freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth));
    }

    if (entry.description.size() > 0)
        append_field("d", entry.description);

    serialized.shrink_to_fit();
    return serialized;
}

freqman_index_t parse_tone_key(std::string_view value) {
    // Split into whole and fractional parts.
    auto parts = split_string(value, '.');
    int32_t tone_freq = 0;
    int32_t whole_part = 0;
    parse_int(parts[0], whole_part);

    // Tones are stored as frequency / 100 for some reason.
    // E.g. 14572 would be 145.7 (NB: 1s place is dropped).
    // TODO: Might be easier to just store the codes?
    // Multiply the whole part by 100 to get the tone frequency.
    tone_freq = whole_part * 100;

    // Add the fractional part, if present.
    if (parts.size() > 1) {
        auto c = parts[1].front();
        auto digit = std::isdigit(c) ? c - '0' : 0;
        tone_freq += digit * 10;
    }

    return static_cast<freqman_index_t>(tonekey::tone_key_index_by_value(tone_freq));
}

bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
    if (str.empty() || str[0] == '#')
        return false;

    entry = freqman_entry{};
    auto cols = split_string(str, ',');

    for (auto col : cols) {
        if (col.empty())
            continue;

        auto pair = split_string(col, '=');
        if (pair.size() != 2)
            continue;

        auto key = pair[0];
        auto value = pair[1];

        if (key == "a") {
            entry.type = freqman_type::Range;
            parse_int(value, entry.frequency_a);
        } else if (key == "b") {
            parse_int(value, entry.frequency_b);
        } else if (key == "bw") {
            // NB: Requires modulation to be set first
            if (entry.modulation < std::size(freqman_bandwidths)) {
                entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value);
            }
        } else if (key == "c") {
            entry.tone = parse_tone_key(value);
        } else if (key == "d") {
            entry.description = trim(value);
        } else if (key == "f") {
            entry.type = freqman_type::Single;
            parse_int(value, entry.frequency_a);
        } else if (key == "m") {
            entry.modulation = find_by_name(freqman_modulations, value);
        } else if (key == "r") {  // HamRadio relay receive freq
            entry.type = freqman_type::HamRadio;
            parse_int(value, entry.frequency_a);
        } else if (key == "l") {  // Portapack Repeater mode listen freq. Used as a single freq if Repeater mode isn't active
            entry.type = freqman_type::Repeater;
            parse_int(value, entry.frequency_a);
        } else if (key == "s") {
            entry.step = find_by_name(freqman_steps_short, value);
        } else if (key == "t") {  // Tx freq: scanned as a single freq in HamRadio mode, used as TX freq in Repeater mode and ignored by the scanner
            parse_int(value, entry.frequency_b);
        }
    }
    return is_valid(entry);
}

bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
    FreqmanDB freqman_db;
    freqman_db.set_read_raw(false);  // Don't return malformed lines.
    if (!freqman_db.open(path))
        return false;

    // Attempt to avoid a re-alloc if possible.
    db.clear();
    db.reserve(freqman_db.entry_count());

    for (auto entry : freqman_db) {
        // Filter by entry type.
        if (entry.type == freqman_type::Unknown ||
            (entry.type == freqman_type::Single && !options.load_freqs) ||
            (entry.type == freqman_type::Range && !options.load_ranges) ||
            (entry.type == freqman_type::HamRadio && !options.load_hamradios) ||
            (entry.type == freqman_type::Repeater && !options.load_repeaters)) {
            continue;
        }

        // Use previous entry's mod/band if current's aren't set.
        if (!db.empty()) {
            if (is_invalid(entry.modulation))
                entry.modulation = db.back()->modulation;
            if (is_invalid(entry.bandwidth))
                entry.bandwidth = db.back()->bandwidth;
        }

        // Move the entry onto the heap and push.
        db.push_back(std::make_unique<freqman_entry>(std::move(entry)));

        // Limit to max_entries when specified.
        if (options.max_entries > 0 && db.size() >= options.max_entries)
            break;
    }

    db.shrink_to_fit();
    return true;
}

bool is_valid(const freqman_entry& entry) {
    // No valid frequency combination was set.
    if (entry.type == freqman_type::Unknown)
        return false;

    // Frequency A must be set for all types
    if (entry.frequency_a == 0)
        return false;

    // Frequency B must be set for type Range or HamRadio or Repeater
    if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio || entry.type == freqman_type::Repeater) {
        if (entry.frequency_b == 0)
            return false;
    }

    // Ranges should have frequencies A <= B.
    if (entry.type == freqman_type::Range) {
        if (entry.frequency_a > entry.frequency_b)
            return false;
    }

    // TODO: Consider additional validation:
    // - Tone only on HamRadio.
    // - Step only on Range
    // - Fail on failed parse_int.
    // - Fail if bandwidth set before modulation.

    return true;
}

/* FreqmanDB ***********************************/

bool FreqmanDB::open(const std::filesystem::path& path, bool create) {
    auto result = FileWrapper::open(path, create);
    if (!result)
        return false;

    wrapper_ = *std::move(result);
    return true;
}

void FreqmanDB::close() {
    wrapper_.reset();
}

freqman_entry FreqmanDB::operator[](Index index) const {
    auto length = wrapper_->line_length(index);
    auto line_text = wrapper_->get_text(index, 0, length);

    if (line_text) {
        freqman_entry entry;
        if (parse_freqman_entry(*line_text, entry))
            return entry;
        else if (read_raw_) {
            entry.type = freqman_type::Raw;
            entry.description = trim(*line_text);
            return entry;
        }
    }

    return {};
}

void FreqmanDB::insert_entry(Index index, const freqman_entry& entry) {
    index = clip<uint32_t>(index, 0u, entry_count());
    wrapper_->insert_line(index);
    replace_entry(index, entry);
}

void FreqmanDB::append_entry(const freqman_entry& entry) {
    insert_entry(entry_count(), entry);
}

void FreqmanDB::replace_entry(Index index, const freqman_entry& entry) {
    auto range = wrapper_->line_range(index);
    if (!range)
        return;

    // Don't overwrite the '\n'.
    range->end--;
    wrapper_->replace_range(*range, to_freqman_string(entry));
}

void FreqmanDB::delete_entry(Index index) {
    wrapper_->delete_line(index);
}

bool FreqmanDB::delete_entry(const freqman_entry& entry) {
    auto it = find_entry(entry);
    if (it == end())
        return false;

    delete_entry(it.index());
    return true;
}

FreqmanDB::iterator FreqmanDB::find_entry(const freqman_entry& entry) {
    return find_entry([&entry](const auto& other) {
        return entry == other;
    });
}

uint32_t FreqmanDB::entry_count() const {
    // FileWrapper always presents a single line even for empty files.
    return empty() ? 0u : wrapper_->line_count();
}

bool FreqmanDB::empty() const {
    // FileWrapper always presents a single line even for empty files.
    // A DB is only really empty if the file size is 0.
    return !wrapper_ || wrapper_->size() == 0;
}