#pragma once

#include <cstddef>
#include <cstdint>
#include <cstring>

// ESP WebSocket events may split one frame across several transport reads.
// Storage belongs to the caller so the receive buffer can live in PSRAM.
class WebSocketTextReceiver {
public:
    enum class Result { IGNORED, INCOMPLETE, COMPLETE, REJECTED };

    void set_storage(char *storage, size_t capacity) {
        storage_ = storage;
        capacity_ = capacity; // Caller provides one extra byte for the terminator.
        reset();
    }
    void reset() { active_ = false; frame_open_ = false; size_ = 0; }
    const char *data() const { return storage_; }
    size_t size() const { return size_; }

    Result receive(uint8_t opcode, bool fin, const char *data, int length,
                   int payload_length, int offset) {
        // Ping/pong/close are handled by the SDK and may interleave text frames.
        if (opcode & 0x8) return Result::IGNORED;
        if (opcode != 0x1 && opcode != 0x0) return reject();
        if (!storage_ || length < 0 || payload_length < 0 || offset < 0 ||
            offset > payload_length || length > payload_length - offset ||
            (length && !data)) return reject();
        if (offset == 0) {
            if (opcode == 0x1) { reset(); active_ = true; }
            else if (!active_ || frame_open_) return reject();
            if (static_cast<size_t>(payload_length) > capacity_ - size_) return reject();
            frame_open_ = true;
            frame_length_ = payload_length;
            frame_offset_ = 0;
            frame_opcode_ = opcode;
            frame_fin_ = fin;
        }
        if (!active_ || !frame_open_ || opcode != frame_opcode_ || fin != frame_fin_ ||
            payload_length != frame_length_ || offset != frame_offset_) return reject();
        if (length) std::memcpy(storage_ + size_, data, static_cast<size_t>(length));
        size_ += static_cast<size_t>(length);
        frame_offset_ += length;
        if (frame_offset_ != frame_length_) return Result::INCOMPLETE;
        frame_open_ = false;
        if (!fin) return Result::INCOMPLETE;
        storage_[size_] = '\0';
        active_ = false;
        return Result::COMPLETE;
    }

private:
    Result reject() { reset(); return Result::REJECTED; }
    char *storage_ = nullptr;
    size_t capacity_ = 0, size_ = 0;
    bool active_ = false, frame_open_ = false, frame_fin_ = false;
    uint8_t frame_opcode_ = 0;
    int frame_length_ = 0, frame_offset_ = 0;
};
