/*
 * 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 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.encodeLatitudeCeil;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextLatitude;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextLongitude;

import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import java.util.Arrays;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Point;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.search.Query;
import org.apache.lucene.tests.geo.GeoTestUtil;
import org.apache.lucene.tests.util.TestUtil;

/** Base test case for testing geospatial indexing and search functionality * */
public abstract class BaseLatLonSpatialTestCase extends BaseSpatialTestCase {

  @Override
  protected abstract ShapeType getShapeType();

  @Override
  protected Object nextShape() {
    return getShapeType().nextShape();
  }

  @Override
  protected Component2D toLine2D(Object... lines) {
    return LatLonGeometry.create(Arrays.stream(lines).toArray(Line[]::new));
  }

  @Override
  protected Component2D toPolygon2D(Object... polygons) {
    return LatLonGeometry.create(Arrays.stream(polygons).toArray(Polygon[]::new));
  }

  @Override
  protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) {
    return LatLonGeometry.create(new Rectangle(minY, maxY, minX, maxX));
  }

  @Override
  protected Component2D toPoint2D(Object... points) {
    double[][] p = Arrays.stream(points).toArray(double[][]::new);
    Point[] pointArray = new Point[points.length];
    for (int i = 0; i < points.length; i++) {
      pointArray[i] = new Point(p[i][0], p[i][1]);
    }
    return LatLonGeometry.create(pointArray);
  }

  @Override
  protected Component2D toCircle2D(Object circle) {
    return LatLonGeometry.create((Circle) circle);
  }

  @Override
  protected Circle nextCircle() {
    final double radiusMeters =
        random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
    return new Circle(nextLatitude(), nextLongitude(), radiusMeters);
  }

  @Override
  public Rectangle randomQueryBox() {
    return GeoTestUtil.nextBox();
  }

  @Override
  protected Object[] nextPoints() {
    int numPoints = TestUtil.nextInt(random(), 1, 20);
    double[][] points = new double[numPoints][2];
    for (int i = 0; i < numPoints; i++) {
      points[i][0] = nextLatitude();
      points[i][1] = nextLongitude();
    }
    return points;
  }

  @Override
  protected double rectMinX(Object rect) {
    return ((Rectangle) rect).minLon;
  }

  @Override
  protected double rectMaxX(Object rect) {
    return ((Rectangle) rect).maxLon;
  }

  @Override
  protected double rectMinY(Object rect) {
    return ((Rectangle) rect).minLat;
  }

  /** factory method to create a new polygon query */
  protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
    return LatLonShape.newPolygonQuery(field, queryRelation, polygons);
  }

  @Override
  protected double rectMaxY(Object rect) {
    return ((Rectangle) rect).maxLat;
  }

  @Override
  protected boolean rectCrossesDateline(Object rect) {
    return ((Rectangle) rect).crossesDateline();
  }

  @Override
  public Line nextLine() {
    return GeoTestUtil.nextLine();
  }

  @Override
  protected Polygon nextPolygon() {
    return GeoTestUtil.nextPolygon();
  }

  @Override
  protected Encoder getEncoder() {
    return new Encoder() {
      @Override
      double decodeX(int encoded) {
        return decodeLongitude(encoded);
      }

      @Override
      double decodeY(int encoded) {
        return decodeLatitude(encoded);
      }

      @Override
      double quantizeX(double raw) {
        return decodeLongitude(encodeLongitude(raw));
      }

      @Override
      double quantizeXCeil(double raw) {
        return decodeLongitude(encodeLongitudeCeil(raw));
      }

      @Override
      double quantizeY(double raw) {
        return decodeLatitude(encodeLatitude(raw));
      }

      @Override
      double quantizeYCeil(double raw) {
        return decodeLatitude(encodeLatitudeCeil(raw));
      }
    };
  }

  /** internal shape type for testing different shape types */
  protected enum ShapeType {
    POINT() {
      @Override
      public Point nextShape() {
        return GeoTestUtil.nextPoint();
      }
    },
    LINE() {
      @Override
      public Line nextShape() {
        return GeoTestUtil.nextLine();
      }
    },
    POLYGON() {
      @Override
      public Polygon nextShape() {
        while (true) {
          Polygon p = GeoTestUtil.nextPolygon();
          try {
            Tessellator.tessellate(p, random().nextBoolean());
            return p;
          } catch (
              @SuppressWarnings("unused")
              IllegalArgumentException e) {
            // if we can't tessellate; then random polygon generator created a malformed shape
          }
        }
      }
    },
    MIXED() {
      @Override
      public Object nextShape() {
        return RandomPicks.randomFrom(random(), subList).nextShape();
      }
    };

    private static final ShapeType[] subList = new ShapeType[] {POINT, LINE, POLYGON};

    public abstract Object nextShape();
  }
}
