/*
    INDI alpaca FilterWheel Driver
    
    Copyright (C) 2024 Gord Tulloch

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "indi_alpaca_filterwheel.h"
#include <cstring>
#include <memory>

static std::unique_ptr<AlpacaFilterWheel> alpacaFilterWheel(new AlpacaFilterWheel());

AlpacaFilterWheel::AlpacaFilterWheel()
{
    setVersion(1, 1);
}

const char *AlpacaFilterWheel::getDefaultName()
{
    return "Alpaca Filter Wheel";
}

bool AlpacaFilterWheel::initProperties()
{
    INDI::FilterWheel::initProperties();

    // Set connection mode to TCP only
    setFilterConnection(CONNECTION_TCP);

    // Server address
    ServerAddressTP[HOST_INDEX].fill("HOST", "Host", m_Host.c_str());
    ServerAddressTP[PORT_INDEX].fill("PORT", "Port", std::to_string(m_Port).c_str());
    ServerAddressTP.fill(getDeviceName(), "SERVER_ADDRESS", "Server", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
    ServerAddressTP.load();
    
    // Update m_Host and m_Port from loaded config
    if (ServerAddressTP[HOST_INDEX].text != nullptr && strlen(ServerAddressTP[HOST_INDEX].text) > 0)
    {
        m_Host = ServerAddressTP[HOST_INDEX].text;
    }
    if (ServerAddressTP[PORT_INDEX].text != nullptr && strlen(ServerAddressTP[PORT_INDEX].text) > 0)
    {
        m_Port = std::stoi(ServerAddressTP[PORT_INDEX].text);
    }

    // Device information
    DeviceInfoTP[DESCRIPTION].fill("DESCRIPTION", "Description", "");
    DeviceInfoTP[DRIVER_INFO].fill("DRIVER_INFO", "Driver Info", "");
    DeviceInfoTP[DRIVER_VERSION].fill("DRIVER_VERSION", "Driver Version", "");
    DeviceInfoTP[INTERFACE_VERSION].fill("INTERFACE_VERSION", "Interface Version", "");
    DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", INFO_TAB, IP_RO, 60, IPS_IDLE);

    // Focus offsets for each filter
    FocusOffsetsNP[0].fill("OFFSET_0", "Dark Offset", "%.0f", -1000, 1000, 1, 0);
    FocusOffsetsNP[1].fill("OFFSET_1", "IR Offset", "%.0f", -1000, 1000, 1, 0);
    FocusOffsetsNP[2].fill("OFFSET_2", "LP Offset", "%.0f", -1000, 1000, 1, 0);
    FocusOffsetsNP.fill(getDeviceName(), "FOCUS_OFFSETS", "Focus Offsets", FILTER_TAB, IP_RO, 60, IPS_IDLE);

    addDebugControl();

    return true;
}

void AlpacaFilterWheel::ISGetProperties(const char *dev)
{
    INDI::FilterWheel::ISGetProperties(dev);

    // Always define the server address so it can be configured before connecting
    defineProperty(ServerAddressTP);
}

bool AlpacaFilterWheel::updateProperties()
{
    INDI::FilterWheel::updateProperties();

    if (isConnected())
    {
        defineProperty(DeviceInfoTP);
        defineProperty(FocusOffsetsNP);
    }
    else
    {
        deleteProperty(DeviceInfoTP);
        deleteProperty(FocusOffsetsNP);
    }

    return true;
}

bool AlpacaFilterWheel::Connect()
{
    LOG_INFO("Connecting to alpaca FilterWheel...");

    // Create HTTP client
    m_AlpacaClient.reset(new httplib::Client(m_Host, m_Port));
    if (!m_AlpacaClient)
    {
        LOG_ERROR("Failed to create HTTP client");
        return false;
    }

    m_AlpacaClient->set_connection_timeout(5, 0);
    m_AlpacaClient->set_read_timeout(10, 0);

    // Test connection with /connected endpoint
    nlohmann::json response;
    if (!sendAlpacaGET("/connected", response))
    {
        LOG_ERROR("Failed to connect to Alpaca device");
        return false;
    }

    LOG_INFO("Connected to Alpaca device");

    // Connect the device
    if (!sendAlpacaPUT("/connected", "Connected=true", response))
    {
        LOG_ERROR("Failed to set device connected state");
        return false;
    }

    // Query device information
    if (sendAlpacaGET("/description", response) && response.contains("Value"))
    {
        std::string desc = response["Value"].get<std::string>();
        DeviceInfoTP[DESCRIPTION].setText(desc.c_str());
    }

    if (sendAlpacaGET("/driverinfo", response) && response.contains("Value"))
    {
        std::string info = response["Value"].get<std::string>();
        DeviceInfoTP[DRIVER_INFO].setText(info.c_str());
    }

    if (sendAlpacaGET("/driverversion", response) && response.contains("Value"))
    {
        std::string ver = response["Value"].get<std::string>();
        DeviceInfoTP[DRIVER_VERSION].setText(ver.c_str());
    }

    if (sendAlpacaGET("/interfaceversion", response) && response.contains("Value"))
    {
        int ifver = response["Value"].get<int>();
        DeviceInfoTP[INTERFACE_VERSION].setText(std::to_string(ifver).c_str());
    }

    DeviceInfoTP.apply();

    // Setup filter wheel
    if (!setupFilterWheel())
    {
        LOG_ERROR("Failed to setup filter wheel");
        return false;
    }

    LOG_INFO("alpaca FilterWheel connected successfully");
    return true;
}

bool AlpacaFilterWheel::Disconnect()
{
    LOG_INFO("Disconnecting alpaca FilterWheel...");

    // Disconnect the device
    nlohmann::json response;
    sendAlpacaPUT("/connected", "Connected=false", response);

    m_AlpacaClient.reset();

    LOG_INFO("alpaca FilterWheel disconnected");
    return true;
}

bool AlpacaFilterWheel::setupFilterWheel()
{
    nlohmann::json response;

    // Query filter names
    if (sendAlpacaGET("/names", response) && response.contains("Value"))
    {
        auto names = response["Value"];
        if (names.is_array() && !names.empty())
        {
            // Rebuild the filter name property with properly named/labeled widgets.
            // A plain resize() leaves new widgets without a "name" attribute, which
            // causes the receiving end to discard them as "defTextVector with no
            // valid members".
            FilterNameTP.resize(0);

            for (size_t i = 0; i < names.size(); i++)
            {
                std::string name = names[i].get<std::string>();

                char filterName[MAXINDINAME];
                char filterLabel[MAXINDILABEL];
                snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%zu", i + 1);
                snprintf(filterLabel, MAXINDILABEL, "Filter#%zu", i + 1);

                INDI::WidgetText oneText;
                oneText.fill(filterName, filterLabel, name.c_str());
                FilterNameTP.push(std::move(oneText));

                LOGF_INFO("Filter %zu: %s", i, name.c_str());
            }

            FilterNameTP.fill(getDeviceName(), "FILTER_NAME", "Filter", FilterSlotNP.getGroupName(), IP_RW, 60, IPS_IDLE);
            FilterNameTP.shrink_to_fit();

            // Match the filter slot range to the number of filters reported
            FilterSlotNP[0].setMax(static_cast<double>(names.size()));

            if (isConnected())
                FilterNameTP.apply();

            LOGF_INFO("Found %zu filters", names.size());
        }
    }

    // Query focus offsets
    if (sendAlpacaGET("/focusoffsets", response) && response.contains("Value"))
    {
        auto offsets = response["Value"];
        if (offsets.is_array())
        {
            for (size_t i = 0; i < offsets.size() && i < 3; i++)
            {
                int offset = offsets[i].get<int>();
                FocusOffsetsNP[i].setValue(offset);
                LOGF_INFO("Filter %zu focus offset: %d steps", i, offset);
            }
            FocusOffsetsNP.apply();
        }
    }

    // Query current position
    int currentPos = QueryFilter();
    if (currentPos >= 0)
    {
        LOGF_INFO("Current filter position: %d", currentPos);
        FilterSlotNP[0].setValue(currentPos + 1); // INDI uses 1-based indexing
        FilterSlotNP.apply();
    }

    return true;
}

bool AlpacaFilterWheel::SelectFilter(int position)
{
    // Convert from INDI 1-based to ASCOM 0-based
    int targetPos = position - 1;

    const char *filterName = (static_cast<size_t>(targetPos) < FilterNameTP.size()) ? FilterNameTP[targetPos].getText() : "Unknown";
    LOGF_INFO("Selecting filter position %d (%s)", targetPos, filterName);

    // Check if already at position to avoid error 1279
    int currentPos = QueryFilter();
    if (currentPos == targetPos)
    {
        LOGF_INFO("Already at position %d, no movement needed", targetPos);
        SelectFilterDone(position);
        return true;
    }

    nlohmann::json response;
    std::string data = "Position=" + std::to_string(targetPos);
    
    if (!sendAlpacaPUT("/position", data, response))
    {
        LOGF_ERROR("Failed to set filter position to %d", targetPos);
        return false;
    }

    // Check for errors
    if (response.contains("ErrorNumber"))
    {
        int errorNum = response["ErrorNumber"].get<int>();
        if (errorNum != 0)
        {
            std::string errorMsg = response.contains("ErrorMessage") ? 
                                 response["ErrorMessage"].get<std::string>() : "Unknown error";
            LOGF_ERROR("Error setting filter position: %d - %s", errorNum, errorMsg.c_str());
            return false;
        }
    }

    LOGF_INFO("Filter position set to %d", targetPos);

    // Notify the client that the filter change has completed
    SelectFilterDone(position);

    return true;
}

int AlpacaFilterWheel::QueryFilter()
{
    nlohmann::json response;
    
    if (!sendAlpacaGET("/position", response))
    {
        LOG_ERROR("Failed to query filter position");
        return -1;
    }

    if (response.contains("Value"))
    {
        int pos = response["Value"].get<int>();
        return pos; // Return 0-based position
    }

    return -1;
}

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

    ServerAddressTP.save(fp);

    return true;
}

bool AlpacaFilterWheel::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
{
    if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
    {
        // Server address
        if (ServerAddressTP.isNameMatch(name))
        {
            ServerAddressTP.update(texts, names, n);
            
            m_Host = ServerAddressTP[HOST_INDEX].text;
            m_Port = std::stoi(ServerAddressTP[PORT_INDEX].text);
            
            ServerAddressTP.apply();
            saveConfig(true, "SERVER_ADDRESS");
            
            LOGF_INFO("Server address updated: %s:%d", m_Host.c_str(), m_Port);
            return true;
        }
    }

    return INDI::FilterWheel::ISNewText(dev, name, texts, names, n);
}

bool AlpacaFilterWheel::sendAlpacaGET(const std::string &endpoint, nlohmann::json &response)
{
    if (!m_AlpacaClient)
        return false;

    std::string url = "/api/v1/filterwheel/" + std::to_string(m_DeviceNumber) + endpoint;
    
    LOGF_DEBUG("GET %s", url.c_str());
    
    auto res = m_AlpacaClient->Get(url.c_str());
    
    if (!res)
    {
        LOGF_ERROR("HTTP GET failed for %s", url.c_str());
        return false;
    }

    if (res->status != 200)
    {
        LOGF_ERROR("HTTP GET returned status %d for %s", res->status, url.c_str());
        return false;
    }

    try
    {
        response = nlohmann::json::parse(res->body);
        
        // Check for Alpaca errors
        if (response.contains("ErrorNumber"))
        {
            int errorNum = response["ErrorNumber"].get<int>();
            if (errorNum != 0)
            {
                std::string errorMsg = response.contains("ErrorMessage") ? 
                                     response["ErrorMessage"].get<std::string>() : "Unknown error";
                LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
                return false;
            }
        }
        
        return true;
    }
    catch (const nlohmann::json::exception &e)
    {
        LOGF_ERROR("JSON parse error: %s", e.what());
        return false;
    }
}

bool AlpacaFilterWheel::sendAlpacaPUT(const std::string &endpoint, const std::string &data, nlohmann::json &response)
{
    if (!m_AlpacaClient)
        return false;

    std::string url = "/api/v1/filterwheel/" + std::to_string(m_DeviceNumber) + endpoint;
    
    // Add client info to data
    std::string fullData = data;
    if (!fullData.empty())
        fullData += "&";
    fullData += "ClientID=" + std::to_string(m_ClientID);
    fullData += "&ClientTransactionID=" + std::to_string(++m_TransactionID);
    
    LOGF_DEBUG("PUT %s: %s", url.c_str(), fullData.c_str());
    
    auto res = m_AlpacaClient->Put(url.c_str(), fullData, "application/x-www-form-urlencoded");
    
    if (!res)
    {
        LOGF_ERROR("HTTP PUT failed for %s", url.c_str());
        return false;
    }

    if (res->status != 200)
    {
        LOGF_ERROR("HTTP PUT returned status %d for %s", res->status, url.c_str());
        return false;
    }

    try
    {
        response = nlohmann::json::parse(res->body);
        
        // Check for Alpaca errors (but ignore error 1279 - already at position)
        if (response.contains("ErrorNumber"))
        {
            int errorNum = response["ErrorNumber"].get<int>();
            if (errorNum != 0 && errorNum != 1279)
            {
                std::string errorMsg = response.contains("ErrorMessage") ? 
                                     response["ErrorMessage"].get<std::string>() : "Unknown error";
                LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
                return false;
            }
        }
        
        return true;
    }
    catch (const nlohmann::json::exception &e)
    {
        LOGF_ERROR("JSON parse error: %s", e.what());
        return false;
    }
}
