/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.document;

import java.util.Random;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPoint;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.geo.ShapeTestUtil;
import org.apache.lucene.tests.index.RandomIndexWriter;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.IOUtils;

/** Test case for indexing cartesian shapes and search by bounding box, lines, and polygons */
public class TestXYShape extends LuceneTestCase {

  protected static String FIELDNAME = "field";

  protected static void addPolygonsToDoc(String field, Document doc, XYPolygon polygon) {
    Field[] fields = XYShape.createIndexableFields(field, polygon);
    for (Field f : fields) {
      doc.add(f);
    }
  }

  protected static void addLineToDoc(String field, Document doc, XYLine line) {
    Field[] fields = XYShape.createIndexableFields(field, line);
    for (Field f : fields) {
      doc.add(f);
    }
  }

  protected Query newRectQuery(String field, float minX, float maxX, float minY, float maxY) {
    return XYShape.newBoxQuery(field, QueryRelation.INTERSECTS, minX, maxX, minY, maxY);
  }

  /** test we can search for a point with a standard number of vertices */
  public void testBasicIntersects() throws Exception {
    int numVertices = TestUtil.nextInt(random(), 50, 100);
    Directory dir = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);

    // add a random polygon document
    XYPolygon p = ShapeTestUtil.createRegularPolygon(0, 90, atLeast(1000000), numVertices);
    Document document = new Document();
    addPolygonsToDoc(FIELDNAME, document, p);
    writer.addDocument(document);

    // add a line document
    document = new Document();
    // add a line string
    float[] x = new float[p.numPoints() - 1];
    float[] y = new float[p.numPoints() - 1];
    for (int i = 0; i < x.length; ++i) {
      x[i] = p.getPolyX(i);
      y[i] = p.getPolyY(i);
    }
    XYLine l = new XYLine(x, y);
    addLineToDoc(FIELDNAME, document, l);
    writer.addDocument(document);

    ////// search /////
    // search an intersecting bbox
    IndexReader reader = writer.getReader();
    writer.close();
    IndexSearcher searcher = newSearcher(reader);
    float minX = Math.min(x[0], x[1]);
    float minY = Math.min(y[0], y[1]);
    float maxX = Math.max(x[0], x[1]);
    float maxY = Math.max(y[0], y[1]);
    Query q = newRectQuery(FIELDNAME, minX, maxX, minY, maxY);
    assertEquals(2, searcher.count(q));

    // search a disjoint bbox
    q = newRectQuery(FIELDNAME, p.minX - 1f, p.minX + 1f, p.minY - 1f, p.minY + 1f);
    assertEquals(0, searcher.count(q));

    // search w/ an intersecting polygon
    q =
        XYShape.newPolygonQuery(
            FIELDNAME,
            QueryRelation.INTERSECTS,
            new XYPolygon(
                new float[] {minX, minX, maxX, maxX, minX},
                new float[] {minY, maxY, maxY, minY, minY}));
    assertEquals(2, searcher.count(q));

    // search w/ an intersecting line
    q =
        XYShape.newLineQuery(
            FIELDNAME,
            QueryRelation.INTERSECTS,
            new XYLine(new float[] {minX, minX, maxX, maxX}, new float[] {minY, maxY, maxY, minY}));
    assertEquals(2, searcher.count(q));

    IOUtils.close(reader, dir);
  }

  public void testBoundingBoxQueries() throws Exception {
    Random random = random();
    XYRectangle r1 = ShapeTestUtil.nextBox(random);
    XYRectangle r2 = ShapeTestUtil.nextBox(random);
    XYPolygon p;
    // find two boxes so that r1 contains r2
    while (true) {
      // TODO: Should XYRectangle hold values as float?
      if (areBoxDisjoint(r1, r2)) {
        p = toPolygon(r2);
        try {
          Tessellator.tessellate(p, random.nextBoolean());
          break;
        } catch (
            @SuppressWarnings("unused")
            Exception e) {
          // ignore, try other combination
        }
      }
      r1 = ShapeTestUtil.nextBox(random);
      r2 = ShapeTestUtil.nextBox(random);
    }

    Directory dir = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random, dir);

    // add the polygon to the index
    Document document = new Document();
    addPolygonsToDoc(FIELDNAME, document, p);
    writer.addDocument(document);

    ////// search /////
    IndexReader reader = writer.getReader();
    writer.close();
    IndexSearcher searcher = newSearcher(reader);
    // Query by itself should match
    Query q = newRectQuery(FIELDNAME, r2.minX, r2.maxX, r2.minY, r2.maxY);
    assertEquals(1, searcher.count(q));
    // r1 contains r2, intersects should match
    q = newRectQuery(FIELDNAME, r1.minX, r1.maxX, r1.minY, r1.maxY);
    assertEquals(1, searcher.count(q));
    // r1 contains r2, WITHIN should match
    q = XYShape.newBoxQuery(FIELDNAME, QueryRelation.WITHIN, r1.minX, r1.maxX, r1.minY, r1.maxY);
    assertEquals(1, searcher.count(q));

    IOUtils.close(reader, dir);
  }

  public void testPointIndexAndDistanceQuery() throws Exception {
    Directory dir = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
    Document document = new Document();
    float pX = ShapeTestUtil.nextFloat(random());
    float py = ShapeTestUtil.nextFloat(random());
    Field[] fields = XYShape.createIndexableFields(FIELDNAME, pX, py);
    for (Field f : fields) {
      document.add(f);
    }
    writer.addDocument(document);

    //// search
    IndexReader r = writer.getReader();
    writer.close();
    IndexSearcher s = newSearcher(r);
    XYCircle circle = ShapeTestUtil.nextCircle();
    Component2D circle2D = XYGeometry.create(circle);
    int expected;
    int expectedDisjoint;
    if (circle2D.contains(pX, py)) {
      expected = 1;
      expectedDisjoint = 0;
    } else {
      expected = 0;
      expectedDisjoint = 1;
    }

    Query q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.INTERSECTS, circle);
    assertEquals(expected, s.count(q));

    q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.WITHIN, circle);
    assertEquals(expected, s.count(q));

    q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.DISJOINT, circle);
    assertEquals(expectedDisjoint, s.count(q));

    IOUtils.close(r, dir);
  }

  public void testContainsWrappingBooleanQuery() throws Exception {

    float[] ys = new float[] {-30, -30, 30, 30, -30};
    float[] xs = new float[] {-30, 30, 30, -30, -30};
    XYPolygon polygon = new XYPolygon(xs, ys);

    Directory dir = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
    Document document = new Document();
    addPolygonsToDoc(FIELDNAME, document, polygon);
    writer.addDocument(document);

    //// search
    IndexReader r = writer.getReader();
    writer.close();
    IndexSearcher s = newSearcher(r);

    XYGeometry[] geometries = new XYGeometry[] {new XYRectangle(0, 1, 0, 1), new XYPoint(4, 4)};
    // geometries within the polygon
    Query q = XYShape.newGeometryQuery(FIELDNAME, QueryRelation.CONTAINS, geometries);
    TopDocs topDocs = s.search(q, 1);
    assertEquals(1, topDocs.scoreDocs.length);
    assertEquals(1.0, topDocs.scoreDocs[0].score, 0.0);
    IOUtils.close(r, dir);
  }

  public void testContainsIndexedGeometryCollection() throws Exception {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    XYPolygon polygon =
        new XYPolygon(
            new float[] {-132, 132, 132, -132, -132}, new float[] {-64, -64, 64, 64, -64});
    Field[] polygonFields = XYShape.createIndexableFields(FIELDNAME, polygon);
    // POINT(5, 5) inside the indexed polygon
    Field[] pointFields = XYShape.createIndexableFields(FIELDNAME, 5, 5);
    int numDocs = random().nextInt(1000);
    // index the same multi geometry many times
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      for (Field f : polygonFields) {
        doc.add(f);
      }
      for (int j = 0; j < 10; j++) {
        for (Field f : pointFields) {
          doc.add(f);
        }
      }
      w.addDocument(doc);
    }
    w.forceMerge(1);

    ///// search //////
    IndexReader reader = w.getReader();
    w.close();
    IndexSearcher searcher = newSearcher(reader);
    // Contains is only true if the query geometry is inside a geometry and does not intersect with
    // any other geometry
    // belonging to the same document. In this case the query geometry contains the indexed polygon
    // but the point is
    // inside the query as well, hence the result is 0.
    XYPolygon polygonQuery =
        new XYPolygon(new float[] {4, 6, 6, 4, 4}, new float[] {4, 4, 6, 6, 4});
    Query query = XYShape.newGeometryQuery(FIELDNAME, QueryRelation.CONTAINS, polygonQuery);
    assertEquals(0, searcher.count(query));

    XYRectangle rectangle = new XYRectangle(4, 6, 4, 6);
    query = XYShape.newGeometryQuery(FIELDNAME, QueryRelation.CONTAINS, rectangle);
    assertEquals(0, searcher.count(query));

    XYCircle circle = new XYCircle(5, 5, 1);
    query = XYShape.newGeometryQuery(FIELDNAME, QueryRelation.CONTAINS, circle);
    assertEquals(0, searcher.count(query));

    IOUtils.close(w, reader, dir);
  }

  private static boolean areBoxDisjoint(XYRectangle r1, XYRectangle r2) {
    return (r1.minX <= r2.minX && r1.minY <= r2.minY && r1.maxX >= r2.maxX && r1.maxY >= r2.maxY);
  }

  private static XYPolygon toPolygon(XYRectangle r) {
    return new XYPolygon(
        new float[] {r.minX, r.maxX, r.maxX, r.minX, r.minX},
        new float[] {r.minY, r.minY, r.maxY, r.maxY, r.minY});
  }
}
