#include "BurnProtocolDfuSTM32.h"
#include "protocol/dfu/DfuDevice.h"
#include "protocol/dfu/DfuDeviceErrors.h"
#include "../BurnProtocolDfuErrors.h"
#include "LQError.h"
#include "stm_dfu_defs.h"
#include <QElapsedTimer>
#include <QThread>

QString BurnProtocolDfuSTM32::protoclolId() const {
    static const QString id{"dfu-stm32"};
    return id;
}

int BurnProtocolDfuSTM32::maxReadBlockSize() const {
    return 2048;
}

int BurnProtocolDfuSTM32::maxWriteBlockSize() const {
    return 2048;
}

void BurnProtocolDfuSTM32::detect(LQError &err) {
    BurnProtocolDfu::detect(err);
    if (err.isSuccess()) {
        /* Проверяем состояние устройства после установки связи. */
        DfuDevice::StatusInfo si;
        device().getStatus(si, err);
        if (err.isSuccess()) {
            if (si.state() == DfuDevice::State::dfuERROR) {
                /* Если устройство находится в состоянии ошибки, то сбрасываем признак
                   ошибки (иначе остальные операции будут невыполнимы) */
                device().clearStatus(err);
            } else if (si.state() != DfuDevice::State::dfuIDLE) {
                /* Если незакончена операция от предудущего сеанса, то принудительно
                 * отменяем ее */
                device().abort(err);
            }
        }
    }

    if (err.isSuccess()) {
        /* проверяем работу стройства простым запросом списка поддерживаемых команд */
        getCommandList(m_sup_cmds, err);
    }
}

void BurnProtocolDfuSTM32::getDevInfo(BurnDeviceInfo &info, LQError &err) {
    /* не поддерживается при работе по DFU */
}

void BurnProtocolDfuSTM32::devCleanup(burn_mem_addr_t start_addr, LQError &err) {
    if (err.isSuccess()) {
        QByteArray dummy;
        LQError checkErr;
         /* посылаем тестовую команду чтения, чтобы проверить, не включен ли RDP */
        readMemBlock(start_addr, 4, dummy, checkErr);
        if (!checkErr.isSuccess()) {
            /* если включен, то снимаем этот режим */
            readUnprotect(err);
        }
    }

    /* полная очистка чипа */
    if (err.isSuccess()) {
        eraseChip(err);
    }
}

void BurnProtocolDfuSTM32::writeMemBlock(burn_mem_addr_t addr, const QByteArray &data, LQError &err) {
    if ((data.size() < 1) || (data.size() > maxWriteBlockSize())) {
        err = BurnProtocolDfuErrors::invalidDataSize();
    }
    if (err.isSuccess()) {
        /* для простоты всегда сперва устанавливаем адрес явно, а затем пишим в нулевой блок */
        setAddrPointer(addr, err);
    }
    if (err.isSuccess()) {
        startWrOp(err);
    }

    if (err.isSuccess()) {
        if (data.size() != 1) {
            device().dnload(STM_DFU_ZERO_BLOCK_NUM, data, err);
        } else {
            /* минимальный размер записи - 2 байта. Если записываем один,
             * то добавляем в качестве дополнительного байта 0xFF */
            QByteArray pd_data {data.at(0), static_cast<char>(0xFF)};
            device().dnload(STM_DFU_ZERO_BLOCK_NUM, pd_data, err);
        }
    }
    if (err.isSuccess()) {
        execBsyOp(mem_write_tout, err);
    }
}

void BurnProtocolDfuSTM32::readMemBlock(burn_mem_addr_t addr, int req_size, QByteArray &data, LQError &err) {
    if ((req_size < 1) || (req_size > maxReadBlockSize())) {
        err = BurnProtocolDfuErrors::invalidDataSize();
    }
    if (err.isSuccess()) {
        /* сперва устанавливаем явно адрес, откуда читать */
        setAddrPointer(addr, err);
    }
    if (err.isSuccess()) {
        startRdOp(err);
    }

    if (err.isSuccess()) {
        device().upload(STM_DFU_ZERO_BLOCK_NUM, req_size, data, err);
    }
}

void BurnProtocolDfuSTM32::startApp(burn_mem_addr_t addr, LQError &err) {
    if (err.isSuccess()) {
        setAddrPointer(addr, err);
    }
    if (err.isSuccess()) {
        startWrOp(err);
    }
    if (err.isSuccess()) {
        device().dnload(2, QByteArray{}, err);
    }
    if (err.isSuccess()) {
        getStatusCheck(DfuDevice::State::dfuMANIFEST, err);
    }
}

void BurnProtocolDfuSTM32::getCommandList(QByteArray &commands, LQError &err) {
    device().upload(0, 4, commands, err);
}


void BurnProtocolDfuSTM32::setAddrPointer(quint32 addr, LQError &err) {
     const char tx_arr[] = {STM_DFU_CMD_SET_ADDR_PTR,
                            static_cast<char>((addr >> 0) & 0xFF),
                            static_cast<char>((addr >> 8) & 0xFF),
                            static_cast<char>((addr >> 16) & 0xFF),
                            static_cast<char>((addr >> 24) & 0xFF)
                           };
     device().dnload(0, QByteArray::fromRawData(tx_arr, sizeof(tx_arr)), err);
     if (err.isSuccess()) {
         execBsyOp(set_addr_tout, err);
     }
}




void BurnProtocolDfuSTM32::eraseChip(LQError &err) {
    if (err.isSuccess()) {
        startWrOp(err);
    }
    if (err.isSuccess()) {
        const char tx_arr[] = {STM_DFU_CMD_ERASE};
        device().dnload(0, QByteArray::fromRawData(tx_arr, sizeof(tx_arr)), err);
    }
    if (err.isSuccess()) {
        execBsyOp(mass_erase_tout, err);
    }
}

void BurnProtocolDfuSTM32::erasePage(burn_mem_addr_t addr, LQError &err) {
    const char tx_arr[] = {STM_DFU_CMD_ERASE,
                           static_cast<char>((addr >> 0) & 0xFF),
                           static_cast<char>((addr >> 8) & 0xFF),
                           static_cast<char>((addr >> 16) & 0xFF),
                           static_cast<char>((addr >> 24) & 0xFF)
                          };
    device().dnload(0, QByteArray::fromRawData(tx_arr, sizeof(tx_arr)), err);
    if (err.isSuccess()) {
        execBsyOp(page_erase_tout, err);
    }
}

void BurnProtocolDfuSTM32::readUnprotect(LQError &err) {
    const char tx_arr[] = {static_cast<char>(STM_DFU_CMD_READ_UNPROTECT)};
    device().dnload(0, QByteArray::fromRawData(tx_arr, sizeof(tx_arr)), err);
    if (err.isSuccess()) {
        getStatusCheck(DfuDevice::State::dfuDNBUSY, err);
        if (err.isSuccess()) {
            device().close();
            QThread::msleep(reboot_wait_tout);
            device().detect(err);
        }
    }
}

/* получение текущего статуса с проверкой, что state должно быть обязательно равно expState */
void BurnProtocolDfuSTM32::getStatusCheck(DfuDevice::State expState, LQError &err) {
    DfuDevice::StatusInfo st;
    device().getStatus(st, err);
    if (err.isSuccess()) {
        if (st.state() != expState) {
            if (st.state() == DfuDevice::State::dfuERROR) {
                err = DfuDeviceErrors::devStatusError(st.status());
            } else {
                err = BurnProtocolDfuErrors::unexpectedDevState(expState, st.state());
            }
        }
    }
}

/* ожидание, пока State не станет отличным от Busy */
void BurnProtocolDfuSTM32::waitRdy(DfuDevice::StatusInfo &si, unsigned tout, LQError &err) {
    bool done {false};
    QElapsedTimer tmr;
    tmr.start();
    do {
        DfuDevice::StatusInfo st;
        device().getStatus(st, err);
        if (err.isSuccess()) {
            done = st.state() != DfuDevice::State::dfuDNBUSY;
            if (done) {
                if (st.state() == DfuDevice::State::dfuERROR) {
                    err = DfuDeviceErrors::devStatusError(st.status());
                }
                si = st;
            } else if (tmr.elapsed() > tout) {
                err = BurnProtocolDfuErrors::opExecTimeout();
            }
        }
    } while (!done && err.isSuccess());
}

void BurnProtocolDfuSTM32::execBsyOp(unsigned tout, LQError &err) {
    /* первый GetStatus запускает операцию и должен вернуть State = dfuDNBUSY.
     * далее необходимо вызывать GetStatus пока состояние не изменится на какое-либо иное */
    getStatusCheck(DfuDevice::State::dfuDNBUSY, err);
    if (err.isSuccess()) {
        DfuDevice::StatusInfo si;
        waitRdy(si, tout, err);
    }
}

/* проверка и перевод при необходимости в устройства в состояние готовности к записи */
void BurnProtocolDfuSTM32::startWrOp(LQError &err) {
    DfuDevice::StatusInfo st;
    device().getStatus(st, err);
    if (err.isSuccess()) {
        /* сброс ошибки, если осталась с предыдущей операции */
        if (st.state() == DfuDevice::State::dfuERROR) {
            device().clearStatus(err);
        } else if ((st.state() != DfuDevice::State::dfuIDLE)
                    && (st.state() != DfuDevice::State::dfuDNLOAD_IDLE)) {
            /* если неверное состояние, то делаем ABORT (в том числе требуется при переходе от чтения) */
            device().abort(err);
        }
    }
}

/* проверка и перевод при необходимости в устройства в состояние готовности к чтению */
void BurnProtocolDfuSTM32::startRdOp(LQError &err) {
    DfuDevice::StatusInfo st;
    device().getStatus(st, err);
    if (err.isSuccess()) {
        /* сброс ошибки, если осталась с предыдущей операции */
        if (st.state() == DfuDevice::State::dfuERROR) {
            device().clearStatus(err);
        } else if ((st.state() != DfuDevice::State::dfuIDLE)
                    && (st.state() != DfuDevice::State::dfuUPLOAD_IDLE)) {
            /* если неверное состояние, то делаем ABORT (в том числе требуется при переходе от записи) */
            device().abort(err);
        }
    }
}
