/*
 *
 *    Copyright (c) 2020 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#pragma once

#include <algorithm>
#include <cstdint>
#include <optional>

#include <lib/core/CHIPError.h>
#include <lib/core/PeerId.h>
#include <lib/dnssd/TxtFields.h>
#include <lib/support/CHIPMemString.h>
#include <lib/support/SafeString.h>
#include <lib/support/Span.h>
#include <platform/CHIPDeviceConfig.h>

namespace chip {
namespace Dnssd {

static constexpr uint16_t kMdnsPort = 5353;
// Need 8 bytes to fit a thread mac.
static constexpr size_t kMaxMacSize = 8;

enum class CommssionAdvertiseMode : uint8_t
{
    kCommissionableNode,
    kCommissioner,
};

enum class CommissioningMode
{
    kDisabled,           // Commissioning Mode is disabled, CM=0 in DNS-SD key/value pairs
    kEnabledBasic,       // Basic Commissioning Mode, CM=1 in DNS-SD key/value pairs
    kEnabledEnhanced,    // Enhanced Commissioning Mode, CM=2 in DNS-SD key/value pairs
    kEnabledJointFabric, // Joint Fabric Commissioning Mode, CM=3 in DNS-SD key/value pairs
};

enum class ICDModeAdvertise : uint8_t
{
    kNone, // The device does not support the LIT feature-set. No ICD= key is advertised in DNS-SD.
    kSIT,  // The ICD supports the LIT feature-set, but is currently operating as a SIT. ICD=0 in DNS-SD key/value pairs.
    kLIT,  // The ICD is currently operating as a LIT. ICD=1 in DNS-SD key/value pairs.
};

enum class TCPModeAdvertise : uint16_t
{
    kNone            = 0,                         // The device does not support TCP.
    kTCPClient       = 1 << 1,                    // The device supports the TCP client.
    kTCPServer       = 1 << 2,                    // The device supports the TCP server.
    kTCPClientServer = (kTCPClient | kTCPServer), // The device supports both the TCP client and server.
};

template <class Derived>
class BaseAdvertisingParams
{
public:
    static constexpr uint8_t kCommonTxtMaxNumber     = KeyCount(TxtKeyUse::kCommon);
    static constexpr size_t kCommonTxtMaxKeySize     = MaxKeyLen(TxtKeyUse::kCommon);
    static constexpr size_t kCommonTxtMaxValueSize   = MaxValueLen(TxtKeyUse::kCommon);
    static constexpr size_t kCommonTxtTotalKeySize   = TotalKeyLen(TxtKeyUse::kCommon);
    static constexpr size_t kCommonTxtTotalValueSize = TotalValueLen(TxtKeyUse::kCommon);

    Derived & SetPort(uint16_t port)
    {
        mPort = port;
        return *reinterpret_cast<Derived *>(this);
    }
    uint16_t GetPort() const { return mPort; }

    Derived & SetInterfaceId(Inet::InterfaceId interfaceId)
    {
        mInterfaceId = interfaceId;
        return *reinterpret_cast<Derived *>(this);
    }

    Inet::InterfaceId GetInterfaceId() const { return mInterfaceId; }

    Derived & EnableIpV4(bool enable)
    {
        mEnableIPv4 = enable;
        return *reinterpret_cast<Derived *>(this);
    }
    bool IsIPv4Enabled() const { return mEnableIPv4; }
    Derived & SetMac(chip::ByteSpan mac)
    {
        mMacLength = std::min(mac.size(), kMaxMacSize);
        memcpy(mMacStorage, mac.data(), mMacLength);
        return *reinterpret_cast<Derived *>(this);
    }
    const chip::ByteSpan GetMac() const { return chip::ByteSpan(mMacStorage, mMacLength); }

    // Common Flags
    Derived & SetLocalMRPConfig(const std::optional<ReliableMessageProtocolConfig> & config)
    {
        mLocalMRPConfig = config;
        return *reinterpret_cast<Derived *>(this);
    }
    const std::optional<ReliableMessageProtocolConfig> & GetLocalMRPConfig() const { return mLocalMRPConfig; }

    Derived & SetTCPSupportModes(TCPModeAdvertise tcpSupportModes)
    {
        mTcpSupportModes = tcpSupportModes;
        return *reinterpret_cast<Derived *>(this);
    }
    TCPModeAdvertise GetTCPSupportModes() const { return mTcpSupportModes; }

    Derived & SetICDModeToAdvertise(ICDModeAdvertise operatingMode)
    {
        mICDModeAdvertise = operatingMode;
        return *reinterpret_cast<Derived *>(this);
    }
    ICDModeAdvertise GetICDModeToAdvertise() const { return mICDModeAdvertise; }

private:
    uint16_t mPort                   = CHIP_PORT;
    Inet::InterfaceId mInterfaceId   = Inet::InterfaceId::Null();
    bool mEnableIPv4                 = true;
    uint8_t mMacStorage[kMaxMacSize] = {};
    size_t mMacLength                = 0;
    std::optional<ReliableMessageProtocolConfig> mLocalMRPConfig;
    TCPModeAdvertise mTcpSupportModes  = TCPModeAdvertise::kNone;
    ICDModeAdvertise mICDModeAdvertise = ICDModeAdvertise::kNone;
};

/// Defines parameters required for advertising a CHIP node
/// over mDNS as an 'operationally ready' node.
class OperationalAdvertisingParameters : public BaseAdvertisingParams<OperationalAdvertisingParameters>
{
public:
    // Operational uses only common keys
    static constexpr uint8_t kTxtMaxNumber     = kCommonTxtMaxNumber;
    static constexpr uint8_t kTxtMaxKeySize    = kCommonTxtMaxKeySize;
    static constexpr uint8_t kTxtMaxValueSize  = kCommonTxtMaxValueSize;
    static constexpr size_t kTxtTotalKeySize   = kCommonTxtTotalKeySize;
    static constexpr size_t kTxtTotalValueSize = kCommonTxtTotalValueSize;

    OperationalAdvertisingParameters & SetPeerId(const PeerId & peerId)
    {
        mPeerId = peerId;
        return *this;
    }
    PeerId GetPeerId() const { return mPeerId; }

    CompressedFabricId GetCompressedFabricId() const { return mPeerId.GetCompressedFabricId(); }

private:
    PeerId mPeerId;
};

class CommissionAdvertisingParameters : public BaseAdvertisingParams<CommissionAdvertisingParameters>
{
public:
    static constexpr uint8_t kTxtMaxNumber     = kCommonTxtMaxNumber + KeyCount(TxtKeyUse::kCommission);
    static constexpr uint8_t kTxtMaxKeySize    = std::max(kCommonTxtMaxKeySize, MaxKeyLen(TxtKeyUse::kCommission));
    static constexpr uint8_t kTxtMaxValueSize  = std::max(kCommonTxtMaxValueSize, MaxValueLen(TxtKeyUse::kCommission));
    static constexpr size_t kTxtTotalKeySize   = kCommonTxtTotalKeySize + TotalKeyLen(TxtKeyUse::kCommission);
    static constexpr size_t kTxtTotalValueSize = kCommonTxtTotalValueSize + TotalValueLen(TxtKeyUse::kCommission);

    CommissionAdvertisingParameters & SetShortDiscriminator(uint8_t discriminator)
    {
        mShortDiscriminator = discriminator;
        return *this;
    }
    uint8_t GetShortDiscriminator() const { return mShortDiscriminator; }

    CommissionAdvertisingParameters & SetLongDiscriminator(uint16_t discriminator)
    {
        mLongDiscriminator = discriminator;
        return *this;
    }
    uint16_t GetLongDiscriminator() const { return mLongDiscriminator; }

    CommissionAdvertisingParameters & SetVendorId(std::optional<uint16_t> vendorId)
    {
        mVendorId = vendorId;
        return *this;
    }
    std::optional<uint16_t> GetVendorId() const { return mVendorId; }

    CommissionAdvertisingParameters & SetProductId(std::optional<uint16_t> productId)
    {
        mProductId = productId;
        return *this;
    }
    std::optional<uint16_t> GetProductId() const { return mProductId; }

    CommissionAdvertisingParameters & SetCommissioningMode(CommissioningMode mode)
    {
        mCommissioningMode = mode;
        return *this;
    }
    CommissioningMode GetCommissioningMode() const { return mCommissioningMode; }

#if CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
    CommissionAdvertisingParameters & SetJointFabricMode(BitFlags<JointFabricMode> mode)
    {
        mJointFabricMode = mode;
        return *this;
    }
    BitFlags<JointFabricMode> GetJointFabricMode() const { return mJointFabricMode; }
#endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC

    CommissionAdvertisingParameters & SetDeviceType(std::optional<uint32_t> deviceType)
    {
        mDeviceType = deviceType;
        return *this;
    }
    std::optional<uint32_t> GetDeviceType() const { return mDeviceType; }

    CommissionAdvertisingParameters & SetDeviceName(std::optional<const char *> deviceName)
    {
        if (deviceName.has_value())
        {
            Platform::CopyString(mDeviceName, sizeof(mDeviceName), *deviceName);
            mDeviceNameHasValue = true;
        }
        else
        {
            mDeviceNameHasValue = false;
        }
        return *this;
    }
    std::optional<const char *> GetDeviceName() const
    {
        return mDeviceNameHasValue ? std::make_optional<const char *>(mDeviceName) : std::nullopt;
    }

    CommissionAdvertisingParameters & SetRotatingDeviceId(std::optional<const char *> rotatingId)
    {
        if (rotatingId.has_value())
        {
            Platform::CopyString(mRotatingId, sizeof(mRotatingId), *rotatingId);
            mRotatingIdHasValue = true;
        }
        else
        {
            mRotatingIdHasValue = false;
        }
        return *this;
    }
    std::optional<const char *> GetRotatingDeviceId() const
    {
        return mRotatingIdHasValue ? std::make_optional<const char *>(mRotatingId) : std::nullopt;
    }

    CommissionAdvertisingParameters & SetPairingInstruction(std::optional<const char *> pairingInstr)
    {
        if (pairingInstr.has_value())
        {
            Platform::CopyString(mPairingInstr, sizeof(mPairingInstr), *pairingInstr);
            mPairingInstrHasValue = true;
        }
        else
        {
            mPairingInstrHasValue = false;
        }
        return *this;
    }
    std::optional<const char *> GetPairingInstruction() const
    {
        return mPairingInstrHasValue ? std::make_optional<const char *>(mPairingInstr) : std::nullopt;
    }

    CommissionAdvertisingParameters & SetPairingHint(std::optional<uint16_t> pairingHint)
    {
        mPairingHint = pairingHint;
        return *this;
    }
    std::optional<uint16_t> GetPairingHint() const { return mPairingHint; }

    CommissionAdvertisingParameters & SetCommissionAdvertiseMode(CommssionAdvertiseMode mode)
    {
        mMode = mode;
        return *this;
    }
    CommssionAdvertiseMode GetCommissionAdvertiseMode() const { return mMode; }

    CommissionAdvertisingParameters & SetCommissionerPasscodeSupported(std::optional<bool> commissionerPasscodeSupported)
    {
        mCommissionerPasscodeSupported = commissionerPasscodeSupported;
        return *this;
    }
    std::optional<bool> GetCommissionerPasscodeSupported() const { return mCommissionerPasscodeSupported; }

private:
    uint8_t mShortDiscriminator          = 0;
    uint16_t mLongDiscriminator          = 0; // 12-bit according to spec
    CommssionAdvertiseMode mMode         = CommssionAdvertiseMode::kCommissionableNode;
    CommissioningMode mCommissioningMode = CommissioningMode::kEnabledBasic;
#if CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
    BitFlags<JointFabricMode> mJointFabricMode;
#endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
    std::optional<uint16_t> mVendorId;
    std::optional<uint16_t> mProductId;
    std::optional<uint32_t> mDeviceType;
    std::optional<uint16_t> mPairingHint;

    char mDeviceName[kKeyDeviceNameMaxLength + 1];
    bool mDeviceNameHasValue = false;

    char mRotatingId[kKeyRotatingDeviceIdMaxLength + 1];
    bool mRotatingIdHasValue = false;

    char mPairingInstr[kKeyPairingInstructionMaxLength + 1];
    bool mPairingInstrHasValue = false;

    std::optional<bool> mCommissionerPasscodeSupported;
};

/**
 * Interface for advertising CHIP DNS-SD services.
 *
 * A user of this interface must first initialize the advertiser using the `Init` method.
 *
 * Then, whenever advertised services need to be refreshed, the following sequence of events must
 * occur:
 * 1. Call the `RemoveServices` method.
 * 2. Call one of the `Advertise` methods for each service to be added or updated.
 * 3. Call the `FinalizeServiceUpdate` method to finalize the update and apply all pending changes.
 */
class ServiceAdvertiser
{
public:
    virtual ~ServiceAdvertiser() {}

    /**
     * Initializes the advertiser.
     *
     * The method must be called before other methods of this class.
     * If the advertiser has already been initialized, the method exits immediately with no error.
     */
    virtual CHIP_ERROR Init(chip::Inet::EndPointManager<chip::Inet::UDPEndPoint> * udpEndPointManager) = 0;

    /**
     * Returns whether the advertiser has completed the initialization.
     *
     * Returns true if the advertiser is ready to advertise services.
     */
    virtual bool IsInitialized() = 0;

    /**
     * Shuts down the advertiser.
     */
    virtual void Shutdown() = 0;

    /**
     * Removes or marks all services being advertised for removal.
     *
     * Depending on the implementation, the method may either stop advertising existing services
     * immediately, or mark them for removal upon the subsequent `FinalizeServiceUpdate` method call.
     */
    virtual CHIP_ERROR RemoveServices() = 0;

    /**
     * Advertises the given operational node service.
     */
    virtual CHIP_ERROR Advertise(const OperationalAdvertisingParameters & params) = 0;

    /**
     * Advertises the given commissionable/commissioner node service.
     */
    virtual CHIP_ERROR Advertise(const CommissionAdvertisingParameters & params) = 0;

    /**
     * Finalizes updating advertised services.
     *
     * This method can be used by some implementations to apply changes made with the `RemoveServices`
     * and `Advertise` methods in case they could not be applied immediately.
     */
    virtual CHIP_ERROR FinalizeServiceUpdate() = 0;

    /**
     * Returns the commissionable node service instance name formatted as hex string.
     */
    virtual CHIP_ERROR GetCommissionableInstanceName(char * instanceName, size_t maxLength) const = 0;

    /**
     * Generates an updated commissionable instance name.  This happens
     * automatically when Init() is called, but may be needed at other times as
     * well.
     */
    virtual CHIP_ERROR UpdateCommissionableInstanceName() = 0;

    /**
     * Returns the system-wide implementation of the service advertiser.
     *
     * The method returns a reference to the advertiser object configured by
     * a user using the \c ServiceAdvertiser::SetInstance() method, or the
     * default advertiser returned by the \c GetDefaultAdvertiser() function.
     */
    static ServiceAdvertiser & Instance();

    /**
     * Sets the system-wide implementation of the service advertiser.
     */
    static void SetInstance(ServiceAdvertiser & advertiser);

private:
    static ServiceAdvertiser * sInstance;
};

/**
 * Returns the default implementation of the service advertiser.
 */
extern ServiceAdvertiser & GetDefaultAdvertiser();

} // namespace Dnssd
} // namespace chip
