package org.ebookdroid.ui.library.adapters;

import org.sufficientlysecure.viewer.R;
import org.ebookdroid.common.notifications.INotificationManager;
import org.ebookdroid.common.settings.LibSettings;
import org.ebookdroid.ui.library.IBrowserActivity;
import org.ebookdroid.ui.library.views.BookshelfView;

import android.database.DataSetObserver;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.emdev.common.filesystem.FileSystemScanner;
import org.emdev.common.filesystem.MediaManager;
import org.emdev.ui.adapters.BaseViewHolder;
import org.emdev.ui.tasks.AsyncTask;
import org.emdev.utils.FileUtils;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.StringUtils;
import org.emdev.utils.collections.SparseArrayEx;
import org.emdev.utils.collections.TLIterator;

public class BooksAdapter extends PagerAdapter implements FileSystemScanner.Listener, Iterable<BookShelfAdapter> {

    public final static int SERVICE_SHELVES = 2;

    public static final int RECENT_INDEX = 0;

    public static final int SEARCH_INDEX = 1;

    private final static AtomicInteger SEQ = new AtomicInteger(SERVICE_SHELVES);

    final IBrowserActivity base;

    final AtomicBoolean inScan = new AtomicBoolean();

    final SparseArrayEx<BookShelfAdapter> data = new SparseArrayEx<BookShelfAdapter>();

    final TreeMap<String, BookShelfAdapter> folders = new TreeMap<String, BookShelfAdapter>();

    private final RecentUpdater updater = new RecentUpdater();

    private final RecentAdapter recent;

    private final FileSystemScanner scanner;

    private final List<DataSetObserver> _dsoList = new ArrayList<DataSetObserver>();

    private String searchQuery;

    public BooksAdapter(final IBrowserActivity base, final RecentAdapter adapter) {
        this.base = base;
        this.recent = adapter;
        this.recent.registerDataSetObserver(updater);

        this.scanner = new FileSystemScanner(base.getActivity());
        this.scanner.addListener(this);
        this.scanner.addListener(base);

        this.searchQuery = LibSettings.current().searchBookQuery;
    }

    public void onDestroy() {
        scanner.shutdown();
        data.clear();
        folders.clear();
    }

    @Override
    public void destroyItem(final View collection, final int position, final Object view) {
        ((ViewPager) collection).removeView((View) view);
        ((View) view).destroyDrawingCache();
    }

    @Override
    public void finishUpdate(final View arg0) {
        // TODO Auto-generated method stub
    }

    @Override
    public TLIterator<BookShelfAdapter> iterator() {
        return data.iterator();
    }

    @Override
    public int getCount() {
        return getListCount();
    }

    @Override
    public Object instantiateItem(final View arg0, final int arg1) {
        final BookshelfView view = new BookshelfView(base, arg0, getList(arg1));
        ((ViewPager) arg0).addView(view, 0);
        return view;
    }

    @Override
    public boolean isViewFromObject(final View arg0, final Object arg1) {
        return arg0.equals(arg1);
    }

    @Override
    public void restoreState(final Parcelable arg0, final ClassLoader arg1) {
        // TODO Auto-generated method stub
    }

    @Override
    public Parcelable saveState() {
        return null;
    }

    @Override
    public void startUpdate(final View arg0) {
        // TODO Auto-generated method stub
    }

    private void addShelf(final BookShelfAdapter a) {
        data.append(a.id, a);
        folders.put(a.path, a);
        if (a.mpath != null) {
            folders.put(a.mpath, a);
        }
    }

    private void removeShelf(final BookShelfAdapter a) {
        data.remove(a.id);
        folders.remove(a.path);
        if (a.mpath != null) {
            folders.remove(a.mpath);
        }
    }

    public synchronized BookShelfAdapter getShelf(final String path) {
        final BookShelfAdapter a = folders.get(path);
        if (a != null) {
            return a;
        }
        final String mpath = FileUtils.invertMountPrefix(path);
        return mpath != null ? folders.get(path) : null;
    }

    public synchronized int getShelfPosition(final BookShelfAdapter shelf) {
        checkServiceAdapters();
        return data.indexOfValue(shelf);
    }

    public synchronized BookShelfAdapter getList(final int index) {
        return data.valueAt(index);
    }

    public synchronized int getListCount() {
        return data.size();
    }

    public synchronized int getListCount(final int currentList) {
        checkServiceAdapters();
        if (0 <= currentList && currentList < data.size()) {
            return getList(currentList).nodes.size();
        }
        return 0;
    }

    public String getListName(final int currentList) {
        checkServiceAdapters();
        final BookShelfAdapter list = getList(currentList);
        return list != null ? LengthUtils.safeString(list.name) : "";
    }

    @Override
    public CharSequence getPageTitle(final int position) {
        return getListName(position);
    }

    public String getListPath(final int currentList) {
        checkServiceAdapters();
        final BookShelfAdapter list = getList(currentList);
        return list != null ? LengthUtils.safeString(list.path) : "";
    }

    public synchronized List<String> getListNames() {
        checkServiceAdapters();

        final int size = data.size();

        if (size == 0) {
            return null;
        }

        final List<String> result = new ArrayList<String>(data.size());
        for (int index = 0; index < size; index++) {
            final BookShelfAdapter a = data.valueAt(index);
            result.add(a.name);
        }
        return result;
    }

    public synchronized List<String> getListPaths() {
        checkServiceAdapters();

        final int size = data.size();

        if (size == 0) {
            return null;
        }

        final List<String> result = new ArrayList<String>(data.size());
        for (int index = 0; index < size; index++) {
            final BookShelfAdapter a = data.valueAt(index);
            result.add(a.path);
        }
        return result;
    }

    public synchronized BookNode getItem(final int currentList, final int position) {
        checkServiceAdapters();
        if (0 <= currentList && currentList < data.size()) {
            return getList(currentList).nodes.get(position);
        }
        throw new RuntimeException("Wrong list id: " + currentList + "/" + data.size());
    }

    public long getItemId(final int position) {
        return position;
    }

    public synchronized void clearData() {
        getService(SEARCH_INDEX).nodes.clear();

        final BookShelfAdapter[] service = new BookShelfAdapter[SERVICE_SHELVES];
        for (int i = 0; i < service.length; i++) {
            service[i] = data.get(i);
        }

        data.clear();
        folders.clear();
        SEQ.set(SERVICE_SHELVES);

        for (int i = 0; i < service.length; i++) {
            if (service[i] != null) {
                data.append(i, service[i]);
            } else {
                getService(i);
            }
        }

        notifyDataSetChanged();
    }

    public synchronized void clearSearch() {
        final BookShelfAdapter search = getService(SEARCH_INDEX);
        search.nodes.clear();
        search.notifyDataSetChanged();
    }

    protected synchronized void checkServiceAdapters() {
        for (int i = 0; i < SERVICE_SHELVES; i++) {
            getService(i);
        }
    }

    protected synchronized BookShelfAdapter getService(final int index) {
        BookShelfAdapter a = data.get(index);
        if (a == null) {
            switch (index) {
                case RECENT_INDEX:
                    a = new BookShelfAdapter(base, 0, base.getContext().getString(R.string.recent_title), "");
                    break;
                case SEARCH_INDEX:
                    a = new BookShelfAdapter(base, 0, base.getContext().getString(R.string.search_results_title), "");
                    break;
            }
            if (a != null) {
                data.append(index, a);
            }
        }
        return a;
    }

    public void startScan() {
        clearData();
        final LibSettings libSettings = LibSettings.current();
        final Set<String> folders = new LinkedHashSet<String>(libSettings.autoScanDirs);
        if (libSettings.autoScanRemovableMedia) {
            folders.addAll(MediaManager.getReadableMedia());
        }
        scanner.startScan(libSettings.allowedFileTypes, folders);
    }

    public void startScan(final String path) {
        final LibSettings libSettings = LibSettings.current();
        scanner.startScan(libSettings.allowedFileTypes, path);
    }

    public void startScan(final Collection<String> paths) {
        final LibSettings libSettings = LibSettings.current();
        scanner.startScan(libSettings.allowedFileTypes, paths);
    }

    public void stopScan() {
        scanner.stopScan();
    }

    public synchronized void removeAll(final Collection<String> paths) {
        boolean found = false;
        for (final String path : paths) {
            scanner.stopObservers(path);
            found |= removeAllImpl(path);
        }
        if (found) {
            notifyDataSetChanged();
        }
    }

    public synchronized void removeAll(final String path) {
        scanner.stopObservers(path);
        if (removeAllImpl(path)) {
            notifyDataSetChanged();
        }
    }

    private boolean removeAllImpl(final String path) {
        final String ap = path + "/";
        final TLIterator<BookShelfAdapter> iter = data.iterator();
        boolean found = false;
        while (iter.hasNext()) {
            final BookShelfAdapter next = iter.next();
            final boolean eq = next.path.startsWith(ap) || next.path.equals(path) || next.mpath != null
                    && (next.mpath.startsWith(ap) || next.mpath.equals(path));
            if (eq) {
                folders.remove(next.path);
                if (next.mpath != null) {
                    folders.remove(next.mpath);
                }
                iter.remove();
                found = true;
            }
        }
        iter.release();
        return found;
    }

    public boolean startSearch(final String searchQuery) {
        this.searchQuery = LengthUtils.safeString(searchQuery).trim();
        LibSettings.updateSearchBookQuery(this.searchQuery);

        clearSearch();

        if (LengthUtils.isEmpty(this.searchQuery)) {
            return false;
        }

        if (!scanner.isScan()) {
            new SearchTask().execute("");
        }

        return true;
    }

    public String getSearchQuery() {
        return searchQuery;
    }

    protected synchronized void onNodesFound(final List<BookNode> nodes) {
        final BookShelfAdapter search = getService(SEARCH_INDEX);
        search.nodes.addAll(nodes);
        Collections.sort(search.nodes);
        search.notifyDataSetChanged();
    }

    @Override
    public synchronized void onFileScan(final File parent, final File[] files) {
        final String dir = parent.getAbsolutePath();
        BookShelfAdapter a = getShelf(dir);

        if (LengthUtils.isEmpty(files)) {
            if (a != null) {
                onDirDeleted(parent.getParentFile(), parent);
            }
            return;
        }

        boolean newShelf = false;
        if (a == null) {
            a = new BookShelfAdapter(base, SEQ.getAndIncrement(), parent.getName(), dir);
            addShelf(a);
            newShelf = true;
        }

        final BookShelfAdapter search = getService(SEARCH_INDEX);
        boolean found = false;
        for (final File f : files) {
            BookNode node = recent.getNode(f.getAbsolutePath());
            if (node == null) {
                node = new BookNode(f, null);
            }
            a.nodes.add(node);
            if (acceptSearch(node)) {
                found = true;
                search.nodes.add(node);
            }
        }
        if (newShelf) {
            notifyDataSetChanged();
        } else {
            a.notifyDataSetChanged();
        }
        if (found) {
            Collections.sort(search.nodes);
            search.notifyDataSetChanged();
        }
    }

    @Override
    public synchronized void onFileAdded(final File parent, final File f) {
        if (f == null) {
            return;
        }

        if (!LibSettings.current().allowedFileTypes.accept(f)) {
            return;
        }

        final String dir = parent.getAbsolutePath();
        boolean newShelf = false;
        BookShelfAdapter a = getShelf(dir);

        if (a == null) {
            a = new BookShelfAdapter(base, SEQ.getAndIncrement(), parent.getName(), dir);
            addShelf(a);
            newShelf = true;
        }

        BookNode node = recent.getNode(f.getAbsolutePath());
        if (node == null) {
            node = new BookNode(f, null);
        }
        a.nodes.add(node);
        Collections.sort(a.nodes);
        if (newShelf) {
            notifyDataSetChanged();
        } else {
            a.notifyDataSetChanged();
        }

        if (acceptSearch(node)) {
            final BookShelfAdapter search = getService(SEARCH_INDEX);
            search.nodes.add(node);
            Collections.sort(search.nodes);
            search.notifyDataSetChanged();
        }

        if (LibSettings.current().showNotifications) {
            INotificationManager.instance.notify(R.string.notification_file_add, f.getAbsolutePath(), null);
        }
    }

    @Override
    public synchronized void onFileDeleted(final File parent, final File f) {
        if (f == null) {
            return;
        }
        final BookShelfAdapter a = getShelf(parent.getAbsolutePath());
        if (a == null) {
            return;
        }

        final String path = f.getAbsolutePath();
        final BookShelfAdapter search = getService(SEARCH_INDEX);

        for (final Iterator<BookNode> i = a.nodes.iterator(); i.hasNext();) {
            final BookNode node = i.next();
            if (path.equals(node.path)) {
                i.remove();
                if (a.nodes.isEmpty()) {
                    removeShelf(a);
                    this.notifyDataSetChanged();
                } else {
                    a.notifyDataSetChanged();
                }
                if (search.nodes.remove(node)) {
                    search.notifyDataSetChanged();
                }
                if (LibSettings.current().showNotifications) {
                    INotificationManager.instance.notify(R.string.notification_file_delete, f.getAbsolutePath(), null);
                }
                return;
            }
        }
    }

    @Override
    public void onDirAdded(final File parent, final File f) {
        final LibSettings libSettings = LibSettings.current();
        scanner.startScan(libSettings.allowedFileTypes, f.getAbsolutePath());
    }

    @Override
    public synchronized void onDirDeleted(final File parent, final File f) {
        final String dir = f.getAbsolutePath();
        final BookShelfAdapter a = getShelf(dir);
        if (a != null) {
            removeShelf(a);
            this.notifyDataSetChanged();
        }
    }

    protected boolean acceptSearch(final BookNode node) {
        if (LengthUtils.isEmpty(searchQuery)) {
            return false;
        }
        final String bookTitle = StringUtils.cleanupTitle(node.name).toLowerCase();
        final int pos = bookTitle.indexOf(searchQuery);
        return pos >= 0;
    }

    public void registerDataSetObserver(final DataSetObserver dataSetObserver) {
        _dsoList.add(dataSetObserver);
    }

    protected void notifyDataSetInvalidated() {
        for (final DataSetObserver dso : _dsoList) {
            dso.onInvalidated();
        }
    }

    @Override
    public void notifyDataSetChanged() {
        super.notifyDataSetChanged();
        for (final DataSetObserver dso : _dsoList) {
            dso.onChanged();
        }
    }

    public static class ViewHolder extends BaseViewHolder {

        ImageView imageView;
        TextView textView;

        @Override
        public void init(final View convertView) {
            super.init(convertView);
            this.imageView = (ImageView) convertView.findViewById(R.id.thumbnailImage);
            this.textView = (TextView) convertView.findViewById(R.id.thumbnailText);
        }
    }

    private final class RecentUpdater extends DataSetObserver {

        @Override
        public void onChanged() {
            updateRecentBooks();
        }

        @Override
        public void onInvalidated() {
            updateRecentBooks();
        }

        private void updateRecentBooks() {
            final BookShelfAdapter ra = getService(RECENT_INDEX);
            ra.nodes.clear();
            final int count = recent.getCount();
            for (int i = 0; i < count; i++) {
                final BookNode book = recent.getItem(i);
                ra.nodes.add(book);
                final BookShelfAdapter a = getShelf(new File(book.path).getParent());
                if (a != null) {
                    a.notifyDataSetInvalidated();
                }
            }
            ra.notifyDataSetChanged();
            BooksAdapter.this.notifyDataSetInvalidated();
        }
    }

    class SearchTask extends AsyncTask<String, String, Void> {

        private final BlockingQueue<BookNode> queue = new ArrayBlockingQueue<BookNode>(160, true);

        @Override
        protected void onPreExecute() {
            base.showProgress(true);
        }

        @Override
        protected Void doInBackground(final String... paths) {
            int aIndex = SERVICE_SHELVES;
            while (aIndex < getListCount()) {
                int nIndex = 0;
                while (nIndex < getListCount(aIndex)) {
                    final BookNode node = getItem(aIndex, nIndex);
                    if (acceptSearch(node)) {
                        queue.offer(node);
                        publishProgress("");
                    }
                    nIndex++;
                }
                aIndex++;
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(final String... values) {
            final ArrayList<BookNode> nodes = new ArrayList<BookNode>();
            while (!queue.isEmpty()) {
                nodes.add(queue.poll());
            }
            if (!nodes.isEmpty()) {
                onNodesFound(nodes);
            }
        }

        @Override
        protected void onPostExecute(final Void v) {
            onProgressUpdate("");
            base.showProgress(false);
        }
    }
}