package com.tyndalehouse.step.core.service.impl.suggestion;
import com.tyndalehouse.step.core.data.common.TermsAndMaxCount;
import com.tyndalehouse.step.core.models.BookName;
import com.tyndalehouse.step.core.service.InternationalRangeService;
import com.tyndalehouse.step.core.service.helpers.SuggestionContext;
import com.tyndalehouse.step.core.service.jsword.JSwordPassageService;
import com.tyndalehouse.step.core.service.jsword.JSwordVersificationService;
import com.tyndalehouse.step.core.utils.JSwordUtils;
import com.tyndalehouse.step.core.utils.StringUtils;
import org.crosswire.jsword.book.Book;
import org.crosswire.jsword.book.basic.AbstractPassageBook;
import org.crosswire.jsword.passage.Key;
import org.crosswire.jsword.passage.NoSuchKeyException;
import org.crosswire.jsword.passage.Verse;
import org.crosswire.jsword.passage.VerseKey;
import org.crosswire.jsword.versification.BibleBook;
import org.crosswire.jsword.versification.DivisionName;
import org.crosswire.jsword.versification.Versification;
import javax.inject.Inject;
import java.util.*;
/**
* The getExactTerms method will attempt to parse the key as is, using the key factory. If sucessful,
* it will mark a reference as a whole book if applicable.
* <p/>
* The getNonExactTerms method will attempt to match against BibleBook names first, and then will use
* parsed key if available and suggest a few chapters that
* would make sense.
*
* @author chrisburrell
*/
public class ReferenceSuggestionServiceImpl extends AbstractIgnoreMergedListSuggestionServiceImpl<BookName> {
private static final String BOOK_CHAPTER_FORMAT = "%s %d";
private static final String BOOK_CHAPTER_OSIS_FORMAT = "%s.%d";
private final JSwordVersificationService versificationService;
private final InternationalRangeService internationalRangeService;
@Inject
public ReferenceSuggestionServiceImpl(final JSwordVersificationService versificationService,
final InternationalRangeService internationalRangeService) {
this.versificationService = versificationService;
this.internationalRangeService = internationalRangeService;
}
@Override
public BookName[] getExactTerms(final SuggestionContext context, final int max, final boolean popularSort) {
final String masterBook = getDefaultedVersion(context);
final Book master = this.versificationService.getBookFromVersion(masterBook);
final Versification masterV11n = this.versificationService.getVersificationForVersion(masterBook);
final String input = prepInput(context.getInput());
try {
Key k = master.getKey(input);
if (k != null) {
// check this book actually contains this key, based on the scope...
if (!JSwordUtils.containsAny(master, k)) {
return new BookName[0];
// return getExactRange(input);
}
BookName bk;
if (k instanceof VerseKey) {
final VerseKey verseKey = (VerseKey) k;
final boolean wholeBook = isBook(masterV11n, verseKey);
if (wholeBook) {
final BibleBook book = ((Verse) verseKey.iterator().next()).getBook();
bk = getBookFromBibleBook(book, masterV11n);
} else {
bk = new BookName(verseKey.getName(), verseKey.getName(), BookName.Section.PASSAGE, wholeBook, ((Verse) verseKey.iterator().next()).getBook(), k.getOsisRef());
}
return new BookName[]{bk};
} else {
return new BookName[]{new BookName(k.getName(), k.getName(), BookName.Section.OTHER_NON_BIBLICAL, false, k.getOsisRef())};
}
}
} catch (NoSuchKeyException ex) {
//silently fail
}
// return getExactRange(input);
return new BookName[0];
}
/**
* We adjust the input slightly to make more hits. For example, it is clear that someone typing a '-' at the end may or may not want the whole passage to the end of the chapter
* but he certainly doesn't want gen.1.1-. Similarly, with an 'f' at the end, we might as well do the same things
*
* @param input
* @return
*/
String prepInput(String input) {
final int inputLength = input.length();
if (inputLength > 0) {
final char lastChar = input.charAt(inputLength - 1);
//if the reference finishes with a -, might as well suggest a -ff
if (lastChar == '-') {
return input.substring(0, inputLength - 1) + "ff";
}
//if the length is longer, then we might finish with for these cases: -f and 1f
if (inputLength > 1) {
final char secondLastChar = input.charAt((inputLength - 2));
if (inputLength > 1 && lastChar == 'f') {
if(Character.isDigit(secondLastChar)) {
return input.substring(0, inputLength - 1) + "ff";
} else if(secondLastChar == '-')
return input.substring(0, inputLength - 2) + "ff";
}
}
}
return input;
}
/**
* // * @param input the input
*
* @return the list of matching ranges
*/
// private BookName[] getExactRange(final String input) {
// final List<BookName> ranges = internationalRangeService.getRanges(input, true);
// return ranges.toArray(new BookName[ranges.size()]);
// }
@Override
public BookName[] collectNonExactMatches(final TermsAndMaxCount<BookName> collector,
final SuggestionContext context, final BookName[] alreadyRetrieved,
final int leftToCollect) {
if (context.isExampleData()) {
return this.getSampleData(context);
}
//we've already attempted to parse the whole key, so left to do here, is to iterate through the books
//and match against those names that make sense.
final List<BookName> books = new ArrayList<BookName>();
final String masterBook = getDefaultedVersion(context);
final Book master = this.versificationService.getBookFromVersion(masterBook);
final Versification masterV11n = this.versificationService.getVersificationForVersion(master);
final String input = context.getInput().toLowerCase();
final Iterator<BibleBook> bookIterator = getBestIterator(master, masterV11n);
addMatchingBooks(books, masterV11n, input, bookIterator);
//de-duplicate by adding to a set
final Set<BookName> bookNames = new LinkedHashSet<BookName>();
bookNames.addAll(Arrays.asList(alreadyRetrieved));
bookNames.addAll(books);
//now, how many items do we have, and do we need to add a few chapters here?
int spaceLeft = collector.getTotalCount() - bookNames.size();
if (spaceLeft > 0) {
final List<BookName> extras = new ArrayList<BookName>();
//find a 'whole' book
for (BookName bn : bookNames) {
if (bn.isWholeBook() && bn.getBibleBook() != null) {
int lastChapter = masterV11n.getLastChapter(bn.getBibleBook());
for (int ii = 1; ii < lastChapter && spaceLeft > 0; ii++) {
extras.add(addChapter(masterV11n, bn.getBibleBook(), ii));
spaceLeft--;
}
collector.setTotalCount(lastChapter);
} else if (Character.isDigit(bn.getShortName().charAt(bn.getShortName().length() - 1))) {
String shortName = bn.getShortName();
int lastPart = shortName.lastIndexOf(' ');
if (lastPart != -1) {
try {
int chapter = Integer.parseInt(shortName.substring(lastPart + 1));
//we'll add all the chapters that exist.
int lastChapter = masterV11n.getLastChapter(bn.getBibleBook());
for (int ii = chapter * 10; ii < lastChapter && ii < chapter * 10 + 10; ii++) {
extras.add(addChapter(masterV11n, bn.getBibleBook(), ii));
spaceLeft--;
}
for (int ii = chapter * 100; ii < lastChapter && chapter < chapter * 100 + 100; ii++) {
extras.add(addChapter(masterV11n, bn.getBibleBook(), ii));
spaceLeft--;
}
} catch (NumberFormatException ex) {
//ignore
}
}
}
}
bookNames.addAll(extras);
}
// if(spaceLeft > 0) {
// bookNames.addAll(this.internationalRangeService.getRanges(input, false));
// }
return bookNames.toArray(new BookName[bookNames.size()]);
}
/**
* @param master the master book
* @param masterV11n the v11n of the book
* @return
*/
private Iterator<BibleBook> getBestIterator(Book master, Versification masterV11n) {
if (master instanceof AbstractPassageBook) {
return ((AbstractPassageBook) master).getBibleBooks().iterator();
}
return masterV11n.getBookIterator();
}
private void addMatchingBooks(final List<BookName> books, final Versification masterV11n, final String input, final Iterator<BibleBook> bookIterator) {
while (bookIterator.hasNext()) {
final BibleBook book = bookIterator.next();
if (masterV11n.getLongName(book).toLowerCase().startsWith(input)
|| masterV11n.getPreferredName(book).toLowerCase().startsWith(input)
|| masterV11n.getShortName(book).toLowerCase().startsWith(input)) {
addBookName(books, book, masterV11n);
}
}
}
/**
* Returns all 66 books (or more) of the Bible.
*
* @param context the context
* @return the list of all book names
*/
private BookName[] getSampleData(final SuggestionContext context) {
final List<BookName> books = new ArrayList<BookName>();
final String masterBook = getDefaultedVersion(context);
final Versification masterV11n = this.versificationService.getVersificationForVersion(masterBook);
final Iterator<BibleBook> bookIterator = masterV11n.getBookIterator();
while (bookIterator.hasNext()) {
final BibleBook book = bookIterator.next();
addBookName(books, book, masterV11n);
}
return books.toArray(new BookName[books.size()]);
}
/**
* Adds a chapter
*
* @param bibleBook the bible book
* @param chapterNumber the chapter number
* @return a bookName representing a chapter number
*/
private BookName addChapter(Versification versification,
final BibleBook bibleBook, final int chapterNumber) {
// make sure first letter is CAPS, followed by the rest of the word and the chapter number
final String chapNumber = String
.format(BOOK_CHAPTER_FORMAT, versification.getShortName(bibleBook), chapterNumber);
final String longChapNumber = String.format(BOOK_CHAPTER_FORMAT, versification.getLongName(bibleBook),
chapterNumber);
return new BookName(chapNumber, longChapNumber, BookName.Section.PASSAGE, false, null, true, JSwordUtils.getChapterOsis(bibleBook, chapterNumber));
}
/**
* @param masterV11n the v11n of the verse key
* @param k the key which may be a whole book
* @return true if the first key and the last key match to a whole book
*/
private boolean isBook(final Versification masterV11n, final VerseKey k) {
int cardinality = k.getCardinality();
if (cardinality == 0) {
return false;
}
Verse firstKey = (Verse) k.get(0);
final boolean startOfBook = masterV11n.isStartOfBook(firstKey);
if (!startOfBook) {
return false;
}
final Verse lastKey = (Verse) k.get(cardinality - 1);
return firstKey.getBook() == lastKey.getBook() && masterV11n.isEndOfBook(lastKey);
}
/**
* Gets the version that the user has selected, defaulting to ESV otherwise
*
* @param context the context for the request
* @return the name of the version
*/
private String getDefaultedVersion(final SuggestionContext context) {
String masterBook = context.getMasterBook();
if (StringUtils.isBlank(masterBook)) {
masterBook = JSwordPassageService.REFERENCE_BOOK;
}
return masterBook;
}
/**
* Adds all Bible books except for INTROs to NT, OT and Bible.
*
* @param matchingNames the list of current names
* @param bookName the book that we are examining
* @param versification the versification attached to the book.
*/
private void addBookName(final List<BookName> matchingNames, final BibleBook bookName,
final Versification versification) {
if (BibleBook.INTRO_BIBLE.equals(bookName) || BibleBook.INTRO_NT.equals(bookName)
|| BibleBook.INTRO_OT.equals(bookName)) {
return;
}
matchingNames.add(getBookFromBibleBook(bookName, versification));
}
/**
* Returns a book name, constructed from the correct versification table
*
* @param bookName the bible book (JSword object)
* @param versification its attached versification
* @return the book name suggestion that we return to the user
*/
private BookName getBookFromBibleBook(final BibleBook bookName, final Versification versification) {
BookName.Section section = DivisionName.BIBLE.contains(bookName) ? BookName.Section.BIBLE_BOOK : BookName.Section.APOCRYPHA;
return new BookName(versification.getShortName(bookName), versification
.getLongName(bookName), section, versification.getLastChapter(bookName) != 1, bookName, false, bookName.getOSIS());
}
}