/*
 * 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.sandbox.search;

import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;

import org.apache.lucene.document.Document;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.sandbox.document.LatLonBoundingBox;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.geo.GeoTestUtil;
import org.apache.lucene.tests.index.RandomIndexWriter;
import org.apache.lucene.tests.search.BaseRangeFieldQueryTestCase;

/** Random testing for GeoBoundingBoxField type. */
public class TestLatLonBoundingBoxQueries extends BaseRangeFieldQueryTestCase {
  private static final String FIELD_NAME = "geoBoundingBoxField";

  @Override
  protected LatLonBoundingBox newRangeField(Range r) {
    // addRange is called instead of this method
    throw new UnsupportedOperationException("this method should never be called");
  }

  @Override
  protected void addRange(Document doc, Range r) {
    GeoBBox b = (GeoBBox) r;
    doc.add(new LatLonBoundingBox(FIELD_NAME, b.minLat, b.minLon, b.maxLat, b.maxLon));
  }

  /** Basic test for 2d boxes */
  public void testBasics() throws Exception {
    Directory dir = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);

    // Shared meridian test (disjoint)
    Document document = new Document();
    document.add(new LatLonBoundingBox(FIELD_NAME, -20d, -180d, 20d, -100d));
    writer.addDocument(document);

    // intersects (crosses)
    document = new Document();
    document.add(new LatLonBoundingBox(FIELD_NAME, 0d, 14.096488952636719d, 10d, 20d));
    writer.addDocument(document);

    // intersects (contains, crosses)
    document = new Document();
    document.add(
        new LatLonBoundingBox(FIELD_NAME, -10.282592503353953d, -1d, 1d, 14.096488952636719d));
    writer.addDocument(document);

    // intersects (crosses)
    document = new Document();
    document.add(new LatLonBoundingBox(FIELD_NAME, -1d, -11d, 1d, 1d));
    writer.addDocument(document);

    // intersects (crosses)
    document = new Document();
    document.add(new LatLonBoundingBox(FIELD_NAME, -1d, 14.096488952636719d, 5d, 30d));
    writer.addDocument(document);

    // intersects (within)
    document = new Document();
    document.add(new LatLonBoundingBox(FIELD_NAME, -5d, 0d, -1d, 14.096488952636719d));
    writer.addDocument(document);

    // search
    IndexReader reader = writer.getReader();
    IndexSearcher searcher = newSearcher(reader);
    assertEquals(
        5,
        searcher.count(
            LatLonBoundingBox.newIntersectsQuery(
                FIELD_NAME, -10.282592503353953d, 0.0d, 0.0d, 14.096488952636719d)));
    assertEquals(
        1,
        searcher.count(
            LatLonBoundingBox.newWithinQuery(
                FIELD_NAME, -10.282592503353953d, 0.0d, 0.0d, 14.096488952636719d)));
    assertEquals(
        1,
        searcher.count(
            LatLonBoundingBox.newContainsQuery(
                FIELD_NAME, -10.282592503353953d, 0.0d, 0.0d, 14.096488952636719d)));
    assertEquals(
        4,
        searcher.count(
            LatLonBoundingBox.newCrossesQuery(
                FIELD_NAME, -10.282592503353953d, 0.0d, 0.0d, 14.096488952636719d)));

    reader.close();
    writer.close();
    dir.close();
  }

  public void testToString() {
    LatLonBoundingBox field = new LatLonBoundingBox(FIELD_NAME, -20d, -180d, 20d, -100d);
    String expected =
        "LatLonBoundingBox <geoBoundingBoxField:[-20.000000023283064,-180.0,19.99999998137355,-100.0000000745058]>";
    assertEquals(expected, field.toString());
  }

  @Override
  protected int dimension() {
    return 2;
  }

  @Override
  protected Range nextRange(int dimensions) {
    // create a random bounding box in 2 dimensions
    return new GeoBBox(dimensions);
  }

  @Override
  protected Query newIntersectsQuery(Range r) {
    GeoBBox b = (GeoBBox) r;
    return LatLonBoundingBox.newIntersectsQuery(FIELD_NAME, b.minLat, b.minLon, b.maxLat, b.maxLon);
  }

  @Override
  protected Query newContainsQuery(Range r) {
    GeoBBox b = (GeoBBox) r;
    return LatLonBoundingBox.newContainsQuery(FIELD_NAME, b.minLat, b.minLon, b.maxLat, b.maxLon);
  }

  @Override
  protected Query newWithinQuery(Range r) {
    GeoBBox b = (GeoBBox) r;
    return LatLonBoundingBox.newWithinQuery(FIELD_NAME, b.minLat, b.minLon, b.maxLat, b.maxLon);
  }

  @Override
  protected Query newCrossesQuery(Range r) {
    GeoBBox b = (GeoBBox) r;
    return LatLonBoundingBox.newCrossesQuery(FIELD_NAME, b.minLat, b.minLon, b.maxLat, b.maxLon);
  }

  protected static class GeoBBox extends Range {
    protected double minLat, minLon, maxLat, maxLon;
    protected int dimension;

    GeoBBox(int dimension) {
      this.dimension = dimension;
      final Rectangle box = GeoTestUtil.nextBoxNotCrossingDateline();
      minLat = quantizeLat(box.minLat);
      minLon = quantizeLon(box.minLon);
      maxLat = quantizeLat(box.maxLat);
      maxLon = quantizeLon(box.maxLon);

      //      minLat = quantizeLat(Math.min(box.minLat, box.maxLat));
      //      minLon = quantizeLon(Math.max(box.minLat, box.maxLat));
      //      maxLat = quantizeLat(box.maxLat);
      //      maxLon = quantizeLon(box.maxLon);

      //      if (maxLon == -180d) {
      //        // index and search handle this fine, but the test validator
      //        // struggles when maxLon == -180; so lets correct
      //        maxLon = 180d;
      //      }
    }

    protected static double quantizeLat(double lat) {
      return decodeLatitude(encodeLatitude(lat));
    }

    protected double quantizeLon(double lon) {
      return decodeLongitude(encodeLongitude(lon));
    }

    @Override
    protected int numDimensions() {
      return dimension;
    }

    @Override
    protected Double getMin(int dim) {
      if (dim == 0) {
        return minLat;
      } else if (dim == 1) {
        return minLon;
      }
      throw new IndexOutOfBoundsException("dimension " + dim + " is greater than " + dimension);
    }

    @Override
    protected void setMin(int dim, Object val) {
      if (dim == 0) {
        setMinLat((Double) val);
      } else if (dim == 1) {
        setMinLon((Double) val);
      } else {
        throw new IndexOutOfBoundsException("dimension " + dim + " is greater than " + dimension);
      }
    }

    private void setMinLat(double d) {
      if (d > maxLat) {
        minLat = maxLat;
        maxLat = d;
      } else {
        minLat = d;
      }
    }

    private void setMinLon(double d) {
      if (d > maxLon) {
        minLon = maxLon;
        maxLon = d;
      } else {
        minLon = d;
      }
    }

    private void setMaxLat(double d) {
      if (d < minLat) {
        maxLat = minLat;
        minLat = d;
      } else {
        maxLat = d;
      }
    }

    private void setMaxLon(double d) {
      if (d < minLon) {
        maxLon = minLon;
        minLon = d;
      } else {
        maxLon = d;
      }
    }

    @Override
    protected Double getMax(int dim) {
      if (dim == 0) {
        return maxLat;
      } else if (dim == 1) {
        return maxLon;
      }
      throw new IndexOutOfBoundsException("dimension " + dim + " is greater than " + dimension);
    }

    @Override
    protected void setMax(int dim, Object val) {
      if (dim == 0) {
        setMaxLat((Double) val);
      } else if (dim == 1) {
        setMaxLon((Double) val);
      } else {
        throw new IndexOutOfBoundsException("dimension " + dim + " is greater than " + dimension);
      }
    }

    @Override
    protected boolean isEqual(Range other) {
      GeoBBox o = (GeoBBox) other;
      if (this.dimension != o.dimension) return false;
      if (this.minLat != o.minLat) return false;
      if (this.minLon != o.minLon) return false;
      if (this.maxLat != o.maxLat) return false;
      if (this.maxLon != o.maxLon) return false;
      return true;
    }

    @Override
    protected boolean isDisjoint(Range other) {
      GeoBBox o = (GeoBBox) other;
      if (minLat > o.maxLat || maxLat < o.minLat) return true;
      if (minLon > o.maxLon || maxLon < o.minLon) return true;
      return false;
    }

    @Override
    protected boolean isWithin(Range other) {
      GeoBBox o = (GeoBBox) other;
      return o.contains(this);
    }

    @Override
    protected boolean contains(Range other) {
      GeoBBox o = (GeoBBox) other;
      if (minLat > o.minLat || maxLat < o.maxLat) return false;
      if (minLon > o.minLon || maxLon < o.maxLon) return false;
      return true;
    }

    @Override
    public String toString() {
      StringBuilder b = new StringBuilder();
      b.append("GeoBoundingBox(lat: ");
      b.append(minLat);
      b.append(" TO ");
      b.append(maxLat);
      b.append(", lon: ");
      b.append(minLon);
      b.append(" TO ");
      b.append(maxLon);
      b.append(")");

      return b.toString();
    }
  }
}
