content: package im.actor.runtime.js.mvvm;
  
  import com.google.gwt.core.client.JavaScriptObject;
  import com.google.gwt.core.client.JsArray;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import im.actor.runtime.Log;
  import im.actor.runtime.bser.BserObject;
  import im.actor.runtime.js.storage.JsListEngine;
  import im.actor.runtime.js.storage.JsListEngineCallback;
  import im.actor.runtime.js.utils.JsModernArray;
  import im.actor.runtime.storage.ListEngineItem;
  
  public class JsDisplayListBind<T extends JavaScriptObject, V extends BserObject & ListEngineItem> implements JsListEngineCallback<V> {
  
      /**
       * Underlying list engine
       */
      private final JsListEngine<V> listEngine;
      /**
       * Underlying entity converted
       */
      private final JsEntityConverter<V, T> entityConverter;
      /**
       * Convenience flag if overlays supported by entity converter
       */
      private final boolean isOverlaysSupported;
  
  
      /**
       * Subscribers to bind
       */
      private final JsDisplayListCallback<T> callback;
      /**
       * Is subscriber inverted
       */
      private final boolean isInverted;
  
  
      /**
       * Current list values
       */
      private ArrayList<V> values;
      /**
       * Current converted values
       */
      private JsModernArray<T> jsValues;
      /**
       * Current overlay values (if supported)
       */
      private JsModernArray<JavaScriptObject> jsOverlays;
      /**
       * Current dirty overlay items
       */
      private ArrayList<Boolean> isOverlayDirty;
  
      /**
       * If all messages loaded from top of the list
       */
      private boolean isOpenTop;
      /**
       * If all messages loaded from bottom of the list
       */
      private boolean isOpenBottom;
      /**
       * Current window top limit
       */
      private long windowTop;
      /**
       * Current window bottom limit
       */
      private long windowBottom;
  
      /**
       * If list is inited and ready to receive list updates
       */
      private boolean isInited;
  
      /**
       * If ForceReconvert required flag
       */
      private boolean isForceReconverted = false;
  
      public JsDisplayListBind(JsDisplayListCallback<T> callback, boolean isInverted, JsListEngine<V> listEngine, JsEntityConverter<V, T> entityConverter) {
  
          this.callback = callback;
          this.isInverted = isInverted;
          this.listEngine = listEngine;
          this.entityConverter = entityConverter;
          this.isOverlaysSupported = entityConverter.isSupportOverlays();
  
          this.values = new ArrayList<>();
          this.jsValues = JsModernArray.createArray().cast();
          if (isOverlaysSupported) {
              this.isOverlayDirty = new ArrayList<>();
              this.jsOverlays = JsModernArray.createArray().cast();
          }
  
          isInited = false;
  
          listEngine.addListener(this);
      }
  
      public ArrayList<V> getRawItems() {
          return values;
      }
  
      private void clearState() {
          values.clear();
          jsValues.clear();
          if (isOverlaysSupported) {
              jsOverlays.clear();
              isOverlayDirty.clear();
          }
      }
  
      public void initAll() {
          clearState();
  
          long[] rids = listEngine.getOrderedIds();
          for (long rid : rids) {
              V item = listEngine.getValue(rid);
              if (item == null) {
                  Log.w("JsDisplayList", "Unable to find item #" + rid);
                  continue;
              }
              values.add(item);
              jsValues.push(entityConverter.convert(item));
  
              if (isOverlaysSupported) {
                  jsOverlays.push(null);
                  isOverlayDirty.add(true);
              }
          }
  
          processDirtyOverlays();
  
          isInited = true;
          windowTop = 0;
          isOpenTop = true;
          windowBottom = 0;
          isOpenBottom = true;
  
          notifySubscriber();
      }
  
      public void initTop(int limit) {
          clearState();
  
          long[] rids = listEngine.getOrderedIds();
          for (int i = 0; i < rids.length && i < limit; i++) {
              V item = listEngine.getValue(rids[i]);
              if (item == null) {
                  Log.w("JsDisplayList", "Unable to find item #" + rids[i]);
                  continue;
              }
              values.add(item);
              jsValues.push(entityConverter.convert(item));
  
              if (isOverlaysSupported) {
                  jsOverlays.push(null);
                  isOverlayDirty.add(true);
              }
          }
  
          processDirtyOverlays();
  
          isInited = true;
          windowTop = 0;
          isOpenTop = true;
          if (rids.length > 0) {
              windowBottom = rids[rids.length - 1];
              isOpenBottom = false;
          } else {
              windowBottom = 0;
              isOpenBottom = true;
          }
  
          notifySubscriber();
      }
  
      public void loadBottom(int limit) {
          if (!isInited) {
              return;
          }
          if (isOpenBottom) {
              return;
          }
          long[] rids = listEngine.getPrevIdsExclusive(windowBottom);
          for (int i = 0; i < rids.length && i < limit; i++) {
              V item = listEngine.getValue(rids[i]);
              if (item == null) {
                  Log.w("JsDisplayList", "Unable to find item #" + rids[i]);
                  continue;
              }
              values.add(item);
              jsValues.push(entityConverter.convert(item));
  
              if (isOverlaysSupported) {
                  jsOverlays.push(null);
                  isOverlayDirty.add(true);
              }
          }
  
          processDirtyOverlays();
  
          if (rids.length > 0) {
              windowBottom = rids[rids.length - 1];
              isOpenBottom = false;
          } else {
              windowBottom = 0;
              isOpenBottom = true;
          }
  
          notifySubscriber();
      }
  
      public void dispose() {
          listEngine.removeListener(this);
      }
  
      public void notifySubscriber() {
          if (isInverted) {
              if (isOverlaysSupported) {
                  callback.onCollectionChanged(jsValues.reverse(), jsOverlays.reverse());
              } else {
                  callback.onCollectionChanged(jsValues.reverse(), null);
              }
          } else {
              if (isOverlaysSupported) {
                  callback.onCollectionChanged(jsValues, jsOverlays);
              } else {
                  callback.onCollectionChanged(jsValues, null);
              }
          }
      }
  
      private void addItemOrUpdateImpl(V item) {
          long id = item.getEngineId();
          long sortKey = item.getEngineSort();
  
          if (!isOpenTop && windowTop > sortKey) {
              // Doesn't fit in top window limit
              return;
          }
          if (!isOpenBottom && windowBottom < sortKey) {
              // Doesn't fit in bottom window limit
              return;
          }
  
          for (int i = 0; i < values.size(); i++) {
              if (values.get(i).getEngineId() == id) {
                  values.remove(i);
                  jsValues.remove(i);
                  if (isOverlaysSupported) {
                      markAsDirty(i);
                      jsOverlays.remove(i);
                      isOverlayDirty.remove(i);
                  }
                  break;
              }
          }
  
          for (int i = 0; i < values.size(); i++) {
              if (sortKey > values.get(i).getEngineSort()) {
                  values.add(i, item);
                  jsValues.insert(i, entityConverter.convert(item));
                  if (isOverlaysSupported) {
                      jsOverlays.insert(i, null);
                      isOverlayDirty.add(i, true);
                      markAsDirty(i);
                  }
                  return;
              }
          }
  
          values.add(item);
          jsValues.push(entityConverter.convert(item));
          if (isOverlaysSupported) {
              jsOverlays.push(null);
              isOverlayDirty.add(true);
              markAsDirty(values.size() - 1);
          }
      }
  
      private void remoteItemImpl(long id) {
          for (int i = 0; i < values.size(); i++) {
              if (values.get(i).getEngineId() == id) {
                  values.remove(i);
                  jsValues.remove(i);
                  if (isOverlaysSupported) {
                      markAsDirty(i);
                      jsOverlays.remove(i);
                      isOverlayDirty.add(true);
                  }
                  break;
              }
          }
      }
  
      /*
       * List Engine Updates
       */
  
      @Override
      public void onItemAddedOrUpdated(V item) {
          if (!isInited) {
              return;
          }
  
          addItemOrUpdateImpl(item);
          processDirtyOverlays();
          notifySubscriber();
      }
  
      @Override
      public void onItemsAddedOrUpdated(List<V> items) {
          if (!isInited) {
              return;
          }
  
          for (V item : items) {
              addItemOrUpdateImpl(item);
          }
          processDirtyOverlays();
          notifySubscriber();
      }
  
      @Override
      public void onItemRemoved(long id) {
          if (!isInited) {
              return;
          }
          remoteItemImpl(id);
          processDirtyOverlays();
          notifySubscriber();
      }
  
      @Override
      public void onItemsRemoved(long[] ids) {
          if (!isInited) {
              return;
          }
          for (long id : ids) {
              remoteItemImpl(id);
          }
          processDirtyOverlays();
          notifySubscriber();
      }
  
      @Override
      public void onItemsReplaced(List<V> items) {
          if (!isInited) {
              return;
          }
  
          values.clear();
          jsValues.clear();
          if (isOverlaysSupported) {
              jsOverlays.clear();
              isOverlayDirty.clear();
          }
          onItemsAddedOrUpdated(items);
      }
  
      @Override
      public void onClear() {
          if (!isInited) {
              return;
          }
  
          values.clear();
          jsValues.clear();
          if (isOverlaysSupported) {
              jsOverlays.clear();
              isOverlayDirty.clear();
          }
  
          notifySubscriber();
      }
  
      //
      // Reconverting
      //
  
      public void startReconverting() {
          isForceReconverted = false;
      }
  
      public void forceReconvert(long id) {
          for (int i = 0; i < values.size(); i++) {
              V value = values.get(i);
              if (value.getEngineId() == id) {
                  jsValues.update(i, entityConverter.convert(value));
                  // Do not update overlays as this is method is a hack for binding
                  isForceReconverted = true;
                  break;
              }
          }
      }
  
      public void stopReconverting() {
          if (isForceReconverted) {
              isForceReconverted = false;
              notifySubscriber();
          }
      }
  
      /*
       * Overlay support methods
       */
  
      private boolean isDirty(int index) {
          return isOverlayDirty.get(index);
      }
  
      private void markAsDirty(int index) {
          isOverlayDirty.set(index, true);
          if (index - 1 >= 0) {
              isOverlayDirty.set(index - 1, true);
          }
  
          if (index + 1 < isOverlayDirty.size()) {
              isOverlayDirty.set(index + 1, true);
          }
      }
  
      private void markAsClean(int index) {
          isOverlayDirty.set(index, false);
      }
  
      private boolean processDirtyOverlays() {
          if (!isOverlaysSupported) {
              return false;
          }
          boolean isChanged = false;
          for (int i = 0; i < values.size(); i++) {
              if (!isDirty(i)) {
                  continue;
              }
  
              V prev = null;
              V current = values.get(i);
              V next = null;
  
              if (i - 1 >= 0) {
                  prev = values.get(i - 1);
              }
              if (i + 1 < values.size()) {
                  next = values.get(i + 1);
              }
  
              jsOverlays.update(i, entityConverter.buildOverlay(prev, current, next));
              markAsClean(i);
              isChanged = true;
          }
          return isChanged;
      }
  }