/*
 * @copyright 2012 Philip Warner
 * @license GNU General Public License
 * 
 * This file is part of Book Catalogue.
 *
 * Book Catalogue 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 3 of the License, or
 * (at your option) any later version.
 *
 * Book Catalogue 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 Book Catalogue.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.eleybourn.bookcatalogue;

import static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.DOM_READ;
import static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.DOM_TITLE;
import static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.TBL_BOOKS;

import java.util.ArrayList;
import java.util.Iterator;

import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.eleybourn.bookcatalogue.BooksMultitypeListHandler.BooklistChangeListener;
import com.eleybourn.bookcatalogue.booklist.BooklistBuilder;
import com.eleybourn.bookcatalogue.booklist.BooklistBuilder.BookRowInfo;
import com.eleybourn.bookcatalogue.booklist.BooklistGroup.RowKinds;
import com.eleybourn.bookcatalogue.booklist.BooklistPreferencesActivity;
import com.eleybourn.bookcatalogue.booklist.BooklistPseudoCursor;
import com.eleybourn.bookcatalogue.booklist.BooklistStyle;
import com.eleybourn.bookcatalogue.booklist.BooklistStylePropertiesActivity;
import com.eleybourn.bookcatalogue.booklist.BooklistStyles;
import com.eleybourn.bookcatalogue.compat.BookCatalogueActivity;
import com.eleybourn.bookcatalogue.debug.Tracker;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogItem;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogMenuItem;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogOnClickListener;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsUtils;
import com.eleybourn.bookcatalogue.utils.HintManager;
import com.eleybourn.bookcatalogue.utils.Logger;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTask;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTaskContext;
import com.eleybourn.bookcatalogue.utils.TrackedCursor;
import com.eleybourn.bookcatalogue.utils.Utils;
import com.eleybourn.bookcatalogue.utils.ViewTagger;

/**
 * Activity that displays a flattened book hierarchy based on the Booklist* classes.
 * 
 * @author Philip Warner
 */
public class BooksOnBookshelf extends BookCatalogueActivity implements BooklistChangeListener {
	/** Counter for com.eleybourn.bookcatalogue.debug purposes */
	private static Integer mInstanceCount = 0;

	/** Prefix used in preferences for this activity */
	private final static String TAG = "BooksOnBookshelf";

	/** Preference name */
	public final static String PREF_BOOKSHELF = TAG + ".BOOKSHELF";
	/** Preference name */
	private final static String PREF_TOP_ROW = TAG + ".TOP_ROW";
	/** Preference name */
	private final static String PREF_TOP_ROW_TOP = TAG + ".TOP_ROW_TOP";
	/** Preference name */
	private final static String PREF_LIST_STYLE = TAG + ".LIST_STYLE";

	/** Currently selected bookshelf */
	private String mCurrentBookshelf = ""; //getString(R.string.all_books);
	/** Currently selected list style */
	BooklistStyle mCurrentStyle = null;

	/** Flag indicating activity has been destroyed. Used for background tasks */
	private boolean mIsDead = false;
	/** Flag to indicate that a list has been successfully loaded -- affects the way we save state */
	private boolean mListHasBeenLoaded = false;

	/** Used by onScroll to detect when the top row has actuallt changed. */
	private int mLastTop = -1;
	/** ProgressDialog used to display "Getting books...". Needed here so we can dismiss it on close. */
	private ProgressDialog mListDialog = null;
	/** A book ID used for keeping/updating current list position, eg. when a book is edited. */
	private long mMarkBookId = 0;
	/** Text to use in search query */
	private String mSearchText = "";
	/** Saved position of last top row */
	private int mTopRow = 0;
	/** Saved position of last top row offset from view top */
	private int mTopRowTop = 0;

	/** Database connection */
	private CatalogueDBAdapter mDb;

	/** Handler to manage all Views on the list */
	private BooksMultitypeListHandler mListHandler;
	/** Current displayed list cursor */
	private BooklistPseudoCursor mList;
	/** Multi-type adapter to manage list connection to cursor */
	private MultitypeListAdapter mAdapter;
	/** Task queue to get book lists in background */
	private SimpleTaskQueue mTaskQueue = new SimpleTaskQueue("BoB-List", 1);
	/** Preferred booklist state in next rebuild */
	private int mRebuildState;

	/** Total number of books in current list */
	private int mTotalBooks = 0;
	/** Total number of unique books in current list */
	private int mUniqueBooks = 0;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		Tracker.enterOnCreate(this);
		try {
			super.onCreate(savedInstanceState);
	
			if (savedInstanceState == null)
				// Get preferred booklist state to use from preferences; default to always expanded (MUCH faster than 'preserve' with lots of books)
				mRebuildState = BooklistPreferencesActivity.getRebuildState();
			else
				// Always preserve state when rebuilding/recreating etc
				mRebuildState = BooklistPreferencesActivity.BOOKLISTS_STATE_PRESERVED;
	
			mDb = new CatalogueDBAdapter(this);
			mDb.open();
	
			// Extract the sort type from the bundle. getInt will return 0 if there is no attribute 
			// sort (which is exactly what we want)
			try {
				BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
				// Restore bookshelf and position
				mCurrentBookshelf = prefs.getString(PREF_BOOKSHELF, mCurrentBookshelf);
				mTopRow = prefs.getInt(PREF_TOP_ROW, 0);
				mTopRowTop = prefs.getInt(PREF_TOP_ROW_TOP, 0);
			} catch (Exception e) {
				Logger.logError(e);
			}
	
			// Restore view style
			refreshStyle();
	
			// This sets the search capability to local (application) search
			setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
			
			// This sets the search capability to local (application) search
			setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
			setContentView(R.layout.booksonbookshelf);
	
			Intent intent = getIntent();
			if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
				// Return the search results instead of all books (for the bookshelf)
				mSearchText = intent.getStringExtra(SearchManager.QUERY).trim();
			} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
				// Handle a suggestions click (because the suggestions all use ACTION_VIEW)
				mSearchText = intent.getDataString();
			}
			if (mSearchText == null || mSearchText.equals(".")) {
				mSearchText = "";
			}

			TextView searchTextView = (TextView) findViewById(R.id.search_text);
			if (mSearchText.equals("")) {
				searchTextView.setVisibility(View.GONE);
			} else {
				searchTextView.setVisibility(View.VISIBLE);
				searchTextView.setText(getString(R.string.search) + ": " + mSearchText);
			}

			// We want context menus to be available
			getListView().setOnItemLongClickListener(new OnItemLongClickListener() {
				@Override
				public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {
					mList.moveToPosition(position);
					ArrayList<SimpleDialogItem> menu = new ArrayList<SimpleDialogItem>();
					mListHandler.buildContextMenu(mList.getRowView(), menu);
					StandardDialogs.selectItemDialog(getLayoutInflater(), null, menu, null, new SimpleDialogOnClickListener() {
						@Override
						public void onClick(SimpleDialogItem item) {
							mList.moveToPosition(position);
							int id = ((SimpleDialogMenuItem)item).getItemId();
							mListHandler.onContextItemSelected(mDb, mList.getRowView(), BooksOnBookshelf.this, mDb, id);
						}});
					return true;
				}
			});
	
			// use the custom fast scroller (the ListView in the XML is our custome version).
			getListView().setFastScrollEnabled(true);
	
			// Handle item click events
			getListView().setOnItemClickListener(new OnItemClickListener() {
				@Override
				public void onItemClick(AdapterView<?> arg0, View view, int position, long rowId) {
					handleItemClick(arg0, view, position, rowId);
				}});
			
			// Debug; makes list structures vary across calls to ensure code is correct...
			mMarkBookId = -1;
			
			// This will cause the list to be generated.
			initBookshelfSpinner();
			setupList(true);

			if (savedInstanceState == null) {
				HintManager.displayHint(this, R.string.hint_view_only_book_details, null);
				HintManager.displayHint(this, R.string.hint_book_list, null);
			}
		} finally {
			Tracker.exitOnCreate(this);
		}
	}

	/**
	 * Support routine now that this activity is no longer a ListActivity
	 */
	private ListView getListView() {
		return (ListView)findViewById(android.R.id.list);
	}

	/**
	 * Handle a list item being clicked.
	 * 
	 * @param arg0		Parent adapter
	 * @param view		Row View that was clicked
	 * @param position	Position of view in listView
	 * @param rowId		_id field from cursor
	 */
	private void handleItemClick(AdapterView<?> arg0, View view, int position, long rowId) {
		// Move the cursor to the position
		mList.moveToPosition(position);
		// If it's a book, edit it.
		if (mList.getRowView().getKind() == RowKinds.ROW_KIND_BOOK) {
			BookEdit.openBook(this, mList.getRowView().getBookId(), mList.getBuilder(), position);
//			boolean isReadOnly = BookCatalogueApp.getAppPreferences()
//					.getBoolean(BookCataloguePreferences.PREF_OPEN_BOOK_READ_ONLY, false);
//			if (isReadOnly){
//				BookEdit.viewBook(this, mList.getRowView().getBookId());
//			} else {
//				BookEdit.editBook(this, mList.getRowView().getBookId(), BookEdit.TAB_EDIT);
//			}
		} else {
			// If it's leve1, expand/collapse. Technically, we could expand/collapse any level
			// but storing and recovering the view becomes unmanageable.
			if (mList.getRowView().getLevel() == 1) {
				mList.getBuilder().toggleExpandNode(mList.getRowView().getAbsolutePosition());
				mList.requery();
				mAdapter.notifyDataSetChanged();
			}
		}
	}

//	/**
//	 * Build the context menu.
//	 */
//	@Override
//	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
//		super.onCreateContextMenu(menu, v, menuInfo);
//		AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
//
//		try {
//			// Just move the cursor and call the handler to do the work.
//			mList.moveToPosition(info.position);
//			mListHandler.onCreateContextMenu(mList.getRowView(), menu);
//		} catch (NullPointerException e) {
//			Logger.logError(e);
//		}
//	}
	
	/**
	 * Handle selections from context menu
	 */
	@Override
	public boolean onContextItemSelected(android.view.MenuItem item) {
		AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
		mList.moveToPosition(info.position);
		if (mListHandler.onContextItemSelected(mDb, mList.getRowView(), this, mDb, item.getItemId()))
			return true;
		else
			return super.onContextItemSelected(item);
	}

	/**
	 * Handle the style that a user has selected.
	 * 
	 * @param name		Name of the selected style
	 */
	private void handleSelectedStyle(String name) {
		// Find the style, if no match warn user and exit
		BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
		BooklistStyle style = styles.findCanonical(name);
		if (style == null) {
			Toast.makeText(this, "Could not find appropriate list", Toast.LENGTH_LONG).show();
			return;
		}

		// Set the rebuild state like this is the first time in, which it sort of is, given we are changing style.
		// There is very little ability to preserve position when going from a list sorted by author/series to 
		// on sorted by unread/addedDate/publisher. Keeping the current row/pos is probably the most useful 
		// thing we can do since we *may* come back to a similar list.
		try {
			ListView lv = getListView();
			mTopRow = lv.getFirstVisiblePosition();
			View v = lv.getChildAt(0);
			mTopRowTop = v == null ? 0 : v.getTop();			
		} catch (Exception e) {};

		// New style, so use user-pref for rebuild
		mRebuildState = BooklistPreferencesActivity.getRebuildState();
		// Do a rebuild
		mCurrentStyle = style;
		setupList(true);
	}

	/**
	 * Background task to build and retrieve the list of books based on current settings.
	 *
	 * @author Philip Warner
	 */
	private class GetListTask implements SimpleTask {
		/** Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data */
		private final boolean mIsFullRebuild;
		/** Resulting Cursor */
		BooklistPseudoCursor mTempList = null;
		/** used to determine new cursor position */
		ArrayList<BookRowInfo> mTargetRows = null;

		/**
		 * Constructor.
		 * 
		 * @param isFullRebuild		Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data
		 */
		public GetListTask(boolean isFullRebuild) {
			mIsFullRebuild = isFullRebuild;
		}

		@Override
		public void run(SimpleTaskContext taskContext) {
			try {
				long t0 = System.currentTimeMillis();
				// Build the underlying data
				BooklistBuilder b = buildBooklist(mIsFullRebuild);
				long t1 = System.currentTimeMillis();
				// Try to sync the previously selected book ID
				if (mMarkBookId != 0) {
					// get all positions of the book
					mTargetRows = b.getBookAbsolutePositions(mMarkBookId);

					if (mTargetRows != null && mTargetRows.size() > 0) {
						// First, get the ones that are currently visible...
						ArrayList<BookRowInfo> visRows = new ArrayList<BookRowInfo>();
						for(BookRowInfo i: mTargetRows) {
							if (i.visible) {
								visRows.add(i);
							}
						}
						// If we have any visible rows, only consider them for the new position
						if (visRows.size() > 0)
							mTargetRows = visRows;
						else {
							// Make them ALL visible
							for(BookRowInfo i: mTargetRows) {
								if (!i.visible) {
									b.ensureAbsolutePositionVisible(i.absolutePosition);
								}
							}
							// Recalculate all positions
							for(BookRowInfo i: mTargetRows) {
								i.listPosition = b.getPosition(i.absolutePosition);
							}
						}

//						// Find the nearest row to the recorded 'top' row.
//						int targetRow = bookRows[0];
//						int minDist = Math.abs(mTopRow - b.getPosition(targetRow));
//						for(int i=1; i < bookRows.length; i++) {
//							int pos = b.getPosition(bookRows[i]);
//							int dist = Math.abs(mTopRow - pos);
//							if (dist < minDist)
//								targetRow = bookRows[i];
//						}
//						// Make sure the target row is visible/expanded.
//						b.ensureAbsolutePositionVisible(targetRow);
//						// Now find the position it will occupy in the view
//						mTargetPos = b.getPosition(targetRow);
					}
				} else
					mTargetRows = null;
				long t2 = System.currentTimeMillis();

				// Now we have expanded groups as needed, get the list cursor
				mTempList = b.getList();

				// Clear it so it wont be reused.
				mMarkBookId = 0;
				
				// get a count() from the cursor in background task because the setAdapter() call
				// will do a count() and potentially block the UI thread while it pages through the
				// entire cursor. If we do it here, subsequent calls will be fast.
				long t3 = System.currentTimeMillis();
				int count = mTempList.getCount();
				long t4 = System.currentTimeMillis();
				mUniqueBooks = mTempList.getUniqueBookCount();
				long t5 = System.currentTimeMillis();
				mTotalBooks = mTempList.getBookCount();
				long t6 = System.currentTimeMillis();

				System.out.println("Build: " + (t1-t0));
				System.out.println("Position: " + (t2-t1));
				System.out.println("Select: " + (t3-t2));
				System.out.println("Count(" + count + "): " + (t4-t3) + "/" + (t5-t4) + "/" + (t6-t5));
				System.out.println("====== " );
				System.out.println("Total: " + (t6-t0));
				// Save a flag to say list was loaded at least once successfully
				mListHasBeenLoaded = true;

			} finally {
				if (taskContext.isTerminating()) {
					// onFinish() will not be called, and we can discard our
					// work...
					if (mTempList != null && mTempList != mList) {
						if (mList == null || mTempList.getBuilder() != mList.getBuilder())
							try { mTempList.getBuilder().close(); } catch (Exception e)  { /* Ignore */ };

						try { mTempList.close(); } catch (Exception e)  { /* Ignore */ };

					}
				}
			}
			
		}

		@Override
		public void onFinish(Exception e) {
			// If activity dead, just do a local cleanup and exit.
			if (mIsDead) {
				mTempList.close();
				return;
			}
			// Dismiss the progress dialog, if present
			if (mListDialog != null && !mTaskQueue.hasActiveTasks()) {
				mListDialog.dismiss();
				mListDialog = null;
			}
			// Update the data
			if (mTempList != null) {
				displayList(mTempList, mTargetRows);
			}
			mTempList = null;
		}

	}
	
	/**
	 * Queue a rebuild of the underlying cursor and data.
	 * 
	 * @param isFullRebuild		Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data
	 */
	private void setupList(boolean isFullRebuild) {
		isFullRebuild = true;
		mTaskQueue.enqueue(new GetListTask(isFullRebuild));
		if (mListDialog == null) {
			mListDialog = ProgressDialog.show(this, "", getString(R.string.getting_books_ellipsis), true, true, new OnCancelListener() {
				@Override
				public void onCancel(DialogInterface dialog) {
					// Cancelling the list cancels the activity.
					BooksOnBookshelf.this.finish();
					dialog.dismiss();
					mListDialog = null;
				}});
		}
	}

	/**
	 * Set the listview background based on user preferences
	 */
	private void initBackground() {
		ListView lv = getListView();
		View root = findViewById(R.id.root);
		View header = findViewById(R.id.header);

		//
		// Sanity checks as a result of user bug report that was caused by either:
		// (a) root being null
		// or
		// (b) getResources() returning null
		//
		if (root == null)
			throw new RuntimeException("Sanity Check Fail: Root view not found; isFinishing() = " + isFinishing());
		if (header == null)
			throw new RuntimeException("Sanity Check Fail: Header view not found; isFinishing() = " + isFinishing());
		if (getResources() == null)
			throw new RuntimeException("Sanity Check Fail: getResources() returned null; isFinishing() = " + isFinishing());

		if (BooklistPreferencesActivity.isBackgroundFlat() || BookCatalogueApp.isBackgroundImageDisabled()) {
			final int backgroundColor = getResources().getColor(R.color.background_grey);
			lv.setBackgroundColor(backgroundColor);
			Utils.setCacheColorHintSafely(lv, backgroundColor);
			if (BookCatalogueApp.isBackgroundImageDisabled()) {
				root.setBackgroundColor(backgroundColor);
				header.setBackgroundColor(backgroundColor);
			} else {
				Drawable d = Utils.makeTiledBackground(false);
				root.setBackgroundDrawable(d);
				header.setBackgroundDrawable(d);
//				root.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient)));
//				header.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_vertical_gradient)));
			}
		} else {
			Utils.setCacheColorHintSafely(lv, 0x00000000);
			// ICS does not cope well with transparent ListView backgrounds with a 0 cache hint, but it does
			// seem to cope with a background image on the ListView itself.
			Drawable d = Utils.makeTiledBackground(false);
			if (Build.VERSION.SDK_INT >= 11) {
				// Honeycomb
				lv.setBackgroundDrawable(d);
//				lv.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient_dim)));
			} else {
				lv.setBackgroundColor(0x00000000);				
			}
			root.setBackgroundDrawable(d);
//			root.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient_dim)));
			header.setBackgroundColor(0x00000000);
		}
		root.invalidate();
	}
	
	/**
	 * Fix background
	 */
	@Override 
	public void onResume() {
		Tracker.enterOnResume(this);
		super.onResume();

		// Try to prevent null-pointer errors for rapidly pressing 'back'; this
		// is in response to errors reporting NullPointerException when, most likely,
		// a null is returned by getResources(). The most likely explanation for that
		// is the call occurs after Activity is destroyed.
		if (mIsDead) 
			return;

		initBackground();		
		Tracker.exitOnResume(this);
	}

	/**
	 * Display the passed cursor in the ListView, and change the position to targetRow.
	 * 
	 * @param newList		New cursor to use
	 * @param targetPos		
	 */
	private void displayList(BooklistPseudoCursor newList, final ArrayList<BookRowInfo> targetRows) {	
		if (newList == null) {
			throw new RuntimeException("Unexpected empty list");
		}

		final int showHeaderFlags = (mCurrentStyle == null ? BooklistStyle.SUMMARY_SHOW_ALL : mCurrentStyle.getShowHeaderInfo());

		initBackground();

		TextView bookCounts = (TextView)findViewById(R.id.bookshelf_count);
		if ( (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_COUNT) != 0) {
			if (mUniqueBooks != mTotalBooks) 
				bookCounts.setText("(" + this.getString(R.string.displaying_n_books_in_m_entries, mUniqueBooks, mTotalBooks) + ")");
			else
				bookCounts.setText("(" + this.getString(R.string.displaying_n_books, mUniqueBooks) + ")");
			bookCounts.setVisibility(View.VISIBLE);
		} else {
			bookCounts.setVisibility(View.GONE);
		}
			
		long t0 = System.currentTimeMillis();
		// Save the old list so we can close it later, and set the new list locally
		BooklistPseudoCursor oldList = mList;
		mList = newList;

		// Get new handler and adapter since list may be radically different structure
		mListHandler = new BooksMultitypeListHandler();
		mAdapter = new MultitypeListAdapter(this, mList, mListHandler);

		// Get the ListView and set it up
		final ListView lv = (ListView)getListView();
		final ListViewHolder lvHolder = new ListViewHolder();
		ViewTagger.setTag(lv, R.id.TAG_HOLDER, lvHolder);

		lv.setAdapter(mAdapter);
		mAdapter.notifyDataSetChanged();

		// Force a rebuild of FastScroller
		lv.setFastScrollEnabled(false);
		lv.setFastScrollEnabled(true);

		// Restore saved position
		final int count = mList.getCount();
		try {
			if (mTopRow >= count) {
				mTopRow = count-1;
				lv.setSelection(mTopRow);
			} else {
				lv.setSelectionFromTop(mTopRow, mTopRowTop);
			}			
		} catch (Exception e) {}; // Don't really care

		// If a target position array is set, then queue a runnable to set the position
		// once we know how many items appear in a typical view and once we can tell 
		// if it is already in the view.
		if (targetRows != null) {
			// post a runnable to fix the position once the control is drawn
			getListView().post(new Runnable() {
				@Override
				public void run() {
					// Find the actual extend of the current view and get centre.
					int first = lv.getFirstVisiblePosition();
					int last = lv.getLastVisiblePosition();
					int centre = (last+first)/2;
					System.out.println("New List: (" + first + ", " + last + ")<-" + centre );
					// Get the first 'target' and make it 'best candidate'
					BookRowInfo best = targetRows.get(0);
					int dist = Math.abs(best.listPosition - centre);
					// Scan all other rows, looking for a nearer one
					for (int i = 1; i < targetRows.size(); i++) {
						BookRowInfo ri = targetRows.get(i);
						int newDist = Math.abs(ri.listPosition - centre);
						if (newDist < dist) {
							dist = newDist;
							best = ri;
						}
					}

					System.out.println("Best @" + best.listPosition );
					// Try to put at top if not already visible, or only partially visible
					if (first >= best.listPosition || last <= best.listPosition) {
						System.out.println("Adjusting position");
						//
						// setSelectionfromTop does not seem to always do what is expected.
						// But adding smoothScrollToPosition seems to get the job done reasonably well.
						//
						// Specific problem occurs if:
						// - put phone in portrait mode
						// - edit a book near bottom of list
						// - turn phone to landscape
						// - save the book (don't cancel)
						// Book will be off bottom of screen without the smoothScroll in the second Runnable.
						//
						lv.setSelectionFromTop(best.listPosition, 0);
						// Code below does not behave as expected. Results in items often being near bottom.
						//lv.setSelectionFromTop(best.listPosition, lv.getHeight() / 2);
						
						// smoothScrollToPosition is only available at API level 8.
						// Without this call some positioning may be off by one row (see above).
						if (android.os.Build.VERSION.SDK_INT >= 8) {
							final int newPos = best.listPosition;
							getListView().post(new Runnable() {
								@TargetApi(8)
								@Override
								public void run() {
									lv.smoothScrollToPosition(newPos);
								}});
						}

						//int newTop = best.listPosition - (last-first)/2;
						//System.out.println("New Top @" + newTop );
						//lv.setSelection(newTop);
					}
				}});
			//}
		}

		final boolean hasLevel1 = (mList.numLevels() > 1);
		final boolean hasLevel2 = (mList.numLevels() > 2);

		if ( hasLevel2 && (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_LEVEL_2) != 0 ) {
			lvHolder.level2Text.setVisibility(View.VISIBLE);
			lvHolder.level2Text.setText("");
		} else {
			lvHolder.level2Text.setVisibility(View.GONE);
		}
		if (hasLevel1 && (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_LEVEL_1) != 0) {
			lvHolder.level1Text.setVisibility(View.VISIBLE);
			lvHolder.level1Text.setText("");
		} else
			lvHolder.level1Text.setVisibility(View.GONE);

		// Update the header details
		if (count > 0 && (showHeaderFlags & (BooklistStyle.SUMMARY_SHOW_LEVEL_1 ^ BooklistStyle.SUMMARY_SHOW_LEVEL_2)) != 0)
			updateListHeader(lvHolder, mTopRow, hasLevel1, hasLevel2, showHeaderFlags);

		// Define a scroller to update header detail when top row changes
		lv.setOnScrollListener(new OnScrollListener() {
			@Override
			public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
				// TODO: Investigate why BooklistPseudoCursor causes a scroll even when it is closed!
				// Need to check isDead because BooklistPseudoCursor misbehaves when activity terminates and closes cursor
				if (mLastTop != firstVisibleItem && !mIsDead && (showHeaderFlags != 0) ) {
					ListViewHolder holder = (ListViewHolder)ViewTagger.getTag(view, R.id.TAG_HOLDER);
					updateListHeader(holder, firstVisibleItem, hasLevel1, hasLevel2, showHeaderFlags);
				}
			}

			@Override
			public void onScrollStateChanged(AbsListView view, int scrollState) {
			}}
		);

		if (mCurrentStyle == null)
			this.getSupportActionBar().setSubtitle("");
		else
			this.getSupportActionBar().setSubtitle(mCurrentStyle.getDisplayName());
			
		// Close old list
		if (oldList != null) {
			if (mList.getBuilder() != oldList.getBuilder())
				oldList.getBuilder().close();
			oldList.close();
		}
		long t1 = System.currentTimeMillis();
		System.out.println("displayList: " + (t1 - t0));
	}

	/**
	 * Update the list header to match the current top item.
	 * 
	 * @param holder		Holder object for header
	 * @param topItem		Top row
	 * @param hasLevel1		flag indicating level 1 is present
	 * @param hasLevel2		flag indicating level 2 is present
	 */
	private void updateListHeader(ListViewHolder holder, int topItem, boolean hasLevel1, boolean hasLevel2, int flags) {
		if (topItem < 0)
			topItem = 0;

		mLastTop = topItem;
		if (hasLevel1 && ( flags & BooklistStyle.SUMMARY_SHOW_LEVEL_1) != 0) {
			if ( mList.moveToPosition(topItem) ) {
				holder.level1Text.setText(mList.getRowView().getLevel1Data());
				String s = null;
				if (hasLevel2 && ( flags & BooklistStyle.SUMMARY_SHOW_LEVEL_2) != 0 ) {
					s = mList.getRowView().getLevel2Data();
					holder.level2Text.setText(s);
				}				
			}
		}		
	}

	/**
	 * Build the underlying flattened list of books.
	 * 
	 * @param isFullRebuild		Indicates a complete structural rebuild is required
	 *
	 * @return 	The BooklistBuilder object used to build the data
	 */
	private BooklistBuilder buildBooklist(boolean isFullRebuild) {
		// If not a full rebuild then just use the current builder to requery the underlying data
		if (mList != null && !isFullRebuild) {
			System.out.println("Doing rebuild()");
			BooklistBuilder b = mList.getBuilder();
			b.rebuild();
			return b;
		} else {
			System.out.println("Doing full reconstruct");
			// Make sure we have a style chosen
			BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
			if (mCurrentStyle == null) {
				String prefStyle = BookCatalogueApp.getAppPreferences().getString(BookCataloguePreferences.PREF_BOOKLIST_STYLE, getString(R.string.sort_author_series));
				mCurrentStyle = styles.findCanonical(prefStyle);
				if (mCurrentStyle == null)
					mCurrentStyle = styles.get(0);
				BookCatalogueApp.getAppPreferences().setString(BookCataloguePreferences.PREF_BOOKLIST_STYLE, mCurrentStyle.getCanonicalName());
			}

			// get a new builder and add the required extra domains
			BooklistBuilder builder = new BooklistBuilder(mDb, mCurrentStyle);

			builder.requireDomain(DOM_TITLE, TBL_BOOKS.dot(DOM_TITLE), true);
			builder.requireDomain(DOM_READ, TBL_BOOKS.dot(DOM_READ), false);
			
			// Build based on our current criteria and return
			builder.build(mRebuildState, mMarkBookId, mCurrentBookshelf, "", "", "", "", mSearchText);

			// After first build, always preserve this object state
			mRebuildState = BooklistPreferencesActivity.BOOKLISTS_STATE_PRESERVED;

			return builder;			
		}
	}

	/**
	 * record to hold the current ListView header details.
	 * 
	 * @author Philip Warner
	 */
	private class ListViewHolder {
		TextView level1Text;
		TextView level2Text;
		public ListViewHolder() {
			level1Text = (TextView)findViewById(R.id.level_1_text);
			level2Text = (TextView)findViewById(R.id.level_2_text);
		}
	}

	/**
	 * Save current position information, including view nodes that are expanded.
	 * 
	 * ENHANCE: Handle positions a little better when books are deleted.
	 * 
	 * Deleting a book by 'n' authors from the last author in list results
	 * in the list decreasing in length by, potentially, n*2 items. The 
	 * current 'savePosition()' code will return to the old position in the
	 * list after such an operation...which will be too far down.
	 */
	private void savePosition() {
		if (mIsDead) 
			return;

		final Editor ed = BookCatalogueApp.getAppPreferences().edit();

		// Save position in list
		if (mListHasBeenLoaded) {
			final ListView lv = getListView();
			mTopRow = lv.getFirstVisiblePosition();
			ed.putInt(PREF_TOP_ROW, mTopRow);
			View v = lv.getChildAt(0);
			mTopRowTop = v == null ? 0 : v.getTop();
			ed.putInt(PREF_TOP_ROW_TOP, mTopRowTop);			
		}

		if (mCurrentStyle != null)
			ed.putString(PREF_LIST_STYLE, mCurrentStyle.getCanonicalName());

		ed.commit();
	}

	/**
	 * Save position when paused
	 */
	@Override
	public void onPause() {
		Tracker.enterOnPause(this);
		super.onPause();
		System.out.println("onPause");
		if (mSearchText == null || mSearchText.equals(""))
			savePosition();
		
		if (isFinishing())
			mTaskQueue.finish();

		if (mListDialog != null)
			mListDialog.dismiss();

		Tracker.exitOnPause(this);
	}

	/**
	 * Cleanup
	 */
	@Override
	public void onDestroy() {
		Tracker.enterOnDestroy(this);
		super.onDestroy();
		System.out.println("onDestroy");
		mIsDead = true;

		mTaskQueue.finish();

		try {
			if (mList != null) {
				try {
					if ( mList.getBuilder() != null)
						mList.getBuilder().close();					
				} catch (Exception e) {
					Logger.logError(e);
				}
				mList.close();
			}
			mDb.close();
		} catch (Exception e) {
			Logger.logError(e);			
		};
		mListHandler = null;
		mAdapter = null;
		mBookshelfSpinner = null;
		mBookshelfAdapter = null;
		synchronized(mInstanceCount) {
			mInstanceCount--;
			System.out.println("BoB instances: " + mInstanceCount);
		}
		TrackedCursor.dumpCursors();
		Tracker.exitOnDestroy(this);
	}
	
	/**
	 * Setup the bookshelf spinner. This function will also call fillData when 
	 * complete having loaded the appropriate bookshelf. 
	 */
	private Spinner mBookshelfSpinner;
	private ArrayAdapter<String> mBookshelfAdapter;
	
	private void initBookshelfSpinner() {
		// Setup the Bookshelf Spinner 
		mBookshelfSpinner = (Spinner) findViewById(R.id.bookshelf_name);
		mBookshelfAdapter = new ArrayAdapter<String>(this, R.layout.spinner_frontpage);
		mBookshelfAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
		mBookshelfSpinner.setAdapter(mBookshelfAdapter);
		
		// Add the default All Books bookshelf
		int pos = 0;
		int bspos = pos;
		mBookshelfAdapter.add(getString(R.string.all_books)); 
		pos++;
		
		Cursor bookshelves = mDb.fetchAllBookshelves();
		if (bookshelves.moveToFirst()) { 
			do {
				String this_bookshelf = bookshelves.getString(1);
				if (this_bookshelf.equals(mCurrentBookshelf)) {
					bspos = pos;
				}
				pos++;
				mBookshelfAdapter.add(this_bookshelf); 
			} 
			while (bookshelves.moveToNext()); 
		} 
		bookshelves.close(); // close the cursor
		// Set the current bookshelf. We use this to force the correct bookshelf after
		// the state has been restored. 
		mBookshelfSpinner.setSelection(bspos);
		
		/**
		 * This is fired whenever a bookshelf is selected. It is also fired when the 
		 * page is loaded with the default (or current) bookshelf.
		 */
		mBookshelfSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
			public void onItemSelected(AdapterView<?> parentView, View view, int position, long id) {
				// Check to see if mBookshelfAdapter is null, which should only occur if
				// the activity is being torn down: see Issue 370.
				if (mBookshelfAdapter == null)
					return;

				String new_bookshelf = mBookshelfAdapter.getItem(position);
				if (position == 0) {
					new_bookshelf = "";
				}
				if (!new_bookshelf.equalsIgnoreCase(mCurrentBookshelf)) {
					mCurrentBookshelf = new_bookshelf;
					// save the current bookshelf into the preferences
					BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
					SharedPreferences.Editor ed = prefs.edit();
					ed.putString(PREF_BOOKSHELF, mCurrentBookshelf);
					ed.commit();
					setupList(true);
				}
			}
			
			public void onNothingSelected(AdapterView<?> parentView) {
				// Do Nothing				
			}
		});
		
		ImageView bookshelfDown = (ImageView) findViewById(R.id.bookshelf_down);
		bookshelfDown.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				mBookshelfSpinner.performClick();
				return;
			}
		});
		
		TextView bookshelfNum = (TextView) findViewById(R.id.bookshelf_num);
		if (bookshelfNum != null) {
			bookshelfNum.setOnClickListener(new OnClickListener() {
				@Override
				public void onClick(View v) {
					mBookshelfSpinner.performClick();
					return;
				}
			});
		}
	}

	private MenuHandler mMenuHandler;
	private static final int MNU_SORT = MenuHandler.FIRST+1; 
	private static final int MNU_EXPAND = MenuHandler.FIRST+2; 
	private static final int MNU_COLLAPSE = MenuHandler.FIRST+3; 
	private static final int MNU_EDIT_STYLE = MenuHandler.FIRST+4; 
	private static final int MNU_GOODREADS = MenuHandler.FIRST+5; 
	
	/**
	 * Run each time the menu button is pressed. This will setup the options menu
	 */
	@Override
	public boolean onPrepareOptionsMenu(Menu menu) {
		MenuItem i;
		mMenuHandler = new MenuHandler();
		mMenuHandler.init(menu);

		mMenuHandler.addCreateBookItems(menu);

		i = mMenuHandler.addItem(menu, MNU_SORT, R.string.sort_and_style_ellipsis, android.R.drawable.ic_menu_sort_alphabetically);
		i.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
			;
		//mMenuHandler.addItem(menu, MNU_EDIT_STYLE, R.string.edit_style, android.R.drawable.ic_menu_manage);

		mMenuHandler.addItem(menu, MNU_EXPAND, R.string.menu_sort_by_author_expanded, R.drawable.ic_menu_expand);

		mMenuHandler.addItem(menu, MNU_COLLAPSE, R.string.menu_sort_by_author_collapsed, R.drawable.ic_menu_collapse);
		
		mMenuHandler.addSearchItem(menu)
					.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);

		final boolean showGr = GoodreadsManager.hasCredentials();
		if (showGr) {
			mMenuHandler.addItem(menu, MNU_GOODREADS, R.string.goodreads, R.drawable.ic_menu_gr_logo);
		}

		mMenuHandler.addCreateHelpAndAdminItems(menu);
		
		return super.onPrepareOptionsMenu(menu);
	}
	/**
	 * This will be called when a menu item is selected. A large switch statement to
	 * call the appropriate functions (or other activities) 
	 */
	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		if (mMenuHandler != null && !mMenuHandler.onMenuItemSelected(this, featureId, item)) {
			switch(item.getItemId()) {

			case MNU_SORT:
				HintManager.displayHint(this, R.string.hint_booklist_style_menu, new Runnable() {
					@Override
					public void run() {
						doSortMenu(false);
					}});
				return true;

			case MNU_EDIT_STYLE:
				doEditStyle();
				return true;

			case MNU_EXPAND:
			{
				// It is possible that the list will be empty, if so, ignore
				if (getListView().getChildCount() != 0) {
					int oldAbsPos = mListHandler.getAbsolutePosition(getListView().getChildAt(0));
					savePosition();
					mList.getBuilder().expandAll(true);
					mTopRow = mList.getBuilder().getPosition(oldAbsPos);
					BooklistPseudoCursor newList = mList.getBuilder().getList();
					displayList(newList, null);							
				}
				break;
			}
			case MNU_COLLAPSE:
			{
				// It is possible that the list will be empty, if so, ignore
				if (getListView().getChildCount() != 0) {
					int oldAbsPos = mListHandler.getAbsolutePosition(getListView().getChildAt(0));
					savePosition();
					mList.getBuilder().expandAll(false);
					mTopRow = mList.getBuilder().getPosition(oldAbsPos);
					displayList(mList.getBuilder().getList(), null);												
				}
				break;
			}
			case MNU_GOODREADS:
			{
				GoodreadsUtils.showGoodreadsOptions(this);
				break;
			}
			/*
			case INSERT_ID:
				createBook();
				return true;
			case INSERT_ISBN_ID:
				createBookISBN("isbn");
				return true;
			case INSERT_BARCODE_ID:
				createBookScan();
				return true;
			case ADMIN:
				// Start the Main Menu, not just the Admin page
				mainMenuPage();
				return true;
			case SEARCH:
				onSearchRequested();
				return true;
			case INSERT_NAME_ID:
				createBookISBN("name");
				return true;
			*/
			}			
		}
		
		return super.onMenuItemSelected(featureId, item);
	}

	/**
	 * Called when an activity launched exits, giving you the requestCode you started it with, 
	 * the resultCode it returned, and any additional data from it. 
	 */
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
		super.onActivityResult(requestCode, resultCode, intent);
		System.out.println("In onActivityResult for BooksOnBookshelf for request " + requestCode);

		mMarkBookId = 0;

		switch(requestCode) {
		case UniqueId.ACTIVITY_CREATE_BOOK_SCAN:
			try {
				if (intent != null && intent.hasExtra(CatalogueDBAdapter.KEY_ROWID)) {
					long newId = intent.getLongExtra(CatalogueDBAdapter.KEY_ROWID, 0);
					if (newId != 0) {
						mMarkBookId = newId;
					}
				}
				// Always rebuild, even after a cancelled edit because the series may have had global edits
				// ENHANCE: Allow detection of global changes to avoid unnecessary rebuilds
				this.setupList(false);
			} catch (NullPointerException e) {
				// This is not a scan result, but a normal return
				//fillData();
			}
			break;
		case UniqueId.ACTIVITY_CREATE_BOOK_ISBN:
		case UniqueId.ACTIVITY_CREATE_BOOK_MANUALLY:
		case UniqueId.ACTIVITY_VIEW_BOOK:
		case UniqueId.ACTIVITY_EDIT_BOOK:
			try {
				if (intent != null && intent.hasExtra(CatalogueDBAdapter.KEY_ROWID)) {
					long id = intent.getLongExtra(CatalogueDBAdapter.KEY_ROWID, 0);
					if (id != 0) {
						mMarkBookId = id;
					}
				}
				// Always rebuild, even after a cancelled edit because the series may have had global edits
				// ENHANCE: Allow detection of global changes to avoid unnecessary rebuilds
				this.setupList(false);
			} catch (Exception e) {
				Logger.logError(e);
			}
			break;
		case UniqueId.ACTIVITY_BOOKLIST_STYLE_PROPERTIES:
			try {
				if (intent != null && intent.hasExtra(BooklistStylePropertiesActivity.KEY_STYLE)) {
					BooklistStyle style = (BooklistStyle)intent.getSerializableExtra(BooklistStylePropertiesActivity.KEY_STYLE);
					if (style != null)
						mCurrentStyle = style;
				}
			} catch (Exception e) {
				Logger.logError(e);
			}
			this.savePosition();
			this.setupList(true);
			break;
		case UniqueId.ACTIVITY_BOOKLIST_STYLES:
		case UniqueId.ACTIVITY_ADMIN:
		case UniqueId.ACTIVITY_PREFERENCES:
			// Refresh the style because prefs may have changed
			refreshStyle();
			this.savePosition();
			this.setupList(true);
			break;
		//case ACTIVITY_SORT:
		//case ACTIVITY_ADMIN:
			/*
			try {
				// Use the ADDED_* fields if present.
				if (intent != null && intent.hasExtra(BookEditFields.ADDED_HAS_INFO)) {
					if (sort == SORT_TITLE) {
						justAdded = intent.getStringExtra(BookEditFields.ADDED_TITLE);
						int position = mDbHelper.fetchBookPositionByTitle(justAdded, bookshelf);
						adjustCurrentGroup(position, 1, true, false);
					} else if (sort == SORT_AUTHOR) {
						justAdded = intent.getStringExtra(BookEditFields.ADDED_AUTHOR);
						int position = mDbHelper.fetchAuthorPositionByName(justAdded, bookshelf);
						adjustCurrentGroup(position, 1, true, false);
					} else if (sort == SORT_AUTHOR_GIVEN) {
						justAdded = intent.getStringExtra(BookEditFields.ADDED_AUTHOR);
						int position = mDbHelper.fetchAuthorPositionByGivenName(justAdded, bookshelf);
						adjustCurrentGroup(position, 1, true, false);
					} else if (sort == SORT_SERIES) {
						justAdded = intent.getStringExtra(BookEditFields.ADDED_SERIES);
						int position = mDbHelper.fetchSeriesPositionBySeries(justAdded, bookshelf);
						adjustCurrentGroup(position, 1, true, false);
					} else if (sort == SORT_GENRE) {
						justAdded = intent.getStringExtra(BookEditFields.ADDED_GENRE);
						int position = mDbHelper.fetchGenrePositionByGenre(justAdded, bookshelf);
						adjustCurrentGroup(position, 1, true, false);
					}					
				}
			} catch (Exception e) {
				Logger.logError(e);
			}
			*/
			// We call bookshelf not fillData in case the bookshelves have been updated.
		}
		
	}

	/**
	 * Update and/or create the current style definition.
	 */
	private void refreshStyle() {
		BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
		String styleName;
		
		if (mCurrentStyle == null) {
			BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
			styleName = prefs.getString(PREF_LIST_STYLE, "");
		} else {
			styleName = mCurrentStyle.getCanonicalName();
		}

		BooklistStyle style = styles.findCanonical(styleName);
		if (style != null)
			mCurrentStyle = style;
		if (mCurrentStyle == null)
			mCurrentStyle = styles.get(0);
	}

	/**
	 * Setup the sort options. This function will also call fillData when 
	 * complete having loaded the appropriate view. 
	 */
	private void doSortMenu(final boolean showAll) {
		LayoutInflater inf = this.getLayoutInflater();
		View root = inf.inflate(R.layout.booklist_style_menu, null);
		RadioGroup group = (RadioGroup)root.findViewById(R.id.radio_buttons);
		LinearLayout main = (LinearLayout)root.findViewById(R.id.menu);

		final AlertDialog sortDialog = new AlertDialog.Builder(this).setView(root).create();
		sortDialog.setTitle(R.string.select_style);
		sortDialog.show();

		Iterator<BooklistStyle> i;
		if (!showAll) 
			i = BooklistStyles.getPreferredStyles(mDb).iterator();
		else
			i = BooklistStyles.getAllStyles(mDb).iterator();

		while(i.hasNext()) {
			BooklistStyle style = i.next();
			makeRadio(sortDialog, inf, group, style);
		}
		int moreLess;

		if (showAll)
			moreLess = R.string.show_fewer_ellipsis;
		else
			moreLess = R.string.show_more_ellipsis;

		makeText(main, inf, moreLess, new OnClickListener() {
			@Override
			public void onClick(View v) {
				sortDialog.dismiss();
				doSortMenu(!showAll);
			}});

		makeText(main, inf, R.string.customize_ellipsis, new OnClickListener() {
			@Override
			public void onClick(View v) {
				sortDialog.dismiss();
				BooklistStyles.startEditActivity(BooksOnBookshelf.this);
			}});
	}

	/**
	 * Add a radio box to the sort options dialogue.
	 * 
	 * @param sortDialog
	 * @param group
	 * @param style
	 */
	private void makeRadio (final AlertDialog sortDialog, final LayoutInflater inf, RadioGroup group, final BooklistStyle style) {
		View v = inf.inflate(R.layout.booklist_style_menu_radio, null);
		RadioButton btn = (RadioButton)v;
		btn.setText(style.getDisplayName());

		if (mCurrentStyle.getCanonicalName().equalsIgnoreCase(style.getCanonicalName())) {
			btn.setChecked(true);
		} else {
			btn.setChecked(false);
		}
		group.addView(btn);

		btn.setOnClickListener( new OnClickListener() {
			@Override
			public void onClick(View v) {
				handleSelectedStyle(style.getCanonicalName());
				sortDialog.dismiss();
				return;
			}
		});
	}

	/**
	 * Add a text box to the sort options dialogue.
	 * 
	 * @param sortDialog
	 * @param stringId
	 * @param listener
	 */
	private void makeText (final LinearLayout parent, final LayoutInflater inf, final int stringId, OnClickListener listener) {
		TextView view = (TextView)inf.inflate(R.layout.booklist_style_menu_text, null);
		Typeface tf = view.getTypeface();
		view.setTypeface(tf, Typeface.ITALIC);
		view.setText(stringId);
		view.setOnClickListener( listener );
		parent.addView(view);
	}

	/**
	 * Start the BooklistPreferences Activity
	 */
	public void doEditStyle() {
		Intent i = new Intent(this, BooklistStylePropertiesActivity.class);
		i.putExtra(BooklistStylePropertiesActivity.KEY_STYLE, mCurrentStyle);
		i.putExtra(BooklistStylePropertiesActivity.KEY_SAVE_TO_DATABASE, false);
		startActivityForResult(i, UniqueId.ACTIVITY_BOOKLIST_STYLE_PROPERTIES);		
	}

	@Override
	public void onBooklistChange(int flags) {
		if (flags != 0) {
			// Author or series changed. Just regenerate.
			savePosition();
			this.setupList(true);
		}
	}
	
	/**
	 * TODO DEBUG ONLY. Count instances
	 */
	public BooksOnBookshelf() {
		super();
		synchronized(mInstanceCount) {
			mInstanceCount++;
			System.out.println("BoB instances: " + mInstanceCount);
		}
	}
}
