/// @file
#include "application.h"
#include "registers.h"

#include "modules/leds.h"
#include "modules/globals.h"
#include "modules/user_input.h"
#include "modules/finda.h"
#include "modules/fsensor.h"
#include "modules/serial.h"

#include "logic/command_base.h"
#include "logic/cut_filament.h"
#include "logic/eject_filament.h"
#include "logic/home.h"
#include "logic/hw_sanity.h"
#include "logic/load_filament.h"
#include "logic/move_selector.h"
#include "logic/no_command.h"
#include "logic/set_mode.h"
#include "logic/tool_change.h"
#include "logic/unload_filament.h"

#include "version.hpp"

#include "panic.h"

/// Global instance of the protocol codec
static mp::Protocol protocol;

Application application;

Application::Application()
    : lastCommandProcessedMs(0)
    , currentCommand(&logic::noCommand)
    , currentCommandRq(mp::RequestMsgCodes::Reset, 0) {}

void Application::CheckManualOperation() {
    uint16_t ms = mt::timebase.Millis();
    constexpr uint16_t idleDelay = 1000U;
    if (ms - lastCommandProcessedMs < idleDelay) {
        if (currentCommand->State() == ProgressCode::OK) {
            mui::userInput.Clear(); // consume bogus UI events while no command in progress and not in idle state yet
        }
        return;
    }

    lastCommandProcessedMs = ms - idleDelay; // prevent future overflows

    if (currentCommand->State() == ProgressCode::OK && mg::globals.FilamentLoaded() <= mg::FilamentLoadState::AtPulley) {
        if (mui::userInput.AnyEvent()) {
            switch (mui::userInput.ConsumeEvent()) {
            case mui::Event::Left:
                // move selector left if possible
                if (mg::globals.ActiveSlot() > 0) {
                    logic::moveSelector.Reset(mg::globals.ActiveSlot() - 1);
                    currentCommand = &logic::moveSelector;
                }
                break;
            case mui::Event::Middle:
                // plan load
                if (mg::globals.ActiveSlot() < config::toolCount) { // do we have a meaningful selector position?
                    logic::loadFilament.Reset(mg::globals.ActiveSlot());
                    currentCommand = &logic::loadFilament;
                }
                break;
            case mui::Event::Right:
                // move selector right if possible (including the park position)
                if (mg::globals.ActiveSlot() < config::toolCount) {
                    logic::moveSelector.Reset(mg::globals.ActiveSlot() + 1); // we allow also the park position
                    currentCommand = &logic::moveSelector;
                }
                break;
            default:
                break;
            }
        }
    }
}

mp::ResponseCommandStatus Application::RunningCommandStatus() const {
    if (mui::userInput.PrinterInCharge()) {
        mui::Event event = mui::userInput.ConsumeEventForPrinter();
        if (event != mui::Event::NoEvent) {
            return mp::ResponseCommandStatus(mp::ResponseMsgParamCodes::Button, (uint8_t)event);
        }
    }

    switch (currentCommand->Error()) {
    case ErrorCode::RUNNING:
        return mp::ResponseCommandStatus(mp::ResponseMsgParamCodes::Processing, (uint16_t)currentCommand->State());
    case ErrorCode::OK:
        return mp::ResponseCommandStatus(mp::ResponseMsgParamCodes::Finished, (uint16_t)currentCommand->Result());
    default:
        return mp::ResponseCommandStatus(mp::ResponseMsgParamCodes::Error, (uint16_t)currentCommand->Error());
    }
}

void Application::ReportCommandAccepted(const mp::RequestMsg &rq, mp::ResponseMsgParamCodes status) {
    uint8_t tmp[mp::Protocol::MaxResponseSize()];
    uint8_t len = protocol.EncodeResponseCmdAR(rq, status, tmp);
    modules::serial::WriteToUSART(tmp, len);
}

void Application::PlanCommand(const modules::protocol::RequestMsg &rq) {
    if (currentCommand->State() == ProgressCode::OK) {
        // We are allowed to start a new command as the previous one is in the OK finished state
        // The previous command may be in an error state, but as long as it is in ProgressCode::OK (aka finished)
        // we are safe here. It is the responsibility of the printer to ask for a command error code
        // before issuing another one - if needed.
        switch (rq.code) {
        case mp::RequestMsgCodes::Cut:
            currentCommand = &logic::cutFilament;
            break;
        case mp::RequestMsgCodes::Eject:
            currentCommand = &logic::ejectFilament;
            break;
        case mp::RequestMsgCodes::Home:
            currentCommand = &logic::home;
            break;
        case mp::RequestMsgCodes::Load:
            currentCommand = &logic::loadFilament;
            break;
        case mp::RequestMsgCodes::Tool:
            currentCommand = &logic::toolChange;
            break;
        case mp::RequestMsgCodes::Unload:
            currentCommand = &logic::unloadFilament;
            break;
        case mp::RequestMsgCodes::Mode:
            currentCommand = &logic::setMode;
            break;
        default:
            currentCommand = &logic::noCommand;
            break;
        }
        currentCommandRq = rq; // save the Current Command Request for indentification of responses
        bool accepted = currentCommand->Reset(rq.value);
        ReportCommandAccepted(rq, accepted ? mp::ResponseMsgParamCodes::Accepted : mp::ResponseMsgParamCodes::Rejected);
    } else {
        ReportCommandAccepted(rq, mp::ResponseMsgParamCodes::Rejected);
    }
}

void Application::ReportFINDA(const mp::RequestMsg &rq) {
#ifdef DEBUG_FINDA
    using namespace hal;
    hu::usart1.puts("FINDA:");
    if (hal::gpio::ReadPin(FINDA_PIN) == hal::gpio::Level::high) {
        hu::usart1.puts(" TIRGGERED\n");
    } else {
        hu::usart1.puts(" NOT TRIGGERED\n");
    }
#endif //DEBUG_FINDA
    uint8_t rsp[mp::Protocol::MaxResponseSize()];
    uint8_t len = protocol.EncodeResponseReadFINDA(rq, mf::finda.Pressed(), rsp);
    modules::serial::WriteToUSART(rsp, len);
}

void Application::ReportRunningCommand() {
    uint8_t rsp[mp::Protocol::MaxResponseSize()];
    uint8_t len = protocol.EncodeResponseQueryOperation(currentCommandRq, RunningCommandStatus(), rsp);
    modules::serial::WriteToUSART(rsp, len);
}

void Application::ReportReadRegister(const mp::RequestMsg &rq) {
    uint8_t rsp[mp::Protocol::MaxResponseSize()];
    uint16_t value2;
    bool accepted = ReadRegister(rq.value, value2);
    uint8_t len = protocol.EncodeResponseRead(rq, accepted, value2, rsp);
    modules::serial::WriteToUSART(rsp, len);
}

void Application::ReportWriteRegister(const mp::RequestMsg &rq) {
    uint8_t rsp[mp::Protocol::MaxResponseSize()];
    mp::ResponseMsgParamCodes ar = WriteRegister(rq.value, rq.value2) ? mp::ResponseMsgParamCodes::Accepted : mp::ResponseMsgParamCodes::Rejected;
    uint8_t len = protocol.EncodeResponseCmdAR(rq, ar, rsp);
    modules::serial::WriteToUSART(rsp, len);
}

void Application::ProcessRequestMsg(const mp::RequestMsg &rq) {
    switch (rq.code) {
    case mp::RequestMsgCodes::Button:
        // behave just like if the user pressed a button
        mui::userInput.ProcessMessage(rq.value);
        ReportCommandAccepted(rq, mp::ResponseMsgParamCodes::Accepted);
        break;
    case mp::RequestMsgCodes::Finda:
        // immediately report FINDA status
        ReportFINDA(rq);
        break;
    case mp::RequestMsgCodes::Query:
        // immediately report progress of currently running command
        ReportRunningCommand();
        break;
    case mp::RequestMsgCodes::Reset:
        // immediately reset the board - there is no response in this case
        hal::cpu::Reset();
        break;
    case mp::RequestMsgCodes::Version:
    case mp::RequestMsgCodes::Read:
        ReportReadRegister(rq);
        break;
    case mp::RequestMsgCodes::Write:
        ReportWriteRegister(rq);
        break;
    case mp::RequestMsgCodes::Cut:
    case mp::RequestMsgCodes::Eject:
    case mp::RequestMsgCodes::Home:
    case mp::RequestMsgCodes::Load:
    case mp::RequestMsgCodes::Tool:
    case mp::RequestMsgCodes::Unload:
        PlanCommand(rq);
        break;
    case mp::RequestMsgCodes::FilamentSensor: // set filament sensor state in the printer
        mfs::fsensor.ProcessMessage(rq.value != 0);
        ReportCommandAccepted(rq, mp::ResponseMsgParamCodes::Accepted);
        break;
    default:
        // respond with an error message
        break;
    }
}

bool Application::CheckMsgs() {
    using mpd = mp::DecodeStatus;
    while (modules::serial::Available()) {
        switch (protocol.DecodeRequest(modules::serial::ConsumeByte())) {
        case mpd::MessageCompleted:
            // process the input message
            return true;
            break;
        case mpd::NeedMoreData:
            // just continue reading
            break;
        case mpd::Error:
            // @@TODO what shall we do? Start some watchdog? We cannot send anything spontaneously
            break;
        }
    }
    return false;
}

void Application::Panic(ErrorCode ec) {
    currentCommand->Panic(ec);
}

void Application::Step() {
    CheckManualOperation();

    if (CheckMsgs()) {
        ProcessRequestMsg(protocol.GetRequestMsg());
    }

    currentCommand->Step();
}

uint8_t Application::CurrentProgressCode() {
    return (uint8_t)currentCommand->State();
}

uint16_t Application::CurrentErrorCode() {
    return (uint16_t)currentCommand->Error();
}
