content: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.base.content;
  
  import android.content.Context;
  import android.content.SharedPreferences;
  import android.support.annotation.NonNull;
  import android.support.annotation.Nullable;
  import android.util.Log;
  
  import com.achep.base.Build;
  import com.achep.base.interfaces.IOnLowMemory;
  import com.achep.base.interfaces.ISubscriptable;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.Set;
  
  /**
   * Simple list which automatically saves items to private storage and restores on initialize.
   * This may be useful for implementing blacklists or something fun.
   *
   * @author Artem Chepurnoy
   */
  public abstract class SharedList<V, T extends SharedList.Saver<V>> implements
          ISubscriptable<SharedList.OnSharedListChangedListener<V>>,
          IOnLowMemory, Iterable<V> {
  
      private static final String TAG = "SharedList";
  
      /**
       * Key's prefix for SharedList's internal usage.
       */
      private static final String KEY_PREFIX = "__";
  
      private static final String KEY_NUMBER = KEY_PREFIX + "n";
      private static final String KEY_USED_ITEM = KEY_PREFIX + "used_";
  
      private HashMap<V, Integer> mList;
      private ArrayList<Integer> mPlaceholder;
  
      private ArrayList<OnSharedListChangedListener<V>> mListeners;
  
      private boolean mRecyclableCreated;
      private Comparator<V> mComparator;
      private T mSaver;
  
      /**
       * Interface definition for a callback to be invoked
       * when a shared list changed.
       *
       * @author Artem Chepurnoy
       * @see SharedList
       * @see SharedList#registerListener(SharedList.OnSharedListChangedListener)
       * @see SharedList#unregisterListener(SharedList.OnSharedListChangedListener)
       */
      public interface OnSharedListChangedListener<V> {
  
          /**
           * Called on object put to / replaced in the list.
           *
           * @param objectNew current object
           * @param objectOld old object from the list
           * @param diff      the difference between old and new objects (provided by {@link SharedList.Comparator})
           */
          public void onPut(@NonNull V objectNew, @Nullable V objectOld, int diff);
  
          /**
           * Called on object removed from the list.
           *
           * @param objectRemoved removed object from the list
           */
          public void onRemoved(@NonNull V objectRemoved);
      }
  
      /**
       * The provider of the diffs between "old" and "new" objects in list.
       *
       * @author Artem Chepurnoy
       * @see OnSharedListChangedListener#onPut(Object, Object, int)
       */
      public static abstract class Comparator<V> {
  
          /**
           * Compares old and new object and returns the difference between them.
           *
           * @return The difference between old and new objects.
           */
          public abstract int compare(@NonNull V object, @Nullable V old);
      }
  
      /**
       * Skeleton of the saver class which needed to store and get values
       * into the {@link android.content.SharedPreferences}.
       *
       * @author Artem Chepurnoy
       */
      // I could use Parcelable for that too.
      public static abstract class Saver<V> {
  
          /**
           * Should put object's data to given shared prefs editor.
           * <b>Note:</b> This should not write any values with
           * a key starting with {@link #KEY_PREFIX}!
           *
           * @param position position of given object in list
           * @see #get(android.content.SharedPreferences, int)
           */
          @NonNull
          public abstract SharedPreferences.Editor put(@NonNull V object,
                                                       @NonNull SharedPreferences.Editor editor,
                                                       int position);
  
          /**
           * Restores previously save Object from shared preferences.
           *
           * @param position position of given object in list
           * @see #put(Object, android.content.SharedPreferences.Editor, int)
           */
          public abstract V get(@NonNull SharedPreferences prefs, int position);
  
      }
  
      /**
       * Note, that you must unregister your listener lately.
       *
       * @see #unregisterListener(SharedList.OnSharedListChangedListener)
       * @see SharedList.OnSharedListChangedListener
       */
      @Override
      public void registerListener(@NonNull OnSharedListChangedListener<V> listener) {
          mListeners.add(listener);
      }
  
      /**
       * Unregisters previously registered listener.
       *
       * @see #registerListener(SharedList.OnSharedListChangedListener)
       * @see SharedList.OnSharedListChangedListener
       */
      @Override
      public void unregisterListener(@NonNull OnSharedListChangedListener<V> listener) {
          mListeners.remove(listener);
      }
  
      protected SharedList() { /* You must call #init(Context) later! */ }
  
      protected SharedList(@NonNull Context context) {
          init(context);
      }
  
      protected void init(@NonNull Context context) {
          mList = new HashMap<>();
          mPlaceholder = new ArrayList<>(3);
          mListeners = new ArrayList<>(6);
  
          createRecyclableFields();
  
          // Restore previously saved list.
          SharedPreferences prefs = getSharedPreferences(context);
          final int n = prefs.getInt(KEY_NUMBER, 0);
          for (int i = 0; i < n; i++) {
              if (prefs.getBoolean(KEY_USED_ITEM + i, false)) {
                  // Create previously saved object.
                  V object = mSaver.get(prefs, i);
                  mList.put(object, i);
              } else {
                  // This is an empty place which we can re-use
                  // later.
                  mPlaceholder.add(i);
              }
          }
      }
  
      @NonNull
      private SharedPreferences getSharedPreferences(@NonNull Context context) {
          return context.getSharedPreferences(getPreferencesFileName(), Context.MODE_PRIVATE);
      }
  
      /**
       * @return the name of the shared list's file.
       * @see #getSharedPreferences(android.content.Context)
       */
      @NonNull
      protected abstract String getPreferencesFileName();
  
      /**
       * @return Instance of saver which will save your Object to shared preferences.
       * @see Saver
       */
      @NonNull
      protected abstract T onCreateSaver();
  
      /**
       * @return The comparator of this shared list (may be null.)
       * @see OnSharedListChangedListener#onPut(Object, Object, int)
       * @see #put(android.content.Context, Object)
       * @see #put(android.content.Context, Object, OnSharedListChangedListener)
       * @see #getComparator()
       */
      @Nullable
      protected Comparator<V> onCreateComparator() {
          return null;
      }
  
      /**
       * @return Previously created comparator.
       * @see #onCreateComparator()
       */
      @Nullable
      public Comparator<V> getComparator() {
          return mComparator;
      }
  
      protected boolean isOverwriteAllowed(@NonNull V object) {
          return false;
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onLowMemory() {
          mRecyclableCreated = false;
          // This probably won't free a lot, but
          // yes, we can do it.
          mComparator = null;
          mSaver = null;
      }
  
      private void createRecyclableFields() {
          if (mRecyclableCreated & (mRecyclableCreated = true)) return;
          mComparator = onCreateComparator();
          mSaver = onCreateSaver();
      }
  
      public void remove(@NonNull Context context, V object) {
          remove(context, object, null);
      }
  
      public void remove(@NonNull Context context, V object, @Nullable OnSharedListChangedListener l) {
          if (!mList.containsKey(object)) {
              Log.w(TAG, "Tried to remove non-existing object from the list.");
              return;
          }
  
          V objectRemoved = find(object);
          assert objectRemoved != null; // Defined by the condition above
          int pos = mList.remove(object);
  
          // Put the position of newly removed object
          // to sorted list (keeping it sorted).
          int i = 0;
          final int size = mPlaceholder.size();
          for (; i < size; i++)
              if (mPlaceholder.get(i) > pos)
                  break;
          mPlaceholder.add(i, pos);
  
          // Mark this item as unused, so we can restore placeholders too.
          getSharedPreferences(context).edit()
                  .putBoolean(KEY_USED_ITEM + pos, false)
                  .apply();
  
          notifyOnRemoved(objectRemoved, l);
      }
  
      @Nullable
      public V put(@NonNull Context context, @NonNull V object) {
          return put(context, object, null);
      }
  
      @Nullable
      public V put(@NonNull Context context, @NonNull V object, @Nullable OnSharedListChangedListener l) {
          boolean growUp = false;
          int pos;
  
          V old = null;
          if (contains(object)) {
              // This is completely useless if equality-checking
              // method had been implemented correctly (content truly equals).
              if (!isOverwriteAllowed(object)) {
                  if (Build.DEBUG) Log.w(TAG, "Trying to put an existing object to the shared list.");
                  return null; // Do nothing.
              }
  
              // Search for an old object...
              old = find(object);
  
              // Remember the position of old object
              // and pop it out.
              pos = mList.get(old);
              mList.remove(old);
          } else {
  
              // Increase the size of the list if there no
              // empty place, that we can use.
              growUp = mPlaceholder.size() == 0;
  
              // Get where-to-save this object.
              if (!growUp) {
                  pos = mPlaceholder.get(0);
                  mPlaceholder.remove(0);
              } else {
                  pos = mList.size();
              }
          }
  
          mList.put(object, pos);
          createRecyclableFields();
  
          // Save object to internal memory.
          SharedPreferences.Editor editor = mSaver
                  .put(object, getSharedPreferences(context).edit(), pos)
                  .putBoolean(KEY_USED_ITEM + pos, true);
          if (growUp) editor.putInt(KEY_NUMBER, mList.size());
          editor.apply();
  
          notifyOnPut(object, old, l);
          return old;
      }
  
      @Nullable
      private V find(@NonNull V object) {
          for (V o : mList.keySet()) {
              if (o.equals(object)) {
                  return o;
              }
          }
          return null;
      }
  
      /**
       * Notifies {@link #registerListener(OnSharedListChangedListener) registered} listeners
       * about removed from list object.
       *
       * @param objectRemoved removed object from the list
       * @param l             Listener that will be ignored while notifying.
       */
      protected void notifyOnRemoved(@NonNull V objectRemoved,
                                     @Nullable OnSharedListChangedListener l) {
          for (OnSharedListChangedListener<V> listener : mListeners) {
              if (listener == l) continue;
              listener.onRemoved(objectRemoved);
          }
      }
  
      /**
       * Notifies {@link #registerListener(OnSharedListChangedListener) registered} listeners
       * that list got one more item / or one item is overwritten.
       *
       * @param object new object
       * @param old    old object from the list
       * @param l      Listener that will be ignored while notifying.
       */
      protected void notifyOnPut(V object, V old, @Nullable OnSharedListChangedListener l) {
          createRecyclableFields();
  
          int diff = mComparator != null ? mComparator.compare(object, old) : 0;
          for (OnSharedListChangedListener<V> listener : mListeners) {
              if (listener == l) continue;
              listener.onPut(object, old, diff);
          }
      }
  
      /**
       * Returns whether this list contains the specified object.
       *
       * @return {@code true} if this list contains the specified object, {@code true} otherwise.
       */
      public boolean contains(@Nullable V object) {
          return mList.containsKey(object);
      }
  
      @NonNull
      public Set<V> values() {
          return mList.keySet();
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public Iterator<V> iterator() {
          return values().iterator();
      }
  
  }
  