// Simple test that we can use xapian from java
//
// Copyright (C) 2005,2006,2007,2008,2011,2016,2017,2019,2023 Olly Betts
//
// This program 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 2 of the
// License, or (at your option) any later version.
//
// This program 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 this program; if not, see
// <https://www.gnu.org/licenses/>.

import org.xapian.*;

// FIXME: need to sort out throwing wrapped Xapian::Error subclasses
//import org.xapian.errors.*;

// FIXME: "implements" not "extends" in JNI Java API
class MyMatchDecider extends MatchDecider {
    public boolean accept(Document d) {
	// NB It's not normally appropriate to call getData() in a MatchDecider
	// but we do it here to make sure we don't get an empty document.
/*	try { */
	    return d.getData().length() == 0;
/*
	} catch (XapianError e) {
	    return true;
	}
*/
    }
}

// FIXME: "implements" not "extends" in JNI Java API
class MyExpandDecider extends ExpandDecider {
    public boolean accept(String s) { return s.charAt(0) != 'a'; }
}

class MyFieldProcessor extends FieldProcessor {
    public Query apply(String str) {
	if (str.equals("spam"))
	    return new Query("eggs");
	return new Query("spam");
    }
}

public class SmokeTest {
    public static void main(String[] args) throws Exception {
	TermGenerator termGenerator = new TermGenerator();
	termGenerator.setFlags(TermGenerator.FLAG_SPELLING);
	try {
	    // Test the version number reporting functions give plausible
	    // results.
	    String v = "";
	    v += Version.major();
	    v += ".";
	    v += Version.minor();
	    v += ".";
	    v += Version.revision();
	    String v2 = Version.string();
	    if (!v.equals(v2)) {
		System.err.println("Unexpected version output (" + v + " != " + v2 + ")");
		System.exit(1);
	    }

	    Stem stem = new Stem("english");
	    if (!stem.toString().equals("Xapian::Stem(english)")) {
		System.err.println("Unexpected stem.toString()");
		System.exit(1);
	    }
	    Document doc = new Document();
	    doc.setData("a\000b");
	    String s = doc.getData();
	    if (s.equals("a")) {
		System.err.println("getData+setData truncates at a zero byte");
		System.exit(1);
	    }
	    if (!s.equals("a\000b")) {
		System.err.println("getData+setData doesn't transparently handle a zero byte");
		System.exit(1);
	    }
	    if (!doc.toString().equals("Document(docid=0, data=a\\xc0\\x80b)")) {
		System.err.println("XPASS: UTF-8 encoding of zero byte fixed!");
		System.exit(1);
	    }

	    // Surrogate pair case:
	    String falafel = new String(Character.toChars(0x1f9c6));
	    doc.setData(falafel);
	    s = doc.getData();
	    if (!s.equals(falafel)) {
		System.err.println("getData+setData doesn't transparently handle a surrogate pair");
		System.exit(1);
	    }
	    if (!doc.toString().equals("Document(docid=0, data=\\xed\\xa0\\xbe\\xed\\xb7\\x86)")) {
		System.err.println("XPASS: UTF-8 encoding of character >= U+10000 fixed!");
		System.exit(1);
	    }

	    doc.setData("is there anybody out there?");
	    doc.addTerm("XYzzy");
// apply was stemWord() in the JNI bindings
	    doc.addPosting(stem.apply("is"), 1);
	    doc.addPosting(stem.apply("there"), 2);
	    doc.addPosting(stem.apply("anybody"), 3);
	    doc.addPosting(stem.apply("out"), 4);
	    doc.addPosting(stem.apply("there"), 5);
	    WritableDatabase db = new WritableDatabase("", Xapian.DB_BACKEND_INMEMORY);
	    db.addDocument(doc);
	    if (db.getDocCount() != 1) {
		System.err.println("Unexpected db.getDocCount()");
		System.exit(1);
	    }

	    QueryParser qp = new QueryParser();

	    // Test wrapping of null-able grouping parameter.
	    qp.addBooleanPrefix("colour", "XC");
	    qp.addBooleanPrefix("color", "XC");
	    qp.addBooleanPrefix("foo", "XFOO", null);
	    qp.addBooleanPrefix("bar", "XBAR", "XBA*");
	    qp.addBooleanPrefix("baa", "XBAA", "XBA*");
	    DateRangeProcessor rpdate = new DateRangeProcessor(1, Xapian.RP_DATE_PREFER_MDY, 1960);
	    qp.addRangeprocessor(rpdate);
	    qp.addRangeprocessor(rpdate, null);
	    qp.addRangeprocessor(rpdate, "foo");

            if (!Query.MatchAll.toString().equals("Query(<alldocuments>)")) {
		System.err.println("Unexpected Query.MatchAll.toString()");
		System.exit(1);
            }

            if (!Query.MatchNothing.toString().equals("Query()")) {
		System.err.println("Unexpected Query.MatchNothing.toString()");
		System.exit(1);
            }

	    String[] terms = { "smoke", "test", "terms" };
	    Query query = new Query(Query.OP_OR, terms);
	    if (!query.toString().equals("Query((smoke OR test OR terms))")) {
		System.err.println("Unexpected query.toString()");
		System.exit(1);
	    }
	    Query[] queries = { new Query("smoke"), query, new Query("string") };
	    Query query2 = new Query(Query.OP_XOR, queries);
	    if (!query2.toString().equals("Query((smoke XOR (smoke OR test OR terms) XOR string))")) {
		System.err.println("Unexpected query2.toString()");
		System.exit(1);
	    }
	    String[] subqs = { "a", "b" };
	    Query query3 = new Query(Query.OP_OR, subqs);
	    if (!query3.toString().equals("Query((a OR b))")) {
		System.err.println("Unexpected query3.toString()");
		System.exit(1);
	    }
	    Enquire enq = new Enquire(db);

	    // Check Xapian::BAD_VALUENO is wrapped suitably.
	    enq.setCollapseKey(Xapian.BAD_VALUENO);

	    // Test that the non-constant wrapping prior to 1.4.10 still works.
	    enq.setCollapseKey(Xapian.getBAD_VALUENO());

	    enq.setQuery(new Query(Query.OP_OR, "there", "is"));
	    MSet mset = enq.getMSet(0, 10);
	    if (mset.size() != 1) {
		System.err.println("Unexpected mset.size()");
		System.exit(1);
	    }
	    MSetIterator m_itor = mset.begin();
	    Document m_doc = null;
	    long m_id;
	    while(m_itor.hasNext()) {
		m_id = m_itor.next();
		if(m_itor.hasNext()) {
		    m_doc = mset.getDocument(m_id);
		}
	    }

	    // Only one doc exists in this mset
	    if(m_doc != null && m_doc.getDocId() != 0) {
		System.err.println("Unexpected docid");
		    System.exit(1);
	    }

	    String term_str = "";
	    TermIterator itor = enq.getMatchingTermsBegin(mset.getElement(0));
	    while (itor.hasNext()) {
		term_str += itor.next();
		if (itor.hasNext())
		    term_str += ' ';
	    }
	    if (!term_str.equals("is there")) {
		System.err.println("Unexpected term_str");
		System.exit(1);
	    }
/* FIXME:dc: Fails since Xapian::Error is still unmapped
	    boolean ok = false;
	    try {
		Database db_fail = new Database("NOsuChdaTabASe");
		// Ignore the return value.
		db_fail.getDocCount();
	    } catch (DatabaseOpeningError e) {
		ok = true;
	    }
	    if (!ok) {
		System.err.println("Managed to open non-existent database");
		System.exit(1);
	    }
*/
/*
	    if (Query.OP_ELITE_SET != 10) {
		System.err.println("OP_ELITE_SET is " + Query.OP_ELITE_SET + " not 10");
		System.exit(1);
	    }
*/
	    RSet rset = new RSet();
	    rset.addDocument(1);
	    ESet eset = enq.getESet(10, rset, new MyExpandDecider());
	    // FIXME: temporary simple check
	    if (0 == eset.size()) {
		System.err.println("ESet.size() was 0");
		System.exit(1);
	    }

	    int count = 0;
	    for(ESetIterator eit = eset.begin(); eit.hasNext(); ) {
	    // for (int i = 0; i < eset.size(); i++) {
		if (eit.getTerm().charAt(0) == 'a') {
		    System.err.println("MyExpandDecider wasn't used");
		    System.exit(1);
		}
		++count;
		eit.next();
	    }
	    if (count != eset.size()) {
		System.err.println("ESet.size() mismatched number of terms returned by ESetIterator");
		System.err.println(count + " " + eset.size());
		System.exit(1);
	    }

/*
	    MSet mset2 = enq.getMSet(0, 10, null, new MyMatchDecider());
	    if (mset2.size() > 0) {
		System.err.println("MyMatchDecider wasn't used");
		System.exit(1);
	    }
*/

	    if (!enq.getQuery().toString().equals("Query((there OR is))")) {
		System.err.println("Enquire::getQuery() returned the wrong query: " + enq.getQuery().toString());
		System.exit(1);
	    }

	    {
		qp.addPrefix("food", new MyFieldProcessor());
		if (!qp.parseQuery("food:spam").toString().equals("Query(eggs)")) {
		    System.err.println("FieldProcessor subclass doesn't work as expected");
		    System.exit(1);
		}
	    }

	    {
		// Wrapped functions which take/return byte[] for std::string.

		// Check that serialisation returns byte[], that
		// unserialisation takes byte[], and round-tripping works.
		byte[] res = Xapian.sortableSerialise(1.675);
		if (Xapian.sortableUnserialise(res) != 1.675) {
		    System.err.println("sortableSerialise() and/or sortableUnserialise() don't work as expected");
		    System.exit(1);
		}

		// Check that serialisation returns byte[], that
		// unserialisation takes byte[], and round-tripping works.
		Query q = new Query("foo");
		res = q.serialise();
		Query q_out = Query.unserialise(res);
		if (!q.toString().equals(q_out.toString())) {
		    System.err.println("Query serialisation doesn't work as expected");
		    System.exit(1);
		}

		// Check Document.addValue() takes byte[], that getValue()
		// returns byte[], that serialisation returns byte[], that
		// unserialisation takes byte[], and round-tripping works.
		Document d = new Document();
		d.setData("xyzzy");
		d.addValue(7, res);
		byte[] res2 = d.getValue(7);
		if (!java.util.Arrays.equals(res, res2)) {
		    System.err.println("Document.getValue() returns a different byte[] to the one set with addValue()");
		    System.exit(1);
		}
		res = d.serialise();
		Document d_out = Document.unserialise(res);
		// Make sure the "terms_here" flag is set so the descriptions match.
		d_out.termListCount();
		if (!d.toString().equals(d_out.toString())) {
		    System.err.println("Document serialisation doesn't work as expected");
		    System.err.println(d.toString());
		    System.err.println(d_out.toString());
		    System.exit(1);
		}

		// Check that serialisation returns byte[], that
		// unserialisation takes byte[], and round-tripping works.
		LatLongCoord llc = new LatLongCoord(10.5, 45.25);
		res = llc.serialise();
		LatLongCoord llc_out = new LatLongCoord();
		llc_out.unserialise(res);
		if (!llc.toString().equals(llc_out.toString())) {
		    System.err.println("LatLongCoord serialisation doesn't work as expected");
		    System.exit(1);
		}

		// Check that serialisation returns byte[], that
		// unserialisation takes byte[], and round-tripping works.
		LatLongCoords llcs = new LatLongCoords();
		llcs.append(llc);
		res = llcs.serialise();
		LatLongCoords llcs_out = new LatLongCoords();
		llcs_out.unserialise(res);
		if (!llcs.toString().equals(llcs_out.toString())) {
		    System.err.println("LatLongCoords serialisation doesn't work as expected");
		    System.exit(1);
		}

		// Check `range_limit` mapped to byte[].
		q = new Query(Query.op.OP_VALUE_GE, 0, res);
		if (q.toString().length() == 0) {
		    // Mostly just a way to actually use the constructed object.
		    System.err.println("Query description shouldn't be empty");
		    System.exit(1);
		}

		// Check `range_limit` mapped to byte[].
		q = new Query(Query.op.OP_VALUE_LE, 1, res);
		if (q.toString().length() == 0) {
		    // Mostly just a way to actually use the constructed object.
		    System.err.println("Query description shouldn't be empty");
		    System.exit(1);
		}

		// Check `range_lower` and `range_upper` mapped to byte[].
		q = new Query(Query.op.OP_VALUE_RANGE, 2, res, res);
		if (q.toString().length() == 0) {
		    // Mostly just a way to actually use the constructed object.
		    System.err.println("Query description shouldn't be empty");
		    System.exit(1);
		}

		// Check ValueSetMatchDecider.addValue() and removeValue() take
		// byte[].
		ValueSetMatchDecider vsmd = new ValueSetMatchDecider(1, false);
		vsmd.addValue(res);
		vsmd.removeValue(res);

		// Check Database.getValueLowerBound() and getValueUpperBound()
		// return byte[].
		byte[] lo = "abba".getBytes();
		byte[] hi = "xyzzy".getBytes();
		{
		    WritableDatabase wdb = new WritableDatabase("", Xapian.DB_BACKEND_INMEMORY);
		    Document document = new Document();
		    document.addValue(42, hi);
		    wdb.addDocument(document);
		    document.addValue(42, lo);
		    wdb.addDocument(document);
		    db = wdb;
		}

		if (!java.util.Arrays.equals(db.getValueLowerBound(42), lo)) {
		    System.err.println("Database.getValueLowerBound() doesn't work as expected");
		    System.exit(1);
		}
		if (!java.util.Arrays.equals(db.getValueUpperBound(42), hi)) {
		    System.err.println("Database.getValueUpperBound() doesn't work as expected");
		    System.exit(1);
		}

		ValueIterator it = db.valuestreamBegin(42);
		if (!java.util.Arrays.equals(it.getValue(), hi)) {
		    System.err.println("ValueIterator.getValue() doesn't work as expected");
		    System.exit(1);
		}
	    }
	} catch (Exception e) {
	    System.err.println("Caught unexpected exception " + e.toString());
	    System.exit(1);
	}
    }
}
