// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Peer ID Editor
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2025 Bryan Biedenkapp, N2PLL
 *
 */
/**
 * @file PeerEditWnd.h
 * @ingroup peered
 */
#if !defined(__PEER_EDIT_WND_H__)
#define __PEER_EDIT_WND_H__

#include "common/Log.h"

#include "peered/CloseWndBase.h"
#include "peered/PeerEdMain.h"

#include <final/final.h>
using namespace finalcut;

// ---------------------------------------------------------------------------
//  Global Functions
// ---------------------------------------------------------------------------

/* Helper to generate a mostly random password. */

std::string randPasswordGen(int len)
{
    srand((unsigned int)(time(NULL)));

    const char numbers[] = "0123456789";
    const char letter[] = "abcdefghijklmnoqprstuvwyzx";
    const char LETTER[] = "ABCDEFGHIJKLMNOQPRSTUYWVZX";

    std::string password;
    int randomizer = rand() % 6;

    for (int i = 0; i < len; i++) {
        if (randomizer == 1 || randomizer == 4) {
            password.append(1U, numbers[rand() % 10]);
            randomizer = rand() % 4;
        }
        else if (randomizer == 2 || randomizer == 5) {
            password.append(1U, LETTER[rand() % 26]);
            randomizer = rand() % 4;
        }
        else {
            password.append(1U, letter[rand() % 26]);
            randomizer = rand() % 4;
        }
    }

    return password;
}

// ---------------------------------------------------------------------------
//  Class Declaration
// ---------------------------------------------------------------------------

/**
 * @brief This class implements the line edit control for peer IDs.
 * @ingroup peered
 */
class HOST_SW_API PeerIdLineEdit final : public FLineEdit {
public:
    /**
     * @brief Initializes a new instance of the PeerIdLineEdit class.
     * @param widget 
     */
    explicit PeerIdLineEdit(FWidget* widget = nullptr) : FLineEdit{widget}
    {
        setInputFilter("[[:digit:]]");
    }

    /*
    ** Event Handlers
    */

    /**
     * @brief Event that occurs on keyboard key press.
     * @param e Keyboard Event.
     */
    void onKeyPress(finalcut::FKeyEvent* e) override
    {
        const auto key = e->key();
        if (key == FKey::Up) {
            emitCallback("up-pressed");
            e->accept();
            return;
        } else if (key == FKey::Down) {
            emitCallback("down-pressed");
            e->accept();
            return;
        }

        FLineEdit::onKeyPress(e);
    }
};

/**
 * @brief This class implements the peer ID editor window.
 * @ingroup peered
 */
class HOST_SW_API PeerEditWnd final : public CloseWndBase {
public:
    /**
     * @brief Initializes a new instance of the PeerEditWnd class.
     * 
     * @param rule
     * @param widget 
     */
    explicit PeerEditWnd(lookups::PeerId rule, FWidget* widget = nullptr) : CloseWndBase{widget}
    {
        m_new = false;
        m_rule = rule;

        if (m_rule.peerDefault() || (m_rule.peerId() == 0U)) {
            m_new = true;
        } else {
            m_origPeerId = m_rule.peerId();
        }

        if (m_new) {
            std::string password = randPasswordGen(20);
            m_rule.peerPassword(password);
        }
    }

private:
    bool m_new;
    bool m_skipSaving;
    lookups::PeerId m_rule;

    uint32_t m_origPeerId;

    FLabel m_peerAliasLabel{"Alias: ", this};
    FLineEdit m_peerAlias{this};

    FCheckBox m_saveCopy{"Save Copy", this};
    FCheckBox m_incOnSave{"Increment On Save", this};

    FButtonGroup m_sourceGroup{"Peer ID", this};
    FLabel m_peerIdLabel{"Peer ID: ", &m_sourceGroup};
    PeerIdLineEdit m_peerId{&m_sourceGroup};
    FLabel m_peerPasswordLabel{"Password: ", &m_sourceGroup};
    FLineEdit m_peerPassword{&m_sourceGroup};

    FButtonGroup m_configGroup{"Configuration", this};
    FCheckBox m_peerReplicaEnabled{"Peer Replica", &m_configGroup};
    FCheckBox m_canReqKeysEnabled{"Request Keys", &m_configGroup};
    FCheckBox m_canInhibitEnabled{"Issue Inhibit", &m_configGroup};
    FCheckBox m_callPriorityEnabled{"Call Priority", &m_configGroup};

    FButtonGroup m_jitterGroup{"Adaptive Jitter Buffer", this};
    FCheckBox m_jitterEnabled{"Enabled", &m_jitterGroup};
    FLabel m_jitterMaxFramesLabel{"Max Frames:", &m_jitterGroup};
    FLineEdit m_jitterMaxFrames{&m_jitterGroup};
    FLabel m_jitterMaxWaitLabel{"Max Wait (us):", &m_jitterGroup};
    FLineEdit m_jitterMaxWait{&m_jitterGroup};

    /**
     * @brief Initializes the window layout.
     */
    void initLayout() override
    {
        FDialog::setText("Peer ID");
        FDialog::setSize(FSize{65, 18});

        m_enableSetButton = false;
        CloseWndBase::initLayout();
    }

    /**
     * @brief Initializes window controls.
     */
    void initControls() override
    {
        m_closeButton.setText("&OK");

        m_peerAliasLabel.setGeometry(FPoint(2, 2), FSize(8, 1));
        m_peerAlias.setGeometry(FPoint(11, 2), FSize(24, 1));
        if (!m_rule.peerDefault()) {
            m_peerAlias.setText(m_rule.peerAlias());
        }
        m_peerAlias.setShadow(false);
        m_peerAlias.addCallback("changed", [&]() { m_rule.peerAlias(m_peerAlias.getText().toString()); });

        m_saveCopy.setGeometry(FPoint(41, 2), FSize(18, 1));
        m_saveCopy.addCallback("toggled", [&]() {
            if (m_saveCopy.isChecked()) {
                m_incOnSave.setEnable();
            } else {
                m_incOnSave.setChecked(false);
                m_incOnSave.setDisable();
            }

            redraw();
        });
        m_incOnSave.setGeometry(FPoint(41, 3), FSize(18, 1));
        m_incOnSave.setDisable();

        // talkgroup source
        {
            m_sourceGroup.setGeometry(FPoint(2, 5), FSize(35, 5));
            m_peerIdLabel.setGeometry(FPoint(2, 1), FSize(10, 1));
            m_peerId.setGeometry(FPoint(11, 1), FSize(22, 1));
            m_peerId.setAlignment(finalcut::Align::Right);
            if (!m_rule.peerDefault()) {
                m_peerId.setText(std::to_string(m_rule.peerId()));
            } else {
                m_rule.peerId(1U);
                m_peerId.setText("1");
            }
            m_peerId.setShadow(false);
            m_peerId.addCallback("up-pressed", [&]() {
                uint32_t peerId = ::atoi(m_peerId.getText().c_str());
                peerId++;
                if (peerId > 999999999U) {
                    peerId = 999999999U;
                }

                m_peerId.setText(std::to_string(peerId));

                m_rule.peerId(peerId);
                redraw(); 
            });
            m_peerId.addCallback("down-pressed", [&]() {
                uint32_t peerId = ::atoi(m_peerId.getText().c_str());
                peerId--;
                if (peerId < 1U) {
                    peerId = 1U;
                }

                m_peerId.setText(std::to_string(peerId));

                m_rule.peerId(peerId);
                redraw(); 
            });
            m_peerId.addCallback("changed", [&]() { 
                if (m_peerId.getText().getLength() == 0) {
                    m_rule.peerId(1U);
                    return;
                }

                uint32_t peerId = ::atoi(m_peerId.getText().c_str());
                if (peerId < 1U) {
                    peerId = 1U;
                }

                if (peerId > 999999999U) {
                    peerId = 999999999U;
                }

                m_peerId.setText(std::to_string(peerId));

                m_rule.peerId(peerId);
            });

            m_peerPasswordLabel.setGeometry(FPoint(2, 2), FSize(10, 1));
            m_peerPassword.setGeometry(FPoint(11, 2), FSize(22, 1));
            if (!m_rule.peerDefault()) {
                m_peerPassword.setText(m_rule.peerPassword());
            }
            m_peerPassword.setShadow(false);
            m_peerPassword.addCallback("changed", [&]() { m_rule.peerPassword(m_peerPassword.getText().toString()); });
        }

        // configuration
        {
            m_configGroup.setGeometry(FPoint(39, 5), FSize(23, 6));

            m_peerReplicaEnabled.setGeometry(FPoint(2, 1), FSize(10, 1));
            m_peerReplicaEnabled.setChecked(m_rule.peerReplica());
            m_peerReplicaEnabled.addCallback("toggled", [&]() {
                m_rule.peerReplica(m_peerReplicaEnabled.isChecked());
            });

            m_canReqKeysEnabled.setGeometry(FPoint(2, 2), FSize(10, 1));
            m_canReqKeysEnabled.setChecked(m_rule.canRequestKeys());
            m_canReqKeysEnabled.addCallback("toggled", [&]() {
                m_rule.canRequestKeys(m_canReqKeysEnabled.isChecked());
            });

            m_canInhibitEnabled.setGeometry(FPoint(2, 3), FSize(10, 1));
            m_canInhibitEnabled.setChecked(m_rule.canIssueInhibit());
            m_canInhibitEnabled.addCallback("toggled", [&]() {
                m_rule.canIssueInhibit(m_canInhibitEnabled.isChecked());
            });

            m_callPriorityEnabled.setGeometry(FPoint(2, 4), FSize(10, 1));
            m_callPriorityEnabled.setChecked(m_rule.hasCallPriority());
            m_callPriorityEnabled.addCallback("toggled", [&]() {
                m_rule.hasCallPriority(m_callPriorityEnabled.isChecked());
            });
        }

        // jitter buffer
        {
            m_jitterGroup.setGeometry(FPoint(2, 10), FSize(35, 5));

            m_jitterEnabled.setGeometry(FPoint(2, 1), FSize(3, 1));
            m_jitterEnabled.setChecked(false);
            m_jitterEnabled.addCallback("toggled", [&]() {
                m_rule.jitterBufferEnabled(m_jitterEnabled.isChecked());
                if (m_jitterEnabled.isChecked()) {
                    m_jitterMaxFrames.setEnable();
                    m_jitterMaxWait.setEnable();
                } else {
                    m_jitterMaxFrames.setDisable();
                    m_jitterMaxWait.setDisable();
                }

                redraw();
            });

            m_jitterMaxFramesLabel.setGeometry(FPoint(2, 2), FSize(16, 1));
            m_jitterMaxFrames.setGeometry(FPoint(18, 2), FSize(10, 1));
            m_jitterMaxFrames.setAlignment(finalcut::Align::Right);
            m_jitterMaxFrames.setText(std::to_string(m_rule.jitterBufferMaxSize()));
            m_jitterMaxFrames.setShadow(false);
            m_jitterMaxFrames.setEnable(false);
            m_jitterMaxFrames.addCallback("changed", [&]() {
                if (m_jitterMaxFrames.getText().getLength() == 0) {
                    m_rule.jitterBufferMaxSize(4U);
                    return;
                }

                uint32_t maxSize = ::atoi(m_jitterMaxFrames.getText().c_str());
                if (maxSize < 2U) {
                    maxSize = 2U;
                }

                if (maxSize > 10U) {
                    maxSize = 10U;
                }

                m_jitterMaxFrames.setText(std::to_string(maxSize));

                m_rule.jitterBufferMaxSize(maxSize);
            });

            m_jitterMaxWaitLabel.setGeometry(FPoint(2, 3), FSize(16, 1));
            m_jitterMaxWait.setGeometry(FPoint(18, 3), FSize(10, 1));
            m_jitterMaxWait.setAlignment(finalcut::Align::Right);
            m_jitterMaxWait.setText(std::to_string(m_rule.jitterBufferMaxWait()));
            m_jitterMaxWait.setShadow(false);
            m_jitterMaxWait.setEnable(false);
            m_jitterMaxWait.addCallback("changed", [&]() {
                if (m_jitterMaxWait.getText().getLength() == 0) {
                    m_rule.jitterBufferMaxWait(40000U);
                    return;
                }

                uint32_t maxWait = ::atoi(m_jitterMaxWait.getText().c_str());
                if (maxWait < 10000U) {
                    maxWait = 10000U;
                }

                if (maxWait > 200000U) {
                    maxWait = 200000U;
                }

                m_jitterMaxWait.setText(std::to_string(maxWait));

                m_rule.jitterBufferMaxWait(maxWait);
            });
        }

        CloseWndBase::initControls();
    }

    /**
     * @brief 
     */
    void logRuleInfo()
    {
        std::string peerAlias = m_rule.peerAlias();
        uint32_t peerId = m_rule.peerId();
        bool peerReplica = m_rule.peerReplica();
        bool canRequestKeys = m_rule.canRequestKeys();
        bool canIssueInhibit = m_rule.canIssueInhibit();
        bool hasCallPriority = m_rule.hasCallPriority();
        bool jitterBufferEnabled = m_rule.jitterBufferEnabled();

        ::LogInfoEx(LOG_HOST, "Peer ALIAS: %s PEERID: %u REPLICA: %u CAN REQUEST KEYS: %u CAN ISSUE INHIBIT: %u HAS CALL PRIORITY: %u JITTER BUFFER ENABLED: %u", peerAlias.c_str(), peerId, peerReplica, canRequestKeys, canIssueInhibit, hasCallPriority, jitterBufferEnabled);
    }

    /*
    ** Event Handlers
    */

    /**
     * @brief Event that occurs on keyboard key press.
     * @param e Keyboard Event.
     */
    void onKeyPress(finalcut::FKeyEvent* e) override
    {
        const auto key = e->key();
        if (key == FKey::Enter) {
            this->close();
        } else if (key == FKey::Escape) {
            m_skipSaving = true;
            this->close();
        }
    }

    /**
     * @brief Event that occurs when the window is closed.
     * @param e Close event.
     */
    void onClose(FCloseEvent* e) override
    {
        if (m_skipSaving) {
            m_skipSaving = false;
            CloseWndBase::onClose(e);
            return;
        }

        if (!m_rule.peerDefault()) {
            if (m_incOnSave.isChecked()) {
                uint32_t peerId = m_rule.peerId();
                peerId++;

                if (peerId > 999999999U) {
                    peerId = 999999999U;
                }

                m_rule.peerId(peerId);

                m_peerId.setText(std::to_string(peerId));
                redraw();
            }

            if (m_origPeerId != 0U && !m_saveCopy.isChecked()) {
                if (m_rule.peerId() == 0U) {
                    LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
                    FMessageBox::error(this, "Peer ID must be valid.");
                    return;
                }

                // update peer
                auto peers = g_pidLookups->tableAsList();
                auto it = std::find_if(peers.begin(), peers.end(),
                    [&](lookups::PeerId& x) {
                        return x.peerId() == m_origPeerId;
                    });
                if (it != peers.end()) {
                    LogInfoEx(LOG_HOST, "Updating peer %s (%u) to %s (%u)", it->peerAlias().c_str(), it->peerId(), m_rule.peerAlias().c_str(), m_rule.peerId());
                    g_pidLookups->eraseEntry(m_origPeerId);

                    lookups::PeerId entry = lookups::PeerId(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), false);
                    entry.peerReplica(m_rule.peerReplica());
                    entry.canRequestKeys(m_rule.canRequestKeys());
                    entry.canIssueInhibit(m_rule.canIssueInhibit());
                    entry.hasCallPriority(m_rule.hasCallPriority());

                    entry.jitterBufferEnabled(m_rule.jitterBufferEnabled());
                    entry.jitterBufferMaxSize(m_rule.jitterBufferMaxSize());
                    entry.jitterBufferMaxWait(m_rule.jitterBufferMaxWait());

                    g_pidLookups->addEntry(m_rule.peerId(), entry);

                    logRuleInfo();
                }
            } else {
                if (m_rule.peerId() == 0U) {
                    LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
                    FMessageBox::error(this, "Peer ID must be valid.");
                    return;
                }

                auto peers = g_pidLookups->tableAsList();
                auto it = std::find_if(peers.begin(), peers.end(),
                    [&](lookups::PeerId& x) {
                        return x.peerId() == m_rule.peerId();
                    });
                if (it != peers.end()) {
                    LogError(LOG_HOST, "Not saving duplicate peer, peer %s (%u), peers must be unique.", m_rule.peerAlias().c_str(), m_rule.peerId());
                    FMessageBox::error(this, "Duplicate peer, change peer ID. Peers must be unique.");
                    if (m_saveCopy.isChecked())
                        m_saveCopy.setChecked(false);
                    return;
                }

                // add new peer
                if (m_saveCopy.isChecked()) {
                    LogInfoEx(LOG_HOST, "Copying Peer. Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId());
                } else {
                    LogInfoEx(LOG_HOST, "Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId());
                }

                lookups::PeerId entry = lookups::PeerId(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), false);
                entry.peerReplica(m_rule.peerReplica());
                entry.canRequestKeys(m_rule.canRequestKeys());
                entry.canIssueInhibit(m_rule.canIssueInhibit());
                entry.hasCallPriority(m_rule.hasCallPriority());

                entry.jitterBufferEnabled(m_rule.jitterBufferEnabled());
                entry.jitterBufferMaxSize(m_rule.jitterBufferMaxSize());
                entry.jitterBufferMaxWait(m_rule.jitterBufferMaxWait());

                g_pidLookups->addEntry(m_rule.peerId(), entry);

                logRuleInfo();

                // don't actually close the modal on a copy save
                if (m_saveCopy.isChecked()) {
                    return;
                }
            }
        } else {
            LogError(LOG_HOST, "Not saving peer, peer %s (%u), have a peer ID greater than 0.", m_rule.peerAlias().c_str(), m_rule.peerId());
            FMessageBox::error(this, "Talkgroup must have a peer ID greater than 0.");
            return;
        }

        CloseWndBase::onClose(e);
    }
};

#endif // __PEER_EDIT_WND_H__