package com.folioreader.util;

import android.support.v7.widget.RecyclerView;

import com.folioreader.model.TOCLinkWrapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

/**
 * Multi-level expandable indentable list adapter.
 * Initially all elements in the list are single items. When you want to collapse an item and all its
 * descendants call {@link #collapseGroup(int)}. When you want to exapand a group call {@link #expandGroup(int)}.
 * Note that groups inside other groups are kept collapsed.
 *
 * To collapse an item and all its descendants or expand a group at a certain position
 * you can call {@link #toggleGroup(int)}.
 *
 * To preserve state (i.e. which items are collapsed) when a configuration change happens (e.g. screen rotation)
 * you should call {@link #saveGroups()} inside onSaveInstanceState and save the returned value into
 * the Bundle. When the activity/fragment is recreated you can call {@link #restoreGroups(List)}
 * to restore the previous state. The actual data (e.g. the comments in the sample app) is not preserved,
 * so you should save it yourself with a static field or implementing Parcelable or using setRetainInstance(true)
 * or saving data to a file or something like that.
 *
 * To see an example of how to extend this abstract class see MyAdapter.java in sampleapp.
 */
public abstract class MultiLevelExpIndListAdapter extends RecyclerView.Adapter {
    /**
     * Indicates whether or not the observers must be notified whenever
     * {@link #mData} is modified.
     */
    private boolean mNotifyOnChange;

    /**
     * List of items to display.
     */
    private List<ExpIndData> mData;

    /**
     * Map an item to the relative group.
     * e.g.: if the user click on item 6 then mGroups(item(6)) = {all items/groups below item 6}
     */
    private HashMap<ExpIndData, List<? extends ExpIndData>> mGroups;

    /**
     * Interface that every item to be displayed has to implement. If an object implements
     * this interface it means that it can be expanded/collapsed and has a level of indentation.
     * Note: some methods are commented out because they're not used here, but they should be
     * implemented if you want your data to be expandable/collapsible and indentable.
     * See MyComment in the sample app to see an example of how to implement this.
     */
    public interface ExpIndData {
        /**
         * @return The children of this item.
         */
        List<? extends ExpIndData> getChildren();

        /**
         * @return True if this item is a group.
         */
        boolean isGroup();

        /**
         * @param value True if this item is a group
         */
        void setIsGroup(boolean value);

        /**
         * @param groupSize Set the number of items in the group.
         *                  Note: groups contained in other groups are counted just as one, not
         *                        as the number of items that they contain.
         */
        void setGroupSize(int groupSize);

        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
         * that's why it's not strictly required that you implement this function and so
         * it's commented out.
         * @return The number of items in the group.
         *         Note: groups contained in other groups are counted just as one, not
         *               as the number of items that they contain.
         */
        //int getGroupSize();

        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
         * that's why it's not strictly required that you implement this function and so
         * it's commented out.
         * @return The level of indentation in the range [0, n-1]
         */
        //int getIndentation();

        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
         * that's why it's not strictly required that you implement this function and so
         * it's commented out.
         * @param indentation The level of indentation in the range [0, n-1]
         */
        //int setIndentation(int indentation);
    }

    public MultiLevelExpIndListAdapter() {
        mData = new ArrayList<ExpIndData>();
        mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
        mNotifyOnChange = true;
    }

    public MultiLevelExpIndListAdapter(ArrayList<TOCLinkWrapper> tocLinkWrappers) {
        mData = new ArrayList<ExpIndData>();
        mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
        mNotifyOnChange = true;
        mData.addAll(tocLinkWrappers);
        collapseAllTOCLinks(tocLinkWrappers);
    }

    public void add(ExpIndData item) {
        if (item != null) {
            mData.add(item);
            if (mNotifyOnChange)
                notifyItemChanged(mData.size() - 1);
        }
    }

    public void addAll(int position, Collection<? extends ExpIndData> data) {
        if (data != null && data.size() > 0) {
            mData.addAll(position, data);
            if (mNotifyOnChange)
                notifyItemRangeInserted(position, data.size());
        }
    }

    public void addAll(Collection<? extends ExpIndData> data) {
        addAll(mData.size(), data);
    }

    public void insert(int position, ExpIndData item) {
        mData.add(position, item);
        if (mNotifyOnChange)
            notifyItemInserted(position);
    }

    /**
     * Clear all items and groups.
     */
    public void clear() {
        if (mData.size() > 0) {
            int size = mData.size();
            mData.clear();
            mGroups.clear();
            if (mNotifyOnChange)
                notifyItemRangeRemoved(0, size);
        }
    }

    /**
     * Remove an item or group.If it's a group it removes also all the
     * items and groups that it contains.
     * @param item The item or group to be removed.
     * @return true if this adapter was modified by this operation, false otherwise.
     */
    public boolean remove(ExpIndData item) {
        return remove(item, false);
    }

    /**
     * Remove an item or group. If it's a group it removes also all the
     * items and groups that it contains if expandGroupBeforeRemoval is false.
     * If it's true the group is expanded and then only the item is removed.
     * @param item The item or group to be removed.
     * @param expandGroupBeforeRemoval True to expand the group before removing the item.
     *                                 False to remove also all the items and groups contained if
     *                                 the item to be removed is a group.
     * @return true if this adapter was modified by this operation, false otherwise.
     */
    public boolean remove(ExpIndData item, boolean expandGroupBeforeRemoval) {
        int index;
        boolean removed = false;
        if (item != null && (index = mData.indexOf(item)) != -1 && (removed = mData.remove(item))) {
            if (mGroups.containsKey(item)) {
                if (expandGroupBeforeRemoval)
                    expandGroup(index);
                mGroups.remove(item);
            }
            if (mNotifyOnChange)
                notifyItemRemoved(index);
        }
        return removed;
    }

    public ExpIndData getItemAt(int position) {
        return mData.get(position);
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    /**
     * Expand the group at position "posititon".
     * @param position The position (range [0,n-1]) of the group that has to be expanded
     */
    public void expandGroup(int position) {
        ExpIndData firstItem = getItemAt(position);

        if (!firstItem.isGroup()) {
            return;
        }

        // get the group of the descendants of firstItem
        List<? extends ExpIndData> group = mGroups.remove(firstItem);

        firstItem.setIsGroup(false);
        firstItem.setGroupSize(0);

        notifyItemChanged(position);
        addAll(position + 1, group);
    }

    /**
     * Collapse the descendants of the item at position "position".
     * @param position The position (range [0,n-1]) of the element that has to be collapsed
     */
    public void collapseGroup(int position) {
        ExpIndData firstItem = getItemAt(position);

        if (firstItem.getChildren() == null || firstItem.getChildren().isEmpty())
            return;

        // group containing all the descendants of firstItem
        List<ExpIndData> group = new ArrayList<ExpIndData>();
        // stack for depth first search
        List<ExpIndData> stack = new ArrayList<ExpIndData>();
        int groupSize = 0;

        for (int i = firstItem.getChildren().size() - 1; i >= 0; i--) {
            stack.add(firstItem.getChildren().get(i));
        }

        while (!stack.isEmpty()) {
            ExpIndData item = stack.remove(stack.size() - 1);
            group.add(item);
            groupSize++;
            // stop when the item is a leaf or a group
            if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
                for (int i = item.getChildren().size() - 1; i >= 0; i--) {
                    stack.add(item.getChildren().get(i));
                }
            }

            if (mData.contains(item)) mData.remove(item);
        }

        mGroups.put(firstItem, group);
        firstItem.setIsGroup(true);
        firstItem.setGroupSize(groupSize);

        notifyItemChanged(position);
        notifyItemRangeRemoved(position + 1, groupSize);
    }

    private void collapseAllTOCLinks(ArrayList<TOCLinkWrapper> tocLinkWrappers){
        if (tocLinkWrappers == null || tocLinkWrappers.isEmpty()) return;

        for (TOCLinkWrapper tocLinkWrapper:tocLinkWrappers) {
            groupTOCLink(tocLinkWrapper);
            collapseAllTOCLinks(tocLinkWrapper.getTocLinkWrappers());
        }
    }

    private void groupTOCLink(TOCLinkWrapper tocLinkWrapper){
        // group containing all the descendants of firstItem
        List<ExpIndData> group = new ArrayList<ExpIndData>();
        int groupSize = 0;
        if (tocLinkWrapper.getChildren()!=null && !tocLinkWrapper.getChildren().isEmpty()) {
            group.addAll(tocLinkWrapper.getChildren());
            groupSize = tocLinkWrapper.getChildren().size();
        }
        // stack for depth first search
        //List<ExpIndData> stack = new ArrayList<ExpIndData>();
        //int groupSize = 0;

        /*for (int i = tocLinkWrapper.getChildren().size() - 1; i >= 0; i--)
            stack.add(tocLinkWrapper.getChildren().get(i));

        while (!stack.isEmpty()) {
            ExpIndData item = stack.remove(stack.size() - 1);
            group.add(item);
            groupSize++;
            // stop when the item is a leaf or a group
            if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
                for (int i = item.getChildren().size() - 1; i >= 0; i--)
                    stack.add(item.getChildren().get(i));
            }
        }*/

        mGroups.put(tocLinkWrapper, group);
        tocLinkWrapper.setIsGroup(true);
        tocLinkWrapper.setGroupSize(groupSize);
    }
    /**
     * Collpase/expand the item at position "position"
     * @param position The position (range [0,n-1]) of the element that has to be collapsed/expanded
     */
    public void toggleGroup(int position) {
        if (getItemAt(position).isGroup()){
            expandGroup(position);
        } else {
            collapseGroup(position);
        }
    }

    /**
     * In onSaveInstanceState, you should save the groups' indices returned by this function
     * in the Bundle so that later they can be restored using {@link #restoreGroups(List)}.
     * saveGroups() expand all the groups so you should call this function only inside onSaveInstanceState.
     * @return A list of indices of items that are groups.
     */
    public ArrayList<Integer> saveGroups() {
        boolean notify = mNotifyOnChange;
        mNotifyOnChange = false;
        ArrayList<Integer> groupsIndices = new ArrayList<Integer>();
        for (int i = 0; i < mData.size(); i++) {
            if (mData.get(i).isGroup()) {
                expandGroup(i);
                groupsIndices.add(i);
            }
        }
        mNotifyOnChange = notify;
        return groupsIndices;
    }

    /**
     * Call this function to restore the groups that were collapsed before the configuration change
     * happened (e.g. screen rotation). See {@link #saveGroups()}.
     * @param groupsNum The list of indices of items that are groups and should be collapsed.
     */
    public void restoreGroups(List<Integer> groupsNum) {
        if (groupsNum == null)
            return;
        boolean notify = mNotifyOnChange;
        mNotifyOnChange = false;
        for (int i = groupsNum.size() - 1; i >= 0; i--) {
            collapseGroup(groupsNum.get(i));
        }
        mNotifyOnChange = notify;
    }
}