/**
 *
 *    Copyright (c) 2021 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.
 */

#include "identify-server.h"

#include <app-common/zap-generated/attributes/Accessors.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app-common/zap-generated/ids/Attributes.h>
#include <app-common/zap-generated/ids/Clusters.h>
#include <app-common/zap-generated/ids/Commands.h>
#include <app/CommandHandler.h>
#include <app/ConcreteCommandPath.h>
#include <lib/support/CodeUtils.h>
#include <platform/CHIPDeviceLayer.h>
#include <tracing/macros.h>

#if CHIP_DEVICE_LAYER_NONE
#error "identify requrires a device layer"
#endif

using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters::Identify;
using chip::Protocols::InteractionModel::Status;

static Identify * firstIdentify = nullptr;

static void onIdentifyClusterTick(chip::System::Layer * systemLayer, void * appState);

static Identify * inst(EndpointId endpoint)
{
    Identify * current = firstIdentify;
    while (current != nullptr && current->mEndpoint != endpoint)
    {
        current = current->next();
    }

    return current;
}

static inline void reg(Identify * inst)
{
    inst->setNext(firstIdentify);
    firstIdentify = inst;
}

static inline void unreg(Identify * inst)
{
    if (firstIdentify == inst)
    {
        firstIdentify = firstIdentify->next();
    }
    else
    {
        Identify * previous = firstIdentify;
        Identify * current  = firstIdentify->next();

        while (current != nullptr && current != inst)
        {
            previous = current;
            current  = current->next();
        }

        if (current != nullptr)
        {
            previous->setNext(current->next());
        }
    }
}

void emberAfIdentifyClusterServerInitCallback(EndpointId endpoint)
{
    Identify * identify = inst(endpoint);
    if (identify != nullptr)
    {
        (void) Attributes::IdentifyType::Set(endpoint, identify->mIdentifyType);
    }
}

static void onIdentifyClusterTick(chip::System::Layer * systemLayer, void * appState)
{
    uint16_t identifyTime = 0;
    Identify * identify   = reinterpret_cast<Identify *>(appState);

    if (nullptr != identify)
    {
        EndpointId endpoint = identify->mEndpoint;

        if (Status::Success == Attributes::IdentifyTime::Get(endpoint, &identifyTime) && 0 != identifyTime)
        {
            identifyTime = static_cast<uint16_t>(identifyTime == 0 ? 0 : identifyTime - 1);
            // This tick writes the new attribute, which will trigger the Attribute
            // Changed callback.
            (void) Attributes::IdentifyTime::Set(endpoint, identifyTime);
        }
    }
}

static inline void identify_activate(Identify * identify)
{
    if (nullptr != identify->mOnIdentifyStart && nullptr != identify->mOnIdentifyStop && false == identify->mActive)
    {
        identify->mActive = true;
        identify->mOnIdentifyStart(identify);
    }
}

static inline void identify_deactivate(Identify * identify)
{
    if (nullptr != identify->mOnIdentifyStop)
    {
        identify->mActive = false;
        identify->mOnIdentifyStop(identify);
    }
}

void MatterIdentifyClusterServerAttributeChangedCallback(const app::ConcreteAttributePath & attributePath)
{
    if (attributePath.mAttributeId == Attributes::IdentifyTime::Id)
    {
        EndpointId endpoint = attributePath.mEndpointId;
        Identify * identify = inst(endpoint);
        uint16_t identifyTime;

        if (identify == nullptr)
        {
            return;
        }

        if (Status::Success == Attributes::IdentifyTime::Get(endpoint, &identifyTime))
        {
            /* effect identifier changed during identify */
            if (identify->mTargetEffectIdentifier != identify->mCurrentEffectIdentifier)
            {
                identify->mCurrentEffectIdentifier = identify->mTargetEffectIdentifier;

                /* finish identify process */
                if (EffectIdentifierEnum::kFinishEffect == identify->mCurrentEffectIdentifier && identifyTime > 0)
                {
                    Attributes::IdentifyTime::Set(endpoint, 1);
                }
                /* stop identify process */
                if (EffectIdentifierEnum::kStopEffect == identify->mCurrentEffectIdentifier && identifyTime > 0)
                {
                    Attributes::IdentifyTime::Set(endpoint, 0);
                    identify_deactivate(identify);
                }
                /* change from e.g. Breathe to Blink during identify */
                else
                {
                    /* cancel identify */
                    Attributes::IdentifyTime::Set(endpoint, 0);
                    identify_deactivate(identify);

                    /* trigger effect identifier callback */
                    if (nullptr != identify->mOnEffectIdentifier)
                        identify->mOnEffectIdentifier(identify);
                }
            }
            else if (identifyTime > 0)
            {
                /* we only start if both callbacks are set */
                identify_activate(identify);

                (void) chip::DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds16(1), onIdentifyClusterTick, identify);
                return;
            }
            else
            {
                identify_deactivate(identify);
            }
        }

        (void) chip::DeviceLayer::SystemLayer().CancelTimer(onIdentifyClusterTick, identify);
    }
}

bool emberAfIdentifyClusterIdentifyCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
                                            const Commands::Identify::DecodableType & commandData)
{
    MATTER_TRACE_SCOPE("IdentifyCommand", "Identify");
    auto & identifyTime = commandData.identifyTime;

    // cmd Identify
    commandObj->AddStatus(commandPath, Attributes::IdentifyTime::Set(commandPath.mEndpointId, identifyTime));
    return true;
}

bool emberAfIdentifyClusterTriggerEffectCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
                                                 const Commands::TriggerEffect::DecodableType & commandData)
{
    MATTER_TRACE_SCOPE("TriggerEffect", "Identify");
    auto & effectIdentifier = commandData.effectIdentifier;
    auto & effectVariant    = commandData.effectVariant;

    EndpointId endpoint = commandPath.mEndpointId;

    // cmd TriggerEffect
    Identify * identify   = inst(endpoint);
    uint16_t identifyTime = 0;

    ChipLogProgress(Zcl, "RX identify:trigger effect identifier 0x%X variant 0x%X", to_underlying(effectIdentifier),
                    to_underlying(effectVariant));

    if (identify == nullptr)
    {
        return false;
    }

    identify->mTargetEffectIdentifier = effectIdentifier;
    identify->mEffectVariant          = effectVariant;

    /* only call the callback if no identify is in progress */
    if (nullptr != identify->mOnEffectIdentifier && Status::Success == Attributes::IdentifyTime::Get(endpoint, &identifyTime) &&
        0 == identifyTime)
    {
        identify->mCurrentEffectIdentifier = identify->mTargetEffectIdentifier;
        identify->mOnEffectIdentifier(identify);
    }

    commandObj->AddStatus(commandPath, Status::Success);
    return true;
}

Identify::Identify(EndpointId endpoint, onIdentifyStartCb onIdentifyStart, onIdentifyStopCb onIdentifyStop,
                   IdentifyTypeEnum identifyType, onEffectIdentifierCb onEffectIdentifier, EffectIdentifierEnum effectIdentifier,
                   EffectVariantEnum effectVariant) :
    mEndpoint(endpoint),
    mOnIdentifyStart(onIdentifyStart), mOnIdentifyStop(onIdentifyStop), mIdentifyType(identifyType),
    mOnEffectIdentifier(onEffectIdentifier), mCurrentEffectIdentifier(effectIdentifier), mTargetEffectIdentifier(effectIdentifier),
    mEffectVariant(effectVariant)
{
    reg(this);
};

Identify::~Identify()
{
    unreg(this);
}

void MatterIdentifyPluginServerInitCallback() {}
