// SPDX-License-Identifier: BSD-3-Clause

#include <memory>
#include <iostream>
#include <limits>
#include <cstring>
#include <chrono>

#include "crc.h"
#include "device.h"
#include "time.h"
#include "exceptions.h"
#include "hexdump/hexdump.h"
#include "../logging.h"

using namespace std::chrono_literals;

namespace voltronic {

Device::Device()
    : flags_(FLAG_WRITE_CRC | FLAG_READ_CRC | FLAG_VERIFY_CRC)
    , timeout_(TIMEOUT)
    , timeStarted_(0)
    , verbose_(false)
    , workerType_(WorkerType::Normal)
{
    thread_ = std::thread(&Device::threadLoop, this);

#ifdef __linux__
    // try to set the highest priority for this thread (requires root privs)
    struct sched_param sp;
    sp.sched_priority = sched_get_priority_max(SCHED_FIFO);
    if (sp.sched_priority == -1) {
        myerr << "sched_get_priority_max: " << std::string(strerror(errno));
    } else {
        int res = pthread_setschedparam(thread_.native_handle(), SCHED_FIFO, &sp);
        if (res)
            myerr << "pthread_setschedparam: error " << std::to_string(res);
    }
#endif

    thread_.detach();
}

Device::~Device() {
    workerType_ = WorkerType::Dead;
}

void Device::setFlags(int flags) {
    flags_ = flags;
}

int Device::getFlags() const {
    return flags_;
}

void Device::setWorkerType(WorkerType wt) {
    workerType_ = wt;
}

void Device::setVerbose(bool verbose) {
    verbose_ = verbose;
}

void Device::setTimeout(u64 timeout) {
    timeout_ = timeout;
}

u64 Device::getElapsedTime() const {
    return timestamp() - timeStarted_;
}

u64 Device::getTimeLeft() const {
    if (!timeout_)
        return std::numeric_limits<uint64_t>::max();

    u64 elapsed = getElapsedTime();
    if (elapsed > timeout_)
        return 0;

    return timeout_ - elapsed;
}

size_t Device::enqueue(const u8* inbuf, size_t inbufSize, u8* outbuf, size_t outbufSize) {
    if (verbose_)
        mylog << "waiting to accept new task...";

    mutex_.lock();
    shio_.resetWith(inbuf, inbufSize, outbuf, outbufSize);
    mutex_.unlock();

    cv_.notify_all();

    if (verbose_)
        mylog << "notify the worker thread";

    UniqueLock lock(mutex_);
    cv_.wait(lock, [this]{
        return shio_.state == SharedIOBufferState::Done;
    });

    if (verbose_)
        mylog << "worker thread done it's job";

    switch (shio_.errorType) {
        case ErrorType::DeviceError:      throw DeviceError(shio_.errorMessage);
        case ErrorType::TimeoutError:     throw TimeoutError(shio_.errorMessage);
        case ErrorType::InvalidDataError: throw InvalidDataError(shio_.errorMessage);
        case ErrorType::OverflowError:    throw OverflowError(shio_.errorMessage);
        default: break;
    }

    return shio_.dataSize;
}

// ----------------------------------------
// all code below runs in a separate thread
// ----------------------------------------

void Device::threadLoop() {
    while (workerType_ != Dead) {
        if (verbose_)
            mylog << "waiting for new task...";

        // wait for new task
        UniqueLock lock(mutex_);
        auto pred = [this]{
            return shio_.state == SharedIOBufferState::Ready;
        };

        if (workerType_ == OneShot) {
            cv_.wait(lock, pred);
        } else {
            cv_.wait_for(lock, 1s, pred);
            if (!pred())
                continue;
        };

        if (verbose_)
            mylog << "got something";

        shio_.state = SharedIOBufferState::InProgress;

        try {
            shio_.setResult(run(shio_.inputBuffer,
                                shio_.inputBufferSize,
                                shio_.outputBuffer,
                                shio_.outputBufferSize));
        }
        catch (DeviceError& e) {
            shio_.setResult(ErrorType::DeviceError, e.what());
        }
        catch (TimeoutError& e) {
            shio_.setResult(ErrorType::TimeoutError, e.what());
        }
        catch (InvalidDataError& e) {
            shio_.setResult(ErrorType::InvalidDataError, e.what());
        }
        catch (OverflowError& e) {
            shio_.setResult(ErrorType::OverflowError, e.what());
        }

        if (verbose_)
            mylog << "unlocking";

        lock.unlock();
        cv_.notify_all();

        if (workerType_ == OneShot)
            break;
    }
}

size_t Device::run(const u8* inbuf, size_t inbufSize, u8* outbuf, size_t outbufSize) {
    timeStarted_ = timestamp();

    // ------------------------------
    // add CRC and send to the device
    // ------------------------------

    size_t dataLen;
    std::shared_ptr<u8> data;

    if ((flags_ & FLAG_WRITE_CRC) == FLAG_WRITE_CRC) {
        const CRC crc = crc_calculate(inbuf, inbufSize);
        dataLen = inbufSize + sizeof(u16) + 1;
        data = std::unique_ptr<u8>(new u8[dataLen]);
        crc_write(crc, &data.get()[inbufSize]);
    } else {
        dataLen = inbufSize + 1;
        data = std::unique_ptr<u8>(new u8[dataLen]);
    }

    u8* dataPtr = data.get();
    memcpy((void*)dataPtr, inbuf, inbufSize);
    dataPtr[dataLen - 1] = '\r';
    if (verbose_) {
        myerr << "writing " << dataLen << (dataLen > 1 ? " bytes" : " byte");
        std::cerr << hexdump(dataPtr, dataLen);
    }

    writeAll(dataPtr, dataLen);


    // -----------------
    // check for timeout
    // -----------------

    if (!getTimeLeft()) {
        // FIXME
        // we should read incoming data from the device,
        // or clean the buffer in some other way.
        // otherwise we may get invalid response next time
        throw TimeoutError("sending already took " + std::to_string(getElapsedTime()) + " ms");
    }

    // ------------------------------
    // read from device and check CRC
    // ------------------------------

    size_t bytesRead = readAll(outbuf, outbufSize);

    if (verbose_) {
        myerr << "got " << bytesRead << (bytesRead > 1 ? " bytes" : " byte");
        std::cerr << hexdump(outbuf, bytesRead);
    }

    bool crcNeeded = (flags_ & FLAG_READ_CRC) == FLAG_READ_CRC;
    size_t minSize = crcNeeded ? sizeof(u16) + 1 : 1;

    if (bytesRead < minSize)
        throw InvalidDataError("response is too small");

    const size_t dataSize = bytesRead - minSize;

    if (crcNeeded) {
        const CRC crcActual = crc_read(&outbuf[dataSize]);
        const CRC crcExpected = crc_calculate(outbuf, dataSize);

        if ((flags_ & FLAG_VERIFY_CRC) == FLAG_VERIFY_CRC && crcActual == crcExpected)
            return dataSize;

        std::ostringstream error;
        error << std::hex;
        error << "crc is invalid: expected 0x" << crcExpected << ", got 0x" << crcActual;
        throw InvalidDataError(error.str());
    }

    return dataSize;
}

void Device::writeAll(const u8* data, size_t dataSize) {
    int bytesLeft = static_cast<int>(dataSize);

    while (true) {
        size_t bytesWritten = write(data, bytesLeft);
        if (verbose_)
            myerr << "bytesWritten=" << bytesWritten;

        bytesLeft -= static_cast<int>(bytesWritten);
        if (bytesLeft <= 0)
            break;

        if (!getTimeLeft())
            throw TimeoutError("data writing already took " + std::to_string(getElapsedTime()) + " ms");

        data = &data[bytesWritten];
    }
}

size_t Device::readAll(u8 *buf, size_t bufSize) {
    size_t size = 0;

    while(true) {
        size_t bytesRead = read(buf, bufSize);
        if (verbose_)
            myerr << "bytesRead=" << bytesRead;

        while (bytesRead) {
            bytesRead--;
            size++;

            if (*buf == '\r')
                return size;

            buf++;
            bufSize--;
        }

        if (!getTimeLeft())
            throw TimeoutError("data reading already took " + std::to_string(getElapsedTime()) + " ms");

        if (bufSize <= 0)
            throw OverflowError("input buffer is not large enough");
    }
}

}