package fi.bitrite.android.ws.ui.listadapter;

import android.annotation.SuppressLint;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;

/**
 * A generic RecyclerView adapter that uses Data Binding & DiffUtil.
 *
 * @param <T> Type of the items in the list
 * @param <V> Type of the Binding
 */
public abstract class DataBoundListAdapter<T, V extends DataBoundListAdapter.ViewDataBinding<T>>
        extends RecyclerView.Adapter<DataBoundListAdapter<T, V>.ViewHolder> {

    public interface ViewDataBinding<T> {
        View getRoot();

        void bind(T item);
    }

    /**
     * A generic ViewHolder that works with a {@link ViewDataBinding<T>}.
     */
    class ViewHolder extends RecyclerView.ViewHolder {
        final V binding;

        ViewHolder(V binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }


    @Nullable
    private List<T> items;

    private int mReservedItemCount = -1;

    // Each time data is set, we update this variable so that if DiffUtil calculation returns
    // after repetitive updates, we can ignore the old calculation.
    private int dataVersion = 0;

    @Override
    public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        V binding = createBinding(parent);
        return new ViewHolder(binding);
    }

    protected abstract V createBinding(ViewGroup parent);

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        if (items != null && position < items.size()) {
            holder.binding.bind(items.get(position));
        }
    }

    @SuppressLint("StaticFieldLeak")
    @MainThread
    public void replace(List<T> update) {
        replaceRx(update)
                .onErrorComplete() // Ignore errors
                .subscribe();
    }

    @MainThread
    public Completable replaceRx(List<T> update) {
        return Completable.create(emitter -> {
            dataVersion++;

            mReservedItemCount = -1;

            if (update != null) {
                sort(update);
            }

            if (items == null) {
                if (update != null) {
                    items = update;
                    notifyDataSetChanged();
                }
                emitter.onComplete();
            } else if (update == null) {
                int oldSize = items.size();
                items = null;
                notifyItemRangeRemoved(0, oldSize);
                emitter.onComplete();
            } else {
                final int startVersion = dataVersion;
                final List<T> oldItems = items;

                Single.<DiffUtil.DiffResult>create(emitter2 -> emitter2.onSuccess(
                        DiffUtil.calculateDiff(new DiffUtil.Callback() {
                            @Override
                            public int getOldListSize() {
                                return oldItems.size();
                            }

                            @Override
                            public int getNewListSize() {
                                return update.size();
                            }

                            @Override
                            public boolean areItemsTheSame(int oldItemPosition,
                                                           int newItemPosition) {
                                T oldItem = oldItems.get(oldItemPosition);
                                T newItem = update.get(newItemPosition);
                                return DataBoundListAdapter.this.areItemsTheSame(oldItem, newItem);
                            }

                            @Override
                            public boolean areContentsTheSame(int oldItemPosition,
                                                              int newItemPosition) {
                                T oldItem = oldItems.get(oldItemPosition);
                                T newItem = update.get(newItemPosition);
                                return DataBoundListAdapter.this.areContentsTheSame(oldItem,
                                        newItem);
                            }
                        })))
                        .subscribeOn(Schedulers.computation())
                        .observeOn(AndroidSchedulers.mainThread())
                        .map(diffResult -> {
                            if (startVersion != dataVersion) {
                                // ignore update
                                return 0;
                            }
                            items = update;
                            diffResult.dispatchUpdatesTo(DataBoundListAdapter.this);
                            return 0;
                        })
                        .toCompletable()
                        .subscribe(emitter::onComplete);
            }
        }).subscribeOn(AndroidSchedulers.mainThread());
    }


    protected abstract void sort(List<T> data);

    /**
     * Should return true when two items represent the same element (identified by the same id).
     */
    protected abstract boolean areItemsTheSame(T oldItem, T newItem);

    /**
     * Should return true if two items do not differ in the sense that an update to the UI is
     * necessary.
     */
    protected abstract boolean areContentsTheSame(T oldItem, T newItem);

    public void reserveItems(int count) {
        mReservedItemCount = count;
    }

    @Override
    public int getItemCount() {
        return mReservedItemCount >= 0
                ? mReservedItemCount
                : items == null ? 0 : items.size();
    }
}

@@ -338,16 +338,66 @@
 il.List;
+%0Aimport java.util.concurrent.atomic.AtomicInteger;
 %0A%0Aimport
@@ -1317,17 +1317,18 @@
 List%3CT%3E 
-i
+mI
 tems;%0A%0A 
@@ -1546,27 +1546,57 @@
 ate 
-int dataVersion = 0
+AtomicInteger mDataVersion = new AtomicInteger(0)
 ;%0A%0A 
@@ -1928,25 +1928,26 @@
         if (
-i
+mI
 tems != null
@@ -1961,17 +1961,18 @@
 ition %3C 
-i
+mI
 tems.siz
@@ -2010,17 +2010,18 @@
 ng.bind(
-i
+mI
 tems.get
@@ -2319,32 +2319,96 @@
 st%3CT%3E update) %7B%0A
+        final int dataVersion = mDataVersion.incrementAndGet();%0A
         return C
@@ -2442,36 +2442,8 @@
 %3E %7B%0A
-            dataVersion++;%0A%0A
     
@@ -2571,17 +2571,18 @@
     if (
-i
+mI
 tems == 
@@ -2639,33 +2639,34 @@
                 
-i
+mI
 tems = update;%0A 
@@ -2835,17 +2835,18 @@
 dSize = 
-i
+mI
 tems.siz
@@ -2862,25 +2862,26 @@
             
-i
+mI
 tems = null;
@@ -3018,88 +3018,90 @@
 nal 
-int startVersion = dataVersion;%0A                final List%3CT%3E oldItems = items;%0A
+List%3CT%3E oldItems = mItems;%0A%0A                // FIXME(saemy): Detach at some point?
 %0A   
@@ -4863,13 +4863,12 @@
 if (
-start
+data
 Vers
@@ -4874,17 +4874,18 @@
 sion != 
-d
+mD
 ataVersi
@@ -4886,16 +4886,22 @@
 aVersion
+.get()
 ) %7B%0A    
@@ -5045,17 +5045,18 @@
         
-i
+mI
 tems = u
@@ -5307,16 +5307,34 @@
 Complete
+, emitter::onError
 );%0A     
@@ -6066,17 +6066,18 @@
       : 
-i
+mI
 tems == 
@@ -6087,17 +6087,18 @@
 l ? 0 : 
-i
+mI
 tems.siz

Make DataBoundListAdapter thread safe