/*

TUYA MODULE

Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>

*/

#pragma once

#include <Print.h>
#include <StreamString.h>

#include <iterator>
#include <vector>

namespace tuya {

    class PrintRaw {
    public:
        static void write(Print& printer, uint8_t data) {
            printer.write(data);
        }

        static void write(Print& printer, const uint8_t* data, size_t size) {
            printer.write(data, size);
        }
    };

    class PrintHex {
    public:
        static void write(Print& printer, uint8_t data) {
            char buffer[3] = {0};
            snprintf(buffer, sizeof(buffer), "%02x", data);
            printer.write(buffer, 2);
        }

        static void write(Print& printer, const uint8_t* data, size_t size) {
            for (size_t n = 0; n < size; ++n) {
                write(printer, data[n]);
            }
        }
    };


    class StreamWrapper {
    protected:
        Stream& _stream;

    public:
        StreamWrapper(Stream& stream) :
            _stream(stream)
        {}

        int available() {
            return _stream.available();
        }

        void rewind() {
            while(_stream.read() != -1);
        }
    };

    class Output : public virtual StreamWrapper {
    public:
        Output(Stream& stream) :
            StreamWrapper(stream)
        {}

        Output(StreamString& stream, size_t length) :
            Output(stream)
        {
            stream.reserve((length * 2) + 1);
        }

        template <typename T, typename PrintType>
        void _write(const T& data) {

            const uint8_t header[2] = {0x55, 0xaa};
            uint8_t checksum = 0xff;

            PrintType::write(_stream, header, 2);

            for (auto it = data.cbegin(); it != data.cend(); ++it) {
                checksum += *it;
                PrintType::write(_stream, *it);
            }

            PrintType::write(_stream, checksum);

        }

        template <typename T>
        void write(const T& data) {
            _write<T, PrintRaw>(data);
        }

        template <typename T>
        void writeHex(const T& data) {
            _write<T, PrintHex>(data);
        }


    };

    class Input : public virtual StreamWrapper {
        // Buffer depth based on the SDK recommendations
        constexpr static size_t LIMIT = 256;

        // 9600 baud ~= 1.04 bytes per second
        // 256 * 1.04 = 266.24
        constexpr static size_t TIME_LIMIT = 267;

    public:
        using const_iterator = std::vector<uint8_t>::const_iterator;

        Input(Stream& stream) :
            StreamWrapper(stream)
        {
            _buffer.reserve(LIMIT);
        }

        bool full() { return (_index >= LIMIT); }
        bool done() { return _done; }
        size_t size() { return _index; }

        uint8_t operator[](size_t i) const {
            if (i > LIMIT) return 0;
            return _buffer[i];
        }

        const_iterator cbegin() const {
            return _buffer.cbegin();
        }

        const_iterator cend() const {
            return _buffer.cend();
        }

        void read() {

            if (_done) return;
            if (full()) return;

            if (_last && (millis() - _last > TIME_LIMIT)) reset();
            _last = millis();

            int byte = _stream.read();
            if (byte < 0) return;

            //DEBUG_MSG("i=%u byte=%u chk=%u:%u\n",
            //  _index, byte, _checksum, uint8_t(_checksum + byte));

            // check that header value is 0x55aa
            if (0 == _index) {
                if (0x55 != byte) return;
            } else if (1 == _index) {
                if (0xaa != byte) return;
            }

            // set read boundary from received length
            if (4 == _index) {
                _read_until = byte << 8;
            }

            if (5 == _index) {
                _read_until += byte + _index + 1;
                //DEBUG_MSG("read_until=%u\n", _read_until);
            }

            // verify that the checksum byte is the same that we got so far
            if ((_index > 5) && (_index >= _read_until)) {
                if (_checksum != byte) {
                    //DEBUG_MSG("chk err, recv=%u calc=%u\n", byte, _checksum);
                    reset();
                    return;
                }

                //DEBUG_MSG("chk ok\n");
                _done = true;
                return;
            }

            _buffer[_index] = byte;
            _checksum += byte;

            ++_index;

        }

        void reset() {
            std::fill(_buffer.begin(), _buffer.end(), 0);
            _read_until = LIMIT;
            _checksum = 0;
            _index = 0;
            _done = false;
            _last = 0;
        }

    private:
        bool _done = false;
        size_t _index = 0;
        size_t _read_until = LIMIT;
        uint8_t _checksum = 0;
        std::vector<uint8_t> _buffer;
        unsigned long _last = 0;
    };

    class Transport : public Input, public Output, public virtual StreamWrapper {
    public:
        Transport(Stream& stream) :
            StreamWrapper(stream), Input(stream), Output(stream)
        {}
    };

}
