// SPDX-License-Identifier: GPL-3.0-or-later

#ifndef TRYDANMODBUSMASTER_H
#define TRYDANMODBUSMASTER_H

#include <QObject>
#include <cstring>

/*!
 * \brief Abstract transport interface for the V2C Trydan Modbus connection.
 *
 * Carries all register addresses, float-decode helpers, and the last-polled
 * values.  Concrete subclasses provide the physical transport (Modbus TCP for
 * Étape 1, Modbus RTU for Étape 2) without duplicating any of this logic.
 *
 * \b Register-map rules (cf. V2C Trydan_Modbus_TCP, modbus.py):
 * - Every value occupies \b two holding registers encoded as IEEE-754 float32
 *   with big-endian byte order and big-endian word order (high word first).
 * - Integer values (ChargeState, Intensity, Dynamic …) are still sent as
 *   float32; they must be decoded then rounded — never read as raw uint16.
 * - \b No block read: consecutive addresses (0x0BC2, 0x0BC3 …) overlap their
 *   2-register windows.  Each value must be fetched in its own FC3 transaction.
 * - Writes use FC6 (single register, uint16 direct value — NOT float).
 */
class TrydanModbusMaster : public QObject
{
    Q_OBJECT

public:
    /*!
     * \brief Holding-register read addresses (FC3, 2 registers each, float32 Big/Big).
     *
     * Source: modbus.py lines _read_register + regenera_float (V2C lib).
     * The intentional overlap (0x0BC2 covers regs 0x0BC2..0x0BC3, 0x0BC3
     * covers 0x0BC3..0x0BC4 …) is why block reads are forbidden.
     */
    enum ReadRegister : quint16 {
        RegChargeState   = 0x0BC2,  ///< 0=A(disconnected) 1=B(connected) 2=C(charging)
        RegChargePower   = 0x0BC3,  ///< W
        RegChargeEnergy  = 0x0BC4,  ///< kWh session — diagnostic only, NOT sessionEnergy
        RegSlaveError    = 0x0BC5,  ///< firmware error code (0-10, cf. decodeSlaveError table)
        RegChargeTime    = 0x0BC6,  ///< s, session duration (confirmed hw fw2.4.6, not yet exposed)
        RegHousePower    = 0x0BC8,  ///< W, CT clamp (if installed)
        RegPowerFV       = 0x0BC9,  ///< W, PV production seen by charger (if configured)
        RegPauseState    = 0x0BCA,  ///< 0=active 1=paused
        RegLock          = 0x0BCB,  ///< 0=unlocked 1=locked
        RegIntensity     = 0x0BCD,  ///< A, active charge current
        RegDynamic       = 0x0BCE,  ///< 0=off 1=internal optimizer running — re-read every poll
        RegMinIntensity  = 0x0BD1,  ///< A, lower bound
        RegMaxIntensity  = 0x0BD2,  ///< A, upper bound (firmware-version–dependent, verify on hardware)
        RegPauseDynamic  = 0x0BD3,  ///< 0=optimizer runs 1=optimizer suspended by HEMS
        RegPower_L1      = 0x0BD9,  ///< W, phase L1 power (confirmed hw fw2.4.6)
        RegPower_L2      = 0x0BDA,  ///< W, phase L2 power
        RegPower_L3      = 0x0BDB,  ///< W, phase L3 power
    };

    /*!
     * \brief Write-register addresses (FC6, uint16 raw value — not float).
     *
     * \warning 0x177E (Dynamic) is \b intentionally absent: writing it to 0
     * disables power telemetry (ChargePower goes silent) even though the charger
     * keeps operating.  Use PauseDynamic (0x1783) to suppress the internal PID
     * instead.  cf. evcc charger/trydan.go and github.com/evcc-io/evcc/issues/28047.
     */
    enum WriteRegister : quint16 {
        WRegPauseState   = 0x177A,  ///< setChargingEnabled: 1=pause, 0=active
        WRegLock         = 0x177B,  ///< mirror of PauseState (always written together)
        WRegIntensity    = 0x177D,  ///< setMaxChargingCurrent (integer amperes)
        WRegPauseDynamic = 0x1783,  ///< 1=suspend internal PID, 0=release
    };

    // --- Last values from the most recent update() cycle ---

    /*! \brief ChargeState: 0=A, 1=B, 2=C  (IEC 61851) */
    int   chargeState()   const { return m_chargeState; }
    /*! \brief ChargePower in watts */
    float chargePower()   const { return m_chargePower; }
    /*! \brief Session energy in kWh (diagnostic only — firmware reliability unvalidated) */
    float chargeEnergy()  const { return m_chargeEnergy; }
    /*! \brief Firmware error code */
    int   slaveError()    const { return m_slaveError; }
    /*! \brief House CT-clamp power in watts */
    float housePower()    const { return m_housePower; }
    /*! \brief PV production seen by the charger, in watts */
    float powerFV()       const { return m_powerFV; }
    /*! \brief PauseState register: 0=active, 1=paused */
    int   pauseState()    const { return m_pauseState; }
    /*! \brief Lock register: 0=unlocked, 1=locked */
    int   lock()          const { return m_lock; }
    /*! \brief Current charge current in amperes */
    int   intensity()     const { return m_intensity; }
    /*! \brief Dynamic register: 0=no internal optimizer, 1=optimizer active */
    int   dynamicMode()   const { return m_dynamic; }
    /*! \brief Configured minimum intensity (init-time, usually 6 A) */
    int   minIntensity()  const { return m_minIntensity; }
    /*! \brief Configured maximum intensity (init-time, firmware-version–dependent) */
    int   maxIntensity()  const { return m_maxIntensity; }
    /*! \brief PauseDynamic: 0=optimizer runs, 1=HEMS has suspended it */
    int   pauseDynamic()  const { return m_pauseDynamic; }
    /*! \brief Phase L1 power in watts (diagnostic, 0 if not available) */
    float powerL1()       const { return m_powerL1; }
    /*! \brief Phase L2 power in watts (diagnostic, 0 if not available) */
    float powerL2()       const { return m_powerL2; }
    /*! \brief Phase L3 power in watts (diagnostic, 0 if not available) */
    float powerL3()       const { return m_powerL3; }

    bool reachable() const { return m_reachable; }

    // --- Transport operations (pure virtual) ---

    /*! \brief Establish the physical connection to the charger. */
    virtual void connectDevice()    = 0;
    /*! \brief Drop the physical connection. */
    virtual void disconnectDevice() = 0;
    /*!
     * \brief Read init-time registers (MinIntensity, MaxIntensity).
     * \return false if a read is already in progress.
     */
    virtual bool initialize() = 0;
    /*!
     * \brief Read all poll-cycle registers sequentially.
     * Emits updateFinished() on completion (success or error).
     * \return false if a poll is already in progress.
     */
    virtual bool update() = 0;

    /*!
     * \brief Write PauseState register (FC6).
     * Always pair with writeLock(); the two must stay consistent.
     * cf. evcc trydan.go Enable().
     * \param value 0=active 1=paused
     */
    virtual void writePauseState(quint16 value)   = 0;
    /*!
     * \brief Write Lock register (FC6).
     * \param value 0=unlocked 1=locked
     */
    virtual void writeLock(quint16 value)         = 0;
    /*!
     * \brief Write Intensity register (FC6, integer amperes).
     * Caller must clamp to [minIntensity(), maxIntensity()] before calling.
     */
    virtual void writeIntensity(quint16 amps)     = 0;
    /*!
     * \brief Write PauseDynamic register (FC6).
     * \param value 1=suspend internal optimizer PID, 0=release
     */
    virtual void writePauseDynamic(quint16 value) = 0;

signals:
    /*! \brief Emitted when the transport-level connection state changes. */
    void reachableChanged(bool reachable);
    /*!
     * \brief Emitted once initialize() finishes (success or failure).
     * On success the init-time registers (MinIntensity, MaxIntensity) are valid.
     */
    void initializationFinished(bool success);
    /*!
     * \brief Emitted once each update() cycle completes.
     * All accessor methods reflect the newly read values when this fires.
     */
    void updateFinished();
    /*!
     * \brief Emitted after each write operation completes.
     * \param address The write register address (WRegPauseState, WRegLock, …).
     *                Action handlers must filter by address to avoid processing
     *                background writes (e.g. PauseDynamic from conflict management)
     *                as if they were the writes triggered by the action itself.
     */
    void writeCompleted(quint16 address, bool success);

protected:
    explicit TrydanModbusMaster(QObject *parent = nullptr);

    /*!
     * \brief Decode two holding-register words into a float32 (Big/Big).
     *
     * The V2C Trydan uses big-endian byte order AND big-endian word order:
     * the high 16-bit word arrives first on the wire, forming the most-significant
     * half of the 32-bit pattern.  This matches pymodbus BinaryPayloadDecoder
     * with byteorder=Endian.Big, wordorder=Endian.Big.
     * cf. modbus.py regenera_float()
     *
     * \param high  First register received (most-significant 16 bits of float32)
     * \param low   Second register received (least-significant 16 bits)
     */
    static float decodeFloat32BB(quint16 high, quint16 low);

    /*!
     * \brief Decode an integer value stored as float32 Big/Big.
     *
     * All "integer" registers (ChargeState, Intensity, Dynamic …) are still
     * encoded as float32.  Round to nearest int after decode.
     * cf. modbus.py: int(round(regenera_float(regs)))
     */
    static int decodeIntFromFloat32(quint16 high, quint16 low);

    /*!
     * \brief Update reachability and emit reachableChanged() on transition.
     */
    void setReachable(bool reachable);

    // Cached register values (written by concrete subclasses during polls)
    int   m_chargeState   = 0;
    float m_chargePower   = 0.0f;
    float m_chargeEnergy  = 0.0f;
    int   m_slaveError    = 0;
    float m_housePower    = 0.0f;
    float m_powerFV       = 0.0f;
    int   m_pauseState    = 0;
    int   m_lock          = 0;
    int   m_intensity     = 0;
    int   m_dynamic       = 0;
    int   m_minIntensity  = 6;
    int   m_maxIntensity  = 32;
    int   m_pauseDynamic  = 0;
    float m_powerL1       = 0.0f;
    float m_powerL2       = 0.0f;
    float m_powerL3       = 0.0f;

    bool m_reachable = false;
};

#endif // TRYDANMODBUSMASTER_H
