/**
 * @file
 * @author Steve Karg <skarg@users.sourceforge.net>
 * @date 2007
 * @brief The Load Control Objects from 135-2004-Addendum e
 * @copyright SPDX-License-Identifier: MIT
 */
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* BACnet Stack defines - first */
#include "bacnet/bacdef.h"
/* BACnet Stack API */
#include "bacnet/bacdcode.h"
#include "bacnet/bactext.h"
#include "bacnet/datetime.h"
#include "bacnet/basic/object/lc.h"
#include "bacnet/basic/object/ao.h"
#include "bacnet/wp.h"
#include "bacnet/basic/services.h"
#include "bacnet/basic/sys/debug.h"
#include "bacnet/basic/sys/keylist.h"

/* from Table 12-33. Requested_Shed_Level Default Values and Power Targets */
#define DEFAULT_VALUE_PERCENT 100
#define DEFAULT_VALUE_LEVEL 0
#define DEFAULT_VALUE_AMOUNT 0.0f
/* note: load control objects are required to support LEVEL */

/* minimum interval the load control state machine should process */
#define LOAD_CONTROL_TASK_INTERVAL_MS 1000UL

struct object_data {
    void *Context;
    const char *Object_Name;
    const char *Description;
    /* indicates the current load shedding state of the object */
    BACNET_SHED_STATE Present_Value;
    /* tracking for the Load Control finite state machine */
    BACNET_SHED_STATE Previous_Value;
    /* indicates the desired load shedding */
    BACNET_SHED_LEVEL Requested_Shed_Level;
    /* Indicates the amount of power that the object expects
    to be able to shed in response to a load shed request. */
    BACNET_SHED_LEVEL Expected_Shed_Level;
    /* Indicates the actual amount of power being shed in response
    to a load shed request. */
    BACNET_SHED_LEVEL Actual_Shed_Level;
    /* indicates the start of the duty window in which the load controlled
    by the Load Control object must be compliant with the requested shed. */
    BACNET_DATE_TIME Start_Time;
    BACNET_DATE_TIME End_Time;
    /* indicates the duration of the load shed action,
    starting at Start_Time in minutes */
    uint32_t Shed_Duration;
    /* indicates the time window used for load shed accounting in minutes */
    uint32_t Duty_Window;
    /* indicates and controls whether the Load Control object is
    currently enabled to respond to load shed requests.  */
    bool Load_Control_Enable : 1;
    /* indicates when the object receives a write to any of the properties
    Requested_Shed_Level, Shed_Duration, Duty_Window */
    bool Load_Control_Request_Written : 1;
    /* indicates when the object receives a write to Start_Time */
    bool Start_Time_Property_Written : 1;
    /* optional: indicates the baseline power consumption value
    for the sheddable load controlled by this object,
    if a fixed baseline is used.
    The units of Full_Duty_Baseline are kilowatts.*/
    float Full_Duty_Baseline;
    /* The elements of the Shed Level array are required to be writable,
    allowing local configuration of how this Load Control
    object will participate in load shedding for the
    facility. This array is not required to be resizable
    through BACnet write services. The size of this array
    shall be equal to the size of the Shed_Level_Descriptions
    array. The behavior of this object when the Shed_Levels
    array contains duplicate entries is a local matter. */
    OS_Keylist Shed_Level_List;
    /* the load control manipulates and references
       another object present-value in this device */
    BACNET_OBJECT_TYPE Manipulated_Object_Type;
    uint32_t Manipulated_Object_Instance;
    BACNET_PROPERTY_ID Manipulated_Object_Property;
    uint8_t Priority_For_Writing;
    load_control_manipulated_object_write_callback Manipulated_Object_Write;
    load_control_manipulated_object_relinquish_callback
        Manipulated_Object_Relinquish;
    load_control_manipulated_object_read_callback Manipulated_Object_Read;
    /* state machine task time tracking per object */
    uint32_t Task_Milliseconds;
};
/* Key List for storing the object data sorted by instance number  */
static OS_Keylist Object_List;

/* clang-format off */
/* These three arrays are used by the ReadPropertyMultiple handler */
static const int Load_Control_Properties_Required[] = {
    PROP_OBJECT_IDENTIFIER,
    PROP_OBJECT_NAME,
    PROP_OBJECT_TYPE,
    PROP_PRESENT_VALUE,
    PROP_STATUS_FLAGS,
    PROP_EVENT_STATE,
    PROP_REQUESTED_SHED_LEVEL,
    PROP_START_TIME,
    PROP_SHED_DURATION,
    PROP_DUTY_WINDOW,
    PROP_ENABLE,
    PROP_EXPECTED_SHED_LEVEL,
    PROP_ACTUAL_SHED_LEVEL,
    PROP_SHED_LEVELS,
    PROP_SHED_LEVEL_DESCRIPTIONS,
    -1
};

static const int Load_Control_Properties_Optional[] = {
    PROP_DESCRIPTION,
    PROP_FULL_DUTY_BASELINE,
    -1
};

static const int Load_Control_Properties_Proprietary[] = {
    -1
};
/* clang-format on */

void Load_Control_Property_Lists(
    const int **pRequired, const int **pOptional, const int **pProprietary)
{
    if (pRequired) {
        *pRequired = Load_Control_Properties_Required;
    }
    if (pOptional) {
        *pOptional = Load_Control_Properties_Optional;
    }
    if (pProprietary) {
        *pProprietary = Load_Control_Properties_Proprietary;
    }

    return;
}

/**
 * @brief Gets an object from the list using an instance number as the key
 * @param  object_instance - object-instance number of the object
 * @return object found in the list, or NULL if not found
 */
static struct object_data *Object_Instance_Data(uint32_t object_instance)
{
    return Keylist_Data(Object_List, object_instance);
}

/**
 * @brief Determines if a given object instance is valid
 * @param  object_instance - object-instance number of the object
 * @return  true if the instance is valid, and false if not
 */
bool Load_Control_Valid_Instance(uint32_t object_instance)
{
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        return true;
    }

    return false;
}

/**
 * @brief Determines the number of object instances
 * @return Number of object instances
 */
unsigned Load_Control_Count(void)
{
    return Keylist_Count(Object_List);
}

/* we simply have 0-n object instances.  Yours might be */
/* more complex, and then you need to return the instance */
/* that correlates to the correct index */
uint32_t Load_Control_Index_To_Instance(unsigned index)
{
    uint32_t instance = UINT32_MAX;

    (void)Keylist_Index_Key(Object_List, index, &instance);

    return instance;
}

/**
 * @brief Determines the object instance-number for a given 0..N index
 * of objects where N is the number of objects.
 * @param  index - 0..N where N is the number of objects
 * @return  object instance-number for the given index
 */
unsigned Load_Control_Instance_To_Index(uint32_t object_instance)
{
    return Keylist_Index(Object_List, object_instance);
}

/**
 * @brief For a given object instance-number, read the present-value.
 * @param  object_instance - object-instance number of the object
 * @param  value - Pointer to the new value
 * @return  true if value is within range and copied
 */
BACNET_SHED_STATE Load_Control_Present_Value(uint32_t object_instance)
{
    BACNET_SHED_STATE value = BACNET_SHED_INACTIVE;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        value = pObject->Present_Value;
    }

    return value;
}

/**
 * For a given object instance-number, loads the object-name into
 * a characterstring. Note that the object name must be unique
 * within this device.
 *
 * @param  object_instance - object-instance number of the object
 * @param  object_name - holds the object-name retrieved
 *
 * @return  true if object-name was retrieved
 */
bool Load_Control_Object_Name(
    uint32_t object_instance, BACNET_CHARACTER_STRING *object_name)
{
    bool status = false;
    struct object_data *pObject;
    char name_text[32] = "LOAD_CONTROL-4194303";

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        if (pObject->Object_Name) {
            status =
                characterstring_init_ansi(object_name, pObject->Object_Name);
        } else {
            snprintf(
                name_text, sizeof(name_text), "LOAD_CONTROL-%lu",
                (unsigned long)object_instance);
            status = characterstring_init_ansi(object_name, name_text);
        }
    }

    return status;
}

/**
 * @brief For a given object instance-number, sets the object-name
 * @param  object_instance - object-instance number of the object
 * @param  new_name - holds the object-name to be set
 * @return  true if object-name was set
 */
bool Load_Control_Name_Set(uint32_t object_instance, const char *new_name)
{
    bool status = false;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        status = true;
        pObject->Object_Name = new_name;
    }

    return status;
}

/**
 * @brief Return the object name C string
 * @param object_instance [in] BACnet object instance number
 * @return object name or NULL if not found
 */
const char *Load_Control_Name_ASCII(uint32_t object_instance)
{
    const char *name = NULL;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        name = pObject->Object_Name;
    }

    return name;
}

/**
 * @brief convert the shed level request into a percentage of full duty
 * baseline power
 * @param pObject - object instance to get
 * @return the requested shed level as a percentage of full duty baseline
 */
static float Requested_Shed_Level_Value(struct object_data *pObject)
{
    float requested_level = 0.0f;
    struct shed_level_data *shed_level;
    unsigned shed_level_max = 0, i;
    KEY key;

    switch (pObject->Requested_Shed_Level.type) {
        case BACNET_SHED_TYPE_PERCENT:
            /* (current baseline) * Requested_Shed_Level / 100 */
            requested_level =
                (float)pObject->Requested_Shed_Level.value.percent;
            break;
        case BACNET_SHED_TYPE_AMOUNT:
            /* (current baseline) - Requested_Shed_Level */
            requested_level = pObject->Full_Duty_Baseline -
                pObject->Requested_Shed_Level.value.amount;
            requested_level /= pObject->Full_Duty_Baseline;
            requested_level *= 100.0f;
            break;
        case BACNET_SHED_TYPE_LEVEL:
        default:
            shed_level = Keylist_Data(
                pObject->Shed_Level_List,
                pObject->Requested_Shed_Level.value.level);
            if (shed_level) {
                requested_level = shed_level->Value;
            } else {
                /* If the Load Control object is commanded to go to a level
                    that is not in the Shed_Levels array, it shall go to the
                    Shed_Level whose entry in the Shed_Levels array has the
                    nearest numerically lower value.*/
                /* get the numerically lowest */
                shed_level = Keylist_Data_Index(pObject->Shed_Level_List, 0);
                /* find the nearest */
                shed_level_max = Keylist_Count(pObject->Shed_Level_List);
                for (i = 0; i < shed_level_max; i++) {
                    if (Keylist_Index_Key(pObject->Shed_Level_List, i, &key)) {
                        if (key <= pObject->Requested_Shed_Level.value.level) {
                            shed_level =
                                Keylist_Data_Index(pObject->Shed_Level_List, i);
                        }
                    }
                }
                if (shed_level) {
                    requested_level = shed_level->Value;
                } else {
                    /* no level found so use 100% of baseline (no shed) */
                    requested_level = 100.0f;
                }
            }
            break;
    }

    return requested_level;
}

/**
 * @brief Copy the Shed Level data from source to destination
 * @param dest - destination data
 * @param src - source data
 */
static void
Shed_Level_Copy(BACNET_SHED_LEVEL *dest, const BACNET_SHED_LEVEL *src)
{
    if (dest && src) {
        dest->type = src->type;
        switch (src->type) {
            case BACNET_SHED_TYPE_PERCENT:
                dest->value.percent = src->value.percent;
                break;
            case BACNET_SHED_TYPE_AMOUNT:
                dest->value.amount = src->value.amount;
                break;
            case BACNET_SHED_TYPE_LEVEL:
            default:
                dest->value.level = src->value.level;
                break;
        }
    }
}

/**
 * @brief Set the Shed Level data to the default value
 * @param dest - destination data
 * @param type - type of shed level
 */
static void
Shed_Level_Default_Set(BACNET_SHED_LEVEL *dest, BACNET_SHED_LEVEL_TYPE type)
{
    if (dest) {
        dest->type = type;
        switch (type) {
            case BACNET_SHED_TYPE_PERCENT:
                dest->value.percent = 100;
                break;
            case BACNET_SHED_TYPE_AMOUNT:
                dest->value.amount = 0.0f;
                break;
            case BACNET_SHED_TYPE_LEVEL:
            default:
                dest->value.level = 0;
                break;
        }
    }
}

/**
 * @brief Determine if the object can meet the shed request
 * @param pObject - object instance to get
 * @return true if the object can meet the shed request
 */
static bool Able_To_Meet_Shed_Request(struct object_data *pObject)
{
    float level = 0.0f;
    float requested_level = 0.0f;
    uint8_t priority = 0;
    bool status = false;

    if (pObject->Manipulated_Object_Read) {
        pObject->Manipulated_Object_Read(
            pObject->Manipulated_Object_Type,
            pObject->Manipulated_Object_Instance,
            pObject->Manipulated_Object_Property, &priority, &level);
        requested_level = Requested_Shed_Level_Value(pObject);
        if (level >= requested_level) {
            status = true;
        }
    }
    if (status) {
        status = false;
        /* can we control the output? */
        if (priority >= pObject->Priority_For_Writing) {
            /* is the level able to be lowered? */
            requested_level = Requested_Shed_Level_Value(pObject);
            if (level >= requested_level) {
                status = true;
            }
        }
    }

    return status;
}

/**
 * @brief Load Control State Machine
 * @param object_index - object index in the list
 * @param bdatetime - current date and time
 */
void Load_Control_State_Machine(
    int object_index, const BACNET_DATE_TIME *bdatetime)
{
    int diff = 0; /* used for datetime comparison */
    float amount;
    unsigned percent;
    unsigned level;
    struct object_data *pObject;

    pObject = Keylist_Data_Index(Object_List, object_index);
    if (!pObject) {
        return;
    }
    /* is the state machine enabled? */
    if (!pObject->Load_Control_Enable) {
        pObject->Present_Value = BACNET_SHED_INACTIVE;
        return;
    }

    switch (pObject->Present_Value) {
        case BACNET_SHED_REQUEST_PENDING:
            if (pObject->Load_Control_Request_Written) {
                pObject->Load_Control_Request_Written = false;
                /* request to cancel using default values? */
                switch (pObject->Requested_Shed_Level.type) {
                    case BACNET_SHED_TYPE_PERCENT:
                        percent = pObject->Requested_Shed_Level.value.percent;
                        if (percent == DEFAULT_VALUE_PERCENT) {
                            pObject->Present_Value = BACNET_SHED_INACTIVE;
                        }
                        break;
                    case BACNET_SHED_TYPE_AMOUNT:
                        amount = pObject->Requested_Shed_Level.value.amount;
                        if (islessequal(amount, DEFAULT_VALUE_AMOUNT)) {
                            pObject->Present_Value = BACNET_SHED_INACTIVE;
                        }
                        break;
                    case BACNET_SHED_TYPE_LEVEL:
                    default:
                        level = pObject->Requested_Shed_Level.value.level;
                        if (level == DEFAULT_VALUE_LEVEL) {
                            pObject->Present_Value = BACNET_SHED_INACTIVE;
                        }
                        break;
                }
                if (pObject->Present_Value == BACNET_SHED_INACTIVE) {
                    debug_printf(
                        "Load Control[%d]:Requested Shed Level=Default\n",
                        object_index);
                    break;
                }
            }
            /* clear the flag for Start time if it is written */
            if (pObject->Start_Time_Property_Written) {
                pObject->Start_Time_Property_Written = false;
                /* request to cancel using wildcards in start time? */
                if (datetime_wildcard(&pObject->Start_Time)) {
                    pObject->Present_Value = BACNET_SHED_INACTIVE;
                    debug_printf(
                        "Load Control[%d]:Start Time=Wildcard\n", object_index);
                    break;
                }
            }
            /* cancel because current time is after start time + duration? */
            datetime_copy(&pObject->End_Time, &pObject->Start_Time);
            datetime_add_minutes(&pObject->End_Time, pObject->Shed_Duration);
            diff = datetime_compare(&pObject->End_Time, bdatetime);
            if (diff < 0) {
                /* CancelShed */
                /* FIXME: stop shedding! i.e. relinquish */
                debug_printf(
                    "Load Control[%d]:Current Time"
                    " is after Start Time + Duration\n",
                    object_index);
                pObject->Present_Value = BACNET_SHED_INACTIVE;
                break;
            }
            diff = datetime_compare(bdatetime, &pObject->Start_Time);
            if (diff < 0) {
                /* current time prior to start time */
                /* ReconfigurePending */
                Shed_Level_Copy(
                    &pObject->Expected_Shed_Level,
                    &pObject->Requested_Shed_Level);
                Shed_Level_Default_Set(
                    &pObject->Actual_Shed_Level,
                    pObject->Requested_Shed_Level.type);
            } else if (diff > 0) {
                /* current time after to start time */
                debug_printf(
                    "Load Control[%d]:Current Time"
                    " is after Start Time\n",
                    object_index);
                /* AbleToMeetShed */
                if (Able_To_Meet_Shed_Request(pObject)) {
                    Shed_Level_Copy(
                        &pObject->Expected_Shed_Level,
                        &pObject->Requested_Shed_Level);
                    if (pObject->Manipulated_Object_Write) {
                        pObject->Manipulated_Object_Write(
                            pObject->Manipulated_Object_Type,
                            pObject->Manipulated_Object_Instance,
                            pObject->Manipulated_Object_Property,
                            pObject->Priority_For_Writing,
                            Requested_Shed_Level_Value(pObject));
                    }
                    Shed_Level_Copy(
                        &pObject->Actual_Shed_Level,
                        &pObject->Requested_Shed_Level);
                    pObject->Present_Value = BACNET_SHED_COMPLIANT;
                } else {
                    /* CannotMeetShed */
                    Shed_Level_Default_Set(
                        &pObject->Expected_Shed_Level,
                        pObject->Requested_Shed_Level.type);
                    Shed_Level_Default_Set(
                        &pObject->Actual_Shed_Level,
                        pObject->Requested_Shed_Level.type);
                    pObject->Present_Value = BACNET_SHED_NON_COMPLIANT;
                }
            }
            break;
        case BACNET_SHED_NON_COMPLIANT:
            datetime_copy(&pObject->End_Time, &pObject->Start_Time);
            datetime_add_minutes(&pObject->End_Time, pObject->Shed_Duration);
            diff = datetime_compare(&pObject->End_Time, bdatetime);
            if (diff < 0) {
                /* FinishedUnsuccessfulShed */
                debug_printf(
                    "Load Control[%d]:Current Time is after Start Time + "
                    "Duration\n",
                    object_index);
                pObject->Present_Value = BACNET_SHED_INACTIVE;
                break;
            }
            if (pObject->Load_Control_Request_Written ||
                pObject->Start_Time_Property_Written) {
                /* UnsuccessfulShedReconfigured */
                debug_printf(
                    "Load Control[%d]:Control Property written\n",
                    object_index);
                /* The Written flags will cleared in the next state */
                pObject->Present_Value = BACNET_SHED_REQUEST_PENDING;
                break;
            }
            if (Able_To_Meet_Shed_Request(pObject)) {
                /* CanNowComplyWithShed */
                debug_printf(
                    "Load Control[%d]:Able to meet Shed Request\n",
                    object_index);
                Shed_Level_Copy(
                    &pObject->Expected_Shed_Level,
                    &pObject->Requested_Shed_Level);
                if (pObject->Manipulated_Object_Write) {
                    pObject->Manipulated_Object_Write(
                        pObject->Manipulated_Object_Type,
                        pObject->Manipulated_Object_Instance,
                        pObject->Manipulated_Object_Property,
                        pObject->Priority_For_Writing,
                        Requested_Shed_Level_Value(pObject));
                }
                Shed_Level_Copy(
                    &pObject->Actual_Shed_Level,
                    &pObject->Requested_Shed_Level);
                pObject->Present_Value = BACNET_SHED_COMPLIANT;
            }
            break;
        case BACNET_SHED_COMPLIANT:
            datetime_copy(&pObject->End_Time, &pObject->Start_Time);
            datetime_add_minutes(&pObject->End_Time, pObject->Shed_Duration);
            diff = datetime_compare(&pObject->End_Time, bdatetime);
            if (diff < 0) {
                /* FinishedSuccessfulShed */
                debug_printf(
                    "Load Control[%d]:Current Time is after Start Time + "
                    "Duration\n",
                    object_index);
                datetime_wildcard_set(&pObject->Start_Time);
                if (pObject->Manipulated_Object_Relinquish) {
                    pObject->Manipulated_Object_Relinquish(
                        pObject->Manipulated_Object_Type,
                        pObject->Manipulated_Object_Instance,
                        pObject->Manipulated_Object_Property,
                        pObject->Priority_For_Writing);
                }
                pObject->Present_Value = BACNET_SHED_INACTIVE;
                break;
            }
            if (pObject->Load_Control_Request_Written ||
                pObject->Start_Time_Property_Written) {
                /* UnsuccessfulShedReconfigured */
                debug_printf(
                    "Load Control[%d]:Control Property written\n",
                    object_index);
                /* The Written flags will cleared in the next state */
                pObject->Present_Value = BACNET_SHED_REQUEST_PENDING;
                break;
            }
            if (!Able_To_Meet_Shed_Request(pObject)) {
                /* CanNoLongerComplyWithShed */
                debug_printf(
                    "Load Control[%d]:Not able to meet Shed Request\n",
                    object_index);
                Shed_Level_Default_Set(
                    &pObject->Expected_Shed_Level,
                    pObject->Requested_Shed_Level.type);
                Shed_Level_Default_Set(
                    &pObject->Actual_Shed_Level,
                    pObject->Requested_Shed_Level.type);
                pObject->Present_Value = BACNET_SHED_NON_COMPLIANT;
            }
            break;
        case BACNET_SHED_INACTIVE:
        default:
            if (pObject->Start_Time_Property_Written) {
                debug_printf(
                    "Load Control[%d]:Start Time written\n", object_index);
                /* The Written flag will cleared in the next state */
                Shed_Level_Copy(
                    &pObject->Expected_Shed_Level,
                    &pObject->Requested_Shed_Level);
                Shed_Level_Default_Set(
                    &pObject->Actual_Shed_Level,
                    pObject->Requested_Shed_Level.type);
                pObject->Present_Value = BACNET_SHED_REQUEST_PENDING;
            }
            break;
    }

    return;
}

/**
 * @brief Load Control State Machine Handler
 * @param object_instance - object-instance number of the object
 * @param milliseconds - elapsed time in milliseconds from last call
 */
void Load_Control_Timer(uint32_t object_instance, uint16_t milliseconds)
{
    BACNET_DATE_TIME bdatetime = { 0 };
    struct object_data *pObject;
    int index = 0;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        pObject->Task_Milliseconds += milliseconds;
        if (pObject->Task_Milliseconds >= LOAD_CONTROL_TASK_INTERVAL_MS) {
            pObject->Task_Milliseconds = 0;
            datetime_local(&bdatetime.date, &bdatetime.time, NULL, NULL);
            index = Keylist_Index(Object_List, object_instance);
            Load_Control_State_Machine(index, &bdatetime);
            if (pObject->Present_Value != pObject->Previous_Value) {
                debug_printf(
                    "Load Control[%d]=%s\n", index,
                    bactext_shed_state_name(pObject->Present_Value));
                pObject->Previous_Value = pObject->Present_Value;
            }
        }
    }
}

/**
 * @brief Load Control State Machine Handler
 * @note call every #LOAD_CONTROL_TASK_INTERVAL_MS milliseconds
 * @deprecated Use Load_Control_Timer() instead
 */
void Load_Control_State_Machine_Handler(void)
{
    unsigned count, index;
    uint32_t object_instance;

    count = Keylist_Count(Object_List);
    while (count) {
        count--;
        index = count;
        object_instance = Load_Control_Index_To_Instance(index);
        Load_Control_Timer(object_instance, LOAD_CONTROL_TASK_INTERVAL_MS);
    }
}

/**
 * @brief Get the priority for writing to the Manipulated Variable.
 * @param object_instance [in] The object instance number.
 */
unsigned Load_Control_Priority_For_Writing(uint32_t object_instance)
{
    unsigned priority = 0;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        priority = pObject->Priority_For_Writing;
    }

    return priority;
}

/**
 * @brief Set the priority for writing to the Manipulated Variable.
 * @param object_instance [in] The object instance number.
 * @param priority [in] The priority for writing.
 * @return True if successful, else False.
 */
bool Load_Control_Priority_For_Writing_Set(
    uint32_t object_instance, unsigned priority)
{
    bool status = false;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        pObject->Priority_For_Writing = priority;
        status = true;
    }

    return status;
}

/**
 * @brief Get the Manipulated Variable Reference for the Load Control object.
 * @param object_instance [in] The object instance number.
 * @param object_property_reference [out] The object property reference.
 * @return True if successful, else False.
 */
bool Load_Control_Manipulated_Variable_Reference(
    uint32_t object_instance,
    BACNET_OBJECT_PROPERTY_REFERENCE *object_property_reference)
{
    bool status = false;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        if (object_property_reference) {
            object_property_reference->object_identifier.type =
                pObject->Manipulated_Object_Type;
            object_property_reference->object_identifier.instance =
                pObject->Manipulated_Object_Instance;
            object_property_reference->property_identifier =
                pObject->Manipulated_Object_Property;
            status = true;
        }
    }

    return status;
}

/**
 * @brief Set the Manipulated Variable Reference for the Load Control object.
 * @param object_instance [in] The object instance number.
 * @param object_id [in] The object identifier.
 * @return True if successful, else False.
 */
bool Load_Control_Manipulated_Variable_Reference_Set(
    uint32_t object_instance,
    const BACNET_OBJECT_PROPERTY_REFERENCE *object_property_reference)
{
    bool status = false;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        if (object_property_reference) {
            pObject->Manipulated_Object_Type =
                object_property_reference->object_identifier.type;
            pObject->Manipulated_Object_Instance =
                object_property_reference->object_identifier.instance;
            pObject->Manipulated_Object_Property =
                object_property_reference->property_identifier;
            status = true;
        }
    }

    return status;
}

/**
 * @brief Encode a BACnetARRAY property element
 * @param object_instance [in] BACnet object instance number
 * @param index [in] array index requested:
 *    0 to N for individual array members
 * @param apdu [out] Buffer in which the APDU contents are built, or NULL to
 * return the length of buffer if it had been built
 * @return The length of the apdu encoded or
 *   BACNET_STATUS_ERROR for ERROR_CODE_INVALID_ARRAY_INDEX
 */
static int Load_Control_Shed_Levels_Encode(
    uint32_t object_instance, BACNET_ARRAY_INDEX index, uint8_t *apdu)
{
    int apdu_len = 0, len = 0;
    struct shed_level_data *entry;
    BACNET_UNSIGNED_INTEGER unsigned_value;
    KEY key;
    int count;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        return BACNET_STATUS_ERROR;
    }
    count = Keylist_Count(pObject->Shed_Level_List);
    if (index >= count) {
        return BACNET_STATUS_ERROR;
    }
    key = index + 1;
    entry = Keylist_Data(pObject->Shed_Level_List, key);
    if (entry) {
        unsigned_value = key;
        len = encode_application_unsigned(apdu, unsigned_value);
        apdu_len += len;
    } else {
        apdu_len = BACNET_STATUS_ERROR;
    }

    return apdu_len;
}

/**
 * @brief Encode a BACnetARRAY property element
 * @param object_instance [in] BACnet object instance number
 * @param index [in] array index requested:
 *    0 to N for individual array members
 * @param apdu [out] Buffer in which the APDU contents are built, or NULL to
 * return the length of buffer if it had been built
 * @return The length of the apdu encoded or
 *   BACNET_STATUS_ERROR for ERROR_CODE_INVALID_ARRAY_INDEX
 */
static int Load_Control_Shed_Level_Descriptions_Encode(
    uint32_t object_instance, BACNET_ARRAY_INDEX index, uint8_t *apdu)
{
    int apdu_len = 0, len = 0;
    struct shed_level_data *entry;
    BACNET_CHARACTER_STRING char_string;
    KEY key;
    int count;
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        return BACNET_STATUS_ERROR;
    }
    count = Keylist_Count(pObject->Shed_Level_List);
    if (index >= count) {
        return BACNET_STATUS_ERROR;
    }
    key = index + 1;
    entry = Keylist_Data(pObject->Shed_Level_List, key);
    if (entry) {
        characterstring_init_ansi(&char_string, entry->Description);
        len = encode_application_character_string(apdu, &char_string);
        apdu_len += len;
    } else {
        apdu_len = BACNET_STATUS_ERROR;
    }

    return apdu_len;
}

/**
 * @brief Encode the BACnetShedLevel property
 * @param apdu [out] Buffer in which the APDU contents are built
 * @param value [in] The value to encode
 * @return The length of the apdu encoded
 */
static int
BACnet_Shed_Level_Encode(uint8_t *apdu, const BACNET_SHED_LEVEL *value)
{
    int apdu_len = 0;

    if (!value) {
        return 0;
    }
    switch (value->type) {
        case BACNET_SHED_TYPE_PERCENT:
            apdu_len = encode_context_unsigned(apdu, 0, value->value.percent);
            break;
        case BACNET_SHED_TYPE_AMOUNT:
            apdu_len = encode_context_real(apdu, 2, value->value.amount);
            break;
        case BACNET_SHED_TYPE_LEVEL:
        default:
            apdu_len = encode_context_unsigned(apdu, 1, value->value.level);
            break;
    }

    return apdu_len;
}

/**
 * @brief ReadProperty handler for this object.  For the given ReadProperty
 * data, the application_data is loaded or the error flags are set.
 * @param  rpdata - BACNET_READ_PROPERTY_DATA data, including
 * requested data and space for the reply, or error response.
 * @return number of APDU bytes in the response, or
 * BACNET_STATUS_ERROR on error.
 */
int Load_Control_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata)
{
    int len = 0;
    int apdu_len = 0; /* return value */
    BACNET_BIT_STRING bit_string;
    BACNET_CHARACTER_STRING char_string;
    int enumeration = 0;
    unsigned count = 0;
    bool state = false;
    uint8_t *apdu = NULL;
    int apdu_size = 0;
    struct object_data *pObject;

    if ((rpdata == NULL) || (rpdata->application_data == NULL) ||
        (rpdata->application_data_len == 0)) {
        return 0;
    }
    pObject = Object_Instance_Data(rpdata->object_instance);
    if (pObject == NULL) {
        rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return BACNET_STATUS_ERROR;
    }
    apdu = rpdata->application_data;
    apdu_size = rpdata->application_data_len;
    switch (rpdata->object_property) {
        case PROP_OBJECT_IDENTIFIER:
            apdu_len = encode_application_object_id(
                &apdu[0], OBJECT_LOAD_CONTROL, rpdata->object_instance);
            break;
        case PROP_OBJECT_NAME:
        case PROP_DESCRIPTION:
            Load_Control_Object_Name(rpdata->object_instance, &char_string);
            apdu_len =
                encode_application_character_string(&apdu[0], &char_string);
            break;
        case PROP_OBJECT_TYPE:
            apdu_len =
                encode_application_enumerated(&apdu[0], OBJECT_LOAD_CONTROL);
            break;
        case PROP_PRESENT_VALUE:
            enumeration = Load_Control_Present_Value(rpdata->object_instance);
            apdu_len = encode_application_enumerated(&apdu[0], enumeration);
            break;
        case PROP_STATUS_FLAGS:
            bitstring_init(&bit_string);
            /* IN_ALARM - Logical FALSE (0) if the Event_State property
               has a value of NORMAL, otherwise logical TRUE (1). */
            bitstring_set_bit(&bit_string, STATUS_FLAG_IN_ALARM, false);
            /* FAULT - Logical      TRUE (1) if the Reliability property is
               present and does not have a value of NO_FAULT_DETECTED,
               otherwise logical FALSE (0). */
            bitstring_set_bit(&bit_string, STATUS_FLAG_FAULT, false);
            /* OVERRIDDEN - Logical TRUE (1) if the point has been
               overridden by some mechanism local to the BACnet Device,
               otherwise logical FALSE (0). */
            bitstring_set_bit(&bit_string, STATUS_FLAG_OVERRIDDEN, false);
            /* OUT_OF_SERVICE - This bit shall always be Logical FALSE (0). */
            bitstring_set_bit(&bit_string, STATUS_FLAG_OUT_OF_SERVICE, false);
            apdu_len = encode_application_bitstring(&apdu[0], &bit_string);
            break;
        case PROP_EVENT_STATE:
            apdu_len =
                encode_application_enumerated(&apdu[0], EVENT_STATE_NORMAL);
            break;
        case PROP_REQUESTED_SHED_LEVEL:
            apdu_len =
                BACnet_Shed_Level_Encode(apdu, &pObject->Requested_Shed_Level);
            break;
        case PROP_START_TIME:
            len = encode_application_date(&apdu[0], &pObject->Start_Time.date);
            apdu_len = len;
            len = encode_application_time(
                &apdu[apdu_len], &pObject->Start_Time.time);
            apdu_len += len;
            break;
        case PROP_SHED_DURATION:
            apdu_len =
                encode_application_unsigned(&apdu[0], pObject->Shed_Duration);
            break;
        case PROP_DUTY_WINDOW:
            apdu_len =
                encode_application_unsigned(&apdu[0], pObject->Duty_Window);
            break;
        case PROP_ENABLE:
            state = pObject->Load_Control_Enable;
            apdu_len = encode_application_boolean(&apdu[0], state);
            break;
        case PROP_FULL_DUTY_BASELINE: /* optional */
            apdu_len =
                encode_application_real(&apdu[0], pObject->Full_Duty_Baseline);
            break;
        case PROP_EXPECTED_SHED_LEVEL:
            apdu_len =
                BACnet_Shed_Level_Encode(apdu, &pObject->Expected_Shed_Level);
            break;
        case PROP_ACTUAL_SHED_LEVEL:
            apdu_len =
                BACnet_Shed_Level_Encode(apdu, &pObject->Actual_Shed_Level);
            break;
        case PROP_SHED_LEVELS:
            count = Keylist_Count(pObject->Shed_Level_List);
            apdu_len = bacnet_array_encode(
                rpdata->object_instance, rpdata->array_index,
                Load_Control_Shed_Levels_Encode, count, apdu, apdu_size);
            if (apdu_len == BACNET_STATUS_ABORT) {
                rpdata->error_code =
                    ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED;
            } else if (apdu_len == BACNET_STATUS_ERROR) {
                rpdata->error_class = ERROR_CLASS_PROPERTY;
                rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX;
            }
            break;
        case PROP_SHED_LEVEL_DESCRIPTIONS:
            count = Keylist_Count(pObject->Shed_Level_List);
            apdu_len = bacnet_array_encode(
                rpdata->object_instance, rpdata->array_index,
                Load_Control_Shed_Level_Descriptions_Encode, count, apdu,
                apdu_size);
            if (apdu_len == BACNET_STATUS_ABORT) {
                rpdata->error_code =
                    ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED;
            } else if (apdu_len == BACNET_STATUS_ERROR) {
                rpdata->error_class = ERROR_CLASS_PROPERTY;
                rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX;
            }
            break;
        default:
            rpdata->error_class = ERROR_CLASS_PROPERTY;
            rpdata->error_code = ERROR_CODE_UNKNOWN_PROPERTY;
            apdu_len = BACNET_STATUS_ERROR;
            break;
    }

    return apdu_len;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  object_instance - object-instance number of the object
 * @param  value - property value to be written
 * @param  priority - priority-array index value 1..16
 * @param  error_class - the BACnet error class
 * @param  error_code - BACnet Error code
 * @return  true if values are within range and property is set.
 */
static bool Load_Control_Requested_Shed_Level_Write(
    uint32_t object_instance,
    const BACNET_SHED_LEVEL *value,
    uint8_t priority,
    BACNET_ERROR_CLASS *error_class,
    BACNET_ERROR_CODE *error_code)
{
    bool status = false;
    struct object_data *pObject;
    int count, index;
    KEY key = 0;

    (void)priority;
    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        *error_class = ERROR_CLASS_OBJECT;
        *error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    switch (value->type) {
        case BACNET_SHED_TYPE_PERCENT:
            if (value->value.percent <= 100) {
                pObject->Requested_Shed_Level.type = value->type;
                pObject->Requested_Shed_Level.value.percent =
                    value->value.percent;
                pObject->Load_Control_Request_Written = true;
                status = true;
            } else {
                *error_class = ERROR_CLASS_PROPERTY;
                *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
            }
            break;
        case BACNET_SHED_TYPE_AMOUNT:
            if (value->value.amount >= 0.0f) {
                pObject->Requested_Shed_Level.type = value->type;
                pObject->Requested_Shed_Level.value.amount =
                    value->value.amount;
                pObject->Load_Control_Request_Written = true;
                status = true;
            } else {
                *error_class = ERROR_CLASS_PROPERTY;
                *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
            }
            break;
        case BACNET_SHED_TYPE_LEVEL:
            /* can be 0 (default) or any value <= largest level */
            if (value->value.level == 0) {
                pObject->Requested_Shed_Level.type = value->type;
                pObject->Requested_Shed_Level.value.level = value->value.level;
                pObject->Load_Control_Request_Written = true;
                status = true;
            } else {
                count = Keylist_Count(pObject->Shed_Level_List);
                if (count > 0) {
                    /* keylist is sorted by key,
                       so the last index should be the largest key value */
                    index = count - 1;
                    if (Keylist_Index_Key(
                            pObject->Shed_Level_List, index, &key) &&
                        (value->value.level <= key)) {
                        pObject->Requested_Shed_Level.type = value->type;
                        pObject->Requested_Shed_Level.value.level =
                            value->value.level;
                        pObject->Load_Control_Request_Written = true;
                        status = true;
                    } else {
                        *error_class = ERROR_CLASS_PROPERTY;
                        *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
                    }
                } else {
                    *error_class = ERROR_CLASS_PROPERTY;
                    *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
                }
            }
            break;
        default:
            *error_class = ERROR_CLASS_PROPERTY;
            *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
            break;
    }

    return status;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  object_instance - object-instance number of the object
 * @param  value - property value to be written
 * @param  priority - priority-array index value 1..16
 * @param  error_class - the BACnet error class
 * @param  error_code - BACnet Error code
 * @return  true if values are within range and property is set.
 */
static bool Load_Control_Start_Time_Write(
    uint32_t object_instance,
    const BACNET_DATE_TIME *value,
    uint8_t priority,
    BACNET_ERROR_CLASS *error_class,
    BACNET_ERROR_CODE *error_code)
{
    bool status = false;
    struct object_data *pObject;

    (void)priority;
    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        *error_class = ERROR_CLASS_OBJECT;
        *error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    /* Write time and date and set written flag */
    datetime_copy_date(&pObject->Start_Time.date, &value->date);
    datetime_copy_time(&pObject->Start_Time.time, &value->time);
    pObject->Start_Time_Property_Written = true;
    status = true;

    return status;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  object_instance - object-instance number of the object
 * @param  value - property value to be written
 * @param  priority - priority-array index value 1..16
 * @param  error_class - the BACnet error class
 * @param  error_code - BACnet Error code
 * @return  true if values are within range and property is set.
 */
static bool Load_Control_Shed_Duration_Write(
    uint32_t object_instance,
    BACNET_UNSIGNED_INTEGER value,
    uint8_t priority,
    BACNET_ERROR_CLASS *error_class,
    BACNET_ERROR_CODE *error_code)
{
    struct object_data *pObject;

    (void)priority;
    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        *error_class = ERROR_CLASS_OBJECT;
        *error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    /* limited in our object to int32_t to work with datetime utility */
    if (value > INT32_MAX) {
        *error_class = ERROR_CLASS_PROPERTY;
        *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
        return false;
    }
    pObject->Shed_Duration = (uint32_t)value;
    pObject->Load_Control_Request_Written = true;

    return true;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  object_instance - object-instance number of the object
 * @param  value - property value to be written
 * @param  priority - priority-array index value 1..16
 * @param  error_class - the BACnet error class
 * @param  error_code - BACnet Error code
 * @return  true if values are within range and property is set.
 */
static bool Load_Control_Duty_Window_Write(
    uint32_t object_instance,
    BACNET_UNSIGNED_INTEGER value,
    uint8_t priority,
    BACNET_ERROR_CLASS *error_class,
    BACNET_ERROR_CODE *error_code)
{
    struct object_data *pObject;

    (void)priority;
    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        *error_class = ERROR_CLASS_OBJECT;
        *error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    /* limited in our object to int32_t to work with datetime utility */
    if (value > INT32_MAX) {
        *error_class = ERROR_CLASS_PROPERTY;
        *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
        return false;
    }
    pObject->Duty_Window = (uint32_t)value;
    pObject->Load_Control_Request_Written = true;

    return true;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  wp_data - BACNET_WRITE_PROPERTY_DATA data, including
 * requested data and space for the reply, or error response.
 *
 * @return false if an error is loaded, true if no errors
 */
static bool Load_Control_Shed_Levels_Write(BACNET_WRITE_PROPERTY_DATA *wp_data)
{
    struct object_data *pObject;
    BACNET_UNSIGNED_INTEGER unsigned_value;
    struct shed_level_data *entry;
    int len = 0, index = 0, count = 0, apdu_len = 0, apdu_size = 0;
    KEY key;
    uint8_t *apdu;

    pObject = Object_Instance_Data(wp_data->object_instance);
    if (!pObject) {
        wp_data->error_class = ERROR_CLASS_OBJECT;
        wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    count = Keylist_Count(pObject->Shed_Level_List);
    if (wp_data->array_index == 0) {
        /* This array is not required to be resizable
            through BACnet write services */
        wp_data->error_class = ERROR_CLASS_PROPERTY;
        wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED;
        return false;
    } else if (wp_data->array_index == BACNET_ARRAY_ALL) {
        /* The size of this array shall be equal to the
            size of the Shed_Level_Descriptions array.*/
        /* will the array elements sent fit in the whole array? */
        apdu = wp_data->application_data;
        apdu_size = wp_data->application_data_len;
        while (count > 0) {
            len = bacnet_unsigned_application_decode(
                &apdu[apdu_len], apdu_size - apdu_len, &unsigned_value);
            if (len > 0) {
                if (unsigned_value > UINT32_MAX) {
                    wp_data->error_class = ERROR_CLASS_PROPERTY;
                    wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
                    return false;
                }
                apdu_len += len;
            } else {
                wp_data->error_class = ERROR_CLASS_PROPERTY;
                wp_data->error_code = ERROR_CODE_INVALID_DATA_TYPE;
                return false;
            }
            count--;
        }
        if (apdu_len != wp_data->application_data_len) {
            wp_data->error_class = ERROR_CLASS_PROPERTY;
            wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
            return false;
        }
        /* write entire array - we know the size and values are valid */
        count = Keylist_Count(pObject->Shed_Level_List);
        apdu = wp_data->application_data;
        apdu_size = wp_data->application_data_len;
        while (count > 0) {
            len = bacnet_unsigned_application_decode(
                &apdu[apdu_len], apdu_size - apdu_len, &unsigned_value);
            if (len > 0) {
                apdu_len += len;
                if (unsigned_value <= UINT32_MAX) {
                    index = count - 1;
                    entry = Keylist_Data_Delete_By_Index(
                        pObject->Shed_Level_List, index);
                    key = (uint32_t)unsigned_value;
                    index =
                        Keylist_Data_Add(pObject->Shed_Level_List, key, entry);
                    if (index < 0) {
                        wp_data->error_class = ERROR_CLASS_PROPERTY;
                        wp_data->error_code =
                            ERROR_CODE_NO_SPACE_TO_WRITE_PROPERTY;
                        return false;
                    }
                } else {
                    wp_data->error_class = ERROR_CLASS_PROPERTY;
                    wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
                    return false;
                }
            }
            count--;
        }
    } else if (wp_data->array_index <= count) {
        len = bacnet_unsigned_application_decode(
            wp_data->application_data, wp_data->application_data_len,
            &unsigned_value);
        if (len > 0) {
            if (unsigned_value <= UINT32_MAX) {
                index = wp_data->array_index - 1;
                entry = Keylist_Data_Delete_By_Index(
                    pObject->Shed_Level_List, index);
                key = (uint32_t)unsigned_value;
                index = Keylist_Data_Add(pObject->Shed_Level_List, key, entry);
                if (index < 0) {
                    wp_data->error_class = ERROR_CLASS_PROPERTY;
                    wp_data->error_code = ERROR_CODE_NO_SPACE_TO_WRITE_PROPERTY;
                    return false;
                }

            } else {
                wp_data->error_class = ERROR_CLASS_PROPERTY;
                wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
                return false;
            }
        } else {
            wp_data->error_class = ERROR_CLASS_PROPERTY;
            wp_data->error_code = ERROR_CODE_INVALID_DATA_TYPE;
            return false;
        }
    } else {
        wp_data->error_class = ERROR_CLASS_PROPERTY;
        wp_data->error_code = ERROR_CODE_INVALID_ARRAY_INDEX;
        return false;
    }

    return true;
}

/**
 * @brief For a given object instance-number, writes to the property value
 * @param  object_instance - object-instance number of the object
 * @param  value - property value to be written
 * @param  priority - priority-array index value 1..16
 * @param  error_class - the BACnet error class
 * @param  error_code - BACnet Error code
 * @return  true if values are within range and property is set.
 */
static bool Load_Control_Enable_Write(
    uint32_t object_instance,
    bool value,
    uint8_t priority,
    BACNET_ERROR_CLASS *error_class,
    BACNET_ERROR_CODE *error_code)
{
    struct object_data *pObject;

    (void)priority;
    pObject = Object_Instance_Data(object_instance);
    if (!pObject) {
        *error_class = ERROR_CLASS_OBJECT;
        *error_code = ERROR_CODE_UNKNOWN_OBJECT;
        return false;
    }
    pObject->Load_Control_Enable = value;

    return true;
}

/**
 * WriteProperty handler for this object.  For the given WriteProperty
 * data, the application_data is loaded or the error flags are set.
 *
 * @param  wp_data - BACNET_WRITE_PROPERTY_DATA data, including
 * requested data and space for the reply, or error response.
 *
 * @return false if an error is loaded, true if no errors
 */
bool Load_Control_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data)
{
    bool status = false; /* return value */
    int len = 0;
    BACNET_APPLICATION_DATA_VALUE value = { 0 };

    if (wp_data == NULL) {
        debug_printf("Load_Control_Write_Property(): invalid data\n");
        return false;
    }
    if (wp_data->application_data_len < 0) {
        debug_printf("Load_Control_Write_Property(): invalid data length\n");
        /* error while decoding - a smaller larger than we can handle */
        wp_data->error_class = ERROR_CLASS_PROPERTY;
        wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
        return false;
    }
    /* decode the the request or the first element in array */
    len = bacapp_decode_known_property(
        wp_data->application_data, wp_data->application_data_len, &value,
        wp_data->object_type, wp_data->object_property);
    if (len < 0) {
        debug_printf("Load_Control_Write_Property(): decoding error\n");
        /* error while decoding - a value larger than we can handle */
        wp_data->error_class = ERROR_CLASS_PROPERTY;
        wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
        return false;
    }
    switch (wp_data->object_property) {
        case PROP_REQUESTED_SHED_LEVEL:
            status = write_property_type_valid(
                wp_data, &value, BACNET_APPLICATION_TAG_SHED_LEVEL);
            if (status) {
                status = Load_Control_Requested_Shed_Level_Write(
                    wp_data->object_instance, &value.type.Shed_Level,
                    wp_data->priority, &wp_data->error_class,
                    &wp_data->error_code);
            }
            break;
        case PROP_START_TIME:
            status = write_property_type_valid(
                wp_data, &value, BACNET_APPLICATION_TAG_DATETIME);
            if (status) {
                status = Load_Control_Start_Time_Write(
                    wp_data->object_instance, &value.type.Date_Time,
                    wp_data->priority, &wp_data->error_class,
                    &wp_data->error_code);
            }
            break;

        case PROP_SHED_DURATION:
            status = write_property_type_valid(
                wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT);
            if (status) {
                status = Load_Control_Shed_Duration_Write(
                    wp_data->object_instance, value.type.Unsigned_Int,
                    wp_data->priority, &wp_data->error_class,
                    &wp_data->error_code);
            }
            break;

        case PROP_DUTY_WINDOW:
            status = write_property_type_valid(
                wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT);
            if (status) {
                status = Load_Control_Duty_Window_Write(
                    wp_data->object_instance, value.type.Unsigned_Int,
                    wp_data->priority, &wp_data->error_class,
                    &wp_data->error_code);
            }
            break;

        case PROP_SHED_LEVELS:
            status = Load_Control_Shed_Levels_Write(wp_data);
            break;

        case PROP_ENABLE:
            status = write_property_type_valid(
                wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN);
            if (status) {
                status = Load_Control_Enable_Write(
                    wp_data->object_instance, value.type.Boolean,
                    wp_data->priority, &wp_data->error_class,
                    &wp_data->error_code);
            }
            break;
        default:
            debug_printf(
                "Load_Control_Write_Property() failure detected point Z\n");
            wp_data->error_class = ERROR_CLASS_PROPERTY;
            wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED;
            break;
    }

    debug_printf("Load_Control_Write_Property() returning status=%d\n", status);
    return status;
}

/**
 * @brief Sets a callback used when the manipulated object is written
 * @param object_instance - object-instance number of the object
 * @param cb - callback used to provide manipulations
 */
void Load_Control_Manipulated_Object_Write_Callback_Set(
    uint32_t object_instance, load_control_manipulated_object_write_callback cb)
{
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        pObject->Manipulated_Object_Write = cb;
    }
}

/**
 * @brief Sets a callback used when the manipulated object is relinquished
 * @param object_instance - object-instance number of the object
 * @param cb - callback used to provide manipulations
 */
void Load_Control_Manipulated_Object_Relinquish_Callback_Set(
    uint32_t object_instance,
    load_control_manipulated_object_relinquish_callback cb)
{
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        pObject->Manipulated_Object_Relinquish = cb;
    }
}

/**
 * @brief Sets a callback used when the manipulated object is read
 * @param object_instance - object-instance number of the object
 * @param cb - callback used to provide manipulations
 */
void Load_Control_Manipulated_Object_Read_Callback_Set(
    uint32_t object_instance, load_control_manipulated_object_read_callback cb)
{
    struct object_data *pObject;

    pObject = Object_Instance_Data(object_instance);
    if (pObject) {
        pObject->Manipulated_Object_Read = cb;
    }
}

/**
 * @brief For a given object instance-number, adds an array entity to a list.
 * @param  object_instance - object-instance number of the object
 * @param  array_index - index of the BACnetARRAY 1..N
 * @param  description - description of the array entity
 * @param  Value - value of the array entity
 * @return  true if the entity is added successfully.
 */
bool Load_Control_Shed_Level_Array_Set(
    uint32_t object_instance,
    uint32_t array_index,
    const struct shed_level_data *value)
{
    int key_index;
    struct shed_level_data *entry;
    struct object_data *pObject;
    KEY key = 0;

    pObject = Keylist_Data(Object_List, object_instance);
    if (!pObject) {
        return false;
    }
    if (array_index == 0) {
        return false;
    }
    key = array_index;
    entry = Keylist_Data(pObject->Shed_Level_List, key);
    if (!entry) {
        entry = calloc(1, sizeof(struct shed_level_data));
        if (!entry) {
            return false;
        }
        key_index = Keylist_Data_Add(pObject->Shed_Level_List, key, entry);
        if (key_index < 0) {
            free(entry);
            return false;
        }
    }
    entry->Value = value->Value;
    entry->Description = value->Description;

    return true;
}

/**
 * @brief Gets an entry from a list for a given object instance-number
 * @param  object_instance - object-instance number of the object
 * @param  array_entry - BACnetARRAY index of the array 1..N
 * @param  entry - data entry values are copied into
 * @return true if the data entry is found
 */
bool Load_Control_Shed_Level_Array(
    uint32_t object_instance,
    uint32_t array_entry,
    struct shed_level_data *value)
{
    struct shed_level_data *entry;
    struct object_data *pObject;

    pObject = Keylist_Data(Object_List, object_instance);
    if (!pObject) {
        return false;
    }
    entry = Keylist_Data(pObject->Shed_Level_List, array_entry);
    if (!entry) {
        return false;
    }
    if (value) {
        value->Value = entry->Value;
        value->Description = entry->Description;
    }

    return true;
}

/**
 * @brief Set the context used with a specific object instance
 * @param object_instance [in] BACnet object instance number
 * @param context [in] pointer to the context
 */
void *Load_Control_Context_Get(uint32_t object_instance)
{
    struct object_data *pObject;

    pObject = Keylist_Data(Object_List, object_instance);
    if (pObject) {
        return pObject->Context;
    }

    return NULL;
}

/**
 * @brief Set the context used with a specific object instance
 * @param object_instance [in] BACnet object instance number
 * @param context [in] pointer to the context
 */
void Load_Control_Context_Set(uint32_t object_instance, void *context)
{
    struct object_data *pObject;

    pObject = Keylist_Data(Object_List, object_instance);
    if (pObject) {
        pObject->Context = context;
    }
}

/**
 * @brief Creates a Load Control object
 * @param object_instance - object-instance number of the object
 * @return the object-instance that was created, or BACNET_MAX_INSTANCE
 */
uint32_t Load_Control_Create(uint32_t object_instance)
{
    struct object_data *pObject = NULL;
    int index = 0;
    /* The Shed Level array shall be ordered by increasing shed amount */
    struct shed_level_data shed_levels[] = { { 90.0f, "Special" },
                                             { 80.0f, "Medium" },
                                             { 70.0f, "High" } };
    struct shed_level_data *entry;
    unsigned i = 0;

    if (object_instance > BACNET_MAX_INSTANCE) {
        return BACNET_MAX_INSTANCE;
    } else if (object_instance == BACNET_MAX_INSTANCE) {
        /* wildcard instance */
        /* the Object_Identifier property of the newly created object
            shall be initialized to a value that is unique within the
            responding BACnet-user device. The method used to generate
            the object identifier is a local matter.*/
        object_instance = Keylist_Next_Empty_Key(Object_List, 1);
    }
    pObject = Keylist_Data(Object_List, object_instance);
    if (!pObject) {
        pObject = calloc(1, sizeof(struct object_data));
        if (pObject) {
            pObject->Object_Name = NULL;
            /* defaults */
            pObject->Present_Value = BACNET_SHED_INACTIVE;
            pObject->Requested_Shed_Level.type = BACNET_SHED_TYPE_LEVEL;
            pObject->Requested_Shed_Level.value.level = 0;
            datetime_wildcard_set(&pObject->Start_Time);
            datetime_wildcard_set(&pObject->End_Time);
            pObject->Shed_Duration = 0;
            pObject->Duty_Window = 0;
            pObject->Load_Control_Enable = true;
            pObject->Full_Duty_Baseline = 1500.0f;
            pObject->Expected_Shed_Level.type = BACNET_SHED_TYPE_LEVEL;
            pObject->Expected_Shed_Level.value.level = 0;
            pObject->Actual_Shed_Level.type = BACNET_SHED_TYPE_LEVEL;
            pObject->Actual_Shed_Level.value.level = 0;
            pObject->Load_Control_Request_Written = false;
            pObject->Start_Time_Property_Written = false;
            pObject->Shed_Level_List = Keylist_Create();
            for (i = 0; i < ARRAY_SIZE(shed_levels); i++) {
                entry = calloc(1, sizeof(struct shed_level_data));
                if (entry) {
                    entry->Value = shed_levels[i].Value;
                    entry->Description = shed_levels[i].Description;
                    index = Keylist_Data_Add(
                        pObject->Shed_Level_List, 1 + i, entry);
                    if (index < 0) {
                        free(entry);
                    }
                }
            }
            pObject->Priority_For_Writing = 4;
            pObject->Manipulated_Object_Read = NULL;
            pObject->Manipulated_Object_Write = NULL;
            pObject->Manipulated_Object_Relinquish = NULL;
            pObject->Manipulated_Object_Type = OBJECT_ANALOG_OUTPUT;
            pObject->Manipulated_Object_Instance = object_instance;
            pObject->Manipulated_Object_Property = PROP_PRESENT_VALUE;
            /* some state machine variables */
            pObject->Previous_Value = BACNET_SHED_INACTIVE;
            /* add to list */
            index = Keylist_Data_Add(Object_List, object_instance, pObject);
            if (index < 0) {
                free(pObject);
                return BACNET_MAX_INSTANCE;
            }
        } else {
            return BACNET_MAX_INSTANCE;
        }
    }

    return object_instance;
}

/**
 * Deletes an Load Control object
 * @param object_instance - object-instance number of the object
 * @return true if the object is deleted
 */
bool Load_Control_Delete(uint32_t object_instance)
{
    bool status = false;
    struct object_data *pObject = NULL;

    pObject = Keylist_Data_Delete(Object_List, object_instance);
    if (pObject) {
        free(pObject);
        status = true;
    }

    return status;
}

/**
 * Deletes all the Load Controls and their data
 */
void Load_Control_Cleanup(void)
{
    struct object_data *pObject;

    if (Object_List) {
        do {
            pObject = Keylist_Data_Pop(Object_List);
            if (pObject) {
                free(pObject);
            }
        } while (pObject);
        Keylist_Delete(Object_List);
        Object_List = NULL;
    }
}

/**
 * Initializes the Load Control object data
 */
void Load_Control_Init(void)
{
    if (!Object_List) {
        Object_List = Keylist_Create();
    }
}
