[IM] Implement list chunking feature (#12019)

* Update AttributeValueEncoder

* TagBoundEncoder -> ListEncodeHelper

* Fix Null ListIndex support

* Offload build logic to separate class

* Add attribute for testing list chunking

* Fix Error

* Add even more tests

* auto -> const auto *

* Address comments

* Update

* Update Encode

* Update

* Update

* Update

* Fix style

* Fix

* Run Codegen
diff --git a/src/app/AttributeAccessInterface.h b/src/app/AttributeAccessInterface.h
index 8f37e99..9f7cca7 100644
--- a/src/app/AttributeAccessInterface.h
+++ b/src/app/AttributeAccessInterface.h
@@ -18,8 +18,9 @@
 
 #pragma once
 
+#include <app/ClusterInfo.h>
 #include <app/ConcreteAttributePath.h>
-#include <app/MessageDef/AttributeDataIB.h>
+#include <app/MessageDef/AttributeReportIBs.h>
 #include <app/data-model/Decode.h>
 #include <app/data-model/Encode.h>
 #include <app/data-model/List.h> // So we can encode lists
@@ -41,44 +42,158 @@
 namespace chip {
 namespace app {
 
-class AttributeValueEncoder : protected TagBoundEncoder
+/**
+ * The AttributeReportBuilder is a helper class for filling a single report in AttributeReportIBs.
+ *
+ * Possible usage of AttributeReportBuilder might be:
+ *
+ * AttributeReportBuilder builder;
+ * ReturnErrorOnFailure(builder.PrepareAttribute(...));
+ * ReturnErrorOnFailure(builder.Encode(...));
+ * ReturnErrorOnFailure(builder.FinishAttribute());
+ */
+class AttributeReportBuilder
 {
 public:
-    AttributeValueEncoder(TLV::TLVWriter * aWriter, FabricIndex aAccessingFabricIndex) :
-        TagBoundEncoder(aWriter, TLV::ContextTag(to_underlying(AttributeDataIB::Tag::kData))),
-        mAccessingFabricIndex(aAccessingFabricIndex)
+    /**
+     * PrepareAttribute encodes the "header" part of an attribute report including the path and data version.
+     * Path will be encoded according to section 10.5.4.3.1 in the spec.
+     * Note: Only append is supported currently (encode a null list index), other operations won't encode a list index in the
+     * attribute path field.
+     * TODO: Add support for encoding a single element in the list (path with a valid list index).
+     */
+    CHIP_ERROR PrepareAttribute(AttributeReportIBs::Builder & aAttributeReportIBs, const ConcreteDataAttributePath & aPath,
+                                DataVersion aDataVersion);
+
+    /**
+     * FinishAttribute encodes the "footer" part of an attribute report (it closes the containers opened in PrepareAttribute)
+     */
+    CHIP_ERROR FinishAttribute();
+
+    /**
+     * EncodeValue encodes the value field of the report, it should be called exactly once.
+     */
+    template <typename... Ts>
+    CHIP_ERROR EncodeValue(Ts... aArgs)
+    {
+        return DataModel::Encode(*mAttributeDataIBBuilder.GetWriter(), TLV::ContextTag(to_underlying(AttributeDataIB::Tag::kData)),
+                                 std::forward<Ts>(aArgs)...);
+    }
+
+private:
+    AttributeReportIB::Builder mAttributeReportIBBuilder;
+    AttributeDataIB::Builder mAttributeDataIBBuilder;
+};
+
+/**
+ * The AttributeValueEncoder is a helper class for filling report payloads into AttributeReportIBs.
+ * The attribute value encoder can be initialized with a AttributeEncodeState for saving and recovering its state between encode
+ * sessions (chunkings).
+ *
+ * When Encode returns recoverable errors (e.g. CHIP_ERROR_NO_MEMORY) the state can be used to initialize the AttributeValueEncoder
+ * for future use on the same attribute path.
+ */
+class AttributeValueEncoder
+{
+public:
+    class ListEncodeHelper
+    {
+    public:
+        ListEncodeHelper(AttributeValueEncoder & encoder) : mAttributeValueEncoder(encoder) {}
+
+        template <typename... Ts>
+        CHIP_ERROR Encode(Ts... aArgs) const
+        {
+            return mAttributeValueEncoder.EncodeListItem(std::forward<Ts>(aArgs)...);
+        }
+
+    private:
+        AttributeValueEncoder & mAttributeValueEncoder;
+    };
+
+    class AttributeEncodeState
+    {
+    public:
+        AttributeEncodeState() : mAllowPartialData(false), mCurrentEncodingListIndex(kInvalidListIndex) {}
+        bool AllowPartialData() const { return mAllowPartialData; }
+
+    private:
+        friend class AttributeValueEncoder;
+        /**
+         * When an attempt to encode an attribute returns an error, the buffer may contain tailing dirty data
+         * (since the put was aborted).  The report engine normally rolls back the buffer to right before encoding
+         * of the attribute started on errors.
+         *
+         * When chunking a list, EncodeListItem will atomically encode list items, ensuring that the
+         * state of the buffer is valid to send (i.e. contains no trailing garbage), and return an error
+         * if the list doesn't entirely fit.  In this situation, mAllowPartialData is set to communicate to the
+         * report engine that it should not roll back the list items.
+         *
+         * TODO: There might be a better name for this variable.
+         */
+        bool mAllowPartialData = false;
+        /**
+         * If set to kInvalidListIndex, indicates that we have not encoded any data for the list yet and
+         * need to start by encoding an empty list before we start encoding any list items.
+         *
+         * When set to a valid ListIndex value, indicates the index of the next list item that needs to be
+         * encoded (i.e. the count of items encoded so far).
+         */
+        ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
+    };
+
+    AttributeValueEncoder(AttributeReportIBs::Builder & aAttributeReportIBsBuilder, FabricIndex aAccessingFabricIndex,
+                          const ConcreteAttributePath & aPath, DataVersion aDataVersion,
+                          const AttributeEncodeState & aState = AttributeEncodeState()) :
+        mAttributeReportIBsBuilder(aAttributeReportIBsBuilder),
+        mAccessingFabricIndex(aAccessingFabricIndex), mPath(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId),
+        mDataVersion(aDataVersion), mEncodeState(aState)
     {}
 
+    /**
+     * Encode builds a single AttributeReportIB in AttributeReportIBs.
+     * When we are encoding a single element in the list, the actual path in the report contains a null list index as "append"
+     * operation.
+     */
     template <typename... Ts>
     CHIP_ERROR Encode(Ts... aArgs)
     {
         mTriedEncode = true;
-        if (mWriter == nullptr)
-        {
-            return CHIP_NO_ERROR;
-        }
-        return TagBoundEncoder::Encode(std::forward<Ts>(aArgs)...);
+        return EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
     }
 
     /**
-     * aCallback is expected to take a const TagBoundEncoder& argument and
-     * Encode() on it as many times as needed to encode all the list elements
-     * one by one.  If any of those Encode() calls returns failure, aCallback
-     * must stop encoding and return failure.  When all items are encoded
-     * aCallback is expected to return success.
+     * aCallback is expected to take a const auto & argument and Encode() on it as many times as needed to encode all the list
+     * elements one by one.  If any of those Encode() calls returns failure, aCallback must stop encoding and return failure.  When
+     * all items are encoded aCallback is expected to return success.
      *
-     * aCallback may not be called.  Consumers must not assume it will be
-     * called.
+     * aCallback may not be called.  Consumers must not assume it will be called.
+     *
+     * When EncodeList returns an error, the consumers must abort the encoding, and return the exact error to the caller.
+     *
+     * TODO: Can we hold a error state in the AttributeValueEncoder itself so functions in ember-compatibility-functions don't have
+     * to rely on the above assumption?
+     *
+     * Consumers are allowed to make either one call to EncodeList or one call to Encode to handle a read.
+     *
      */
     template <typename ListGenerator>
     CHIP_ERROR EncodeList(ListGenerator aCallback)
     {
         mTriedEncode = true;
-        if (mWriter == nullptr)
-        {
-            return CHIP_NO_ERROR;
-        }
-        return TagBoundEncoder::EncodeList(aCallback);
+        // Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs)
+        // EmptyList acts as the beginning of the whole array type attribute report.
+        // An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid
+        // values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0.
+        mPath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll;
+        ReturnErrorOnFailure(EncodeEmptyList());
+        // For all elements in the list, a report with append operation will be generated. This will not be changed during encoding
+        // of each report since the users cannot access mPath.
+        mPath.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem;
+        ReturnErrorOnFailure(aCallback(ListEncodeHelper(*this)));
+        // The Encode procedure finished without any error, clear the state.
+        mEncodeState = AttributeEncodeState();
+        return CHIP_NO_ERROR;
     }
 
     bool TriedEncode() const { return mTriedEncode; }
@@ -88,18 +203,75 @@
      */
     FabricIndex AccessingFabricIndex() const { return mAccessingFabricIndex; }
 
-    // For consumers that can't just do a single Encode call for some reason
-    // (e.g. they're encoding a list a bit at a time).
-    TLV::TLVWriter * PrepareManualEncode()
-    {
-        // If this is called, the consumer is trying to encode a value.
-        mTriedEncode = true;
-        return mWriter;
-    }
+    /**
+     * AttributeValueEncoder is a short lived object, and the state is persisted by mEncodeState and restored by constructor.
+     */
+    const AttributeEncodeState & GetState() const { return mEncodeState; }
 
 private:
+    // We made EncodeListItem() private, and ListEncoderHelper will expose it by Encode()
+    friend class ListEncodeHelper;
+
+    template <typename... Ts>
+    CHIP_ERROR EncodeListItem(Ts... aArgs)
+    {
+        // EncodeListItem must be called after EncodeEmptyList(), thus mCurrentEncodingListIndex and
+        // mEncodeState.mCurrentEncodingListIndex are not invalid values.
+        if (mCurrentEncodingListIndex < mEncodeState.mCurrentEncodingListIndex)
+        {
+            // We have encoded this element in previous chunks, skip it.
+            mCurrentEncodingListIndex++;
+            return CHIP_NO_ERROR;
+        }
+
+        TLV::TLVWriter backup;
+        mAttributeReportIBsBuilder.Checkpoint(backup);
+
+        CHIP_ERROR err = EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
+        if (err != CHIP_NO_ERROR)
+        {
+            // For list chunking, ReportEngine should not rollback the buffer when CHIP_NO_MEMORY or similar error occurred.
+            // However, the error might be raised in the middle of encoding procedure, then the buffer may contain partial data,
+            // unclosed containers etc. This line clears all possible partial data and makes EncodeListItem is atomic.
+            mAttributeReportIBsBuilder.Rollback(backup);
+            return err;
+        }
+
+        mCurrentEncodingListIndex++;
+        mEncodeState.mCurrentEncodingListIndex++;
+        return CHIP_NO_ERROR;
+    }
+
+    /**
+     * Actual logic for encoding a single AttributeReportIB in AttributeReportIBs.
+     */
+    template <typename... Ts>
+    CHIP_ERROR EncodeAttributeReportIB(Ts... aArgs)
+    {
+        mTriedEncode = true;
+        AttributeReportBuilder builder;
+
+        ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion));
+        ReturnErrorOnFailure(builder.EncodeValue(std::forward<Ts>(aArgs)...));
+
+        return builder.FinishAttribute();
+    }
+
+    /**
+     * EncodeEmptyList encodes the first item of one report with lists (an empty list).
+     *
+     * If internal state indicates we have already encoded the empty list, this function will encode nothing, set
+     * mCurrentEncodingListIndex to 0 and return CHIP_NO_ERROR.
+     */
+    CHIP_ERROR EncodeEmptyList();
+
     bool mTriedEncode = false;
+    AttributeReportIBs::Builder & mAttributeReportIBsBuilder;
     const FabricIndex mAccessingFabricIndex;
+    ConcreteDataAttributePath mPath;
+    DataVersion mDataVersion;
+    AttributeEncodeState mEncodeState;
+    ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
 };
 
 class AttributeValueDecoder