content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.runtime.generic.mvvm;
  
  import com.google.j2objc.annotations.AutoreleasePool;
  import com.google.j2objc.annotations.ObjectiveCName;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.CopyOnWriteArrayList;
  
  import im.actor.runtime.generic.mvvm.alg.ChangeBuilder;
  import im.actor.runtime.generic.mvvm.alg.Modification;
  import im.actor.runtime.actors.Actor;
  import im.actor.runtime.actors.ActorCreator;
  import im.actor.runtime.actors.ActorRef;
  import im.actor.runtime.actors.Props;
  import im.actor.runtime.generic.mvvm.alg.Modifications;
  
  import static im.actor.runtime.actors.ActorSystem.system;
  
  // Disabling Bounds checks for speeding up calculations
  
  /*-[
  #define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1
  ]-*/
  
  public class DisplayList<T> {
  
      static {
          system().addDispatcher("display_list");
      }
  
      private static int NEXT_ID = 0;
      private final int DISPLAY_LIST_ID;
      private ActorRef executor;
      private ArrayList<T>[] lists;
      private volatile int currentList;
      private final OperationMode operationMode;
      private volatile Object processedList;
  
      private CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
      private CopyOnWriteArrayList<AndroidChangeListener<T>> androidListeners = new CopyOnWriteArrayList<>();
      private CopyOnWriteArrayList<AppleChangeListener<T>> appleListeners = new CopyOnWriteArrayList<>();
  
      private ListProcessor<T> listProcessor = null;
  
      @ObjectiveCName("initWithMode:")
      public DisplayList(OperationMode operationMode) {
          this(operationMode, new ArrayList<T>());
      }
  
      @ObjectiveCName("initWithMode:withValues:")
      public DisplayList(OperationMode operationMode, List<T> defaultValues) {
          im.actor.runtime.Runtime.checkMainThread();
  
          this.DISPLAY_LIST_ID = NEXT_ID++;
  
          this.operationMode = operationMode;
  
          this.executor = system().actorOf(Props.create(new ActorCreator() {
              @Override
              public ListSwitcher create() {
                  return new ListSwitcher(DisplayList.this);
              }
          }).changeDispatcher("display_list"), "display_lists/" + DISPLAY_LIST_ID);
          this.lists = new ArrayList[2];
  
          this.currentList = 0;
          this.lists[0] = new ArrayList<T>(defaultValues);
          this.lists[1] = new ArrayList<T>(defaultValues);
      }
  
      @ObjectiveCName("size")
      public int getSize() {
          // im.actor.runtime.Runtime.checkMainThread();
          return lists[currentList].size();
      }
  
      @ObjectiveCName("itemWithIndex:")
      public T getItem(int index) {
          // im.actor.runtime.Runtime.checkMainThread();
          return lists[currentList].get(index);
      }
  
      @ObjectiveCName("positionWithFind:")
      public int getPosition(Object find) {
          for (int i = 0; i < lists[currentList].size(); i++) {
              if (lists[currentList].get(i).equals(find)) {
                  return i;
              }
          }
          return -1;
      }
  
      @ObjectiveCName("editList:")
      public void editList(Modification<T> mod) {
          editList(mod, null);
      }
  
      @ObjectiveCName("editList:withCompletion:")
      public void editList(Modification<T> mod, Runnable executeAfter) {
          this.executor.send(new EditList<T>(mod, executeAfter, false));
      }
  
      @ObjectiveCName("editList:withCompletion:withLoadMoreFlag:")
      public void editList(Modification<T> mod, Runnable executeAfter, boolean isLoadMore) {
          this.executor.send(new EditList<T>(mod, executeAfter, isLoadMore));
      }
  
      @ObjectiveCName("editList:withLoadMoreFlag:")
      public void editList(Modification<T> mod, boolean isLoadMore) {
          this.executor.send(new EditList<T>(mod, null, isLoadMore));
      }
  
      @ObjectiveCName("forcePreprocessing")
      public void forcePreprocessing() {
          this.executor.send(new EditList<T>((Modification<T>) Modifications.noOp(), null, false));
      }
  
      @ObjectiveCName("setListProcessor:")
      public void setListProcessor(ListProcessor<T> listProcessor) {
          this.listProcessor = listProcessor;
      }
  
      @ObjectiveCName("getListProcessor")
      public ListProcessor<T> getListProcessor() {
          return listProcessor;
      }
  
      @ObjectiveCName("getProcessedList")
      public Object getProcessedList() {
          return processedList;
      }
  
      @ObjectiveCName("addListener:")
      public void addListener(Listener listener) {
          //im.actor.runtime.Runtime.checkMainThread();
          if (!listeners.contains(listener)) {
              listeners.add(listener);
          }
      }
  
      @ObjectiveCName("removeListener:")
      public void removeListener(Listener listener) {
          //im.actor.runtime.Runtime.checkMainThread();
          listeners.remove(listener);
      }
  
      @ObjectiveCName("addAndroidListener:")
      public void addAndroidListener(AndroidChangeListener<T> listener) {
          if (operationMode != OperationMode.ANDROID && operationMode != OperationMode.GENERAL) {
              throw new RuntimeException("Unable to set Android Listener in iOS mode");
          }
          //im.actor.runtime.Runtime.checkMainThread();
  
          if (!androidListeners.contains(listener)) {
              androidListeners.add(listener);
          }
      }
  
      @ObjectiveCName("removeAndroidListener:")
      public void removeAndroidListener(AndroidChangeListener<T> listener) {
          if (operationMode != OperationMode.ANDROID && operationMode != OperationMode.GENERAL) {
              throw new RuntimeException("Unable to set Android Listener in iOS mode");
          }
          //im.actor.runtime.Runtime.checkMainThread();
  
          androidListeners.remove(listener);
      }
  
      @ObjectiveCName("addAppleListener:")
      public void addAppleListener(AppleChangeListener<T> listener) {
          if (operationMode != OperationMode.IOS && operationMode != OperationMode.GENERAL) {
              throw new RuntimeException("Unable to set Android Listener in Android mode");
          }
          //im.actor.runtime.Runtime.checkMainThread();
  
          if (!appleListeners.contains(listener)) {
              appleListeners.add(listener);
          }
      }
  
      @ObjectiveCName("removeAppleListener:")
      public void removeAppleListener(AppleChangeListener<T> listener) {
          if (operationMode != OperationMode.IOS && operationMode != OperationMode.GENERAL) {
              throw new RuntimeException("Unable to set Android Listener in Android mode");
          }
          //im.actor.runtime.Runtime.checkMainThread();
  
          appleListeners.remove(listener);
      }
  
      // Update actor
  
      private static class ListSwitcher<T> extends Actor {
          private ArrayList<ModificationHolder<T>> pending = new ArrayList<ModificationHolder<T>>();
          private boolean isLocked;
          private DisplayList<T> displayList;
  
          private ListSwitcher(DisplayList<T> displayList) {
              this.displayList = displayList;
          }
  
          @AutoreleasePool
          public void onEditList(final Modification<T> modification, final Runnable runnable, boolean isLoadMore) {
  
              if (modification != null) {
                  pending.add(new ModificationHolder<T>(modification, runnable, isLoadMore));
              }
  
              if (isLocked) {
                  return;
              }
  
              if (pending.size() == 0) {
                  // Nothing to update
                  return;
              }
  
              ArrayList<T> backgroundList = displayList.lists[(displayList.currentList + 1) % 2];
              ArrayList<T> initialList = new ArrayList<T>(backgroundList);
  
              int count = 1;
              for (ModificationHolder h : pending) {
                  if (h.isLoadMore) {
                      break;
                  }
              }
  
              ModificationHolder<T>[] dest = new ModificationHolder[count];
              for (int i = 0; i < count; i++) {
                  dest[i] = pending.remove(0);
              }
  
              ArrayList<ChangeDescription<T>> modRes = new ArrayList<ChangeDescription<T>>();
  
              for (ModificationHolder<T> m : dest) {
                  List<ChangeDescription<T>> changes = m.modification.modify(backgroundList);
                  modRes.addAll(changes);
              }
  
              // Build changes
              ArrayList<ChangeDescription<T>> androidChanges = null;
              AppleListUpdate appleChanges = null;
              if (displayList.operationMode == OperationMode.ANDROID
                      || displayList.operationMode == OperationMode.GENERAL) {
                  androidChanges = ChangeBuilder.processAndroidModifications(modRes, initialList);
              }
              if (displayList.operationMode == OperationMode.IOS
                      || displayList.operationMode == OperationMode.GENERAL) {
                  appleChanges = ChangeBuilder.processAppleModifications(modRes, initialList, dest[0].isLoadMore);
              }
  
              Object processedList = null;
              if (displayList.listProcessor != null) {
                  processedList = displayList.listProcessor.process(backgroundList, displayList.processedList);
              }
  
              requestListSwitch(dest, initialList, androidChanges, appleChanges, dest[0].isLoadMore,
                      processedList);
          }
  
          @AutoreleasePool
          private void requestListSwitch(final ModificationHolder<T>[] modifications,
                                         final ArrayList<T> initialList,
                                         final ArrayList<ChangeDescription<T>> androidChanges,
                                         final AppleListUpdate appleChanges,
                                         final boolean isLoadedMore,
                                         final Object processedList) {
              isLocked = true;
              im.actor.runtime.Runtime.postToMainThread(new Runnable() {
                  @Override
                  public void run() {
  
                      displayList.currentList = (displayList.currentList + 1) % 2;
                      displayList.processedList = processedList;
  
                      if (androidChanges != null) {
                          for (AndroidChangeListener<T> l : displayList.androidListeners) {
                              l.onCollectionChanged(new AndroidListUpdate<T>(initialList, androidChanges, isLoadedMore));
                          }
                      }
  
                      if (appleChanges != null) {
                          for (AppleChangeListener<T> l : displayList.appleListeners) {
                              l.onCollectionChanged(appleChanges);
                          }
                      }
  
                      for (Listener l : displayList.listeners) {
                          l.onCollectionChanged();
                      }
  
                      for (ModificationHolder m : modifications) {
                          if (m.executeAfter != null) {
                              m.executeAfter.run();
                          }
                      }
  
                      self().send(new ListSwitched<T>(modifications));
                  }
              });
          }
  
          @AutoreleasePool
          public void onListSwitched(ModificationHolder<T>[] modifications) {
              isLocked = false;
  
              ArrayList<T> backgroundList = displayList.lists[(displayList.currentList + 1) % 2];
              for (ModificationHolder m : modifications) {
                  m.modification.modify(backgroundList);
              }
  
              if (pending.size() > 0) {
                  self().send(new EditList<T>(null, null, false));
              }
          }
  
          @Override
          public void onReceive(Object message) {
              if (message instanceof ListSwitched) {
                  onListSwitched(((ListSwitched<T>) message).modifications);
              } else if (message instanceof EditList) {
                  onEditList(((EditList<T>) message).modification, ((EditList) message).executeAfter,
                          ((EditList) message).isLoadMore);
              } else {
                  super.onReceive(message);
              }
          }
      }
  
      private static class ListSwitched<T> {
          private ModificationHolder<T>[] modifications;
  
          private ListSwitched(ModificationHolder<T>[] modifications) {
              this.modifications = modifications;
          }
      }
  
      private static class EditList<T> {
          private Modification<T> modification;
          private Runnable executeAfter;
          private boolean isLoadMore;
  
          private EditList(Modification<T> modification, Runnable executeAfter, boolean isLoadMore) {
              this.modification = modification;
              this.executeAfter = executeAfter;
              this.isLoadMore = isLoadMore;
          }
      }
  
      private static class ModificationHolder<T> {
          private Modification<T> modification;
          private Runnable executeAfter;
          private boolean isLoadMore;
  
          private ModificationHolder(Modification<T> modification, Runnable executeAfter, boolean isLoadMore) {
              this.modification = modification;
              this.executeAfter = executeAfter;
              this.isLoadMore = isLoadMore;
          }
      }
  
      public interface Listener {
          @ObjectiveCName("onCollectionChanged")
          void onCollectionChanged();
      }
  
      public interface AndroidChangeListener<T> {
          @ObjectiveCName("onCollectionChangedWithChanges:")
          void onCollectionChanged(AndroidListUpdate<T> modification);
      }
  
      public interface AppleChangeListener<T> {
          @ObjectiveCName("onCollectionChangedWithChanges:")
          void onCollectionChanged(AppleListUpdate modification);
      }
  
      public enum OperationMode {
          GENERAL, ANDROID, IOS
      }
  }
  