#include "NovaFitness.h"
#include <cstring>
namespace GuL
{
    NovaFitness::NovaFitness(UARTInterface &stream, uint16_t id) : _uart(&stream), _payloadSize(0), _reportingMode(PASSIVE), _workMode(SLEEP), _workingPeriodSeconds(0)
    {
        _id = id;
    }
#if defined(ARDUINO)
    NovaFitness::NovaFitness(Stream &stream, uint16_t id) : _streamWrapper(stream), _uart(&_streamWrapper), _payloadSize(0), _reportingMode(PASSIVE), _workMode(SLEEP), _workingPeriodSeconds(0)
    {
        _id = id;
    }

    NovaFitness::NovaFitness(HardwareSerial &stream, uint16_t id) : _hardwareSerialWrapper(stream), _uart(&_hardwareSerialWrapper), _payloadSize(0), _reportingMode(PASSIVE), _workMode(SLEEP), _workingPeriodSeconds(0)
    {
        _id = id;
    }
#endif

    bool NovaFitness::sleep()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::WORK_MODE, SettingsAction::Set, WorkMode::SLEEP, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x05, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::wakeup()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::WORK_MODE, SettingsAction::Set, WorkMode::WAKEUP, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x05, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::setToActiveReporting()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::REPORTING_MODE, SettingsAction::Set, ReportingMode::ACTIVE, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x05, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::setToPassiveReporting()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::REPORTING_MODE, SettingsAction::Set, ReportingMode::PASSIVE, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x05, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::setWorkingPeriod(uint8_t minutes)
    {
        if (minutes > 30)
        {
            return false; // Invalid value, must be between 0 and 30
        }
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::WORKING_PERIOD, SettingsAction::Set, minutes,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x05, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::poll()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::QUERY_DATA, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x00, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::pollFirmwareVersion()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::FIRMWARE_SETTING, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x00, 0xAB};
        return this->send(command, 19);
    }
    bool NovaFitness::pollReportingMode()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::REPORTING_MODE, SettingsAction::Query, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x00, 0xAB};
        return this->send(command, 19);
    }
    bool NovaFitness::pollWorkingMode()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::WORK_MODE, SettingsAction::Query, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x00, 0xAB};
        return this->send(command, 19);
    }
    bool NovaFitness::pollWorkingPeriod()
    {
        uint8_t command[19] = {0xAA, 0xB4, SettingsType::WORKING_PERIOD, SettingsAction::Query, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0xFF, 0xFF, 0x00, 0xAB};
        return this->send(command, 19);
    }

    bool NovaFitness::read()
    {
        uint8_t byte;
        bool gotValidData = false;

        _lastFailureReason = FailureReason::NONE; // Reset failure reason at the start of read
        if (!this->_uart)
        {
            _lastFailureReason = FailureReason::UART_INTERFACE_NOT_AVAILABLE;
            return false; // No UART interface available
        }

        while (this->_uart->available())
        {
            uint8_t byte = this->_uart->read();
            if (_payloadSize == 0 && byte != 0xAA)
            {
                continue;
            }
            if (_payloadSize < _expected_incoming_frame_length)
            {
                _payload[_payloadSize++] = byte;
            }
            // In the future we have to set the expected frame length dynamically based on command ID, for now it is constant so we can just check if the buffer is full after writing the byte
            if (_payloadSize == _expected_incoming_frame_length)
            {
                if (tryParseFrame())
                {
                    _lastFailureReason = FailureReason::NONE; // Clear failure reason on successful parse
                    gotValidData = true;
                }
            }
        }
        if (!gotValidData && _payloadSize > 0)
        {
            _lastFailureReason = FailureReason::INCOMPLETE_FRAME; // If we have data in buffer but couldn't parse a valid frame, it means the frame is incomplete
        }
        return gotValidData;
    }

    bool NovaFitness::tryParseFrame()
    {
        uint8_t head = _payload[0]; // First byte should be 0xAA, by onstruction of buffer this should always be the case
        uint8_t commandId = _payload[1];
        uint8_t checksum = _payload[8];
        uint8_t tail = _payload[9];

        if (head != 0xAA || tail != 0xAB)
        {
            _lastFailureReason = FailureReason::INVALID_FRAME;
            shiftBufferToNextStartByte();
            return false; // Invalid frame structure
        }

        if (this->calcChecksum(_payload, 10) != checksum)
        {
            _lastFailureReason = FailureReason::CHECKSUM_ERROR;
            shiftBufferToNextStartByte();
            return false; // Checksum mismatch
        }

        if (commandId != SETTINGS_COMMAND_ID && commandId != DATA_COMMAND_ID && commandId != DATA100_COMMAND_ID)
        {
            _lastFailureReason = FailureReason::UNKNOWN_COMMAND_ID;
            shiftBufferToNextStartByte();
            return false; // Unknown command ID
        }

        unpackFrame();    // Unpack data from buffer into class members based on command ID
        _payloadSize = 0; // Clear buffer after successful parse
        return true;
    }

    void NovaFitness::shiftBufferToNextStartByte()
    {
        // Shift buffer contents to the left until the next 0xAA byte is found or buffer is empty
        size_t shiftIndex = 1; // Start from the second byte since the first byte is already checked
        while (shiftIndex < _payloadSize && _payload[shiftIndex] != 0xAA)
        {
            shiftIndex++;
        }
        if (shiftIndex < _payloadSize)
        {
            // Found a new start byte, shift buffer contents to the left
            size_t newSize = _payloadSize - shiftIndex;
            memmove(_payload, _payload + shiftIndex, newSize);
            _payloadSize = newSize;
            // In the future we have to set the expected frame length dynamically based on command ID, for now it is constant so we can just check if the buffer is full after writing the byte
        }
        else
        {
            // No new start byte found, clear buffer
            _payloadSize = 0;
        }
    }

    NovaFitness::FirmwareVersion NovaFitness::getVersion()
    {
        return _firmwareVersion;
    }
    uint16_t NovaFitness::getID() { return _id; }
    float NovaFitness::getPM2_5() { return _pm2_5; }
    float NovaFitness::getPM10() { return _pm10; };
    float NovaFitness::getPM100() { return _pm100; }

    NovaFitness::ReportingMode NovaFitness::getReportingMode() { return _reportingMode; }
    NovaFitness::WorkMode NovaFitness::getWorkMode() { return _workMode; }

    void NovaFitness::unpackFrame()
    {
        switch (_payload[1])
        {
        case SETTINGS_COMMAND_ID:
            this->handleSettingsPayload();
            break;
        case DATA_COMMAND_ID:
            this->handleDataPayload();
            break;
        case DATA100_COMMAND_ID:
            this->handleData100Payload();
            break;
        }
    }

    bool NovaFitness::send(uint8_t *cmd, size_t cmdSize)
    {
        if (cmdSize != 19)
        {
            // The frame to send is always 19 bytes long
            return false;
        }
        this->setIdIntoFrame(cmd);
        cmd[17] = this->calcChecksum(cmd, cmdSize);
        size_t sendBytes = _uart->write(cmd, cmdSize);
        return sendBytes == cmdSize;
    }

    void NovaFitness::handleDataPayload()
    {
        _pm2_5 = ((_payload[3] << 8) | _payload[2]) / 10.0f;
        _pm10 = ((_payload[5] << 8) | _payload[4]) / 10.0f;

        // Payload 6 and 7 unhandled 'cause this is only the device id.
        // Maybe handle later if more than one NovaFitness sensor should work on
        // same time -> Each Sensor get own instance of NovaFitness class. Each
        // class should only handle frames of own sensor. Other concept for
        // Stream is neccessary. Maybe observer pattern?
    }

    void NovaFitness::handleData100Payload()
    {
        _pm100 = (_payload[5] << 8) | _payload[4];
    }

    void NovaFitness::handleSettingsPayload()
    {
        switch ((SettingsType)_payload[2])
        {
        case REPORTING_MODE:
            // Device reports 0 = ACTIVE, 1 = PASSIVE
            _reportingMode = _payload[4] == 0 ? ACTIVE : PASSIVE;
            break;
        case ID_SETTING:
            _id = (_payload[6] << 8) | _payload[7];
            break;
        case WORK_MODE:
            _workMode = _payload[4] == 0 ? SLEEP : WAKEUP;
            break;
        case WORKING_PERIOD:
            _workingPeriodSeconds = _payload[4] == 0 ? 1 : _payload[4] * 60;
            break;
        case FIRMWARE_SETTING:
            _firmwareVersion.year = _payload[3];
            _firmwareVersion.month = _payload[4];
            _firmwareVersion.day = _payload[5];
            break;
        default:
            break;
        }
    }

    void NovaFitness::setIdIntoFrame(uint8_t *cmd)
    {
        cmd[15] = (this->_id >> 8) & 0xFF;
        cmd[16] = this->_id & 0xFF;
    }

    uint8_t NovaFitness::calcChecksum(uint8_t *cmd, size_t cmdSize)
    {
        // "Checksum: Low 8bit of the sum result of Data Bytes（not including packet head, tail and Command ID"
        uint8_t sum = 0;

        for (size_t i = 2; i < cmdSize - 2; i++)
        {
            sum += cmd[i];
        }
        return sum;
    }
}