void FilterIFW::simulationTriggered(bool enable)
{
    // toogle buttons to select 5 or 8 filters depend if Simulation active or not
    if (enable)
    {
        if (isConnected())
        {
            defineProperty(&FilterNbrSP);
        }
    }
    else
        deleteProperty(FilterNbrSP.name);
}

/*******************************************************************************

*******************************************************************************/

#include "ifwoptec.h"

#include "indicom.h"
#include "indicontroller.h"
#include "connectionplugins/connectionserial.h"

#include <memory>
#include <regex>
#include <cstring>
#include <unistd.h>

std::unique_ptr<FilterIFW> filter_ifw(new FilterIFW());

/************************************************************************************
*
************************************************************************************/
FilterIFW::FilterIFW()
{
    //ctor
    setVersion(VERSION, SUBVERSION);
    strncpy(filterSim, filterSim5, sizeof(filterSim)); // For simulation mode

    // Set communication to serail only and avoid driver crash at starting up
    setFilterConnection(CONNECTION_SERIAL | CONNECTION_TCP);

    // We add an additional debug level so we can log verbose member function starting
    // DBG_TAG is used by macro DEBUGTAG() define in ifwoptec.h
    int DBG_TAG = 0;

    DBG_TAG = INDI::Logger::getInstance().addDebugLevel("Function tag", "Tag");
}

/************************************************************************************
*
************************************************************************************/
const char *FilterIFW::getDefaultName()
{
    return (const char *)"Optec IFW";
}

/**************************************************************************************
*
***************************************************************************************/
bool FilterIFW::initProperties()
{
    INDI::FilterWheel::initProperties();

    // Settings
    IUFillText(&WheelIDT[0], "ID", "ID", "-");
    IUFillTextVector(&WheelIDTP, WheelIDT, 1, getDeviceName(), "WHEEL_ID", "Wheel", FILTER_TAB, IP_RO, 60, IPS_IDLE);

    // Command
    IUFillSwitch(&HomeS[0], "HOME", "Home", ISS_OFF);
    IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0,
                       IPS_IDLE);

    // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters
    IUFillSwitch(&FilterNbrS[0], "VAL5", "5", ISS_ON);
    IUFillSwitch(&FilterNbrS[1], "VAL6", "6", ISS_OFF);
    IUFillSwitch(&FilterNbrS[2], "VAL8", "8", ISS_OFF);
    IUFillSwitch(&FilterNbrS[3], "VAL9", "9", ISS_OFF);
    IUFillSwitchVector(&FilterNbrSP, FilterNbrS, 4, getDeviceName(), "FILTER_NBR", "Filter nbr", FILTER_TAB, IP_RW,
                       ISR_1OFMANY, 0, IPS_IDLE);

    // User could choice to unrestrict chars set to set the filternames if he accepts to have crazy display name on IFW box
    // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters
    IUFillSwitch(&CharSetS[0], "RES", "Restricted", ISS_ON);
    IUFillSwitch(&CharSetS[1], "UNRES", "All", ISS_OFF);
    IUFillSwitchVector(&CharSetSP, CharSetS, 2, getDeviceName(), "CHARSET", "Chars allowed", FILTER_TAB, IP_RW,
                       ISR_1OFMANY, 0, IPS_IDLE);

    // Firmware of the IFW
    IUFillText(&FirmwareT[0], "FIRMWARE", "Firmware", "Unknown");
    IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "FIRMWARE_ID", "IFW", FILTER_TAB, IP_RO, 60, IPS_IDLE);

    addAuxControls();
    serialConnection->setDefaultBaudRate(Connection::Serial::B_19200);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::updateProperties()
{
   INDI::FilterWheel::updateProperties();
	 if (isConnected())
    {
        defineProperty(&HomeSP);
        defineProperty(&FirmwareTP);
        defineProperty(&WheelIDTP); // ID of the wheel first in Filter tab page
        // Then the button only for Simulation to select the number of filter of the Wheel (5 or 8)
        if (isSimulation())
            defineProperty(&FilterNbrSP);
        defineProperty(&CharSetSP);
        defineProperty(&FilterSlotNP);
        controller->updateProperties();

        GetFirmware(); // Try to get Firmware version of the IFW. NOt all Firmware support this function
        moveHome();    // Initialisation of the physical IFW
    }
    else
    {
        deleteProperty(HomeSP.name);
        deleteProperty(FirmwareTP.name);
        deleteProperty(WheelIDTP.name);
        deleteProperty(CharSetSP.name);
        deleteProperty(FilterNbrSP.name);
        deleteProperty(FilterSlotNP.name);
        deleteProperty(FilterNameTP->name);
        controller->updateProperties();
    }

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::WriteTTY(char *command)
{
    char cmd[OPTEC_MAXLEN_CMD];
    int errcode = 0;
    char errmsg[MAXRBUF];
    int nbytes_written = 0;

    snprintf(cmd, OPTEC_MAXLEN_CMD, "%s%s", command, "\n\r");
    LOGF_DEBUG("CMD (%s)", cmd);

    if (!isSimulation())
    {
        if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK)
        {
            tty_error_msg(errcode, errmsg, MAXRBUF);
            LOGF_ERROR("%s", errmsg);
            return false;
        }
    }
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ReadTTY(char *resp, char *simulation, int timeout)
{
    int errcode = 0;
    char errmsg[MAXRBUF];
    char response[OPTEC_MAXLEN_RESP + 1];
    int nbytes_read = 0;

    memset(response, 0, sizeof(response));

    if (isSimulation())
    {
        strncpy(response, simulation, sizeof(response));
        nbytes_read = strlen(response) + 2; // +2 for simulation = "\n\r" see below
    }
    else
    {
        if ((errcode = tty_read_section(PortFD, response, 0xd, timeout, &nbytes_read)) != TTY_OK)
        {
            tty_error_msg(errcode, errmsg, MAXRBUF);
            LOGF_ERROR("%s() TTY error: %s", __FUNCTION__, errmsg);
            return false;
        }
    }

    if (nbytes_read <= 0)
    {
        LOG_ERROR("Controller error: Nothing returned by the IFW");
        response[0] = '\0';
        return false;
    }

    response[nbytes_read - 2] = '\0'; //Remove control char from string (\n\r)
    LOGF_DEBUG("RES (%s)", response);
    strncpy(resp, response, /* sizeof(response)*/ OPTEC_MAXLEN_RESP +1);
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::Handshake()
{
    char response[OPTEC_MAXLEN_RESP + 1];
    memset(response, 0, sizeof(response));
    if (!WriteTTY((char *)"WSMODE"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        return false;
    }

    if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        return false;
    }

    if (strcmp(response, "!") != 0)
    {
        LOG_ERROR("failed, wrong response from IFW");
        LOGF_DEBUG("Response : (%s)", response);
        return false;
    }

    LOGF_DEBUG("Success, response from IFW is : %s", response);
    LOG_INFO("IFW is online");

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::Disconnect()
{
    DEBUGTAG();
    char response[OPTEC_MAXLEN_RESP + 1];
    memset(response, 0, sizeof(response));

    if (!WriteTTY((char *)"WEXITS"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        return false;
    }

    if (!ReadTTY(response, (char *)"END", OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        return false;
    }

    if (strcmp(response, "END") != 0)
    {
        LOG_ERROR("failed, wrong response from IFW");
        return false;
    }

    LOGF_DEBUG("IFW return in manual mode, response from IFW is : %s", response);
    LOG_INFO("IFW is offline.");

    return INDI::FilterWheel::Disconnect();
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
{
    if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
    {
        // User has changed one or more names from filter related to the Wheel ID present in the IFW
        if (strcmp(FilterNameTP->name, name) == 0)
        {
            // Only these chars are allowed to be able to the IFW display to show names correctly
            std::regex rx("^[A-Z0-9=.#/%[:space:]-]{1,8}$");

            bool match = true;
            //Check only if user allowed chars restriction
            if (CharSetS[0].s == ISS_ON)
            {
                for (int i = 0; i < n; i++)
                {
                    LOGF_DEBUG("FilterName request N°%d : %s", i, texts[i]);
                    match = std::regex_match(texts[i], rx);
                    if (!match)
                        break;
                }
            }

            if (match)
            {
                IUUpdateText(FilterNameTP, texts, names, n);
                FilterNameTP->s = SetFilterNames() ? IPS_OK : IPS_ALERT;
                IDSetText(FilterNameTP, nullptr);
            }
            else
            {
                FilterNameTP->s = IPS_ALERT;
                IDSetText(FilterNameTP, nullptr);
                LOG_INFO("WARNING *****************************************************");
                LOG_INFO(
                      "One of the filter name is not valid. It should not have more than 8 chars");
                LOG_INFO("Valid chars are A to Z, 0 to 9 = . # / - percent or space");
                LOG_INFO("WARNING *****************************************************");
                return false;
            }
            return true;
        }
    }
    return INDI::FilterWheel::ISNewText(dev, name, texts, names, n);
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
{
    if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
    {
        if (strcmp(HomeSP.name, name) == 0)
        {
            bool result = true;
            // User request the IWF reset (Home procedure will read the Wheel ID, load from EEProm the filters names and goes to filter N°1
            IUUpdateSwitch(&HomeSP, states, names, n);
            IUResetSwitch(&HomeSP);
            LOG_INFO("Executing Home command...");

            FilterNameTP->s = IPS_BUSY;
            IDSetText(FilterNameTP, nullptr);

            if (!moveHome())
            {
                HomeSP.s = IPS_ALERT;
                result   = false;
            }
            else
            {
                LOG_DEBUG("Getting filter information...");

                if (!(GetFilterNames() && GetFilterPos() != 0))
                {
                    HomeSP.s = IPS_ALERT;
                    result   = false;
                }
            }

            IDSetSwitch(&HomeSP, nullptr);
            if (!result)
            {
                LOGF_INFO("%s() failed to get information", __FUNCTION__);
                LOG_INFO("Please check unit and press 'Home' button");
                return false;
            }

            return true;
        }

        if (strcmp(FilterNbrSP.name, name) == 0)
        {
            IUUpdateSwitch(&FilterNbrSP, states, names, n);

            // Is simulation active, User can change from 5 positions wheel to 6, 8 or 9 ones
            // Check if selection is different from active one

            if ((FilterNbrS[0].s == ISS_ON) & (FilterSlotN[0].max != 5))
            {
                strncpy(filterSim, filterSim5, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[1].s == ISS_ON) & (FilterSlotN[0].max != 6))
            {
                strncpy(filterSim, filterSim6, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[2].s == ISS_ON) & (FilterSlotN[0].max != 8))
            {
                strncpy(filterSim, filterSim8, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[3].s == ISS_ON) & (FilterSlotN[0].max != 9))
            {
                strncpy(filterSim, filterSim9, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else
                FilterNbrSP.s = IPS_OK;

            if (FilterNbrSP.s == IPS_ALERT)
            {
                IDSetSwitch(&FilterNbrSP, "%s() failed to change number of filters", __FUNCTION__);
                return false;
            }
            else
                IDSetSwitch(&FilterNbrSP, nullptr);

            return true;
        }

        // Set switch from user selection to allowed use of all chars or restricted to display IFW
        if (strcmp(CharSetSP.name, name) == 0)
        {
            IUUpdateSwitch(&CharSetSP, states, names, n);
            CharSetSP.s = IPS_OK;
            IDSetSwitch(&CharSetSP, nullptr);
            return true;
        }
    }

    return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n);
}

/************************************************************************************
*
************************************************************************************/

/************************************************************************************
*
************************************************************************************/
void FilterIFW::TimerHit()
{
    // not use with IFW
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::SelectFilter(int f)
{
    DEBUGTAG();
    bool result = true;
    char cmd[32]={0};
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));
    snprintf(cmd, 32, "%s%d", "WGOTO", f);

    FilterSlotNP.s = IPS_BUSY;
    IDSetNumber(&FilterSlotNP, "*** Moving to filter n° %d ***", f);

    if (!WriteTTY(cmd))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (isSimulation())
        {
            // Time depend of rotation direction. Goes via shortest way
            int maxFilter = FilterSlotN[0].max;
            int way1, way2;
            if (f > actualSimFilter)
            {
                // CW calcul
                way1 = f - actualSimFilter;
                // CCW calcul
                way2 = actualSimFilter + maxFilter - f;
            }
            else
            {
                // CCW calcul
                way1 = actualSimFilter - f;
                // CW calcul
                way2 = maxFilter - actualSimFilter + f;
            }
            // About 2 second to change 1 position
            if (way1 < way2)
                sleep(2 * way1);
            else
                sleep(2 * way2);

            // Save actual value for Simulation
            actualSimFilter = f;
        }

        if (!ReadTTY(response, (char *)"*", OPTEC_TIMEOUT_MOVE))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "*", 1) != 0)
        {
            LOGF_INFO("Error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }

    if (!result)
    {
        FilterSlotNP.s = IPS_ALERT;
        IDSetNumber(&FilterSlotNP, "*** UNABLE TO SELECT THE FILTER ***");
        return false;
    }

    // As to be called when filter has moved to new position:
    SelectFilterDone(GetFilterPos());

    FilterSlotNP.s = IPS_OK;
    IDSetNumber(&FilterSlotNP, "Selected filter position reached");
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetFilterNames()
{
    DEBUGTAG();
    bool result = true;
    char filterName[MAXINDINAME];
    char filterLabel[MAXINDILABEL];
    char filterList[OPTEC_MAXLEN_NAMES + 9]; // tempo string used fo display filtername debug information
    char response[OPTEC_MAXLEN_RESP + 1];
    int lenResponse = 0; // Nbr of char in the response string
    int maxFilter   = 0;

    memset(response, 0, sizeof(response));

    FilterNameTP->s = IPS_BUSY;
    IDSetText(FilterNameTP, nullptr);

    if (!WriteTTY((char *)"WREAD"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else if (!ReadTTY(response, filterSim, OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        result = false;
    }

    if (result)
    {
        // Check the size of response to know if this is a 5 or 8 position wheel as from R2.x IFW support both
        lenResponse = strlen(response);

        switch (lenResponse)
        {
            case 40:
                maxFilter = 5;
                break;
            case 48:
                maxFilter = 6;
                break;
            case 64:
                maxFilter = 8;
                break;
            case 72:
                maxFilter = 9;
                break;
            default:
                maxFilter = 0; // Means error somewhere
        }

        LOGF_DEBUG("Length of response %d", lenResponse);
        LOGF_DEBUG("MaxFilter  %d", maxFilter);
        if (maxFilter != 0)
        {
            LOGF_DEBUG("Success, response from IFW is : %s", response);

            // Start parsing from IFW message
            char *p = response;
            char filterNameIFW[OPTEC_MAX_FILTER][9];
            filterList[0] = '\0';

            for (int i = 0; i < maxFilter; i++)
            {
                strncpy(filterNameIFW[i], p, OPTEC_LEN_FLTNAME);
                filterNameIFW[i][OPTEC_LEN_FLTNAME] = '\0';
                p                                   = p + OPTEC_LEN_FLTNAME;
                LOGF_DEBUG("filterNameIFW[%d] : %s", i, filterNameIFW[i]);
                strncat(filterList, filterNameIFW[i], OPTEC_LEN_FLTNAME);
                strcat(filterList, "/");
            }
            filterList[strlen(filterList) - 1] = '\0'; //Remove last "/"

            LOG_DEBUG("Redo filters name list");
            // Set new max value on the filter_slot property
            FilterSlotN[0].max = maxFilter;
            if (isSimulation())
                FilterSlotN[0].value = actualSimFilter = 1;
            IUUpdateMinMax(&FilterSlotNP);
            IDSetNumber(&FilterSlotNP, nullptr);

            deleteProperty(FilterNameTP->name);

            if (FilterNameT != nullptr)
            {
                for (int i=0; i < FilterNameTP->ntp; i++)
                    free(FilterNameT[i].text);
                delete [] FilterNameT;
            }

            FilterNameT = new IText[maxFilter];
            memset(FilterNameT, 0, sizeof(IText) * maxFilter);

            for (int i = 0; i < maxFilter; i++)
            {
                snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%d", i + 1);
                snprintf(filterLabel, MAXINDILABEL, "Filter n° %d", i + 1);
                IUFillText(&FilterNameT[i], filterName, filterLabel, filterNameIFW[i]);
            }

            IUFillTextVector(FilterNameTP, FilterNameT, maxFilter, getDeviceName(), "FILTER_NAME", "Filters", FilterSlotNP.group,
                             IP_RW, 0, IPS_OK);
            defineProperty(FilterNameTP);

            // filterList only use for purpose information
            // Remove space from filterList
            char *withSpace    = filterList;
            char *withoutSpace = filterList;
            while (*withSpace != '\0')
            {
                if (*withSpace != ' ')
                {
                    *withoutSpace = *withSpace;
                    withoutSpace++;
                }
                withSpace++;
            }
            *withoutSpace = '\0';

            IDSetText(FilterNameTP, "IFW Filters name -> %s", filterList);
            return true;
        }
        else
            LOGF_ERROR("List of filters name is wrong Nbr char red are: %s", lenResponse);
    }

    FilterNameTP->s = IPS_ALERT;
    LOG_ERROR("Failed to read filter names!");
    IDSetText(FilterNameTP, nullptr);
    return false;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::SetFilterNames()
{
    DEBUGTAG();
    bool result = true;
    char cmd[72]={0};
    char tempo[OPTEC_LEN_FLTNAME + 1];
    char response[OPTEC_MAXLEN_RESP + 1];
    int tempolen;
    memset(response, 0, sizeof(response));

    FilterNameTP->s = IPS_BUSY;
    FilterSlotNP.s = IPS_BUSY;
    WheelIDTP.s = IPS_BUSY;
    IDSetText(FilterNameTP, "*** Saving filters name to IFW... ***");
    IDSetNumber(&FilterSlotNP, nullptr);
    IDSetText(&WheelIDTP, nullptr);

    snprintf(cmd, 8, "WLOAD%s*", WheelIDT[0].text);

    for (int i = 0; i < FilterSlotN[0].max; i++)
    {
        // Prepare string in tempo with blank space at right to complete to 8 chars for each filter name
        memset(tempo, ' ', sizeof(tempo));
        //Check max len of 8 char for the filter name
        tempolen = strlen(FilterNameT[i].text);
        if (tempolen > OPTEC_LEN_FLTNAME)
            tempolen = OPTEC_LEN_FLTNAME;
        //memcpy(tempo + (8 - tempolen), FilterNameT[i].text, tempolen);    // spaces at begin of name
        memcpy(tempo, FilterNameT[i].text, tempolen); // spaces at the end of name
        tempo[8] = '\0';
        strcat(cmd, tempo);
        strncpy(FilterNameT[i].text, tempo, OPTEC_LEN_FLTNAME);
        FilterNameT[i].text[OPTEC_LEN_FLTNAME] = '\0';

        LOGF_DEBUG("Value of the command :%s", cmd);
    }

    LOGF_DEBUG("Length of the command to write to IFW = %d", strlen(cmd));

    if (isSimulation())
    {
        strncpy(filterSim, cmd + 7, OPTEC_MAXLEN_NAMES);
        filterSim[OPTEC_MAXLEN_NAMES] = '\0';
    }

    if (!WriteTTY(cmd))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        FilterNameTP->s = IPS_ALERT;
        IDSetText(FilterNameTP, nullptr);
        result = false;
        // Have to wait at least 10 ms for EEPROM writing before next command
        // Wait 50 mS to be safe
        usleep(50000);
    }
    else
    {
        if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else
        {
            if (strncmp(response, "ER=", 3) == 0)
            {
                LOGF_INFO("Error: %s", response);
                PRINT_ER(response);
                result = false;
            }
        }
    }

    if (!result)
    {
        FilterNameTP->s = IPS_ALERT;
        IDSetText(FilterNameTP, "*** UNABLE TO WRITE FILTERS NAME ***");
        return false;
    }

    LOG_INFO("Filters name are saved in IFW");

    // Interface not ready before the message "DATA OK" disapear from the display IFW
    for (int i = OPTEC_WAIT_DATA_OK; i > 0; i--)
    {
        LOGF_INFO("Please wait for HOME command start... %d", i);
        sleep(1);
    }

    // Do HOME command to load EEProm new names and getFilter to read new value to validate
    FilterNameTP->s = moveHome() ? IPS_OK : IPS_ALERT;
    IDSetText(FilterNameTP, nullptr);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetWheelID()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    WheelIDTP.s = IPS_BUSY;
    IDSetText(&WheelIDTP, nullptr);

    if (!WriteTTY((char *)"WIDENT"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (!ReadTTY(response, (char *)"C", OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "ER=", 3) == 0)
        {
            LOGF_INFO("Get wheel ID error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }
    if (!result)
    {
        WheelIDTP.s = IPS_ALERT;
        IDSetText(&WheelIDTP, "*** UNABLE TO GET WHEEL ID ***");
        return false;
    }

    WheelIDTP.s = IPS_OK;
    IUSaveText(&WheelIDT[0], response);
    IDSetText(&WheelIDTP, "IFW wheel active is %s", response);

    return true;
}

/************************************************************************************
*
************************************************************************************/
int FilterIFW::GetFilterPos()
{
    DEBUGTAG();
    int result = 1;
    char response[OPTEC_MAXLEN_RESP + 1];
    char filter[2]={0};

    memset(response, 0, sizeof(response));

    FilterSlotNP.s = IPS_BUSY;
    IDSetNumber(&FilterSlotNP, nullptr);

    if (!WriteTTY((char *)"WFILTR"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = -1;
    }
    else
    {
        // actualSimFilter for simulation value. int value need to be char*
        snprintf(filter, 2, "%d", actualSimFilter);

        if (!ReadTTY(response, filter, OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = -1;
        }
    }

    if (result == -1)
    {
        FilterSlotNP.s = IPS_ALERT;
        IDSetNumber(&FilterSlotNP, "*** UNABLE TO GET ACTIVE FILTER ***");
        return result;
    }

    result               = atoi(response);
    FilterSlotN[0].value = result;
    FilterSlotNP.s       = IPS_OK;
    IDSetNumber(&FilterSlotNP, "IFW filter active is n° %s -> %s", response, FilterNameT[result - 1].text);
    return result;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::moveHome()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    HomeSP.s = IPS_BUSY;
    WheelIDTP.s = IPS_BUSY;
    FilterSlotNP.s = IPS_BUSY;
    IDSetSwitch(&HomeSP, "*** Initialisation of the IFW. Please wait... ***");
    IDSetText(&WheelIDTP, nullptr);
    IDSetNumber(&FilterSlotNP, nullptr);

    if (!WriteTTY((char *)"WHOME"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (isSimulation())
            sleep(10); // About the same time as real filter

        if (!ReadTTY(response, (char *)"A", OPTEC_TIMEOUT_WHOME))
        {
            LOGF_ERROR("(Function %s()) failed to read from TTY", __FUNCTION__);
            result = false;
        }
        else
        {
            if (strncmp(response, "ER=", 3) == 0)
            {
                LOGF_INFO("Move to Home error: %s", response);
                PRINT_ER(response);
                result = false;
            }
        }
    }

    if (!result || !GetWheelID() || !GetFilterNames() || (GetFilterPos() <= 0))
    {
        HomeSP.s = IPS_ALERT;
        WheelIDTP.s = IPS_ALERT;
        IDSetSwitch(&HomeSP, "*** INITIALISATION FAILED ***");
        return false;
    }

    HomeSP.s = IPS_OK;
    WheelIDTP.s = IPS_OK;
    IDSetSwitch(&HomeSP, "IFW ready");
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetFirmware()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    FirmwareTP.s = IPS_BUSY;
    IDSetText(&FirmwareTP, nullptr);

    if (!WriteTTY((char *)"WVAAAA"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (!ReadTTY(response, (char *)"V= 2.04", OPTEC_TIMEOUT_FIRMWARE))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "ER=", 3) == 0)
        {
            LOGF_INFO("Get wheel ID error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }
    if (!result)
    {
        FirmwareTP.s = IPS_ALERT;
        IDSetText(&FirmwareTP, "*** UNABLE TO GET FIRMWARE ***");
        return false;
    }

    // remove chars fomr the string to get only the nzuméric value of the Firmware version
    char *p = nullptr;

    for (int i = 0; i < (int)strlen(response); i++)
    {
        if (isdigit(response[i]) != 0)
        {
            p = response + i;
            break;
        }
    }

    FirmwareTP.s = IPS_OK;
    IUSaveText(&FirmwareT[0], p);
    IDSetText(&FirmwareTP, "IFW Firmware is %s", response);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::saveConfigItems(FILE *fp)
{
    INDI::FilterWheel::saveConfigItems(fp);

    IUSaveConfigSwitch(fp, &CharSetSP);
    IUSaveConfigSwitch(fp, &FilterNbrSP);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::loadConfig(bool silent, const char *property)
{
    bool result;

    if (property == nullptr)
    {
        result = INDI::DefaultDevice::loadConfig(silent, "CHARSET");
        result = (INDI::DefaultDevice::loadConfig(silent, "FILTER_NBR") && result);
        result = (INDI::DefaultDevice::loadConfig(silent, "USEJOYSTICK") && result);
        result = (INDI::DefaultDevice::loadConfig(silent, "JOYSTICKSETTINGS") && result);
    }
    else
        result = INDI::DefaultDevice::loadConfig(silent, property);

    return result;
}


switch (lenResponse)
        {
            case 40:
                maxFilter = 5;
                break;
            case 48:
                maxFilter = 6;
                break;
            case 64:
                maxFilter = 8;
                break;
            case 72:
                maxFilter = 9;
                break;
            default:
                maxFilter = 0; // Means error somewhere
        }

/*******************************************************************************

*******************************************************************************/

#include "ifwoptec.h"

#include "indicom.h"
#include "indicontroller.h"
#include "connectionplugins/connectionserial.h"

#include <memory>
#include <regex>
#include <cstring>
#include <unistd.h>

std::unique_ptr<FilterIFW> filter_ifw(new FilterIFW());

/************************************************************************************
*
************************************************************************************/
FilterIFW::FilterIFW()
{
    //ctor
    setVersion(VERSION, SUBVERSION);
    strncpy(filterSim, filterSim5, sizeof(filterSim)); // For simulation mode

    // Set communication to serail only and avoid driver crash at starting up
    setFilterConnection(CONNECTION_SERIAL | CONNECTION_TCP);

    // We add an additional debug level so we can log verbose member function starting
    // DBG_TAG is used by macro DEBUGTAG() define in ifwoptec.h
    int DBG_TAG = 0;

    DBG_TAG = INDI::Logger::getInstance().addDebugLevel("Function tag", "Tag");
}

/************************************************************************************
*
************************************************************************************/
const char *FilterIFW::getDefaultName()
{
    return (const char *)"Optec IFW";
}

/**************************************************************************************
*
***************************************************************************************/
bool FilterIFW::initProperties()
{
    INDI::FilterWheel::initProperties();

    // Settings
    IUFillText(&WheelIDT[0], "ID", "ID", "-");
    IUFillTextVector(&WheelIDTP, WheelIDT, 1, getDeviceName(), "WHEEL_ID", "Wheel", FILTER_TAB, IP_RO, 60, IPS_IDLE);

    // Command
    IUFillSwitch(&HomeS[0], "HOME", "Home", ISS_OFF);
    IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0,
                       IPS_IDLE);

    // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters
    IUFillSwitch(&FilterNbrS[0], "VAL5", "5", ISS_ON);
    IUFillSwitch(&FilterNbrS[1], "VAL6", "6", ISS_OFF);
    IUFillSwitch(&FilterNbrS[2], "VAL8", "8", ISS_OFF);
    IUFillSwitch(&FilterNbrS[3], "VAL9", "9", ISS_OFF);
    IUFillSwitchVector(&FilterNbrSP, FilterNbrS, 4, getDeviceName(), "FILTER_NBR", "Filter nbr", FILTER_TAB, IP_RW,
                       ISR_1OFMANY, 0, IPS_IDLE);

    // User could choice to unrestrict chars set to set the filternames if he accepts to have crazy display name on IFW box
    // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters
    IUFillSwitch(&CharSetS[0], "RES", "Restricted", ISS_ON);
    IUFillSwitch(&CharSetS[1], "UNRES", "All", ISS_OFF);
    IUFillSwitchVector(&CharSetSP, CharSetS, 2, getDeviceName(), "CHARSET", "Chars allowed", FILTER_TAB, IP_RW,
                       ISR_1OFMANY, 0, IPS_IDLE);

    // Firmware of the IFW
    IUFillText(&FirmwareT[0], "FIRMWARE", "Firmware", "Unknown");
    IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "FIRMWARE_ID", "IFW", FILTER_TAB, IP_RO, 60, IPS_IDLE);

    addAuxControls();
    serialConnection->setDefaultBaudRate(Connection::Serial::B_19200);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::updateProperties()
{
   INDI::FilterWheel::updateProperties();
	 if (isConnected())
    {
        defineProperty(&HomeSP);
        defineProperty(&FirmwareTP);
        defineProperty(&WheelIDTP); // ID of the wheel first in Filter tab page
        // Then the button only for Simulation to select the number of filter of the Wheel (5 or 8)
        if (isSimulation())
            defineProperty(&FilterNbrSP);
        defineProperty(&CharSetSP);
        defineProperty(&FilterSlotNP);
        controller->updateProperties();

        GetFirmware(); // Try to get Firmware version of the IFW. NOt all Firmware support this function
        moveHome();    // Initialisation of the physical IFW
    }
    else
    {
        deleteProperty(HomeSP.name);
        deleteProperty(FirmwareTP.name);
        deleteProperty(WheelIDTP.name);
        deleteProperty(CharSetSP.name);
        deleteProperty(FilterNbrSP.name);
        deleteProperty(FilterSlotNP.name);
        deleteProperty(FilterNameTP->name);
        controller->updateProperties();
    }

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::WriteTTY(char *command)
{
    char cmd[OPTEC_MAXLEN_CMD];
    int errcode = 0;
    char errmsg[MAXRBUF];
    int nbytes_written = 0;

    snprintf(cmd, OPTEC_MAXLEN_CMD, "%s%s", command, "\n\r");
    LOGF_DEBUG("CMD (%s)", cmd);

    if (!isSimulation())
    {
        if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK)
        {
            tty_error_msg(errcode, errmsg, MAXRBUF);
            LOGF_ERROR("%s", errmsg);
            return false;
        }
    }
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ReadTTY(char *resp, char *simulation, int timeout)
{
    int errcode = 0;
    char errmsg[MAXRBUF];
    char response[OPTEC_MAXLEN_RESP + 1];
    int nbytes_read = 0;

    memset(response, 0, sizeof(response));

    if (isSimulation())
    {
        strncpy(response, simulation, sizeof(response));
        nbytes_read = strlen(response) + 2; // +2 for simulation = "\n\r" see below
    }
    else
    {
        if ((errcode = tty_read_section(PortFD, response, 0xd, timeout, &nbytes_read)) != TTY_OK)
        {
            tty_error_msg(errcode, errmsg, MAXRBUF);
            LOGF_ERROR("%s() TTY error: %s", __FUNCTION__, errmsg);
            return false;
        }
    }

    if (nbytes_read <= 0)
    {
        LOG_ERROR("Controller error: Nothing returned by the IFW");
        response[0] = '\0';
        return false;
    }

    response[nbytes_read - 2] = '\0'; //Remove control char from string (\n\r)
    LOGF_DEBUG("RES (%s)", response);
    strncpy(resp, response, /* sizeof(response)*/ OPTEC_MAXLEN_RESP +1);
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::Handshake()
{
    char response[OPTEC_MAXLEN_RESP + 1];
    memset(response, 0, sizeof(response));
    if (!WriteTTY((char *)"WSMODE"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        return false;
    }

    if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        return false;
    }

    if (strcmp(response, "!") != 0)
    {
        LOG_ERROR("failed, wrong response from IFW");
        LOGF_DEBUG("Response : (%s)", response);
        return false;
    }

    LOGF_DEBUG("Success, response from IFW is : %s", response);
    LOG_INFO("IFW is online");

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::Disconnect()
{
    DEBUGTAG();
    char response[OPTEC_MAXLEN_RESP + 1];
    memset(response, 0, sizeof(response));

    if (!WriteTTY((char *)"WEXITS"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        return false;
    }

    if (!ReadTTY(response, (char *)"END", OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        return false;
    }

    if (strcmp(response, "END") != 0)
    {
        LOG_ERROR("failed, wrong response from IFW");
        return false;
    }

    LOGF_DEBUG("IFW return in manual mode, response from IFW is : %s", response);
    LOG_INFO("IFW is offline.");

    return INDI::FilterWheel::Disconnect();
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
{
    if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
    {
        // User has changed one or more names from filter related to the Wheel ID present in the IFW
        if (strcmp(FilterNameTP->name, name) == 0)
        {
            // Only these chars are allowed to be able to the IFW display to show names correctly
            std::regex rx("^[A-Z0-9=.#/%[:space:]-]{1,8}$");

            bool match = true;
            //Check only if user allowed chars restriction
            if (CharSetS[0].s == ISS_ON)
            {
                for (int i = 0; i < n; i++)
                {
                    LOGF_DEBUG("FilterName request N°%d : %s", i, texts[i]);
                    match = std::regex_match(texts[i], rx);
                    if (!match)
                        break;
                }
            }

            if (match)
            {
                IUUpdateText(FilterNameTP, texts, names, n);
                FilterNameTP->s = SetFilterNames() ? IPS_OK : IPS_ALERT;
                IDSetText(FilterNameTP, nullptr);
            }
            else
            {
                FilterNameTP->s = IPS_ALERT;
                IDSetText(FilterNameTP, nullptr);
                LOG_INFO("WARNING *****************************************************");
                LOG_INFO(
                      "One of the filter name is not valid. It should not have more than 8 chars");
                LOG_INFO("Valid chars are A to Z, 0 to 9 = . # / - percent or space");
                LOG_INFO("WARNING *****************************************************");
                return false;
            }
            return true;
        }
    }
    return INDI::FilterWheel::ISNewText(dev, name, texts, names, n);
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
{
    if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
    {
        if (strcmp(HomeSP.name, name) == 0)
        {
            bool result = true;
            // User request the IWF reset (Home procedure will read the Wheel ID, load from EEProm the filters names and goes to filter N°1
            IUUpdateSwitch(&HomeSP, states, names, n);
            IUResetSwitch(&HomeSP);
            LOG_INFO("Executing Home command...");

            FilterNameTP->s = IPS_BUSY;
            IDSetText(FilterNameTP, nullptr);

            if (!moveHome())
            {
                HomeSP.s = IPS_ALERT;
                result   = false;
            }
            else
            {
                LOG_DEBUG("Getting filter information...");

                if (!(GetFilterNames() && GetFilterPos() != 0))
                {
                    HomeSP.s = IPS_ALERT;
                    result   = false;
                }
            }

            IDSetSwitch(&HomeSP, nullptr);
            if (!result)
            {
                LOGF_INFO("%s() failed to get information", __FUNCTION__);
                LOG_INFO("Please check unit and press 'Home' button");
                return false;
            }

            return true;
        }

        if (strcmp(FilterNbrSP.name, name) == 0)
        {
            IUUpdateSwitch(&FilterNbrSP, states, names, n);

            // Is simulation active, User can change from 5 positions wheel to 6, 8 or 9 ones
            // Check if selection is different from active one

            if ((FilterNbrS[0].s == ISS_ON) & (FilterSlotN[0].max != 5))
            {
                strncpy(filterSim, filterSim5, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[1].s == ISS_ON) & (FilterSlotN[0].max != 6))
            {
                strncpy(filterSim, filterSim6, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[2].s == ISS_ON) & (FilterSlotN[0].max != 8))
            {
                strncpy(filterSim, filterSim8, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else if ((FilterNbrS[3].s == ISS_ON) & (FilterSlotN[0].max != 9))
            {
                strncpy(filterSim, filterSim9, sizeof(filterSim));
                FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT;
            }
            else
                FilterNbrSP.s = IPS_OK;

            if (FilterNbrSP.s == IPS_ALERT)
            {
                IDSetSwitch(&FilterNbrSP, "%s() failed to change number of filters", __FUNCTION__);
                return false;
            }
            else
                IDSetSwitch(&FilterNbrSP, nullptr);

            return true;
        }

        // Set switch from user selection to allowed use of all chars or restricted to display IFW
        if (strcmp(CharSetSP.name, name) == 0)
        {
            IUUpdateSwitch(&CharSetSP, states, names, n);
            CharSetSP.s = IPS_OK;
            IDSetSwitch(&CharSetSP, nullptr);
            return true;
        }
    }

    return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n);
}

/************************************************************************************
*
************************************************************************************/
void FilterIFW::simulationTriggered(bool enable)
{
    // toogle buttons to select 5 or 8 filters depend if Simulation active or not
    if (enable)
    {
        if (isConnected())
        {
            defineProperty(&FilterNbrSP);
        }
    }
    else
        deleteProperty(FilterNbrSP.name);
}

/************************************************************************************
*
************************************************************************************/
void FilterIFW::TimerHit()
{
    // not use with IFW
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::SelectFilter(int f)
{
    DEBUGTAG();
    bool result = true;
    char cmd[32]={0};
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));
    snprintf(cmd, 32, "%s%d", "WGOTO", f);

    FilterSlotNP.s = IPS_BUSY;
    IDSetNumber(&FilterSlotNP, "*** Moving to filter n° %d ***", f);

    if (!WriteTTY(cmd))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (isSimulation())
        {
            // Time depend of rotation direction. Goes via shortest way
            int maxFilter = FilterSlotN[0].max;
            int way1, way2;
            if (f > actualSimFilter)
            {
                // CW calcul
                way1 = f - actualSimFilter;
                // CCW calcul
                way2 = actualSimFilter + maxFilter - f;
            }
            else
            {
                // CCW calcul
                way1 = actualSimFilter - f;
                // CW calcul
                way2 = maxFilter - actualSimFilter + f;
            }
            // About 2 second to change 1 position
            if (way1 < way2)
                sleep(2 * way1);
            else
                sleep(2 * way2);

            // Save actual value for Simulation
            actualSimFilter = f;
        }

        if (!ReadTTY(response, (char *)"*", OPTEC_TIMEOUT_MOVE))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "*", 1) != 0)
        {
            LOGF_INFO("Error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }

    if (!result)
    {
        FilterSlotNP.s = IPS_ALERT;
        IDSetNumber(&FilterSlotNP, "*** UNABLE TO SELECT THE FILTER ***");
        return false;
    }

    // As to be called when filter has moved to new position:
    SelectFilterDone(GetFilterPos());

    FilterSlotNP.s = IPS_OK;
    IDSetNumber(&FilterSlotNP, "Selected filter position reached");
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetFilterNames()
{
    DEBUGTAG();
    bool result = true;
    char filterName[MAXINDINAME];
    char filterLabel[MAXINDILABEL];
    char filterList[OPTEC_MAXLEN_NAMES + 9]; // tempo string used fo display filtername debug information
    char response[OPTEC_MAXLEN_RESP + 1];
    int lenResponse = 0; // Nbr of char in the response string
    int maxFilter   = 0;

    memset(response, 0, sizeof(response));

    FilterNameTP->s = IPS_BUSY;
    IDSetText(FilterNameTP, nullptr);

    if (!WriteTTY((char *)"WREAD"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else if (!ReadTTY(response, filterSim, OPTEC_TIMEOUT))
    {
        LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
        result = false;
    }

    if (result)
    {
        // Check the size of response to know if this is a 5 or 8 position wheel as from R2.x IFW support both
        lenResponse = strlen(response);

        LOGF_DEBUG("Length of response %d", lenResponse);
        LOGF_DEBUG("MaxFilter  %d", maxFilter);
        if (maxFilter != 0)
        {
            LOGF_DEBUG("Success, response from IFW is : %s", response);

            // Start parsing from IFW message
            char *p = response;
            char filterNameIFW[OPTEC_MAX_FILTER][9];
            filterList[0] = '\0';

            for (int i = 0; i < maxFilter; i++)
            {
                strncpy(filterNameIFW[i], p, OPTEC_LEN_FLTNAME);
                filterNameIFW[i][OPTEC_LEN_FLTNAME] = '\0';
                p                                   = p + OPTEC_LEN_FLTNAME;
                LOGF_DEBUG("filterNameIFW[%d] : %s", i, filterNameIFW[i]);
                strncat(filterList, filterNameIFW[i], OPTEC_LEN_FLTNAME);
                strcat(filterList, "/");
            }
            filterList[strlen(filterList) - 1] = '\0'; //Remove last "/"

            LOG_DEBUG("Redo filters name list");
            // Set new max value on the filter_slot property
            FilterSlotN[0].max = maxFilter;
            if (isSimulation())
                FilterSlotN[0].value = actualSimFilter = 1;
            IUUpdateMinMax(&FilterSlotNP);
            IDSetNumber(&FilterSlotNP, nullptr);

            deleteProperty(FilterNameTP->name);

            if (FilterNameT != nullptr)
            {
                for (int i=0; i < FilterNameTP->ntp; i++)
                    free(FilterNameT[i].text);
                delete [] FilterNameT;
            }

            FilterNameT = new IText[maxFilter];
            memset(FilterNameT, 0, sizeof(IText) * maxFilter);

            for (int i = 0; i < maxFilter; i++)
            {
                snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%d", i + 1);
                snprintf(filterLabel, MAXINDILABEL, "Filter n° %d", i + 1);
                IUFillText(&FilterNameT[i], filterName, filterLabel, filterNameIFW[i]);
            }

            IUFillTextVector(FilterNameTP, FilterNameT, maxFilter, getDeviceName(), "FILTER_NAME", "Filters", FilterSlotNP.group,
                             IP_RW, 0, IPS_OK);
            defineProperty(FilterNameTP);

            // filterList only use for purpose information
            // Remove space from filterList
            char *withSpace    = filterList;
            char *withoutSpace = filterList;
            while (*withSpace != '\0')
            {
                if (*withSpace != ' ')
                {
                    *withoutSpace = *withSpace;
                    withoutSpace++;
                }
                withSpace++;
            }
            *withoutSpace = '\0';

            IDSetText(FilterNameTP, "IFW Filters name -> %s", filterList);
            return true;
        }
        else
            LOGF_ERROR("List of filters name is wrong Nbr char red are: %s", lenResponse);
    }

    FilterNameTP->s = IPS_ALERT;
    LOG_ERROR("Failed to read filter names!");
    IDSetText(FilterNameTP, nullptr);
    return false;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::SetFilterNames()
{
    DEBUGTAG();
    bool result = true;
    char cmd[72]={0};
    char tempo[OPTEC_LEN_FLTNAME + 1];
    char response[OPTEC_MAXLEN_RESP + 1];
    int tempolen;
    memset(response, 0, sizeof(response));

    FilterNameTP->s = IPS_BUSY;
    FilterSlotNP.s = IPS_BUSY;
    WheelIDTP.s = IPS_BUSY;
    IDSetText(FilterNameTP, "*** Saving filters name to IFW... ***");
    IDSetNumber(&FilterSlotNP, nullptr);
    IDSetText(&WheelIDTP, nullptr);

    snprintf(cmd, 8, "WLOAD%s*", WheelIDT[0].text);

    for (int i = 0; i < FilterSlotN[0].max; i++)
    {
        // Prepare string in tempo with blank space at right to complete to 8 chars for each filter name
        memset(tempo, ' ', sizeof(tempo));
        //Check max len of 8 char for the filter name
        tempolen = strlen(FilterNameT[i].text);
        if (tempolen > OPTEC_LEN_FLTNAME)
            tempolen = OPTEC_LEN_FLTNAME;
        //memcpy(tempo + (8 - tempolen), FilterNameT[i].text, tempolen);    // spaces at begin of name
        memcpy(tempo, FilterNameT[i].text, tempolen); // spaces at the end of name
        tempo[8] = '\0';
        strcat(cmd, tempo);
        strncpy(FilterNameT[i].text, tempo, OPTEC_LEN_FLTNAME);
        FilterNameT[i].text[OPTEC_LEN_FLTNAME] = '\0';

        LOGF_DEBUG("Value of the command :%s", cmd);
    }

    LOGF_DEBUG("Length of the command to write to IFW = %d", strlen(cmd));

    if (isSimulation())
    {
        strncpy(filterSim, cmd + 7, OPTEC_MAXLEN_NAMES);
        filterSim[OPTEC_MAXLEN_NAMES] = '\0';
    }

    if (!WriteTTY(cmd))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        FilterNameTP->s = IPS_ALERT;
        IDSetText(FilterNameTP, nullptr);
        result = false;
        // Have to wait at least 10 ms for EEPROM writing before next command
        // Wait 50 mS to be safe
        usleep(50000);
    }
    else
    {
        if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else
        {
            if (strncmp(response, "ER=", 3) == 0)
            {
                LOGF_INFO("Error: %s", response);
                PRINT_ER(response);
                result = false;
            }
        }
    }

    if (!result)
    {
        FilterNameTP->s = IPS_ALERT;
        IDSetText(FilterNameTP, "*** UNABLE TO WRITE FILTERS NAME ***");
        return false;
    }

    LOG_INFO("Filters name are saved in IFW");

    // Interface not ready before the message "DATA OK" disapear from the display IFW
    for (int i = OPTEC_WAIT_DATA_OK; i > 0; i--)
    {
        LOGF_INFO("Please wait for HOME command start... %d", i);
        sleep(1);
    }

    // Do HOME command to load EEProm new names and getFilter to read new value to validate
    FilterNameTP->s = moveHome() ? IPS_OK : IPS_ALERT;
    IDSetText(FilterNameTP, nullptr);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetWheelID()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    WheelIDTP.s = IPS_BUSY;
    IDSetText(&WheelIDTP, nullptr);

    if (!WriteTTY((char *)"WIDENT"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (!ReadTTY(response, (char *)"C", OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "ER=", 3) == 0)
        {
            LOGF_INFO("Get wheel ID error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }
    if (!result)
    {
        WheelIDTP.s = IPS_ALERT;
        IDSetText(&WheelIDTP, "*** UNABLE TO GET WHEEL ID ***");
        return false;
    }

    WheelIDTP.s = IPS_OK;
    IUSaveText(&WheelIDT[0], response);
    IDSetText(&WheelIDTP, "IFW wheel active is %s", response);

    return true;
}

/************************************************************************************
*
************************************************************************************/
int FilterIFW::GetFilterPos()
{
    DEBUGTAG();
    int result = 1;
    char response[OPTEC_MAXLEN_RESP + 1];
    char filter[2]={0};

    memset(response, 0, sizeof(response));

    FilterSlotNP.s = IPS_BUSY;
    IDSetNumber(&FilterSlotNP, nullptr);

    if (!WriteTTY((char *)"WFILTR"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = -1;
    }
    else
    {
        // actualSimFilter for simulation value. int value need to be char*
        snprintf(filter, 2, "%d", actualSimFilter);

        if (!ReadTTY(response, filter, OPTEC_TIMEOUT))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = -1;
        }
    }

    if (result == -1)
    {
        FilterSlotNP.s = IPS_ALERT;
        IDSetNumber(&FilterSlotNP, "*** UNABLE TO GET ACTIVE FILTER ***");
        return result;
    }

    result               = atoi(response);
    FilterSlotN[0].value = result;
    FilterSlotNP.s       = IPS_OK;
    IDSetNumber(&FilterSlotNP, "IFW filter active is n° %s -> %s", response, FilterNameT[result - 1].text);
    return result;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::moveHome()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    HomeSP.s = IPS_BUSY;
    WheelIDTP.s = IPS_BUSY;
    FilterSlotNP.s = IPS_BUSY;
    IDSetSwitch(&HomeSP, "*** Initialisation of the IFW. Please wait... ***");
    IDSetText(&WheelIDTP, nullptr);
    IDSetNumber(&FilterSlotNP, nullptr);

    if (!WriteTTY((char *)"WHOME"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (isSimulation())
            sleep(10); // About the same time as real filter

        if (!ReadTTY(response, (char *)"A", OPTEC_TIMEOUT_WHOME))
        {
            LOGF_ERROR("(Function %s()) failed to read from TTY", __FUNCTION__);
            result = false;
        }
        else
        {
            if (strncmp(response, "ER=", 3) == 0)
            {
                LOGF_INFO("Move to Home error: %s", response);
                PRINT_ER(response);
                result = false;
            }
        }
    }

    if (!result || !GetWheelID() || !GetFilterNames() || (GetFilterPos() <= 0))
    {
        HomeSP.s = IPS_ALERT;
        WheelIDTP.s = IPS_ALERT;
        IDSetSwitch(&HomeSP, "*** INITIALISATION FAILED ***");
        return false;
    }

    HomeSP.s = IPS_OK;
    WheelIDTP.s = IPS_OK;
    IDSetSwitch(&HomeSP, "IFW ready");
    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::GetFirmware()
{
    DEBUGTAG();
    bool result = true;
    char response[OPTEC_MAXLEN_RESP + 1];

    memset(response, 0, sizeof(response));

    FirmwareTP.s = IPS_BUSY;
    IDSetText(&FirmwareTP, nullptr);

    if (!WriteTTY((char *)"WVAAAA"))
    {
        LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__);
        result = false;
    }
    else
    {
        if (!ReadTTY(response, (char *)"V= 2.04", OPTEC_TIMEOUT_FIRMWARE))
        {
            LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__);
            result = false;
        }
        else if (strncmp(response, "ER=", 3) == 0)
        {
            LOGF_INFO("Get wheel ID error: %s", response);
            PRINT_ER(response);
            result = false;
        }
    }
    if (!result)
    {
        FirmwareTP.s = IPS_ALERT;
        IDSetText(&FirmwareTP, "*** UNABLE TO GET FIRMWARE ***");
        return false;
    }

    // remove chars fomr the string to get only the nzuméric value of the Firmware version
    char *p = nullptr;

    for (int i = 0; i < (int)strlen(response); i++)
    {
        if (isdigit(response[i]) != 0)
        {
            p = response + i;
            break;
        }
    }

    FirmwareTP.s = IPS_OK;
    IUSaveText(&FirmwareT[0], p);
    IDSetText(&FirmwareTP, "IFW Firmware is %s", response);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::saveConfigItems(FILE *fp)
{
    INDI::FilterWheel::saveConfigItems(fp);

    IUSaveConfigSwitch(fp, &CharSetSP);
    IUSaveConfigSwitch(fp, &FilterNbrSP);

    return true;
}

/************************************************************************************
*
************************************************************************************/
bool FilterIFW::loadConfig(bool silent, const char *property)
{
    bool result;

    if (property == nullptr)
    {
        result = INDI::DefaultDevice::loadConfig(silent, "CHARSET");
        result = (INDI::DefaultDevice::loadConfig(silent, "FILTER_NBR") && result);
        result = (INDI::DefaultDevice::loadConfig(silent, "USEJOYSTICK") && result);
        result = (INDI::DefaultDevice::loadConfig(silent, "JOYSTICKSETTINGS") && result);
    }
    else
        result = INDI::DefaultDevice::loadConfig(silent, property);

    return result;
}


void MarlinUI::synchronize(PGM_P const msg/*=nullptr*/) {
  static PGM_P sync_message = msg ?: GET_TEXT(MSG_MOVING);
  push_current_screen();
  goto_screen([]{
    if (should_draw()) MenuItem_static::draw(LCD_HEIGHT >= 4, sync_message);
  });
  defer_status_screen();
  planner.synchronize(); // idle() is called until moves complete
  goto_previous_screen_no_defer();
}

/**
 * Marlin 3D Printer Firmware
 *
 * Based on Sprinter and grbl.
 *
 *
 */

#include "../../inc/MarlinConfigPre.h"

#if HAS_LCD_MENU

#include "menu.h"
#include "../../module/planner.h"
#include "../../module/motion.h"
#include "../../module/printcounter.h"
#include "../../gcode/queue.h"

#if HAS_BUZZER
  #include "../../libs/buzzer.h"
#endif

#if ENABLED(BABYSTEP_ZPROBE_OFFSET)
  #include "../../module/probe.h"
#endif

#if EITHER(ENABLE_LEVELING_FADE_HEIGHT, AUTO_BED_LEVELING_UBL)
  #include "../../feature/bedlevel/bedlevel.h"
#endif

// Menu Navigation
int8_t encoderTopLine, encoderLine, screen_items;

typedef struct {
  screenFunc_t menu_function;     // The screen's function
  uint32_t encoder_position;      // The position of the encoder
  int8_t top_line, items;         // The amount of scroll, and the number of items
  #if SCREENS_CAN_TIME_OUT
    bool sticky;                  // The screen is sticky
  #endif
} menuPosition;
menuPosition screen_history[6];
uint8_t screen_history_depth = 0;

int8_t MenuItemBase::itemIndex;   // Index number for draw and action
PGM_P MenuItemBase::itemString;   // A PSTR for substitution
chimera_t editable;               // Value Editing

// Menu Edit Items
PGM_P        MenuEditItemBase::editLabel;
void*        MenuEditItemBase::editValue;
int32_t      MenuEditItemBase::minEditValue,
             MenuEditItemBase::maxEditValue;
screenFunc_t MenuEditItemBase::callbackFunc;
bool         MenuEditItemBase::liveEdit;

void MarlinUI::return_to_status() { goto_screen(status_screen); }

void MarlinUI::push_current_screen() {
  if (screen_history_depth < COUNT(screen_history))
    screen_history[screen_history_depth++] = { currentScreen, encoderPosition, encoderTopLine, screen_items OPTARG(SCREENS_CAN_TIME_OUT, screen_is_sticky()) };
}

void MarlinUI::_goto_previous_screen(TERN_(TURBO_BACK_MENU_ITEM, const bool is_back/*=false*/)) {
  IF_DISABLED(TURBO_BACK_MENU_ITEM, constexpr bool is_back = false);
  TERN_(HAS_TOUCH_BUTTONS, on_edit_screen = false);
  if (screen_history_depth > 0) {
    menuPosition &sh = screen_history[--screen_history_depth];
    goto_screen(sh.menu_function,
      is_back ? 0 : sh.encoder_position,
      is_back ? 0 : sh.top_line,
      sh.items
    );
    defer_status_screen(TERN_(SCREENS_CAN_TIME_OUT, sh.sticky));
  }
  else
    return_to_status();
}

/////////// Menu Editing Actions ///////////

/**
 * Functions for editing single values
 *
 * The "DEFINE_MENU_EDIT_ITEM" macro generates the classes needed to edit a numerical value.
 *
 * The prerequisite is that in the header the type was already declared:
 *
 *   DEFINE_MENU_EDIT_ITEM_TYPE(int3, int16_t, i16tostr3rj, 1)
 *
 * For example, DEFINE_MENU_EDIT_ITEM(int3) expands into:
 *
 *   template class TMenuEditItem<MenuEditItemInfo_int3>
 *
 * You can then use one of the menu macros to present the edit interface:
 *   EDIT_ITEM(int3, MSG_SPEED, &feedrate_percentage, 10, 999)
 *
 * This expands into a more primitive menu item:
 *  _MENU_ITEM_P(int3, false, GET_TEXT(MSG_SPEED), &feedrate_percentage, 10, 999)
 *
 * ...which calls:
 *       MenuItem_int3::action(plabel, &feedrate_percentage, 10, 999)
 *       MenuItem_int3::draw(encoderLine == _thisItemNr, _lcdLineNr, plabel, &feedrate_percentage, 10, 999)
 */
void MenuEditItemBase::edit_screen(strfunc_t strfunc, loadfunc_t loadfunc) {
  TERN_(HAS_TOUCH_BUTTONS, ui.repeat_delay = BUTTON_DELAY_EDIT);
  if (int32_t(ui.encoderPosition) < 0) ui.encoderPosition = 0;
  if (int32_t(ui.encoderPosition) > maxEditValue) ui.encoderPosition = maxEditValue;
  if (ui.should_draw())
    draw_edit_screen(strfunc(ui.encoderPosition + minEditValue));
  if (ui.lcd_clicked || (liveEdit && ui.should_draw())) {
    if (editValue) loadfunc(editValue, ui.encoderPosition + minEditValue);
    if (callbackFunc && (liveEdit || ui.lcd_clicked)) (*callbackFunc)();
    if (ui.use_click()) ui.goto_previous_screen();
  }
}

void MenuEditItemBase::goto_edit_screen(
  PGM_P const el,         // Edit label
  void * const ev,        // Edit value pointer
  const int32_t minv,     // Encoder minimum
  const int32_t maxv,     // Encoder maximum
  const uint16_t ep,      // Initial encoder value
  const screenFunc_t cs,  // MenuItem_type::draw_edit_screen => MenuEditItemBase::edit()
  const screenFunc_t cb,  // Callback after edit
  const bool le           // Flag to call cb() during editing
) {
  TERN_(HAS_TOUCH_BUTTONS, ui.on_edit_screen = true);
  ui.screen_changed = true;
  ui.push_current_screen();
  ui.refresh();
  editLabel = el;
  editValue = ev;
  minEditValue = minv;
  maxEditValue = maxv;
  ui.encoderPosition = ep;
  ui.currentScreen = cs;
  callbackFunc = cb;
  liveEdit = le;
}

#include "../../MarlinCore.h"

bool printer_busy() {
  return planner.movesplanned() || printingIsActive();
}

/**
 * General function to go directly to a screen
 */
void MarlinUI::goto_screen(screenFunc_t screen, const uint16_t encoder/*=0*/, const uint8_t top/*=0*/, const uint8_t items/*=0*/) {
  if (currentScreen != screen) {

    TERN_(IS_DWIN_MARLINUI, did_first_redraw = false);

    TERN_(HAS_TOUCH_BUTTONS, repeat_delay = BUTTON_DELAY_MENU);

    TERN_(LCD_SET_PROGRESS_MANUALLY, progress_reset());

    #if BOTH(DOUBLECLICK_FOR_Z_BABYSTEPPING, BABYSTEPPING)
      static millis_t doubleclick_expire_ms = 0;
      // Going to menu_main from status screen? Remember first click time.
      // Going back to status screen within a very short time? Go to Z babystepping.
      if (screen == menu_main) {
        if (on_status_screen())
          doubleclick_expire_ms = millis() + DOUBLECLICK_MAX_INTERVAL;
      }
      else if (screen == status_screen && currentScreen == menu_main && PENDING(millis(), doubleclick_expire_ms)) {
        if (BABYSTEP_ALLOWED())
          screen = TERN(BABYSTEP_ZPROBE_OFFSET, lcd_babystep_zoffset, lcd_babystep_z);
        else {
          #if ENABLED(MOVE_Z_WHEN_IDLE)
            ui.manual_move.menu_scale = MOVE_Z_IDLE_MULTIPLICATOR;
            screen = lcd_move_z;
          #endif
        }
      }
    #endif

    currentScreen = screen;
    encoderPosition = encoder;
    encoderTopLine = top;
    screen_items = items;
    if (on_status_screen()) {
      defer_status_screen(false);
      clear_menu_history();
      TERN_(AUTO_BED_LEVELING_UBL, ubl.lcd_map_control = false);
    }

    clear_lcd();

    // Re-initialize custom characters that may be re-used
    #if HAS_MARLINUI_HD44780
      if (TERN1(AUTO_BED_LEVELING_UBL, !ubl.lcd_map_control))
        set_custom_characters(on_status_screen() ? CHARSET_INFO : CHARSET_MENU);
    #endif

    refresh(LCDVIEW_CALL_REDRAW_NEXT);
    screen_changed = true;
    TERN_(HAS_MARLINUI_U8GLIB, drawing_screen = false);

    TERN_(HAS_LCD_MENU, encoder_direction_normal());

    set_selection(false);
  }
}

// Display a "synchronize" screen with a custom message until
// all moves are finished. Go back to calling screen when done.

/**
 * Scrolling for menus and other line-based screens
 *
 *   encoderLine is the position based on the encoder
 *   encoderTopLine is the top menu line to display
 *   screen_items is the total number of items in the menu (after one call)
 */
void scroll_screen(const uint8_t limit, const bool is_menu) {
  ui.encoder_direction_menus();
  ENCODER_RATE_MULTIPLY(false);
  if (int32_t(ui.encoderPosition) < 0) ui.encoderPosition = 0;
  if (ui.first_page) {
    encoderLine = ui.encoderPosition / (ENCODER_STEPS_PER_MENU_ITEM);
    ui.screen_changed = false;
  }
  if (screen_items > 0 && encoderLine >= screen_items - limit) {
    encoderLine = _MAX(0, screen_items - limit);
    ui.encoderPosition = encoderLine * (ENCODER_STEPS_PER_MENU_ITEM);
  }
  if (is_menu) {
    NOMORE(encoderTopLine, encoderLine);
    if (encoderLine >= encoderTopLine + LCD_HEIGHT)
      encoderTopLine = encoderLine - LCD_HEIGHT + 1;
  }
  else
    encoderTopLine = encoderLine;
}

#if HAS_BUZZER
  void MarlinUI::completion_feedback(const bool good/*=true*/) {
    TERN_(HAS_TOUCH_SLEEP, wakeup_screen()); // Wake up on rotary encoder click...
    if (good) {
      BUZZ(100, 659);
      BUZZ(100, 698);
    }
    else BUZZ(20, 440);
  }
#endif

#if HAS_LINE_TO_Z

  void line_to_z(const_float_t z) {
    current_position.z = z;
    line_to_current_position(manual_feedrate_mm_s.z);
  }

#endif

#if ENABLED(BABYSTEP_ZPROBE_OFFSET)

  #include "../../feature/babystep.h"

  void lcd_babystep_zoffset() {
    if (ui.use_click()) return ui.goto_previous_screen_no_defer();
    ui.defer_status_screen();
    const bool do_probe = DISABLED(BABYSTEP_HOTEND_Z_OFFSET) || active_extruder == 0;
    if (ui.encoderPosition) {
      const int16_t babystep_increment = int16_t(ui.encoderPosition) * (BABYSTEP_SIZE_Z);
      ui.encoderPosition = 0;

      const float diff = planner.mm_per_step[Z_AXIS] * babystep_increment,
                  new_probe_offset = probe.offset.z + diff,
                  new_offs = TERN(BABYSTEP_HOTEND_Z_OFFSET
                    , do_probe ? new_probe_offset : hotend_offset[active_extruder].z - diff
                    , new_probe_offset
                  );
      if (WITHIN(new_offs, Z_PROBE_OFFSET_RANGE_MIN, Z_PROBE_OFFSET_RANGE_MAX)) {

        babystep.add_steps(Z_AXIS, babystep_increment);

        if (do_probe)
          probe.offset.z = new_offs;
        else
          TERN(BABYSTEP_HOTEND_Z_OFFSET, hotend_offset[active_extruder].z = new_offs, NOOP);

        ui.refresh(LCDVIEW_CALL_REDRAW_NEXT);
      }
    }
    if (ui.should_draw()) {
      if (do_probe) {
        MenuEditItemBase::draw_edit_screen(GET_TEXT(MSG_ZPROBE_ZOFFSET), BABYSTEP_TO_STR(probe.offset.z));
        TERN_(BABYSTEP_ZPROBE_GFX_OVERLAY, ui.zoffset_overlay(probe.offset.z));
      }
      else {
        #if ENABLED(BABYSTEP_HOTEND_Z_OFFSET)
          MenuEditItemBase::draw_edit_screen(GET_TEXT(MSG_HOTEND_OFFSET_Z), ftostr54sign(hotend_offset[active_extruder].z));
        #endif
      }
    }
  }

#endif // BABYSTEP_ZPROBE_OFFSET

void _lcd_draw_homing() {
  if (ui.should_draw()) {
    constexpr uint8_t line = (LCD_HEIGHT - 1) / 2;
    MenuItem_static::draw(line, GET_TEXT(MSG_LEVEL_BED_HOMING));
  }
}

#if ENABLED(LCD_BED_LEVELING) || (HAS_LEVELING && DISABLED(SLIM_LCD_MENUS))
  #include "../../feature/bedlevel/bedlevel.h"
  void _lcd_toggle_bed_leveling() { set_bed_leveling_enabled(!planner.leveling_active); }
#endif

// Selection screen presents a prompt and two options
bool MarlinUI::selection; // = false
bool MarlinUI::update_selection() {
  encoder_direction_select();
  if (encoderPosition) {
    selection = int16_t(encoderPosition) > 0;
    encoderPosition = 0;
  }
  return selection;
}

void MenuItem_confirm::select_screen(
  PGM_P const yes, PGM_P const no,
  selectFunc_t yesFunc, selectFunc_t noFunc,
  PGM_P const pref, const char * const string/*=nullptr*/, PGM_P const suff/*=nullptr*/
) {
  ui.defer_status_screen();
  const bool ui_selection = ui.update_selection(), got_click = ui.use_click();
  if (got_click || ui.should_draw()) {
    draw_select_screen(yes, no, ui_selection, pref, string, suff);
    if (got_click) {
      selectFunc_t callFunc = ui_selection ? yesFunc : noFunc;
      if (callFunc) callFunc(); else ui.goto_previous_screen();
    }
  }
}

#endif // HAS_LCD_MENU


FResourceFile *CheckWad(const char *filename, FileReader *file, bool quiet)
{
	char head[4];

	if (file->GetLength() >= 12)
	{
		file->Seek(0, SEEK_SET);
		file->Read(&head, 4);
		file->Seek(0, SEEK_SET);
		if (!memcmp(head, "IWAD", 4) || !memcmp(head, "PWAD", 4))
		{
			FResourceFile *rf = new FWadFile(filename, file);
			if (rf->Open(quiet)) return rf;

			rf->Reader = NULL; // to avoid destruction of reader
			delete rf;
		}
	}
	return NULL;
}

/*
** file_wad.cpp
**
**---------------------------------------------------------------------------
** All rights reserved.
**
**---------------------------------------------------------------------------
**
**
*/

#include "resourcefile.h"
#include "cmdlib.h"
#include "templates.h"
#include "v_text.h"
#include "w_wad.h"
#include "gi.h"

// Console Doom LZSS wrapper.
class FileReaderLZSS : public FileReaderBase
{
private:
	enum { BUFF_SIZE = 4096, WINDOW_SIZE = 4096, INTERNAL_BUFFER_SIZE = 128 };

	FileReader &File;
	bool SawEOF;
	BYTE InBuff[BUFF_SIZE];

	enum StreamState
	{
		STREAM_EMPTY,
		STREAM_BITS,
		STREAM_FLUSH,
		STREAM_FINAL
	};
	struct
	{
		StreamState State;

		BYTE *In;
		unsigned int AvailIn;
		unsigned int InternalOut;

		BYTE CFlags, Bits;

		BYTE Window[WINDOW_SIZE+INTERNAL_BUFFER_SIZE];
		const BYTE *WindowData;
		BYTE *InternalBuffer;
	} Stream;

	void FillBuffer()
	{
		if(Stream.AvailIn)
			memmove(InBuff, Stream.In, Stream.AvailIn);

		long numread = File.Read(InBuff+Stream.AvailIn, BUFF_SIZE-Stream.AvailIn);

		if (numread < BUFF_SIZE)
		{
			SawEOF = true;
		}
		Stream.In = InBuff;
		Stream.AvailIn = numread+Stream.AvailIn;
	}

	// Reads a flag byte.
	void PrepareBlocks()
	{
		assert(Stream.InternalBuffer == Stream.WindowData);
		Stream.CFlags = *Stream.In++;
		--Stream.AvailIn;
		Stream.Bits = 0xFF;
		Stream.State = STREAM_BITS;
	}

	// Reads the next chunk in the block. Returns true if successful and
	// returns false if it ran out of input data.
	bool UncompressBlock()
	{
		if(Stream.CFlags & 1)
		{
			// Check to see if we have enough input
			if(Stream.AvailIn < 2)
				return false;
			Stream.AvailIn -= 2;

			WORD pos = BigShort(*(WORD*)Stream.In);
			BYTE len = (pos & 0xF)+1;
			pos >>= 4;
			Stream.In += 2;
			if(len == 1)
			{
				// We've reached the end of the stream.
				Stream.State = STREAM_FINAL;
				return true;
			}

			const BYTE* copyStart = Stream.InternalBuffer-pos-1;

			// Complete overlap: Single byte repeated
			if(pos == 0)
				memset(Stream.InternalBuffer, *copyStart, len);
			// No overlap: One copy
			else if(pos >= len)
				memcpy(Stream.InternalBuffer, copyStart, len);
			else
			{
				// Partial overlap: Copy in 2 or 3 chunks.
				do
				{
					unsigned int copy = MIN<unsigned int>(len, pos+1);
					memcpy(Stream.InternalBuffer, copyStart, copy);
					Stream.InternalBuffer += copy;
					Stream.InternalOut += copy;
					len -= copy;
					pos += copy; // Increase our position since we can copy twice as much the next round.
				}
				while(len);
			}

			Stream.InternalOut += len;
			Stream.InternalBuffer += len;
		}
		else
		{
			*Stream.InternalBuffer++ = *Stream.In++;
			--Stream.AvailIn;
			++Stream.InternalOut;
		}

		Stream.CFlags >>= 1;
		Stream.Bits >>= 1;

		// If we're done with this block, flush the output
		if(Stream.Bits == 0)
			Stream.State = STREAM_FLUSH;

		return true;
	}

public:
	FileReaderLZSS(FileReader &file) : File(file), SawEOF(false)
	{
		Stream.State = STREAM_EMPTY;
		Stream.WindowData = Stream.InternalBuffer = Stream.Window+WINDOW_SIZE;
		Stream.InternalOut = 0;
		Stream.AvailIn = 0;

		FillBuffer();
	}

	~FileReaderLZSS()
	{
	}

	long Read(void *buffer, long len)
	{

		BYTE *Out = (BYTE*)buffer;
		long AvailOut = len;

		do
		{
			while(Stream.AvailIn)
			{
				if(Stream.State == STREAM_EMPTY)
					PrepareBlocks();
				else if(Stream.State == STREAM_BITS && !UncompressBlock())
					break;
				else
					break;
			}

			unsigned int copy = MIN<unsigned int>(Stream.InternalOut, AvailOut);
			if(copy > 0)
			{
				memcpy(Out, Stream.WindowData, copy);
				Out += copy;
				AvailOut -= copy;

				// Slide our window
				memmove(Stream.Window, Stream.Window+copy, WINDOW_SIZE+INTERNAL_BUFFER_SIZE-copy);
				Stream.InternalBuffer -= copy;
				Stream.InternalOut -= copy;
			}

			if(Stream.State == STREAM_FINAL)
				break;

			if(Stream.InternalOut == 0 && Stream.State == STREAM_FLUSH)
				Stream.State = STREAM_EMPTY;

			if(Stream.AvailIn < 2)
				FillBuffer();
		}
		while(AvailOut && Stream.State != STREAM_FINAL);

		assert(AvailOut == 0);
		return (long)(Out - (BYTE*)buffer);
	}
};

// Wad Lump (with console doom LZSS support)

class FWadFileLump : public FResourceLump
{
public:
	bool Compressed;
	int	Position;

	int GetFileOffset() { return Position; }
	FileReader *GetReader()
	{
		if(!Compressed)
		{
			Owner->Reader->Seek(Position, SEEK_SET);
			return Owner->Reader;
		}
		return NULL;
	}
	int FillCache()
	{
		if(!Compressed)
		{
			const char * buffer = Owner->Reader->GetBuffer();

			if (buffer != NULL)
			{
				// This is an in-memory file so the cache can point directly to the file's data.
				Cache = const_cast<char*>(buffer) + Position;
				RefCount = -1;
				return -1;
			}
		}

		Owner->Reader->Seek(Position, SEEK_SET);
		Cache = new char[LumpSize];

		if(Compressed)
		{
			FileReaderLZSS lzss(*Owner->Reader);
			lzss.Read(Cache, LumpSize);
		}
		else
			Owner->Reader->Read(Cache, LumpSize);

		RefCount = 1;
		return 1;
	}
};

// Wad file

class FWadFile : public FResourceFile
{
	FWadFileLump *Lumps;

	bool IsMarker(int lump, const char *marker);
	void SetNamespace(const char *startmarker, const char *endmarker, namespace_t space, bool flathack=false);
	void SkinHack ();

public:
	FWadFile(const char * filename, FileReader *file);
	~FWadFile();
	void FindStrifeTeaserVoices ();
	FResourceLump *GetLump(int lump) { return &Lumps[lump]; }
	bool Open(bool quiet);
};

// Initializes a WAD file

FWadFile::FWadFile(const char *filename, FileReader *file) : FResourceFile(filename, file)
{
	Lumps = NULL;
}

FWadFile::~FWadFile()
{
	delete[] Lumps;
}

// Open it

bool FWadFile::Open(bool quiet)
{
	wadinfo_t header;
	DWORD InfoTableOfs;
	bool isBigEndian = false; // Little endian is assumed until proven otherwise
	const long wadSize = Reader->GetLength();

	Reader->Read(&header, sizeof(header));
	NumLumps = LittleLong(header.NumLumps);
	InfoTableOfs = LittleLong(header.InfoTableOfs);

	// Check to see if the little endian interpretation is valid
	// This should be sufficient to detect big endian wads.
	if (InfoTableOfs + NumLumps*sizeof(wadlump_t) > (unsigned)wadSize)
	{
		NumLumps = BigLong(header.NumLumps);
		InfoTableOfs = BigLong(header.InfoTableOfs);
		isBigEndian = true;
	}

	wadlump_t *fileinfo = new wadlump_t[NumLumps];
	Reader->Seek (InfoTableOfs, SEEK_SET);
	Reader->Read (fileinfo, NumLumps * sizeof(wadlump_t));

	Lumps = new FWadFileLump[NumLumps];

	for(DWORD i = 0; i < NumLumps; i++)
	{
		uppercopy (Lumps[i].Name, fileinfo[i].Name);
		Lumps[i].Name[8] = 0;
		Lumps[i].Compressed = !(gameinfo.flags & GI_SHAREWARE) && (Lumps[i].Name[0] & 0x80) == 0x80;
		Lumps[i].Name[0] &= ~0x80;

		Lumps[i].Owner = this;
		Lumps[i].Position = isBigEndian ? BigLong(fileinfo[i].FilePos) : LittleLong(fileinfo[i].FilePos);
		Lumps[i].LumpSize = isBigEndian ? BigLong(fileinfo[i].Size) : LittleLong(fileinfo[i].Size);
		Lumps[i].Namespace = ns_global;
		Lumps[i].Flags = 0;
		Lumps[i].FullName = NULL;
	}

	delete[] fileinfo;

	if (!quiet)
	{
		Printf(", %d lumps\n", NumLumps);

		// don't bother with namespaces here. We won't need them.
		SetNamespace("S_START", "S_END", ns_sprites);
		SetNamespace("F_START", "F_END", ns_flats, true);
		SetNamespace("C_START", "C_END", ns_colormaps);
		SetNamespace("A_START", "A_END", ns_acslibrary);
		SetNamespace("TX_START", "TX_END", ns_newtextures);
		SetNamespace("V_START", "V_END", ns_strifevoices);
		SetNamespace("HI_START", "HI_END", ns_hires);
		SetNamespace("VX_START", "VX_END", ns_voxels);
		SkinHack();
	}
	return true;
}

// IsMarker

inline bool FWadFile::IsMarker(int lump, const char *marker)
{
	if (Lumps[lump].Name[0] == marker[0])
	{
		return (!strcmp(Lumps[lump].Name, marker) ||
			(marker[1] == '_' && !strcmp(Lumps[lump].Name+1, marker)));
	}
	else return false;
}

// SetNameSpace
// Sets namespace information for the lumps. It always looks for the first
// x_START and the last x_END lump, except when loading flats. In this case
// F_START may be absent and if that is the case all lumps with a size of
// 4096 will be flagged appropriately.

// This class was supposed to be local in the function but GCC
// does not like that.
struct Marker
{
	int markertype;
	unsigned int index;
};

void FWadFile::SetNamespace(const char *startmarker, const char *endmarker, namespace_t space, bool flathack)
{
	bool warned = false;
	int numstartmarkers = 0, numendmarkers = 0;
	unsigned int i;
	TArray<Marker> markers;

	for(i = 0; i < NumLumps; i++)
	{
		if (IsMarker(i, startmarker))
		{
			Marker m = { 0, i };
			markers.Push(m);
			numstartmarkers++;
		}
		else if (IsMarker(i, endmarker))
		{
			Marker m = { 1, i };
			markers.Push(m);
			numendmarkers++;
		}
	}

	if (numstartmarkers == 0)
	{
		if (numendmarkers == 0) return;	// no markers found

		Printf(TEXTCOLOR_YELLOW"WARNING: %s marker without corresponding %s found.\n", endmarker, startmarker);

		if (flathack)
		{
			// We have found no F_START but one or more F_END markers.
			// mark all lumps before the last F_END marker as potential flats.
			unsigned int end = markers[markers.Size()-1].index;
			for(unsigned int i = 0; i < end; i++)
			{
				if (Lumps[i].LumpSize == 4096)
				{
					// We can't add this to the flats namespace but
					// it needs to be flagged for the texture manager.
					DPrintf("Marking %s as potential flat\n", Lumps[i].Name);
					Lumps[i].Flags |= LUMPF_MAYBEFLAT;
				}
			}
		}
		return;
	}

	i = 0;
	while (i < markers.Size())
	{
		int start, end;
		if (markers[i].markertype != 0)
		{
			Printf(TEXTCOLOR_YELLOW"WARNING: %s marker without corresponding %s found.\n", endmarker, startmarker);
			i++;
			continue;
		}
		start = i++;

		// skip over subsequent x_START markers
		while (i < markers.Size() && markers[i].markertype == 0)
		{
			Printf(TEXTCOLOR_YELLOW"WARNING: duplicate %s marker found.\n", startmarker);
			i++;
			continue;
		}
		// same for x_END markers
		while (i < markers.Size()-1 && (markers[i].markertype == 1 && markers[i+1].markertype == 1))
		{
			Printf(TEXTCOLOR_YELLOW"WARNING: duplicate %s marker found.\n", endmarker);
			i++;
			continue;
		}
		// We found a starting marker but no end marker. Ignore this block.
		if (i >= markers.Size())
		{
			Printf(TEXTCOLOR_YELLOW"WARNING: %s marker without corresponding %s found.\n", startmarker, endmarker);
			end = NumLumps;
		}
		else
		{
			end = markers[i++].index;
		}

		// we found a marked block
		DPrintf("Found %s block at (%d-%d)\n", startmarker, markers[start].index, end);
		for(int j = markers[start].index + 1; j < end; j++)
		{
			if (Lumps[j].Namespace != ns_global)
			{
				if (!warned)
				{
					Printf(TEXTCOLOR_YELLOW"WARNING: Overlapping namespaces found (lump %d)\n", j);
				}
				warned = true;
			}
			else if (space == ns_sprites && Lumps[j].LumpSize < 8)
			{
				// ignore sprite lumps smaller than 8 bytes (the smallest possible)
				// in size -- this was used by some dmadds wads
				// as an 'empty' graphics resource
				DPrintf(" Skipped empty sprite %s (lump %d)\n", Lumps[j].Name, j);
			}
			else
			{
				Lumps[j].Namespace = space;
			}
		}
	}
}

// W_SkinHack
// Tests a wad file to see if it contains an S_SKIN marker. If it does,
// every lump in the wad is moved into a new namespace. Because skins are
// only supposed to replace player sprites, sounds, or faces, this should
// not be a problem. Yes, there are skins that replace more than that, but
// they are such a pain, and breaking them like this was done on purpose.
// This also renames any S_SKINxx lumps to just S_SKIN.

void FWadFile::SkinHack ()
{
	static int namespc = ns_firstskin;
	bool skinned = false;
	bool hasmap = false;
	DWORD i;

	for (i = 0; i < NumLumps; i++)
	{
		FResourceLump *lump = &Lumps[i];

		if (lump->Name[0] == 'S' &&
			lump->Name[1] == '_' &&
			lump->Name[2] == 'S' &&
			lump->Name[3] == 'K' &&
			lump->Name[4] == 'I' &&
			lump->Name[5] == 'N')
		{ // Wad has at least one skin.
			lump->Name[6] = lump->Name[7] = 0;
			if (!skinned)
			{
				skinned = true;
				DWORD j;

				for (j = 0; j < NumLumps; j++)
				{
					Lumps[j].Namespace = namespc;
				}
				namespc++;
			}
		}
		if ((lump->Name[0] == 'M' &&
			 lump->Name[1] == 'A' &&
			 lump->Name[2] == 'P' &&
			 lump->Name[3] >= '0' && lump->Name[3] <= '9' &&
			 lump->Name[4] >= '0' && lump->Name[4] <= '9' &&
			 lump->Name[5] >= '\0')
			||
			(lump->Name[0] == 'E' &&
			 lump->Name[1] >= '0' && lump->Name[1] <= '9' &&
			 lump->Name[2] == 'M' &&
			 lump->Name[3] >= '0' && lump->Name[3] <= '9' &&
			 lump->Name[4] >= '\0'))
		{
			hasmap = true;
		}
	}
	if (skinned && hasmap)
	{
		Printf (TEXTCOLOR_BLUE
			"The maps in %s will not be loaded because it has a skin.\n"
			TEXTCOLOR_BLUE
			"You should remove the skin from the wad to play these maps.\n",
			Filename);
	}
}

// FindStrifeTeaserVoices
// Strife0.wad does not have the voices between V_START/V_END markers, so
// figure out which lumps are voices based on their names.

void FWadFile::FindStrifeTeaserVoices ()
{
	for (DWORD i = 0; i <= NumLumps; ++i)
	{
		if (Lumps[i].Name[0] == 'V' &&
			Lumps[i].Name[1] == 'O' &&
			Lumps[i].Name[2] == 'C')
		{
			int j;

			for (j = 3; j < 8; ++j)
			{
				if (Lumps[i].Name[j] != 0 && !isdigit(Lumps[i].Name[j]))
					break;
			}
			if (j == 8)
			{
				Lumps[i].Namespace = ns_strifevoices;
			}
		}
	}
}

// File open