/*
 * This file 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 file 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/>.
 *
 * Author: Oliver Walters
 */


#include <AP_HAL/AP_HAL.h>
#include <AP_AHRS/AP_AHRS.h>

#include "AP_PiccoloCAN.h"

#if HAL_PICCOLO_CAN_ENABLE

#include <AP_BoardConfig/AP_BoardConfig.h>
#include <AP_CANManager/AP_CANManager.h>
#include <AP_Common/AP_Common.h>
#include <AP_Scheduler/AP_Scheduler.h>
#include <AP_HAL/utility/sparse-endian.h>
#include <SRV_Channel/SRV_Channel.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_Logger/AP_Logger.h>

#include <stdio.h>

#include <AP_PiccoloCAN/piccolo_protocol/ESCVelocityProtocol.h>
#include <AP_PiccoloCAN/piccolo_protocol/ESCPackets.h>


extern const AP_HAL::HAL& hal;

#define debug_can(level_debug, fmt, args...) do { AP::can().log_text(level_debug, "PiccoloCAN", fmt, ##args); } while (0)

AP_PiccoloCAN::AP_PiccoloCAN()
{
    debug_can(AP_CANManager::LOG_INFO, "PiccoloCAN: constructed\n\r");
}

AP_PiccoloCAN *AP_PiccoloCAN::get_pcan(uint8_t driver_index)
{
    if (driver_index >= AP::can().get_num_drivers() ||
        AP::can().get_driver_type(driver_index) != AP_CANManager::Driver_Type_PiccoloCAN) {
        return nullptr;
    }

    return static_cast<AP_PiccoloCAN*>(AP::can().get_driver(driver_index));
}

bool AP_PiccoloCAN::add_interface(AP_HAL::CANIface* can_iface) {
    if (_can_iface != nullptr) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: Multiple Interface not supported\n\r");
        return false;
    }

    _can_iface = can_iface;

    if (_can_iface == nullptr) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: CAN driver not found\n\r");
        return false;
    }

    if (!_can_iface->is_initialized()) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: Driver not initialized\n\r");
        return false;
    }

    if (!_can_iface->set_event_handle(&_event_handle)) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: Cannot add event handle\n\r");
        return false;
    }
    return true;
}

// initialize PiccoloCAN bus
void AP_PiccoloCAN::init(uint8_t driver_index, bool enable_filters)
{
    _driver_index = driver_index;

    debug_can(AP_CANManager::LOG_DEBUG, "PiccoloCAN: starting init\n\r");

    if (_initialized) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: already initialized\n\r");
        return;
    }
    // start calls to loop in separate thread
    if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_PiccoloCAN::loop, void), _thread_name, 4096, AP_HAL::Scheduler::PRIORITY_MAIN, 1)) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: couldn't create thread\n\r");
        return;
    }

    _initialized = true;

    snprintf(_thread_name, sizeof(_thread_name), "PiccoloCAN_%u", driver_index);

    debug_can(AP_CANManager::LOG_DEBUG, "PiccoloCAN: init done\n\r");
}

// loop to send output to CAN devices in background thread
void AP_PiccoloCAN::loop()
{
    AP_HAL::CANFrame txFrame;
    AP_HAL::CANFrame rxFrame;

    // How often to transmit CAN messages (milliseconds)
#define CMD_TX_PERIOD 10

    uint16_t txCounter = 0;

    // CAN Frame ID components
    uint8_t frame_id_group;     // Piccolo message group
    uint16_t frame_id_device;   // Device identifier

    uint64_t timeout;

    while (true) {

        if (!_initialized) {
            debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: not initialized\n\r");
            hal.scheduler->delay_microseconds(10000);
            continue;
        }

        timeout = AP_HAL::micros64() + 250;

        // 1ms loop delay
        hal.scheduler->delay_microseconds(1 * 1000);

        // Transmit CAN commands at regular intervals
        if (txCounter++ > CMD_TX_PERIOD) {

            txCounter = 0;

            // Transmit ESC commands
            send_esc_messages();
        }

        // Look for any message responses on the CAN bus
        while (read_frame(rxFrame, timeout)) {
            frame_id_group = (rxFrame.id >> 24) & 0x1F;
            frame_id_device = (rxFrame.id >> 8) & 0xFF;

            // Only accept extended messages
            if ((rxFrame.id & AP_HAL::CANFrame::FlagEFF) == 0) {
                continue;
            }

            switch (MessageGroup(frame_id_group)) {
            // ESC messages exist in the ACTUATOR group
            case MessageGroup::ACTUATOR:

                switch (ActuatorType(frame_id_device)) {
                case ActuatorType::ESC:
                    if (handle_esc_message(rxFrame)) {
                        // Returns true if the message was successfully decoded
                    }
                    break;
                default:
                    // Unknown actuator type
                    break;
                }

                break;
            default:
                break;
            }
        }
    }
}

// write frame on CAN bus, returns true on success
bool AP_PiccoloCAN::write_frame(AP_HAL::CANFrame &out_frame, uint64_t timeout)
{
    if (!_initialized) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: Driver not initialized for write_frame\n\r");
        return false;
    }

    bool read_select = false;
    bool write_select = true;
    bool ret;
    do {
        ret = _can_iface->select(read_select, write_select, &out_frame, timeout);
        if (!ret || !write_select) {
            hal.scheduler->delay_microseconds(50);
        }
    } while (!ret || !write_select);

    return (_can_iface->send(out_frame, timeout, AP_HAL::CANIface::AbortOnError) == 1);
}

// read frame on CAN bus, returns true on succses
bool AP_PiccoloCAN::read_frame(AP_HAL::CANFrame &recv_frame, uint64_t timeout)
{
    if (!_initialized) {
        debug_can(AP_CANManager::LOG_ERROR, "PiccoloCAN: Driver not initialized for read_frame\n\r");
        return false;
    }
    bool read_select = true;
    bool write_select = false;
    bool ret = _can_iface->select(read_select, write_select, nullptr, timeout);
    if (!ret || !read_select) {
        // No frame available
        return false;
    }

    uint64_t time;
    AP_HAL::CANIface::CanIOFlags flags {};

    return (_can_iface->receive(recv_frame, time, flags) == 1);
}

// called from SRV_Channels
void AP_PiccoloCAN::update()
{
    uint64_t timestamp = AP_HAL::micros64();

    /* Read out the ESC commands from the channel mixer */
    for (uint8_t i = 0; i < PICCOLO_CAN_MAX_NUM_ESC; i++) {

        // Check each channel to determine if a motor function is assigned
        SRV_Channel::Aux_servo_function_t motor_function = SRV_Channels::get_motor_function(i);

        if (SRV_Channels::function_assigned(motor_function)) {

            uint16_t output = 0;

            if (SRV_Channels::get_output_pwm(motor_function, output)) {

                _esc_info[i].command = output;
                _esc_info[i].newCommand = true;
            }
        }
    }

    AP_Logger *logger = AP_Logger::get_singleton();

    // Push received telemtry data into the logging system
    if (logger && logger->logging_enabled()) {

        WITH_SEMAPHORE(_telem_sem);

        for (uint8_t i = 0; i < PICCOLO_CAN_MAX_NUM_ESC; i++) {

            PiccoloESC_Info_t &esc = _esc_info[i];

            if (esc.newTelemetry) {

                logger->Write_ESC(i, timestamp,
                                  (int32_t) esc.statusA.rpm * 100,
                                  esc.statusB.voltage,
                                  esc.statusB.current,
                                  (int16_t) esc.statusB.escTemperature,
                                  0,  // TODO - Accumulated current
                                  (int16_t) esc.statusB.motorTemperature);

                esc.newTelemetry = false;
            }
        }
    }
}

// send ESC telemetry messages over MAVLink
void AP_PiccoloCAN::send_esc_telemetry_mavlink(uint8_t mav_chan)
{
    // Arrays to store ESC telemetry data
    uint8_t temperature[4] {};
    uint16_t voltage[4] {};
    uint16_t rpm[4] {};
    uint16_t count[4] {};
    uint16_t current[4] {};
    uint16_t totalcurrent[4] {};

    bool dataAvailable = false;

    uint8_t idx = 0;

    WITH_SEMAPHORE(_telem_sem);

    for (uint8_t ii = 0; ii < PICCOLO_CAN_MAX_NUM_ESC; ii++) {

        // Calculate index within storage array
        idx = (ii % 4);

        PiccoloESC_Info_t &esc = _esc_info[idx];

        // Has the ESC been heard from recently?
        if (is_esc_present(ii)) {
            dataAvailable = true;

            temperature[idx] = esc.statusB.escTemperature;
            voltage[idx] = esc.statusB.voltage;
            current[idx] = esc.statusB.current;
            totalcurrent[idx] = 0;
            rpm[idx] = esc.statusA.rpm;
            count[idx] = 0;
        } else {
            temperature[idx] = 0;
            voltage[idx] = 0;
            current[idx] = 0;
            totalcurrent[idx] = 0;
            rpm[idx] = 0;
            count[idx] = 0;
        }

        // Send ESC telemetry in groups of 4
        if ((ii % 4) == 3) {

            if (dataAvailable) {
                if (!HAVE_PAYLOAD_SPACE((mavlink_channel_t) mav_chan, ESC_TELEMETRY_1_TO_4)) {
                    continue;
                }

                switch (ii) {
                case 3:
                    mavlink_msg_esc_telemetry_1_to_4_send((mavlink_channel_t) mav_chan, temperature, voltage, current, totalcurrent, rpm, count);
                    break;
                case 7:
                    mavlink_msg_esc_telemetry_5_to_8_send((mavlink_channel_t) mav_chan, temperature, voltage, current, totalcurrent, rpm, count);
                    break;
                case 11:
                    mavlink_msg_esc_telemetry_9_to_12_send((mavlink_channel_t) mav_chan, temperature, voltage, current, totalcurrent, rpm, count);
                    break;
                default:
                    break;
                }
            }

            dataAvailable = false;
        }
    }
}


// send ESC messages over CAN
void AP_PiccoloCAN::send_esc_messages(void)
{
    AP_HAL::CANFrame txFrame;

    uint64_t timeout = AP_HAL::micros64() + 250;

    // TODO - How to buffer CAN messages properly?
    // Sending more than 2 messages at each loop instance means that sometimes messages are dropped

    if (hal.util->get_soft_armed()) {

        bool send_cmd = false;
        int16_t cmd[4] {};
        uint8_t idx;

        // Transmit bulk command packets to 4x ESC simultaneously
        for (uint8_t ii = 0; ii < PICCOLO_CAN_MAX_GROUP_ESC; ii++) {

            send_cmd = false;

            for (uint8_t jj = 0; jj < 4; jj++) {

                idx = (ii * 4) + jj;

                /* Check if the ESC is software-inhibited.
                 * If so, send a message to enable it.
                 */
                if (is_esc_present(idx) && !is_esc_enabled(idx)) {
                    encodeESC_EnablePacket(&txFrame);
                    txFrame.id |= (idx + 1);
                    write_frame(txFrame, timeout);
                }
                else if (_esc_info[idx].newCommand) {
                    send_cmd = true;
                    cmd[jj] = _esc_info[idx].command;
                    _esc_info[idx].newCommand = false;
                } else {
                    // A command of 0xFFFF is 'out of range' and will be ignored by the corresponding ESC
                    cmd[jj] = 0xFFFF;
                }
            }

            if (send_cmd) {
                encodeESC_CommandMultipleESCsPacket(
                    &txFrame,
                    cmd[0],
                    cmd[1],
                    cmd[2],
                    cmd[3],
                    (PKT_ESC_SETPOINT_1 + ii)
                );

                // Broadcast the command to all ESCs
                txFrame.id |= 0xFF;

                write_frame(txFrame, timeout);
            }
        }

    } else {
        // System is NOT armed - send a "disable" message to all ESCs on the bus

        // Command all ESC into software disable mode
        encodeESC_DisablePacket(&txFrame);

        // Set the ESC address to the broadcast ID (0xFF)
        txFrame.id |= 0xFF;

        write_frame(txFrame, timeout);
    }
}


// interpret an ESC message received over CAN
bool AP_PiccoloCAN::handle_esc_message(AP_HAL::CANFrame &frame)
{
    uint64_t timestamp = AP_HAL::micros64();

    // The ESC address is the lower byte of the address
    uint8_t addr = frame.id & 0xFF;

    // Ignore any ESC with node ID of zero
    if (addr == 0x00) {
        return false;
    }

    // Subtract to get the address in memory
    addr -= 1;

    // Maximum number of ESCs allowed
    if (addr >= PICCOLO_CAN_MAX_NUM_ESC) {
        return false;
    }

    PiccoloESC_Info_t &esc = _esc_info[addr];

    bool result = true;

    // Throw the packet against each decoding routine
    if (decodeESC_StatusAPacketStructure(&frame, &esc.statusA)) {
        esc.newTelemetry = true;
    } else if (decodeESC_StatusBPacketStructure(&frame, &esc.statusB)) {
        esc.newTelemetry = true;
    } else if (decodeESC_FirmwarePacketStructure(&frame, &esc.firmware)) {
        // TODO
    } else if (decodeESC_AddressPacketStructure(&frame, &esc.address)) {
        // TODO
    } else if (decodeESC_EEPROMSettingsPacketStructure(&frame, &esc.eeprom)) {
        // TODO
    } else {
        result = false;
    }

    if (result) {
        // Reset the Rx timestamp
        esc.last_rx_msg_timestamp = timestamp;
    }

    return result;
}


bool AP_PiccoloCAN::is_esc_present(uint8_t chan, uint64_t timeout_ms)
{

    if (chan >= PICCOLO_CAN_MAX_NUM_ESC) {
        return false;
    }

    PiccoloESC_Info_t &esc = _esc_info[chan];

    // No messages received from this ESC
    if (esc.last_rx_msg_timestamp == 0) {
        return false;
    }

    uint64_t now = AP_HAL::micros64();

    uint64_t timeout_us = timeout_ms * 1000;

    if (now > (esc.last_rx_msg_timestamp + timeout_us)) {
        return false;
    }

    return true;
}


bool AP_PiccoloCAN::is_esc_enabled(uint8_t chan)
{
    if (chan >= PICCOLO_CAN_MAX_NUM_ESC) {
        return false;
    }

    // If the ESC is not present, we cannot determine if it is enabled or not
    if (!is_esc_present(chan)) {
        return false;
    }

    PiccoloESC_Info_t &esc = _esc_info[chan];

    if (esc.statusA.status.hwInhibit || esc.statusA.status.swInhibit) {
        return false;
    }

    // ESC is present, and enabled
    return true;

}


bool AP_PiccoloCAN::pre_arm_check(char* reason, uint8_t reason_len)
{
    // Check that each required ESC is present on the bus
    for (uint8_t ii = 0; ii < PICCOLO_CAN_MAX_NUM_ESC; ii++) {

        SRV_Channel::Aux_servo_function_t motor_function = SRV_Channels::get_motor_function(ii);

        // There is a motor function assigned to this channel
        if (SRV_Channels::function_assigned(motor_function)) {

            if (!is_esc_present(ii)) {
                snprintf(reason, reason_len, "ESC %u not detected", ii + 1);
                return false;
            }

            PiccoloESC_Info_t &esc = _esc_info[ii];

            if (esc.statusA.status.hwInhibit) {
                snprintf(reason, reason_len, "ESC %u is hardware inhibited", (ii + 1));
                return false;
            }
        }
    }

    return true;
}


/* Piccolo Glue Logic
 * The following functions are required by the auto-generated protogen code.
 */

//! \return the packet data pointer from the packet
uint8_t* getESCVelocityPacketData(void* pkt)
{
    AP_HAL::CANFrame* frame = (AP_HAL::CANFrame*) pkt;

    return (uint8_t*) frame->data;
}

//! \return the packet data pointer from the packet, const
const uint8_t* getESCVelocityPacketDataConst(const void* pkt)
{
    AP_HAL::CANFrame* frame = (AP_HAL::CANFrame*) pkt;

    return (const uint8_t*) frame->data;
}

//! Complete a packet after the data have been encoded
void finishESCVelocityPacket(void* pkt, int size, uint32_t packetID)
{
    AP_HAL::CANFrame* frame = (AP_HAL::CANFrame*) pkt;

    if (size > AP_HAL::CANFrame::MaxDataLen) {
        size = AP_HAL::CANFrame::MaxDataLen;
    }

    frame->dlc = size;

    /* Encode the CAN ID
     * 0x07mm20dd
     * - 07 = ACTUATOR group ID
     * - mm = Message ID
     * - 20 = ESC actuator type
     * - dd = Device ID
     *
     * Note: The Device ID (lower 8 bits of the frame ID) will have to be inserted later
     */

    uint32_t id = (((uint8_t) AP_PiccoloCAN::MessageGroup::ACTUATOR) << 24) |       // CAN Group ID
                  ((packetID & 0xFF) << 16) |                                       // Message ID
                  (((uint8_t) AP_PiccoloCAN::ActuatorType::ESC) << 8);              // Actuator type

    // Extended frame format
    id |= AP_HAL::CANFrame::FlagEFF;

    frame->id = id;
}

//! \return the size of a packet from the packet header
int getESCVelocityPacketSize(const void* pkt)
{
    AP_HAL::CANFrame* frame = (AP_HAL::CANFrame*) pkt;

    return (int) frame->dlc;
}

//! \return the ID of a packet from the packet header
uint32_t getESCVelocityPacketID(const void* pkt)
{
    AP_HAL::CANFrame* frame = (AP_HAL::CANFrame*) pkt;

    // Extract the message ID field from the 29-bit ID
    return (uint32_t) ((frame->id >> 16) & 0xFF);
}


#endif // HAL_PICCOLO_CAN_ENABLE

