/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.flat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.lang.reflect.Array;
/**
* Diffing scope stack class that supports 3 main operations: start(), add() an element and
* finish().
*
* When started, it takes a baseline array to compare to. When adding a new element, it checks
* whether a corresponding element in baseline array is the same. On finish(), it will return null
* if baseline array contains exactly the same elements that were added with a sequence of add()
* calls, or a new array of the recorded elements:
*
* Example 1:
* -----
* start([A])
* add(A)
* finish() -> null (because [A] == [A])
*
* Example 2:
* ----
* start([A])
* add(B)
* finish() -> [B] (because [A] != [B])
*
* Example 3:
* ----
* start([A])
* add(B)
* add(A)
* finish() -> [B, A] (because [B, A] != [A])
*
* Example 4:
* ----
* start([A, B])
* add(B)
* add(A)
* finish() -> [B, A] (because [B, A] != [A, B])
*
* It is important that start/finish can be nested:
* ----
* start([A])
* add(A)
* start([B])
* add(B)
* finish() -> null
* add(C)
* finish() -> [A, C]
*
* StateBuilder is using this class to check if e.g. a DrawCommand list for a given View needs to be
* updated.
*/
/* package */ final class ElementsList<E> {
private static final class Scope {
Object[] elements;
int index;
int size;
}
// List of scopes. These are never cleared, but instead recycled when a new scope is needed at
// a given depth.
private final ArrayList<Scope> mScopesStack = new ArrayList<>();
// Working list of all new elements we are gathering across scopes. Whenever we get a call to
// finish() we pop the new elements off the collection, either discarding them if there was no
// change from the base or accumulating and returning them as a list of new elements.
private final ArrayDeque<E> mElements = new ArrayDeque<>();
private final E[] mEmptyArray;
private Scope mCurrentScope = null;
private int mScopeIndex = 0;
public ElementsList(E[] emptyArray) {
mEmptyArray = emptyArray;
mScopesStack.add(mCurrentScope);
}
/**
* Starts a new scope.
*/
public void start(Object[] elements) {
pushScope();
Scope scope = getCurrentScope();
scope.elements = elements;
scope.index = 0;
scope.size = mElements.size();
}
/**
* Finish current scope, returning null if there were no changes recorded, or a new array
* containing all the newly recorded elements otherwise.
*/
public E[] finish() {
Scope scope = getCurrentScope();
popScope();
E[] result = null;
int size = mElements.size() - scope.size;
if (scope.index != scope.elements.length) {
result = extractElements(size);
} else {
// downsize
for (int i = 0; i < size; ++i) {
mElements.pollLast();
}
}
// To prevent resource leaks.
scope.elements = null;
return result;
}
/**
* Adds a new element to the list. This method can be optimized to avoid inserts on same
* elements, but would involve copying from scope.elements when we extract elements.
*/
public void add(E element) {
Scope scope = getCurrentScope();
if (scope.index < scope.elements.length &&
scope.elements[scope.index] == element) {
++scope.index;
} else {
scope.index = Integer.MAX_VALUE;
}
mElements.add(element);
}
/**
* Resets all references to elements in our new stack to null to avoid memory leaks.
*/
public void clear() {
if (getCurrentScope() != null) {
throw new RuntimeException("Must call finish() for every start() call being made.");
}
mElements.clear();
}
/**
* Extracts last size elements into an array. Used to extract our new array of items from our
* stack when the new items != old items.
*/
private E[] extractElements(int size) {
if (size == 0) {
// avoid allocating empty array
return mEmptyArray;
}
E[] elements = (E[]) Array.newInstance(mEmptyArray.getClass().getComponentType(), size);
for (int i = size - 1; i >= 0; --i) {
elements[i] = mElements.pollLast();
}
return elements;
}
/**
* Saves current scope in a stack.
*/
private void pushScope() {
++mScopeIndex;
if (mScopeIndex == mScopesStack.size()) {
// We reached a new deepest scope, we need to create a scope for this depth.
mCurrentScope = new Scope();
mScopesStack.add(mCurrentScope);
} else {
// We have had a scope at this depth before, lets recycle it.
mCurrentScope = mScopesStack.get(mScopeIndex);
}
}
/**
* Restores last saved current scope. Doesn't actually remove the scope, as scopes are
* recycled.
*/
private void popScope() {
--mScopeIndex;
mCurrentScope = mScopesStack.get(mScopeIndex);
}
/**
* Returns current scope.
*/
private Scope getCurrentScope() {
return mCurrentScope;
}
}