package org.ebookdroid.core.models;
import org.ebookdroid.core.DecodeService;
import org.ebookdroid.core.Page;
import org.ebookdroid.core.codec.CodecFeatures;
import org.ebookdroid.ui.viewer.IActivityController;
import org.ebookdroid.ui.viewer.IViewController;
import android.graphics.RectF;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.utils.CompareUtils;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.collections.SparseArrayEx;
public class SearchModel {
protected static final LogContext LCTX = LogManager.root().lctx("SearchModel", true);
private final IActivityController base;
private String pattern;
private Page currentPage;
private int currentMatchIndex;
private final SparseArrayEx<Matches> matches;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public SearchModel(final IActivityController base) {
this.base = base;
this.matches = new SparseArrayEx<Matches>();
}
public String getPattern() {
return pattern;
}
public void setPattern(final String pattern) {
final String p = pattern != null ? pattern.toLowerCase() : null;
if (!CompareUtils.equals(this.pattern, p)) {
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.setPattern(" + p + ")");
}
lock.writeLock().lock();
try {
this.pattern = p;
for (final Matches ref : matches) {
final Matches m = ref;
if (m != null) {
m.cancel();
}
}
this.matches.clear();
this.currentPage = null;
this.currentMatchIndex = -1;
} finally {
lock.writeLock().unlock();
}
}
}
public Matches getMatches(final Page page) {
if (LengthUtils.isEmpty(this.pattern)) {
return null;
}
return getMatches(page.index.docIndex);
}
protected Matches getMatches(final int key) {
lock.readLock().lock();
try {
final Matches ref = matches.get(key);
return ref;
} finally {
lock.readLock().unlock();
}
}
protected Matches getOrCreateMatches(final int key) {
lock.writeLock().lock();
try {
Matches ref = matches.get(key);
Matches m = ref;
if (m == null) {
m = new Matches(key);
ref = m;
matches.put(key, ref);
}
return m;
} finally {
lock.writeLock().unlock();
}
}
public Page getCurrentPage() {
return currentPage;
}
public int getCurrentMatchIndex() {
return currentMatchIndex;
}
public RectF getCurrentRegion() {
if (currentPage == null) {
return null;
}
final Matches m = getMatches(currentPage.index.docIndex);
if (m == null) {
return null;
}
final List<? extends RectF> mm = m.getMatches();
if (0 <= currentMatchIndex && currentMatchIndex < LengthUtils.length(mm)) {
return mm.get(currentMatchIndex);
}
return null;
}
public RectF moveToNext(final ProgressCallback callback) {
final IViewController ctrl = base.getDocumentController();
final int firstVisiblePage = ctrl.getFirstVisiblePage();
final int lastVisiblePage = ctrl.getLastVisiblePage();
if (currentPage == null) {
return searchFirstFrom(firstVisiblePage, callback);
}
final Matches m = getMatches(currentPage.index.docIndex);
if (m == null) {
return searchFirstFrom(currentPage.index.viewIndex, callback);
}
if (firstVisiblePage <= currentPage.index.viewIndex && currentPage.index.viewIndex <= lastVisiblePage) {
currentMatchIndex++;
final List<? extends RectF> mm = m.getMatches();
if (0 <= currentMatchIndex && currentMatchIndex < LengthUtils.length(mm)) {
return mm.get(currentMatchIndex);
} else {
return searchFirstFrom(currentPage.index.viewIndex + 1, callback);
}
} else {
return searchFirstFrom(firstVisiblePage, callback);
}
}
public RectF moveToPrev(final ProgressCallback callback) {
final IViewController ctrl = base.getDocumentController();
final int firstVisiblePage = ctrl.getFirstVisiblePage();
final int lastVisiblePage = ctrl.getLastVisiblePage();
if (currentPage == null) {
return searchLastFrom(lastVisiblePage, callback);
}
final Matches m = getMatches(currentPage.index.docIndex);
if (m == null) {
return searchLastFrom(currentPage.index.viewIndex, callback);
}
if (firstVisiblePage <= currentPage.index.viewIndex && currentPage.index.viewIndex <= lastVisiblePage) {
currentMatchIndex--;
final List<? extends RectF> mm = m.getMatches();
if (0 <= currentMatchIndex && currentMatchIndex < LengthUtils.length(mm)) {
return mm.get(currentMatchIndex);
} else {
return searchLastFrom(currentPage.index.viewIndex - 1, callback);
}
} else {
return searchLastFrom(lastVisiblePage, callback);
}
}
private RectF searchFirstFrom(final int pageIndex, final ProgressCallback callback) {
if (LengthUtils.isEmpty(this.pattern)) {
return null;
}
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.searchFirstFrom(" + pageIndex + "): start");
}
final int pageCount = base.getDocumentModel().getPageCount();
currentPage = null;
currentMatchIndex = -1;
int index = pageIndex - 1;
while (!callback.isCancelled() && ++index < pageCount) {
final Page p = base.getDocumentModel().getPageObject(index);
if (callback != null) {
callback.searchStarted(index);
}
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.searchFirstFrom(" + pageIndex + "): >>> " + index);
}
final Matches m = startSearchOnPage(p);
final List<? extends RectF> mm = m.waitForMatches();
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.searchFirstFrom(" + pageIndex + "): <<< " + index);
}
if (callback != null) {
callback.searchFinished(index);
}
if (LengthUtils.isNotEmpty(mm)) {
currentPage = p;
currentMatchIndex = 0;
return mm.get(currentMatchIndex);
}
}
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.searchFirstFrom(" + pageIndex + "): end");
}
return null;
}
private RectF searchLastFrom(final int pageIndex, final ProgressCallback callback) {
if (LengthUtils.isEmpty(this.pattern)) {
return null;
}
currentPage = null;
currentMatchIndex = -1;
int index = pageIndex + 1;
while (!callback.isCancelled() && 0 <= --index) {
final Page p = base.getDocumentModel().getPageObject(index);
if (callback != null) {
callback.searchStarted(index);
}
final Matches m = startSearchOnPage(p);
final List<? extends RectF> mm = m.waitForMatches();
if (callback != null) {
callback.searchFinished(index);
}
if (LengthUtils.isNotEmpty(mm)) {
currentPage = p;
currentMatchIndex = mm.size() - 1;
return mm.get(currentMatchIndex);
}
}
return null;
}
private Matches startSearchOnPage(final Page page) {
final Matches m = getOrCreateMatches(page.index.docIndex);
m.startSearchOnPage(base.getDecodeService(), page, pattern);
return m;
}
public static class Matches implements DecodeService.SearchCallback {
static final AtomicLong SEQ = new AtomicLong();
final long id = SEQ.getAndIncrement();
final int key;
final AtomicReference<CountDownLatch> running = new AtomicReference<CountDownLatch>();
final AtomicReference<List<? extends RectF>> matches = new AtomicReference<List<? extends RectF>>();
Matches(final int key) {
this.key = key;
}
public void cancel() {
if (LCTX.isDebugEnabled()) {
LCTX.d("Matches.cancel(" + key + ")");
}
setMatches(null);
}
public void startSearchOnPage(final DecodeService ds, final Page page, final String pattern) {
if (running.compareAndSet(null, new CountDownLatch(1))) {
if (!ds.isFeatureSupported(CodecFeatures.FEATURE_TEXT_SEARCH)) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Matches.startSearchOnPage(" + id + ", " + key + "): search not supported");
}
setMatches(null);
return;
}
if (LCTX.isDebugEnabled()) {
LCTX.d("Matches.startSearchOnPage(" + id + ", " + key + ")");
}
ds.searchText(page, pattern, this);
}
}
public void setMatches(final List<? extends RectF> matches) {
this.matches.set(matches);
final CountDownLatch event = running.getAndSet(null);
if (event != null) {
if (LCTX.isDebugEnabled()) {
LCTX.d("SearchModel.Matches.setMatches(" + id + ", " + key + "): " + matches);
}
event.countDown();
}
}
public List<? extends RectF> getMatches() {
return this.matches.get();
}
public List<? extends RectF> waitForMatches() {
final CountDownLatch event = running.get();
if (event != null) {
try {
event.await();
} catch (final InterruptedException ex) {
Thread.interrupted();
}
}
final List<? extends RectF> res = this.matches.get();
if (LCTX.isDebugEnabled()) {
LCTX.d("Matches.waitForMatches(" + id + ", " + key + "): " + res);
}
return res;
}
@Override
public void searchComplete(final Page page, final List<? extends RectF> regions) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Matches.searchComplete(" + id + ", " + key + "): " + regions);
}
this.setMatches(regions);
}
}
public static interface ProgressCallback {
void searchStarted(int pageIndex);
void searchFinished(int pageIndex);
boolean isCancelled();
}
}