package com.orgzly.android;
import com.orgzly.android.util.QuotedStringTokenizer;
import com.orgzly.org.datetime.OrgInterval;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Inspired by:
*
* Advanced searching (org-mode)
* http://orgmode.org/worg/org-tutorials/advanced-searching.html
*
* Advanced search (GMail)
* https://support.google.com/mail/answer/7190?hl=en
*
* Using . as a separator as it's available without
* using a modifier key on most keyboards.
*
*/
public class SearchQuery {
private SearchQueryInterval scheduled;
private SearchQueryInterval deadline;
private Set<String> textSearch = new LinkedHashSet<>();
private String bookName;
private Set<String> notBookName = new TreeSet<>();
private Set<String> noteTags = new TreeSet<>();
private Set<String> tags = new TreeSet<>();
private Set<String> notTags = new TreeSet<>();
private String priority;
private String state;
private Set<String> notState = new TreeSet<>();
private List<SortOrder> sortOrder = new ArrayList<>();
public SearchQuery() {
}
public SearchQuery(String str) {
if (str == null) {
str = "";
}
QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(str, " ", false, true);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (token.startsWith("s.")) { // scheduled
if (token.length() > 2) {
scheduled = SearchQueryInterval.getInstance(token.substring(2).toLowerCase());
}
} else if (token.startsWith("d.")) { // deadline
if (token.length() > 2) {
deadline = SearchQueryInterval.getInstance(token.substring(2).toLowerCase());
}
} else if (token.startsWith("p.")) { // priority
if (token.length() > 2) {
priority = token.substring(2).toLowerCase();
}
} else if (token.startsWith("i.")) { // is (state)
if (token.length() > 2) {
state = token.substring(2).toUpperCase();
}
} else if (token.startsWith(".i.")) { // is not (state)
if (token.length() > 3) {
notState.add(token.substring(3).toUpperCase());
}
} else if (token.startsWith("b.")) { // book name
if (token.length() > 2) {
bookName = QuotedStringTokenizer.unquote(token.substring(2));
}
} else if (token.startsWith(".b.")) { // book name
if (token.length() > 3) {
notBookName.add(QuotedStringTokenizer.unquote(token.substring(3)));
}
} else if (token.startsWith("tn.")) { // note tag
if (token.length() > 3) {
noteTags.add(token.substring(3));
}
} else if (token.startsWith(".t.")) { // has no tag
if (token.length() > 3) {
notTags.add(token.substring(3));
}
} else if (token.startsWith("t.")) { // tag
if (token.length() > 2) {
tags.add(token.substring(2));
}
} else if (token.startsWith(".o.")) { // sort order
if (token.length() > 3) {
String sortOrderName = token.substring(3);
sortOrder.add(new SortOrder(sortOrderName, false));
}
} else if (token.startsWith("o.")) { // sort order
if (token.length() > 2) {
String sortOrderName = token.substring(2);
sortOrder.add(new SortOrder(sortOrderName, true));
}
} else {
textSearch.add(token);
}
}
}
public boolean hasScheduled() {
return scheduled != null;
}
public SearchQueryInterval getScheduled() {
return scheduled;
}
public boolean hasDeadline() {
return deadline != null;
}
public SearchQueryInterval getDeadline() {
return deadline;
}
public Set<String> getTextSearch() {
return textSearch;
}
public boolean hasTextSearch() {
return !textSearch.isEmpty();
}
public Set<String> getNoteTags() {
return noteTags;
}
public boolean hasNoteTags() {
return !noteTags.isEmpty();
}
public Set<String> getTags() {
return tags;
}
public boolean hasTags() {
return !tags.isEmpty();
}
public Set<String> getNotTags() {
return notTags;
}
public String getPriority() {
return priority;
}
/**
* Lowercase priority.
*/
public boolean hasPriority() {
return priority != null;
}
public String getState() {
return state;
}
public boolean hasState() {
return state != null;
}
public void setState(String value) {
state = value;
}
public Set<String> getNotState() {
return notState;
}
public boolean hasNotState() {
return ! notState.isEmpty();
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public boolean hasBookName() {
return bookName != null;
}
public Set<String> getNotBookName() {
return notBookName;
}
public boolean hasNotBookName() {
return ! notBookName.isEmpty();
}
public boolean hasSortOrder() {
return sortOrder.size() > 0;
}
public List<SortOrder> getSortOrder() {
return sortOrder;
}
public String toString() {
StringBuilder s = new StringBuilder();
if (hasBookName()) {
s.append("b.").append(QuotedStringTokenizer.quote(bookName, " "));
}
if (hasNotBookName()) {
for (String name : notBookName) {
s.append(" .b.").append(QuotedStringTokenizer.quote(name, " "));
}
}
if (state != null) {
s.append(" i.").append(state.toLowerCase());
}
if (! notState.isEmpty()) {
for (String state : notState) {
s.append(" .i.").append(state.toLowerCase());
}
}
if (priority != null) {
s.append(" p.").append(priority.toLowerCase());
}
if (! noteTags.isEmpty()) {
for (String tag: noteTags) {
s.append(" tn.").append(tag);
}
}
if (! notTags.isEmpty()) {
for (String tag : notTags) {
s.append(" .t.").append(tag);
}
}
if (! tags.isEmpty()) {
for (String tag: tags) {
s.append(" t.").append(tag);
}
}
if (hasScheduled()) {
s.append(" s.").append(scheduled.toString());
}
if (hasDeadline()) {
s.append(" d.").append(deadline.toString());
}
for (String ts: textSearch) {
s.append(" ").append(ts);
}
for (SortOrder order: sortOrder) {
s.append(" ");
if (! order.isAscending) {
s.append(".");
}
s.append("o.").append(order.name);
}
return s.toString().trim();
}
public static class SortOrder {
public enum Type {
SCHEDULED,
DEADLINE,
NOTEBOOK,
PRIORITY
}
private String name;
private boolean isAscending;
SortOrder(String name, boolean isAscending) {
this.name = name;
this.isAscending = isAscending;
}
public boolean isAscending() {
return isAscending;
}
public boolean isDescending() {
return !isAscending;
}
public Type getType() {
if ("scheduled".equals(name) || "sched".equals(name) || "s".equals(name)) {
return Type.SCHEDULED;
} else if ("deadline".equals(name) || "dead".equals(name) || "d".equals(name)) {
return Type.DEADLINE;
} else if ("priority".equals(name) || "prio".equals(name) || "pri".equals(name) || "p".equals(name)) {
return Type.PRIORITY;
} else if ("notebook".equals(name) || "book".equals(name) || "b".equals(name)) {
return Type.NOTEBOOK;
} else {
return null;
}
}
}
/**
* {@link OrgInterval} with support for today (0d) and tomorrow (1d).
*
* TODO: Cleanup. For example tomorrow, tmws, tom should be constants (set) etc.
*/
public static class SearchQueryInterval extends OrgInterval {
public static final Pattern PATTERN = Pattern.compile("^(\\d+)(h|d|w|m|y)$");
private boolean none = false;
public static SearchQueryInterval getInstance(String str) {
SearchQueryInterval interval = null;
if (str != null) {
str = str.toLowerCase();
if ("none".equals(str) || "no".equals(str)) {
interval = new SearchQueryInterval();
interval.none = true;
} else if ("today".equals(str) || "tod".equals(str)) {
interval = new SearchQueryInterval();
interval.setValue(0);
interval.setUnit(Unit.DAY);
} else if ("tomorrow".equals(str) || "tmrw".equals(str) || "tom".equals(str)) {
interval = new SearchQueryInterval();
interval.setValue(1);
interval.setUnit(Unit.DAY);
} else {
Matcher m = PATTERN.matcher(str);
if (m.find()) {
interval = new SearchQueryInterval();
interval.setValue(m.group(1));
interval.setUnit(m.group(2));
} else {
return null;
}
}
}
return interval;
}
public String toString() {
if (none()) {
return "none";
} else if (unit == Unit.DAY && value == 0) {
return "today";
} else if (unit == Unit.DAY && value == 1) {
return "tomorrow";
}
return super.toString();
}
public boolean none() {
return none;
}
}
}