/*
 *
 *    Copyright (c) 2020-2021 Project CHIP Authors
 *    Copyright (c) 2013-2017 Nest Labs, Inc.
 *    All rights reserved.
 *
 *    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.
 */

/**
 *    @file
 *      Declaration of Commissioner Discovery Controller,
 *      a common class that manages state and callbacks
 *      for handling the Commissioner Discovery
 *      and User Directed Commissioning workflow
 *
 */

#pragma once

#include <app/OperationalSessionSetup.h>
#include <lib/core/CHIPConfig.h>
#include <lib/core/CHIPError.h>
#include <lib/core/NodeId.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/PlatformManager.h>
#include <protocols/user_directed_commissioning/UserDirectedCommissioning.h>

#if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY

using chip::NodeId;
using chip::OperationalSessionSetup;
using chip::Protocols::UserDirectedCommissioning::UDCClientState;
using chip::Protocols::UserDirectedCommissioning::UserConfirmationProvider;
using chip::Protocols::UserDirectedCommissioning::UserDirectedCommissioningServer;
using chip::Transport::PeerAddress;

class DLL_EXPORT UserPrompter
{
public:
    /**
     * @brief
     *   Called to prompt the user for consent to allow the given commissioneeName/vendorId/productId to be commissioned.
     * For example "[commissioneeName] is requesting permission to cast to this TV, approve?"
     *
     * If user responds with OK then implementor should call CommissionerRespondOk();
     * If user responds with Cancel then implementor should call CommissionerRespondCancel();
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void PromptForCommissionOKPermission(uint16_t vendorId, uint16_t productId, const char * commissioneeName) = 0;

    /**
     * @brief
     *   Called to prompt the user to enter the setup passcode displayed by the given commissioneeName/vendorId/productId to be
     * commissioned. For example "Please enter passcode displayed in casting app."
     *
     * If user enters passcode then implementor should call CommissionerRespondPasscode(uint32_t passcode);
     * If user responds with Cancel then implementor should call CommissionerRespondCancel();
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    pairingHint        The pairingHint in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    pairingInstruction The pairingInstruction in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    passcodeLength     The length of the passcode as specificed by the commissionee. 0 means not specified.
     *
     */
    virtual void PromptForCommissionPasscode(uint16_t vendorId, uint16_t productId, const char * commissioneeName,
                                             uint16_t pairingHint, const char * pairingInstruction, uint8_t passcodeLength) = 0;

    /**
     * @brief
     *   Called to when CancelCommissioning is received via UDC.
     * Indicates that commissioner can stop showing the passcode entry or display dialog.
     * For example, can show text such as "Commissioning cancelled by client" before hiding dialog.
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void HidePromptsOnCancel(uint16_t vendorId, uint16_t productId, const char * commissioneeName) = 0;

    /**
     * @brief
     *   Return true if this UserPrompter displays QR code along with passcode
     * When PromptWithCommissionerPasscode is called during Commissioner Passcode functionality.
     */
    virtual bool DisplaysPasscodeAndQRCode() = 0;

    /**
     * @brief
     *   Called to display the given setup passcode to the user,
     * for commissioning the given commissioneeName with the given vendorId and productId,
     * and provide instructions for where to enter it in the commissionee (when pairingHint and pairingInstruction are provided).
     * For example "Casting Passcode: [passcode]. For more instructions, click here."
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    passcode           The passcode to display.
     *  @param[in]    passcodeLength     The length of the passcode as specificed by the commissionee. 0 means not specified.
     *  @param[in]    pairingHint        The pairingHint in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    pairingInstruction The pairingInstruction in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void PromptWithCommissionerPasscode(uint16_t vendorId, uint16_t productId, const char * commissioneeName,
                                                uint32_t passcode, uint8_t passcodeLength, uint16_t pairingHint,
                                                const char * pairingInstruction) = 0;

    /**
     * @brief
     *   Called to alert the user that commissioning has begun."
     *
     *  @param[in]    vendorId           The vendorid from the DAC of the new node.
     *  @param[in]    productId          The productid from the DAC of the new node.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void PromptCommissioningStarted(uint16_t vendorId, uint16_t productId, const char * commissioneeName) = 0;

    /**
     * @brief
     *   Called to prompt the user that commissioning and post-commissioning steps have completed successfully."
     *
     *  @param[in]    vendorId           The vendorid from the DAC of the new node.
     *  @param[in]    productId          The productid from the DAC of the new node.
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void PromptCommissioningSucceeded(uint16_t vendorId, uint16_t productId, const char * commissioneeName) = 0;

    /**
     * @brief
     *   Called to prompt the user that commissioning and post-commissioning steps have failed."
     *
     *  @param[in]    commissioneeName   The commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void PromptCommissioningFailed(const char * commissioneeName, CHIP_ERROR error) = 0;

    virtual ~UserPrompter() = default;
};

struct PasscodeInfo
{
    uint32_t passcode;
    uint8_t displayLength;
};

class DLL_EXPORT PasscodeService
{
public:
    /**
     * @brief
     *   Called to determine if the given target app is available to the commissionee with the given
     * vendorId/productId, and if so, return the passcode.
     *
     * This will be called by the main chip thread so any blocking work should be moved to a separate thread.
     *
     * After lookup and attempting to obtain the passcode, implementor should call HandleContentAppCheck();
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    rotatingId         The rotatingId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    info               App info to look for.
     *
     */
    virtual void LookupTargetContentApp(uint16_t vendorId, uint16_t productId, chip::CharSpan rotatingId,
                                        chip::Protocols::UserDirectedCommissioning::TargetAppInfo & info) = 0;

    /**
     * @brief
     *   Called to get the commissioner-generated setup passcode.
     * Returns {0, 0} if feature is disabled.
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    rotatingId         The rotatingId in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual PasscodeInfo GetCommissionerPasscode(uint16_t vendorId, uint16_t productId, chip::CharSpan rotatingId) = 0;

    /**
     * @brief
     *   Called to get the setup passcode from the content app corresponding to the given vendorId/productId.
     *
     * This will be called by the main chip thread so any blocking work should be moved to a separate thread.
     *
     * After attempting to obtain the passcode, implementor should call HandleContentAppPasscodeResponse();
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    rotatingId         The rotatingId in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual void FetchCommissionPasscodeFromContentApp(uint16_t vendorId, uint16_t productId, chip::CharSpan rotatingId) = 0;

    virtual ~PasscodeService() = default;
};

class DLL_EXPORT AppInstallationService
{
public:
    /**
     * @brief
     *   Called to check if the given target app is available to the commissione with th given
     *   vendorId/productId
     *
     *  @param[in]    vendorId           The vendorId in the DNS-SD advertisement of the requesting commissionee.
     *  @param[in]    productId          The productId in the DNS-SD advertisement of the requesting commissionee.
     *
     */
    virtual bool LookupTargetContentApp(uint16_t vendorId, uint16_t productId) = 0;

    virtual ~AppInstallationService() = default;
};

class DLL_EXPORT PostCommissioningListener
{
public:
    /**
     * @brief
     *   Called to when commissioning completed to allow the listener to perform additional
     * steps such as binding and ACL creation.
     *
     *  @param[in]    vendorId           The vendorid from the DAC of the new node.
     *  @param[in]    productId          The productid from the DAC of the new node.
     *  @param[in]    nodeId             The node id for the newly commissioned node.
     *  @param[in]    rotatingId         The rotating ID to handle account login.
     *  @param[in]    passcode           The passcode to handle account login.
     *  @param[in]    exchangeMgr        The exchange manager to be used to get an exchange context.
     *  @param[in]    sessionHandle      A reference to an established session.
     *
     */
    virtual void CommissioningCompleted(uint16_t vendorId, uint16_t productId, NodeId nodeId, chip::CharSpan rotatingId,
                                        uint32_t passcode, chip::Messaging::ExchangeManager & exchangeMgr,
                                        const chip::SessionHandle & sessionHandle) = 0;

    virtual ~PostCommissioningListener() = default;
};

class DLL_EXPORT CommissionerCallback
{
public:
    /**
     * @brief
     *   Called to notify the commissioner that commissioning can now proceed for
     * the node identified by the given arguments.
     *
     *  @param[in]    passcode            The passcode to use for the commissionee.
     *  @param[in]    longDiscriminator   The long discriminator for the commissionee.
     *  @param[in]    peerAddress         The peerAddress for the commissionee.
     *
     */
    virtual void ReadyForCommissioning(uint32_t passcode, uint16_t longDiscriminator, PeerAddress peerAddress) = 0;

    virtual ~CommissionerCallback() = default;
};

class CommissionerDiscoveryController : public chip::Protocols::UserDirectedCommissioning::UserConfirmationProvider
{
public:
    /**
     * This controller can only handle one outstanding UDC session at a time and will
     * reject attempts to start a second when one is outstanding.
     *
     * A session ends when post-commissioning completes:
     * - PostCommissioningSucceeded()
     * or when one of the following failure methods is called:
     * - PostCommissioningFailed()
     * - CommissioningFailed()
     *
     * Reset the state of this controller so that a new sessions will be accepted.
     */
    void ResetState();

    /**
     * Check whether we have a valid session (and reset state if not).
     */
    void ValidateSession();

    /**
     * UserConfirmationProvider callback.
     *
     * Notification that a UDC protocol message was received.
     *
     * This code will call the registered UserPrompter's PromptForCommissionOKPermission
     */
    void OnUserDirectedCommissioningRequest(UDCClientState state) override;

    /**
     * UserConfirmationProvider callback.
     *
     * Notification that a Cancel UDC protocol message was received.
     *
     * This code will call the registered UserPrompter's HidePromptsOnCancel
     */
    void OnCancel(UDCClientState state) override;

    /**
     * UserConfirmationProvider callback.
     *
     * Notification that a CommissionerPasscodeReady UDC protocol message was received.
     *
     * This code will trigger the Commissioner to begin commissioning
     */
    void OnCommissionerPasscodeReady(UDCClientState state) override;

    /**
     * This method should be called after the user has given consent for commissioning of the client
     * indicated in the UserPrompter's PromptForCommissionOKPermission callback
     */
    void Ok();
    void InternalOk();

    /**
     * This method should be called after the user has declined to give consent for commissioning of the client
     * indicated in the UserPrompter's PromptForCommissionOKPermission callback
     */
    void Cancel();

    /**
     * @brief
     *   Called with the result of attempting to obtain the passcode from the content app corresponding to the given
     * vendorId/productId.
     *
     *  @param[in]    passcode           Passcode for the given commissionee, or 0 if passcode cannot be obtained.
     *
     */
    void HandleContentAppPasscodeResponse(uint32_t passcode);
    void InternalHandleContentAppPasscodeResponse();

    /**
     * Cache the passcode to use for commissioning
     */
    inline void SetPasscode(uint32_t passcode) { mPasscode = passcode; }

    /**
     * @brief
     *   Called with the result of attempting to lookup and obtain the passcode from the content app corresponding to the given
     * target.
     *
     *  @param[in]    target             Target app info for app check.
     *  @param[in]    passcode           Passcode for the given commissionee, or 0 if passcode cannot be obtained.
     *
     */
    void HandleTargetContentAppCheck(chip::Protocols::UserDirectedCommissioning::TargetAppInfo target, uint32_t passcode);

    /**
     * This method should be called with the passcode for the client
     * indicated in the UserPrompter's PromptForCommissionPasscode callback
     */
    void CommissionWithPasscode(uint32_t passcode);
    void InternalCommissionWithPasscode();

    /**
     * This method should be called by the commissioner to indicate that commissioning succeeded.
     * The PostCommissioningCallback will then be invoked to complete setup
     *
     *  @param[in]    vendorId           The vendorid from the DAC of the new node.
     *  @param[in]    productId          The productid from the DAC of the new node.
     *  @param[in]    nodeId             The node id for the newly commissioned node.
     *  @param[in]    exchangeMgr        The exchange manager to be used to get an exchange context.
     *  @param[in]    sessionHandle      A reference to an established session.
     *
     */
    void CommissioningSucceeded(uint16_t vendorId, uint16_t productId, NodeId nodeId,
                                chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle);

    /**
     * This method should be called by the commissioner to indicate that commissioning failed.
     * The UserPrompter will then be invoked to notify the user of the failure.
     */
    void CommissioningFailed(CHIP_ERROR error);

    /**
     * This method should be called by the PostCommissioningListener to indicate that post-commissioning steps completed.
     * The PromptCommissioningSucceeded will then be invoked to notify the user of success.
     */
    void PostCommissioningSucceeded();

    /**
     * This method should be called by the PostCommissioningListener to indicate that post-commissioning steps failed.
     * The PromptCommissioningFailed will then be invoked to notify the user of failure.
     */
    void PostCommissioningFailed(CHIP_ERROR error);

    /**
     * Assign a DeviceCommissioner
     */
    inline void SetUserDirectedCommissioningServer(UserDirectedCommissioningServer * udcServer)
    {
        mUdcServer = udcServer;
        mUdcServer->SetUserConfirmationProvider(this);
    }

    /**
     * Assign a UserPromper
     */
    inline void SetUserPrompter(UserPrompter * userPrompter) { mUserPrompter = userPrompter; }

    /**
     * Assign a PasscodeService
     */
    inline void SetPasscodeService(PasscodeService * passcodeService) { mPasscodeService = passcodeService; }
    inline PasscodeService * GetPasscodeService() { return mPasscodeService; }

    /**
     * Assign an AppInstallationService
     */
    inline void SetAppInstallationService(AppInstallationService * appInstallationService)
    {
        mAppInstallationService = appInstallationService;
    }

    /**
     * Assign a Commissioner Callback to perform commissioning once user consent has been given
     */
    inline void SetCommissionerCallback(CommissionerCallback * commissionerCallback)
    {
        mCommissionerCallback = commissionerCallback;
    }

    /**
     * Assign a PostCommissioning Listener to perform post-commissioning operations
     */
    inline void SetPostCommissioningListener(PostCommissioningListener * postCommissioningListener)
    {
        mPostCommissioningListener = postCommissioningListener;
    }

    /**
     * Get the commissioneeName in the DNS-SD advertisement of the requesting commissionee.
     */
    const char * GetCommissioneeName();

    /**
     * Get the UDCClientState of the requesting commissionee.
     */
    UDCClientState * GetUDCClientState();

protected:
    bool mReady          = true; // ready to start commissioning
    bool mPendingConsent = false;
    char mCurrentInstance[chip::Dnssd::Commission::kInstanceNameMaxLength + 1];
    uint16_t mVendorId  = 0;
    uint16_t mProductId = 0;
    NodeId mNodeId      = 0;
    uint32_t mPasscode  = 0;
    std::string mRotatingId;

    UserDirectedCommissioningServer * mUdcServer           = nullptr;
    UserPrompter * mUserPrompter                           = nullptr;
    PasscodeService * mPasscodeService                     = nullptr;
    AppInstallationService * mAppInstallationService       = nullptr;
    CommissionerCallback * mCommissionerCallback           = nullptr;
    PostCommissioningListener * mPostCommissioningListener = nullptr;
};

#endif // CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY
