/*
   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 3 of the License, 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.  If not, see <http://www.gnu.org/licenses/>.

   Code by Andy Piper, ported from betaflight vtx_tramp
*/

#include "AP_Tramp.h"
#include <AP_Math/crc.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_HAL/utility/sparse-endian.h>
#include <AP_SerialManager/AP_SerialManager.h>

#if AP_TRAMP_ENABLED

#define AP_TRAMP_UART_BAUD            9600
// request and response size is 16 bytes
#define AP_TRAMP_UART_BUFSIZE_RX      32
#define AP_TRAMP_UART_BUFSIZE_TX      32

// Define periods between requests
#define TRAMP_MIN_REQUEST_PERIOD_US (200 * 1000) // 200ms
#define TRAMP_STATUS_REQUEST_PERIOD_US (1000 * 1000) // 1s

//#define TRAMP_DEBUG
#ifdef TRAMP_DEBUG
# define debug(fmt, args...)	do { hal.console->printf("TRAMP: " fmt "\n", ##args); } while (0)
#else
# define debug(fmt, args...)	do {} while(0)
#endif

extern const AP_HAL::HAL &hal;

AP_Tramp::AP_Tramp()
{
    singleton = this;
}

AP_Tramp *AP_Tramp::singleton;

// Calculate tramp protocol checksum of provided buffer
uint8_t AP_Tramp::checksum(uint8_t *buf)
{
    uint8_t cksum = 0;

    for (int i = 1 ; i < TRAMP_BUF_SIZE - 2; i++) {
        cksum += buf[i];
    }

    return cksum;
}

// Send tramp protocol frame to device
void AP_Tramp::send_command(uint8_t cmd, uint16_t param)
{
    if (port == nullptr) {
        return;
    }

    memset(request_buffer, 0, ARRAY_SIZE(request_buffer));
    request_buffer[0] = 0x0F;
    request_buffer[1] = cmd;
    request_buffer[2] = param & 0xFF;
    request_buffer[3] = (param >> 8) & 0xFF;
    request_buffer[14] = checksum(request_buffer);

    port->write(request_buffer, TRAMP_BUF_SIZE);
    port->flush();

    _packets_sent++;

    debug("send command '%c': %u", cmd, param);
}

// Process response and return code if valid else 0
char AP_Tramp::handle_response(void)
{
    _packets_rcvd++;
    _packets_sent = _packets_rcvd;

    const uint8_t respCode = response_buffer[1];

    switch (respCode) {
    case 'r': {
        const uint16_t min_freq = response_buffer[2]|(response_buffer[3] << 8);
        // Check we're not reading the request (indicated by freq zero)
        if (min_freq != 0) {
            // Got response, update device limits
            device_limits.rf_freq_min = min_freq;
            device_limits.rf_freq_max = response_buffer[4]|(response_buffer[5] << 8);
            device_limits.rf_power_max = response_buffer[6]|(response_buffer[7] << 8);
            debug("device limits: min freq: %u, max freq: %u, max power %u",
                unsigned(device_limits.rf_freq_min), unsigned(device_limits.rf_freq_max), unsigned(device_limits.rf_power_max));
            return 'r';
        }
        break;
    }
    case 'v': {
        const uint16_t freq = response_buffer[2]|(response_buffer[3] << 8);
        // Check we're not reading the request (indicated by freq zero)
        if (freq != 0) {
            // Got response, update device status
            const uint16_t power = response_buffer[4]|(response_buffer[5] << 8);
            cur_control_mode = response_buffer[6]; // Currently only used for race lock
            const bool pit_mode = response_buffer[7];
            cur_act_power = response_buffer[8]|(response_buffer[9] << 8);

            // update the vtx
            AP_VideoTX& vtx = AP::vtx();
            bool update_pending = vtx.have_params_changed();
            vtx.set_frequency_mhz(freq);

            AP_VideoTX::VideoBand band;
            uint8_t channel;
            if (vtx.get_band_and_channel(freq, band, channel)) {
                vtx.set_band(band);
                vtx.set_channel(channel);
            }

            vtx.set_power_mw(power);
            if (pit_mode) {
                vtx.set_options(vtx.get_options() | uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
            } else {
                vtx.set_options(vtx.get_options() & ~uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
            }

            // make sure the configured values now reflect reality
            // if they do then announce if there were changes
            if (!vtx.set_defaults() && update_pending && !vtx.have_params_changed()) {
                vtx.announce_vtx_settings();
            }

            debug("device config: freq: %u, cfg pwr: %umw, act pwr: %umw, pitmode: %u",
                unsigned(freq), unsigned(power), unsigned(cur_act_power), unsigned(pit_mode));


            return 'v';
        }
        break;
    }
    case 's': {
        const uint16_t temp = (int16_t)(response_buffer[6]|(response_buffer[7] << 8));
        // Check we're not reading the request (indicated by temp zero)
        if (temp != 0) {
            // Got response, update device status
            cur_temp = temp;
            return 's';
        }
        break;
    }
    }

    // Likely reading a request, return zero to indicate not accepted
    return 0;
}

// Reset receiver state machine
void AP_Tramp::reset_receiver(void)
{
    port->discard_input();
    receive_state = ReceiveState::S_WAIT_LEN;
    receive_pos = 0;
}

// returns completed response code or 0
char AP_Tramp::receive_response()
{
    if (port == nullptr) {
        return 0;
    }

    // wait for complete packet
    const uint16_t bytesNeeded = TRAMP_BUF_SIZE - receive_pos;
    if (port->available() < bytesNeeded) {
        return 0;
    }

    // sanity check
    if (bytesNeeded == 0) {
        reset_receiver();
        return 0;
    }

    for (uint16_t i = 0; i < bytesNeeded; i++) {
        const int16_t b = port->read();
        if (b < 0) {
            // uart claimed bytes available, but there were none
            return 0;
        }
        const uint8_t c = uint8_t(b);
        response_buffer[receive_pos++] = c;

        switch (receive_state) {
        case ReceiveState::S_WAIT_LEN: {
            if (c == 0x0F) {
                // Found header byte, advance to wait for code
                receive_state = ReceiveState::S_WAIT_CODE;
            } else {
                // Unexpected header, reset state machine
                reset_receiver();
            }
            break;
        }
        case ReceiveState::S_WAIT_CODE: {
            if (c == 'r' || c == 'v' || c == 's') {
                // Code is for response is one we're interested in, advance to data
                receive_state = ReceiveState::S_DATA;
            } else {
                // Unexpected code, reset state machine
                reset_receiver();
            }
            break;
        }
        case ReceiveState::S_DATA: {
            if (receive_pos == TRAMP_BUF_SIZE) {
                // Buffer is full, calculate checksum
                const uint8_t cksum = checksum(response_buffer);

                // Reset state machine ready for next response
                reset_receiver();

                if ((response_buffer[TRAMP_BUF_SIZE-2] == cksum) && (response_buffer[TRAMP_BUF_SIZE-1] == 0)) {
                    // Checksum is correct, process response
                    const char r = handle_response();

                    // Check response valid else keep on reading
                    if (r != 0) {
                        return r;
                    }
                }
            }
            break;
        }
        default:
            // Invalid state, reset state machine
            reset_receiver();
            break;
        }
    }

    return 0;
}

void AP_Tramp::send_query(uint8_t cmd)
{
    // Reset receive buffer and issue command
    reset_receiver();
    send_command(cmd, 0);
}

void AP_Tramp::set_status(TrampStatus _status)
{
    status = _status;
#ifdef TRAMP_DEBUG
    switch (status) {
        case TrampStatus::TRAMP_STATUS_OFFLINE:
            debug("status: OFFLINE");
            break;
        case TrampStatus::TRAMP_STATUS_INIT:
            debug("status: INIT");
            break;
        case TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT:
            debug("status: ONLINE_MONITOR_FREQPWRPIT");
            break;
        case TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_TEMP:
            debug("status: ONLINE_MONITOR_TEMP");
            break;
        case TrampStatus::TRAMP_STATUS_ONLINE_CONFIG:
            debug("status: ONLINE_CONFIG");
            break;
    }
#endif
}

void AP_Tramp::process_requests()
{
    if (port == nullptr) {
        return;
    }

    bool configUpdateRequired = false;

    // Read response from device
    const char replyCode = receive_response();
    const uint32_t now = AP_HAL::micros();

#ifdef TRAMP_DEBUG
    if (replyCode != 0) {
        debug("receive response '%c'", replyCode);
    }
#endif

    // Act on state
    switch (status) {
    case TrampStatus::TRAMP_STATUS_OFFLINE: {
        // Offline, check for response
        if (replyCode == 'r') {
            // Device replied to reset? request, enter init
            set_status(TrampStatus::TRAMP_STATUS_INIT);
        } else if ((now - last_time_us) >= TRAMP_MIN_REQUEST_PERIOD_US) {
            // Min request period exceeded, issue another reset?
            send_query('r');

            // Update last time
            last_time_us = now;
        }
        break;
    }
    case TrampStatus::TRAMP_STATUS_INIT: {
        // Initializing, check for response
        if (replyCode == 'v') {
            // Device replied to freq / power / pit query, enter online
            set_status(TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT);
        } else if ((now - last_time_us) >= TRAMP_MIN_REQUEST_PERIOD_US) {

            update_baud_rate();

            // Min request period exceeded, issue another query
            send_query('v');

            // Update last time
            last_time_us = now;
        }
        break;
    }
    case TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT: {
        // Note after config a status update request is made, a new status
        // request is made, this request is handled above and should prevent
        // subsequent config updates if the config is now correct
        if (retry_count > 0 && ((now - last_time_us) >= TRAMP_MIN_REQUEST_PERIOD_US)) {
            AP_VideoTX& vtx = AP::vtx();
            // Config retries remain and min request period exceeded, check freq
            if (!is_race_lock_enabled() && vtx.update_frequency()) {
                debug("Updating frequency to %uMhz", vtx.get_configured_frequency_mhz());
                // Freq can be and needs to be updated, issue request
                send_command('F', vtx.get_configured_frequency_mhz());

                // Set flag
                configUpdateRequired = true;
            } else if (!is_race_lock_enabled() && vtx.update_power()) {
                debug("Updating power to %umw\n", vtx.get_configured_power_mw());
                // Power can be and needs to be updated, issue request
                send_command('P', vtx.get_configured_power_mw());

                // Set flag
                configUpdateRequired = true;
            } else if (vtx.update_options()) {
                // Pit mode needs to be updated, issue request
                send_command('I', vtx.has_option(AP_VideoTX::VideoOptions::VTX_PITMODE) ? 0 : 1);

                // Set flag
                configUpdateRequired = true;
            }

            if (configUpdateRequired) {
                // Update required, decrement retry count
                retry_count--;

                // Update last time
                last_time_us = now;

                // Advance state
                set_status(TrampStatus::TRAMP_STATUS_ONLINE_CONFIG);
            } else {
                // No update required, reset retry count
                retry_count = 0;
            }
        }

        /* Was a config update made? */
        if (!configUpdateRequired) {
            /* No, look to continue monitoring */
            if ((now - last_time_us) >= TRAMP_STATUS_REQUEST_PERIOD_US) {
                // Request period exceeded, issue freq/power/pit query
                send_query('v');

                // Update last time
                last_time_us = now;
            } else if (replyCode == 'v') {
                // Got reply, issue temp query
                send_query('s');

                // Wait for reply
                set_status(TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_TEMP);

                // Update last time
                last_time_us = now;
            }
        }

        break;
    }
    case TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_TEMP: {
        // Check request time
        if (replyCode == 's') {
            // Got reply, return to request freq/power/pit
            set_status(TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_TEMP);
        } else if ((now - last_time_us) >= TRAMP_MIN_REQUEST_PERIOD_US) {
            // Timed out after min request period, return to request freq/power/pit query
            set_status(TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT);
        }
        break;
    }
    case TrampStatus::TRAMP_STATUS_ONLINE_CONFIG: {
        // Param should now be set, check time
        if ((now - last_time_us) >= TRAMP_MIN_REQUEST_PERIOD_US) {
            // Min request period exceeded, re-query
            send_query('v');

            // Advance state
            set_status(TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT);

            // Update last time
            last_time_us = now;
        }
        break;
    }
    default:
        // Invalid state, reset
        set_status(TrampStatus::TRAMP_STATUS_OFFLINE);
        break;
    }
}

bool AP_Tramp::is_device_ready()
{
    return status >= TrampStatus::TRAMP_STATUS_ONLINE_MONITOR_FREQPWRPIT;
}

void AP_Tramp::set_frequency(uint16_t freq)
{
    uint8_t freqValid;

    // Check frequency valid
    if (device_limits.rf_freq_min != 0 && device_limits.rf_freq_max != 0) {
        freqValid = (freq >= device_limits.rf_freq_min && freq <= device_limits.rf_freq_max);
    } else {
        freqValid = (freq >= VTX_TRAMP_MIN_FREQUENCY_MHZ && freq <= VTX_TRAMP_MAX_FREQUENCY_MHZ);
    }

    // Is frequency valid?
    if (freqValid) {
        // Requested freq changed, reset retry count
        retry_count = VTX_TRAMP_MAX_RETRIES;
    } else {
        debug("requested frequency %u is invalid", freq);
        // not valid reset to default
        AP::vtx().set_configured_frequency_mhz(AP::vtx().get_frequency_mhz());
    }
}

void AP_Tramp::update()
{
    if (port == nullptr) {
        return;
    }

    AP_VideoTX& vtx = AP::vtx();

    if (vtx.have_params_changed() && retry_count == 0) {
        // check changes in the order they will be processed
        if (vtx.update_frequency() || vtx.update_band() || vtx.update_channel()) {
            if (vtx.update_frequency()) {
                vtx.update_configured_channel_and_band();
            } else {
                vtx.update_configured_frequency();
            }
            set_frequency(vtx.get_configured_frequency_mhz());
        }
        else if (vtx.update_power()) {
            retry_count = VTX_TRAMP_MAX_RETRIES;
        }
        else if (vtx.update_options()) {
            retry_count = VTX_TRAMP_MAX_RETRIES;
        }
    }

    process_requests();
}

// we missed a response too many times - update the baud rate in case the temperature has increased
void AP_Tramp::update_baud_rate()
{
    // on my Unify Pro32 the VTX will respond immediately on power up to a settings request, so 5 packets is easily more than enough
    // we want to bias autobaud to only frequency hop when the current frequency is clearly exhausted, but after that hop quickly
    // on a Foxeer Reaper Infinity the actual response baudrate is more like 9400, so auto-bauding down in the first instance.
    if (_packets_sent - _packets_rcvd < 5) {
        return;
    }

    if (_smartbaud_direction == AutobaudDirection::UP
        && _smartbaud == VTX_TRAMP_SMARTBAUD_MAX) {
        _smartbaud_direction = AutobaudDirection::DOWN;
    } else if (_smartbaud_direction == AutobaudDirection::DOWN
        && _smartbaud == VTX_TRAMP_SMARTBAUD_MIN) {
        _smartbaud_direction = AutobaudDirection::UP;
    }

    _smartbaud += VTX_TRAMP_SMARTBAUD_STEP * int32_t(_smartbaud_direction);

    debug("autobaud: %d", int(_smartbaud));

    port->discard_input();
    port->begin(_smartbaud);

    _packets_sent = _packets_rcvd = 0;
}


bool AP_Tramp::init(void)
{
    if (AP::vtx().get_enabled() == 0) {
        debug("protocol is not active");
        return false;
    }

    // init uart
    port = AP::serialmanager().find_serial(AP_SerialManager::SerialProtocol_Tramp, 0);
    if (port != nullptr) {
        port->configure_parity(0);
        port->set_stop_bits(1);
        port->set_flow_control(AP_HAL::UARTDriver::FLOW_CONTROL_DISABLE);
        port->set_options((port->get_options() & ~AP_HAL::UARTDriver::OPTION_RXINV));

        port->begin(AP_TRAMP_UART_BAUD, AP_TRAMP_UART_BUFSIZE_RX, AP_TRAMP_UART_BUFSIZE_TX);
        debug("port opened");

        AP::vtx().set_provider_enabled(AP_VideoTX::VTXType::Tramp);

        return true;
    }
    return false;
}

#endif // VTX_TRAMP